
Disclosure and PoC for a root file read/write flaw in the TP-Link Deco XE75 Pro V3 debug channel (tmpsvr opcodes 0xC456/0xF0), with CVSS analysis and handler decompilation.
Companion VLAN project: experimental fixed-VLAN-30 source alpha for the exact XE75 Pro V3 firmware/profile. It requires operator-supplied stock files and has no general installer or flashable image. Guest VLAN 591 remains unresolved; final three-unit cold-boot, UI and client-isolation acceptance is incomplete. See current status.
Research disclosure, TP-Link Deco XE75 Pro V3, firmware 1.5.0 Build
20260603 Rel.15804. Prepared for [email protected] and public release alongside our 802.1Q IoT VLAN implementation (deco-iot-vlan).
The Deco's owner-authenticated debug SSH channel (Dropbear on TCP 20001,
direct-tcpip forwarding only, password auth) reaches an internal
tmpsvr service on 127.0.0.1:20002. One of its Lua handlers,
inner_file_push (opcode 0xC456), opens params.file in "w" mode and
writes params.content verbatim as root, with no path allowlist, no size
limit, and no privilege drop. A sibling opcode (0xF0) provides a
matching root file read. Together: authenticated read + write of the entire
root filesystem, which on a router is the whole device.
SSH :20001 (username = SHA1(owner_email), password = TP-Link account password)
-> direct-tcpip -> 127.0.0.1:20002 (TMPv2)
-> 0xC456 inner_file_push : io.open(file,"w"); write(content); close() [root]
-> 0x00F0 root file read : byte-exact file read [root]
/etc/config is a live writable ramfs → change a key, the router honors it./www is served over HTTPS by uhttpd → pushed files are immediately reachable./tp_data/user_data is persistent ubifs flash → survives reboot.GET /cgi-bin/luci → root Lua execution → io.popen → arbitrary root
binary execution.All four steps were executed and reverted on the owner's own main unit.
tmpsvr is still unfixed 2 years after CCS'24 / Black Hat '24The UCR SecLab team publicly documented the :20001 -> 127.0.0.1:20002 (tmpsvr) debug channel two years ago in CCS 2024 — "Untangling the Knot: Breaking Access Control in Home Wireless Mesh Networks" (and Black Hat USA 2024 — "Fallen Tower of Babel"). Their entry point on older Deco models (X55, FW 1.2.2) was factoring 512-bit RSA hostkeys and abusing unvalidated tmpsvr handlers.
Observed differences and remaining handlers on this exact firmware:
ssh-rsa hostkey size from 512-bit to 2048-bit (one shared observed key across all three mesh units, SHA256:9feb3ac4...932a36, plus per-unit ecdsa-sha2-nistp521), closing the 512-bit factorization path from the 2024 talk.1.5.0:
:20001 (svr_auth_uci) remains active using SHA1(owner_email) + the shared TP-Link ID password.0xC456 (inner_file_push) still opens any caller-supplied path in "w" mode as root with zero path validation.0x00F0 still reads any file on the filesystem as root.0xC505 (tty2tcp/start) — the exact unquoted /bin/sh -c command-injection handler highlighted in the 2024 paper — is still sitting in the June 2026 firmware (1.5.0) completely unfixed.luci/controller/admin/mobile_app/iot_device
module, inner_file_push).Only LAN reachability has been demonstrated. For the observed LAN-only profile we use the AV:A variants: v3.1 6.8 / v4.0 8.5. AV:N scores below are conditional on a demonstrated network path; cloud reachability is unverified.
This is a post-authentication, owner-privileged escalation to root — not an
unauth RCE — the observed-profile scores below are 6.8 (v3.1) and 8.5 (v4.0). Full rationale and the
rejected inflations are in docs/cvss-analysis.md.
CVSS v3.1 — LAN-only assessment 6.8 (Medium); conditional remote variant 7.2 (High):
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H = 7.2 (High)
CVSS:3.1/AV:A/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H = 6.8 (Medium) # if LAN-only
Decisive calls: PR:H (the owner / TP-Link-ID account is the device's
administrative principal) and S:U (the handler already runs as root — no
authority boundary is crossed). An earlier draft scored 9.9 via PR:L + S:C;
both are rejected.
CVSS v4.0 — LAN-only assessment 8.5 (High); conditional remote variant 8.6 (High):
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N = 8.6 (High)
CVSS:4.0/AV:A/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N = 8.5 (High) # if LAN-only
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:L = 9.3 (Critical) # with Subsequent-System LAN/mesh impact
The real-world risk — a stolen/reused TP-Link-ID password converting to
persistent root across the whole mesh plus client-traffic MITM — belongs in the
narrative (and the v4.0 SC:H/SI:H/SA:L Subsequent-System metrics), not in an
inflated v3.1 base vector.
poc/deco_filepush_poc.py — self-contained (stdlib + paramiko). Credentials
via DECO_EMAIL / DECO_PASSWORD env vars only. Live modes require
DECO_HOSTKEY, verified independently: raw base64, a public-key line, or one
ssh-keyscan key line (comments supported). A scan alone does not authenticate
the device. The HTTPS check compares served bytes but does not verify its certificate.
Exactly one mode is required; failed verification exits nonzero.
python3 poc/deco_filepush_poc.py --selftest # offline framing sanity
[email protected] DECO_PASSWORD=*** DECO_HOSTKEY="$TRUSTED_DECO_HOSTKEY" \
python3 poc/deco_filepush_poc.py --run # write marker + readback + HTTPS check
[email protected] DECO_PASSWORD=*** DECO_HOSTKEY="$TRUSTED_DECO_HOSTKEY" \
python3 poc/deco_filepush_poc.py --cleanup <nonce> # overwrite marker with empty content
The PoC writes a single inert marker to /www/log_export (ramfs; also
cleared on reboot), then requires byte-exact readback through the device's
own root-read opcode and unmodified HTTPS serving before reporting
success.
ssh-keyscan -p 20001 <deco> → pin host key (2048-bit RSA shared across
the mesh + per-unit nistp521).SHA1(owner_email) / TP-Link account password, request
direct-tcpip to 127.0.0.1:20002.01010100 → 01010200 → 01010200), then opcode 1
token request.0xC456, {"params":{"file":"/www/log_export/deco-write-<nonce>.html", "content":"..."}} → {"error_code":0,...}.0xF0, {"params":{"file":"..."}} → byte-exact readback.GET https://<deco>/log_export/deco-write-<nonce>.html → 200, exact bytes.tty2tcp (0xc505) path is confirmed present + unvalidated (static)
but not live-verified as a separate injection; listed for completeness.