iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Security

Agentic Era,一年來 LLM 到底都挖了些什麼洞!系列 第 11

Day11. 新花招:"Pwnable" Requests Smuggling 2026

  • 分享至 

  • xImage
  •  

前兩天我們看完了傳統的 CL.TE / TE.CL,以及進階到 HTTP/2 Downgrade 時利用偽標頭與 CRLF 注入的走私手法。
今天我們要來看一個在 2026 年被 LLM 挖出來的 CVE-2026-50052: My Cousin Vinyl [1],發現團隊跟上一個介紹的 CASE 一樣是 calif.io。

Vinyl 101

它有個比較有名的名字 Varnish Cache——這個於 2006 年問世的開源 HTTP 加速器在 2026 年正式更名為 Vinyl Cache。

Vinyl 的核心特色包括:

  • 高效能反向代理與快取:作為 Origin Server 前端的 Reverse Proxy,將請求與快取內容直接放在記憶體處理,是 Adobe Commerce (Magento) 官方推薦的 Full-Page Cache,也是 Fastly CDN 底層深度客製化架構的基石。
  • 雙協議支援(H2 to H1 轉譯):前端對 Client 端支援二進制、以長度前綴(Length-prefixed)為基礎的 HTTP/2,後端對 Origin Server 則轉換為基於純文字以 \r\n 分隔的 HTTP/1.1
  • 連線池(Connection Pooling):Vinyl 為了避免頻繁與後端建立 TCP 連線的 Overhead,會維護一組連線池重複利用。多個 Client 的請求會依序複用同一條後端 TCP 連線——這也代表只要有一條後端連線遭到走私污染,下一個使用該連線的受害者就會遭殃。

以上介紹由 Gemini 生成,簡言之它就是個 前端 Server!
對不起原諒我也不太會介紹這東西
以下手打:

The Bug

這個漏洞發生在 Vinyl 處理 HTTP/2 HPACK 解碼時的偽標頭分發邏輯中。

致命的字串比對巨集:Tstrcmp

include/vdef.h 中,Vinyl 定義了一個比對結構體字串的巨集:

// include/vdef.h
#define Tstrcmp(t, s) (strncmp((t).b, (s), Tlen(t)))

注意到問題了嗎?沒輟!
strncmp 的第三個參數 n,傳入的是 Tlen(t)(即輸入的 Header 名稱長度),而不是目標比對字串 s 的長度!

接著看 cache_http2_hpack.c 中的 h2h_addhdr() 函式:

// cache_http2_hpack.c - h2h_addhdr()
if (!Tstrcmp(nm, ":method")) { 
    ... 
} else if (!Tstrcmp(nm, ":path")) { 
    ... 
} else if (!Tstrcmp(nm, ":scheme")) { 
    ... 
} else if (!Tstrcmp(nm, ":authority")) { 
    memcpy(d->out + 6, "host", 4); 
    hdr.b += 6; 
}

當 Client 傳送一個名為 :a 的偽標頭時:

  • nm:aTlen(nm) 為 2。
  • 呼叫 Tstrcmp(nm, ":authority") 會展開成 strncmp(":a", ":authority", 2)
  • 因為前兩個 byte 都是 :a,比對結果回傳 0(代表相等)!

這意味著只要是 :authority 的任意前綴(:a:au:auth 等),都會被 Vinyl 誤認為是合法的 :authority 標頭,至此就發生了 collision。

為什麼辨識成 :authority 會這麼致命?這牽涉到 Vinyl 為了效能所做的 In-place 覆寫機制:

  1. HTTP/2 的 :authority 轉到 HTTP/1.1 時對應的是 host 標頭。
  2. 正常情況下,解碼緩衝區 d->out 的內容是 :authority: example.com
  3. Vinyl 為了避免在 Heap 上重新分配記憶體,直接將 offset 6 的位置覆寫成 "host"(把 rity 蓋掉),並將標頭起始指標 hdr.b 往後移動 6 個 bytes(hdr.b += 6)。
  4. 這樣指標剛好越過原本的 :autho,直接指向 host: example.com

