上一篇(Day 13)從 cfg_server 的 opcode dispatch 表發現,cm_processREQ_NC 這個 handler 在未認證的狀態下就可以呼叫,而且它處理的 master_key 欄位沒有做長度檢查。今天把這條路走完:從漏洞成因、到 PoC、到實際洩漏出什麼。
這個 CVE 的價值在於它是教科書級的 length-check missing → OOB read → information disclosure 鏈路,而且不繞過任何防禦、也不需要認證。
[!todo] 待補
- 補上實際 PoC 腳本完整內容
- 補上每一步的封包 hex dump
- 補上實測截圖
| 欄位 | 內容 |
|---|---|
| CVE | CVE-2022-38105 |
| TALOS ID | TALOS-2022-1590 |
| 類型 | Information Disclosure(OOB read) |
| CWE | CWE-119 |
| CVSS | 7.5(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| 影響型號 | ASUS RT-AX82U |
| 影響韌體 | 3.0.0.4.386_49674-ge182230 |
| 修補版本 | 3.0.0.4.386_51925 |
| 發現者 | Lilith >_<(Cisco Talos) |
| 入口 | cfg_server port 7788,opcode 0x3 |
旁邊還有一個 DoS:CVE-2022-38393 / TALOS-2022-1592,同樣在設定服務的 opcode 上,透過構造封包觸發崩潰。同一批弱點裡面的第三個 CVE-2022-35401 是認證繞過,屬於 Case 03 的範圍。
[!note] 為什麼 CVSS 是 7.5 而不是 9.x
因為這是 read 而不是 write。能讀到設定檔,但寫不進去。要升級成 RCE 通常需要再串一個寫入型弱點。
cm_processREQ_NC 收到的是一個巢狀 TLV:
struct REQ_NC {
// ... header ...
char *master_key; // 應該剛好 0x20 = 32 bytes(AES-256)
char *client_nonce; // 應該剛好 0x20 = 32 bytes
uint32_t group_id;
};
解析完之後會用它導出 session:
session_key = SHA256(group_id || server_nonce || client_nonce);
之後要對回應做 AES-256 加密/解密。
問題出在這裡:master_key 的長度沒有被檢查。
// 有問題的邏輯(示意)
int len = subpkt->size; // 攻擊者控制
char *mk = malloc(len); // 只配置 len bytes
memcpy(mk, subpkt->data, len);
// 之後直接當成 32-byte key 使用
aes_encrypt(resp, resp_len, session_key_derived_from(mk), 0x20);
// ↑ 假設 mk 一定有 32 bytes
攻擊者送出 master_key 長度 = 4 bytes:
malloc(4) → heap 上配置一個 4 bytes 的 chunk
aes_encrypt 卻從這個 chunk 開始讀 32 bytes
→ 讀到後面相鄰 chunk 的 metadata 與資料
關鍵在於:越界讀出來的 bytes 會被 fold 進 AES session key。
master_key = [4 bytes 合法輸入] || [28 bytes 越界 heap 資料]
↓
SHA256(...)
↓
session_key(攻擊者不知道,但可觀察)
↓
對回應做 AES-256 加密
這形成了一個 byte-by-byte oracle:
[!todo] 待補
- 補上
aes_encrypt的實際簽章與長度參數- 補上 decompiled 關鍵程式碼片段
- 補上
malloc(4)與後續read 32的 ASAN 證據
cm_processREQ_NC 在 opcode dispatch 裡屬於「不需要既有 session」的那一組。攻擊者連 KU(拿 public key)都不需要,直接從 0x3 開始送。
正常流程:KU → 得到 public key → 組出合法的 NC(長度正確)→ 認證成功
攻擊流程:直接送 NC(長度錯誤)→ OOB read → oracle
7788,不需要認證[!todo] 待補
- 確認
7788是 WAN 可達還是僅 LAN(Day 13 表格要補)- 補上實際洩漏到的資料內容截圖(如果 lab 允許的話)
| 項目 | 內容 |
|---|---|
| 設備 | ASUS RT-AX82U |
| 韌體 | 3.0.0.4.386_49674-ge182230 |
| 網路 | 直連 LAN,攻擊機設同網段 |
| 韌體來源 | 需自備 archive |
沿用 Day 07 建立的 EMUX 架構。本 Case 的環境參數集中列在下表,後續 Case 沿用同一組值。
| 項目 | 內容 |
|---|---|
| Container | emux-tenda-ac15 |
| Network | ac15-lab |
| Guest IP | 192.168.100.2 |
| SSH | localhost:22222 |
| 目標服務 | cfg_server(port 7788) |
| 韌體來源 | Day 13 從 RT-AX82U_3.0.0.4.386_49674-ge182230.trx 取得 |
| 架構 | ARM(與前幾天的 MIPS guest 不同) |
| 依賴 | vendor NVRAM / libnvram / 設定檔 header CRC |
| 環境成熟度 | 🟡 部分(見下方待補) |
# 進入既有 container
CONTAINER="emux-tenda-ac15"
GUEST="192.168.100.2"
SSH_PORT="22222"
$ docker exec -it "$CONTAINER" bash
[!warning] Container 名稱一致性
EMUX 的 container 命名規則是emux-<vendor>-<model>,本課程一律使用emux-tenda-ac15。
若你沿用舊的 runbook 看到emux-ac15,那個名稱不存在,docker exec會直接失敗。
這個 Case 比前幾天難在哪裡
cfg_server 是 ARM + 依賴 vendor NVRAM,EMUX 起來的難度比 httpd 高,需要處理:
cfg_server 需要 /var/nvram 或 CFM 檔libnvram / 加密函式[!todo] 待補
- 補上 EMUX 啟動
cfg_server的實際步驟- 記錄卡住的地方(這對 Case 06 的 emulation 內容很有價值)
依 Day 18 的三級證據分類,本 Case 的產出可以這樣標示:
| 結論 | 級別 | 理由 |
|---|---|---|
REQ_TLV header 結構(type/len/crc 12 bytes) |
✅ A | 從二進位直接讀出,與環境無關 |
| 無認證即可讀取 credential 的程式邏輯存在 | ✅ A | 純邏輯分支,判定過程不碰硬體 |
| 設定檔 CRC 驗證可被繞過 | ✅ A | 純演算法 |
port 7788 是 WAN 可達還是僅 LAN |
⚠️ B | 取決於實際路由與防火牆設定 |
| 洩漏 credential 的完整性與可用性 | ⚠️ B | 取決於 vendor NVRAM 的加密強度 |
| 能否從讀取擴大為寫入 | ⚠️ B | 需確認 TLV 處理是否有長度檢查 |
結論:本 Case 的存在性可以在模擬環境確立(A 級),但可達性與影響範圍必須在實機確認(B 級)。
# 封包構造
pip install scapy
# 十六進位觀察
xxd / hexdump -C
# TODO: 補上完整實作
import struct
def make_tlv(tlv_type: int, payload: bytes, crc: int = 0) -> bytes:
header = struct.pack('<III', tlv_type, len(payload), crc)
return header + payload
# 三個 subpkt:master_key / client_nonce / group_id
# TODO: 補上 subpkt 的 TLV 格式定義
master_key 長度這一步的關鍵是:不要只試一個長度,要掃一段,看回應有沒有變化。
for length in range(0, 64):
# 送出不同長度的 master_key,記錄回應密文
# TODO: 記錄到 table
[!todo] 待補
- 補上長度掃描結果表格:length → response hex
- 找出從哪個長度開始回應異常
已知的合法 bytes:[prefix]
猜測區段: [ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ]
已知 OOB 區段: [ 猜測區段之後的 28 bytes,全部可控 ]
比對方式:
送出 A = [prefix] + [guess_byte] + [all 0x41]
送出 B = [prefix] + [guess_byte ^ 1] + [all 0x41]
若 ciphertext(A) != ciphertext(B) → 猜測「有效」
為什麼 A 和 B 一定不同?
因為猜測區段會被 fold 進 SHA256,導致 key 改變
而 OOB 區域(0x41)維持不變
→ 差異只能來自 guess_byte 命中了真正的 heap 內容
[!todo] 待補
- 補上實際的猜測函式
- 記錄猜了幾個 byte、花費多少時間
如果 master_key 長度給得非常大(例如 0xFFFF),會從 OOB read 升級成 OOB access 崩潰:
# 監看 service 狀態
watch -n1 "netstat -tuln | grep 7788"
# 看 log
dmesg | tail -n 30
[!todo] 待補
- 這一段跟 CVE-2022-38393(DoS)可以合併講
- 補上實際的崩潰現象
| 面向 | 評估 |
|---|---|
| 遠端可達性 | 需確認 WAN / LAN;報告標為 AV:N |
| 需要認證 | 否 |
| 複雜度 | 低 |
| 影響 | 設定內容洩漏,可作為後續攻擊前置 |
| 單獨可達 RCE | 否(read-only) |
今天走完了一條完整的鏈路:
7788 與 unauth opcode 的存在master_key 長度未驗證這裡最值得記住的一句話是:不是你寫的長度欄位沒人檢查,而是你假設某個欄位會是某個長度。
同時也留了一個 Case 06 的伏筆:這個 PoC 在真實硬體上好跑,但 cfg_server 在模擬環境裡要處理 NVRAM、CFM、依賴函式庫——這正是下一個 case 要處理的問題。
下一篇(Day 15)進入 Case 05:Firmware Update 面向。更新機制是整條韌體裡信任邊界最寬鬆的一條路徑,下一篇會拆解從「檢查更新」到「寫入 flash」的完整 pipeline,並找出可以截斷或污染的地方。
[1] https://www.talosintelligence.com/vulnerability_reports/TALOS-2022-1590
[2] https://www.talosintelligence.com/vulnerability_reports/TALOS-2022-1592
[3] https://nvd.nist.gov/vuln/detail/CVE-2022-38105
[4] https://nvd.nist.gov/vuln/detail/CVE-2022-38393