但如果我們送的是偽標頭 :a 呢?

  • Case 1: 送個空的 :a: ""
    整個標頭只有 4 bytes(:a: ),此時強制執行 hdr.b += 6,會導致 hdr.b > hdr.e。在後續程式碼觸發 assert(b <= e),導致 Worker 程序 crash(僅能造成 DoS)。
  • Case 2: 送精準長度 == 6 的 :a: xx
    如果 Value 剛好帶 2 bytes(例如 :a: xx),整個標頭長度剛好是 6 bytes(:a + : + xx)。
    執行 hdr.b += 6 後,hdr.b 恰好等於 hdr.ehdr.b == hdr.e)!

這樣不會觸發 assert 崩潰,而是成功建立了一個大小為 0 的 Header。

從 0-byte Header 到裸露的 CRLF(Bare CRLF)

當 Vinyl 把請求轉譯為 HTTP/1.1 寫入後端 TCP 連線時,會走 HTTP1_Write

unsigned HTTP1_Write(struct v1l *v1l, const struct http *hp, const int *hf) {
    ...
    l = http1_WrTxt(v1l, &hp->hd[hf[0]], " ");     // METHOD + " "
    l += http1_WrTxt(v1l, &hp->hd[hf[1]], " ");    // URL + " "
    l += http1_WrTxt(v1l, &hp->hd[hf[2]], "\r\n"); // PROTO + "\r\n"
    
    // 遍歷所有 Header,寫入內容並在結尾加上 \r\n
    for (u = HTTP_HDR_FIRST; u < hp->nhd; u++)
        l += http1_WrTxt(v1l, &hp->hd[u], "\r\n"); 
        
    l += V1L_Write(v1l, "\r\n", -1); // 最後的空行結束 Header 區塊
    return (l);
}

當處理到那個長度為 0 的 Header 時:

  • 內容長度為 0 byte。
  • 後綴(多)寫入了一個 \r\n

在 HTTP/1.1 規範中,連續的 \r\n\r\n 就代表 Header 區塊結束。後端收到這個單獨的 \r\n 就會認為 Header 已經讀完了,但此時 Vinyl 其實還在發送後面剩下的合法 Headers。

組合技:Content-Length Trick & 憑證竊取

來講講這個漏洞怎麼擴展:
若直接截斷,後端會把殘留的標頭當作未知的 Garbage Request 而噴 400 錯誤並中斷連線。但攻擊者可以利用 HTTP/2 偽標頭順序沒有強制約束的特點:

  1. 提前宣告 Content-Length:
    攻擊者在 :a 之前先塞入一個 content-length: <殘留長度>

    [前端 H2 視角]
    content-length: 109
    :a: xx
    (其他正常 headers...)
    [DATA frame: 109 bytes] + [第二段走私的 HTTP/1.1 請求]
    
  2. 後端解析被切斷:
    後端看到 content-length: 109 後,在遇到 :a 產生的裸露 \r\n 時判定 Header 結束。接著後端依據 Content-Length 將 Vinyl 隨後送出的殘留 Header 當作 Body 吞掉剛好 109 bytes。

  3. 走私請求登場:
    緊接在後面的 HTTP/2 DATA frame 內容直接露出,被後端連線當作下一個全新進來的 HTTP/1.1 請求解析!

這時候就可以透過這樣來 somehow 一樣(繞過前端 server block),因為夾帶進去作為 Data Frame 的 HTTP/1.1 請求就直接被放去後端 Server 了,抑或是:

  • 一樣是去汙染下一個請求
    走私進去的請求是一個帶有龐大 Content-Length(例如 4096)的 POST /api/review
    後端發現在當前 TCP 連線上資料還沒讀滿 4096 bytes,連線便處於等待狀態。
    這時,Vinyl 把下一個受害者發往 /account 的合法請求送到同一個連線池連線上,後端會直接把受害者的整個請求內容(包含 CookieAuthorization: Basic ...)當成攻擊者剛才那個 POST 請求的 Body 存入資料庫或評論區,達成無感竊取憑證!

The Patch

修補方式非常乾脆:淘汰非對稱的 Tstrcmp,改用嚴格長度檢查的 Tstreq

// include/vdef.h
#define Tstreq(t, s) (Tlen(t) == strlen(s) && !strncmp((t).b, (s), Tlen(t)))

改成先去確認長度一樣就好 owob

References


上一篇
Day10. Requests Smuggling Attack 102!現代化你的攻擊手段!
系列文
Agentic Era,一年來 LLM 到底都挖了些什麼洞!11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言