深入分析CVE-2021-43848漏洞

接收方應(yīng)使用后一個字段來檢查RESET_STREAM是否是在所有流數(shù)據(jù)發(fā)送完畢后發(fā)送的。如果它確實擁有所有的數(shù)據(jù),它可以將RESET_STREAM解釋為一個設(shè)置了FIN標(biāo)志的、長度為零的STREAM幀;也就是說,它可能會表現(xiàn)得好像流是完全發(fā)送的,而沒有中止。

本文將為大家介紹Web服務(wù)器H2O的QUIC(HTTP/3)協(xié)議實現(xiàn)中的一個安全漏洞。我們之所以對這個漏洞感興趣,因為它會以某種方式影響Fastly,攻擊者可以利用該漏洞從節(jié)點的未初始化內(nèi)存中竊取隨機請求和響應(yīng),這有點類似于CloudBleed漏洞(與CloudBleed漏洞不同的是,該漏洞要求攻擊者執(zhí)行特定的操作)。

最初,我是想在HTTP/3協(xié)議實現(xiàn)中尋找HTTP請求走私漏洞的。我以前對HTTP/2協(xié)議曾經(jīng)做過類似的研究,并且發(fā)現(xiàn)的一個安全漏洞與Cloudflare有關(guān)——Cloudflare是一個著名的CDN/anti-DDoS服務(wù),在其客戶端后面充當(dāng)反向代理。由于QUIC(HTTP/3的底層協(xié)議)最近變成了RFC9000草案,我決定研究一下該協(xié)議的實現(xiàn)代碼,看看能否找到一些安全漏洞。

搭建測試環(huán)境

Fastly是一種CDN和anti-DDoS服務(wù),作為反向代理,接受用戶的請求,根據(jù)一套復(fù)雜的規(guī)則進行處理,并通過其緩存提供服務(wù),或?qū)⑵滢D(zhuǎn)發(fā)給上游——藏在Fastly后面的真正服務(wù)器。

創(chuàng)建Fastly服務(wù)是非常簡單的事情。人們可以注冊一個帳戶,購買一個域名,并設(shè)置DNS記錄以指向Fastly服務(wù)器。之后,人們還需要購買一個TLS證書,因為HTTP/2和HTTP/3需要對所有數(shù)據(jù)進行加密。

值得注意的是,啟用HTTP/3支持是需要手動完成的:人們需要寫一張支持票證,并等待回復(fù)。然而,事實證明,為了進行安全測試,我們根本就無需訂閱啟用HTTP/3的特定服務(wù),相反,只要找到任何支持QUIC的Fastly服務(wù)器并向其發(fā)送請求就夠了。雖然真正的瀏覽器不會這樣做,因為缺少Alt-Svc頭部,但直接發(fā)送到QUIC端口的請求也會得相應(yīng)的處理——不管控制面板中的設(shè)置如何,實際上,對于我們要進行的安全測試來說,這就足夠了。

由于www.fastly.com本身啟用了HTTP/3,所以,我們可以通過解析這個域名來收集一些支持HTTP/3的IP。

$host-t A www.fastly.com

www.fastly.com is an alias for prod.www-fastly-com.map.fastly.net.

prod.www-fastly-com.map.fastly.net has address 151.101.113.57

之后,您可以使用工具http2smugl向它發(fā)送HTTP/3請求,方法是偽造:authority頭部(是的,該工具支持HTTP/3,盡管它的名稱是http2smugl):

$http2smugl request https+h3://151.101.113.57/":authority:a-domain-behind-fastly.com"

:status:200

content-length:0

...

如果我們在上游服務(wù)器的http端口運行netcat,我們將收到一個連接,表明Fastly的確代理了該請求:

$nc-l 80

GET/HTTP/1.1

host:a-domain-behind-fastly.com

content-length:0

user-agent:Mozilla/5.0

Fastly-SSL:1

Fastly-Client-IP:

X-Forwarded-For:

X-Forwarded-Server:cache-fjr7923-FJR

X-Forwarded-Host:a-domain-behind-fastly.com

X-Timer:S1637693479.183877,VS0

X-Varnish:2313353937

Fastly-FF:PPtav1cHmKdVa+PX0PZLG1dkeRjY/RpDKLvKU7LtCKo=!FJR!cache-fjr7923-FJR

CDN-Loop:Fastly

...

太好了。

檢測使用了哪些軟件

據(jù)Fastly披露,它使用Varnish來處理緩存事宜和復(fù)雜的請求。然而,Varnish并不支持HTTP/3協(xié)議;因此,一定還有其他軟件來“轉(zhuǎn)譯”HTTP/3:它接受來自用戶瀏覽器的HTTP/3連接,對其進行解碼,并轉(zhuǎn)發(fā)到Varnish。

當(dāng)我在請求頭部的名稱或值中加入換行符時,F(xiàn)astly會返回出錯信息,這說明HTTP請求走私漏洞根本就不可能存在:

$http2smugl request https+h3://151.101.113.57/":authority:a-domain-behind-fastly.com""eldushechka:n"

:status:400

content-length:42

content-type:text/plain;charset=utf-8found an invalid character in header value

有次來看,尋找HTTP請求走私的路是走不通了。

好消息是,我們可以通過搜索引擎來考察這個錯誤,并發(fā)現(xiàn)它來自H2O:一個最近開始支持QUIC協(xié)議的小型HTTP/Web服務(wù)器。因此,我們現(xiàn)在知道了攻擊的對象是誰,并可以深入研究其源代碼。此外,我們可以編譯其源代碼,并在本地設(shè)置反向代理:因為發(fā)行版中提供了所有必要的配置示例。

測試很快就變得有趣起來——僅需一個帶有CONNECT方法以及非零值content-length的請求就能使服務(wù)器崩潰:

$http2smugl request https+h3://127.0.0.1:8443/":method:CONNECT""content-length:10"...at another tab...h2o:../lib/http3/server.c:462:void shutdown_stream(struct st_h2o_http3_server_stream_t*,int,int,int):Assertion`stream->state<H2O_HTTP3_SERVER_STREAM_STATE_CLOSE_WAIT’failed.

received fatal signal 6

./h2o(backtrace+0x5b)[0x47bacb]

./h2o[0x932766]

...

雖然這只是一個簡單的斷言,并且除了DoS之外,沒有任何安全影響,但這意味著QUIC代碼沒有進行全面的模糊測試,所以,它很可能存在其他安全漏洞。斷言信息提到了一個recvstate類型的對象;我決定在它周圍尋找更多可利用的東西。

QUIC流

HTTP/3是第一個沒有使用TCP協(xié)議的HTTP版本。相反,它使用了一個全新的傳輸協(xié)議QUIC,該協(xié)議使用UDP來傳輸其數(shù)據(jù)。由于UDP不僅沒有提供可靠性,還無法保證數(shù)據(jù)的順序,同時也沒有擁堵控制,因此,QUIC的實現(xiàn)必須自己處理這些問題。然而,這意味著QUIC的實現(xiàn)必須比HTTP/Web服務(wù)器軟件處理更多的東西。

H2O使用了一個專門設(shè)計的QUIC實現(xiàn),并將其分離到Quicly庫中。該庫處理與QUIC有關(guān)的一切:連接的加密握手、重傳、流量控制等。

建立在QUIC連接之上的、用于數(shù)據(jù)傳輸?shù)倪壿嫵橄蟊环Q為流。一個QUIC連接通常用于傳輸多個數(shù)據(jù)流。單個流有點像TCP連接:數(shù)據(jù)可以在一個流的兩個方向上傳輸,交付是有保證的,而且數(shù)據(jù)的順序也保持不變。當(dāng)QUIC用于HTTP/3時,每個流攜帶一個單一的HTTP請求:客戶端發(fā)送請求頭部和正文到一個新的流,服務(wù)器發(fā)送響應(yīng),然后流將被關(guān)閉。需要說明的一點是,有些流可以由服務(wù)器發(fā)起(所謂的“推送”),但我們在這篇文章中用不到它們。

傳輸過程中,QUIC數(shù)據(jù)被編碼在所謂的幀中。在RFC9000中,定義了20種幀類型,看起來有點多;然而,如果我們將連接建立、流控制、路徑探測、交付確認和加密等工作留在幕后,那么,就只剩下三種與數(shù)據(jù)傳輸直接相關(guān)的幀類型:

STREAM幀,它攜帶流數(shù)據(jù);

RESET_STREAM,表示不再發(fā)送流數(shù)據(jù);客戶端可以發(fā)送此幀,來指示以前啟動的請求不再需要了;

STOP_SENDING,它是由不希望在流中獲得更多數(shù)據(jù)的接收者發(fā)送的。

只有前兩種類型會影響流的接收方的狀態(tài),它被存儲在Quicly中的recvstream結(jié)構(gòu)中。為了進一步考察這里發(fā)現(xiàn)的漏洞,我們需要知道這些幀包含什么內(nèi)容,以及它們是如何工作的。

數(shù)據(jù)傳輸

通過QUIC流傳輸?shù)臄?shù)據(jù),在傳輸之前并不需要知道數(shù)據(jù)的長度——這一點與HTTP/1.0不同,它需要通過Content-Length頭部指出數(shù)據(jù)的長度。相反,QUIC總是以塊的形式傳輸,類似于HTTP/1.1的分塊傳輸編碼。在QUIC世界中,塊就是STREAM幀。

每個STREAM幀都包含多個字段:

流ID;

幀的偏移量,它指示該特定塊在該傳輸方向的整個半流(即客戶端→服務(wù)器或服務(wù)器→客戶端)中的偏移量;

幀的長度;

FIN標(biāo)志,指示此幀是否在這個流方向的半流中為最終幀。如果該標(biāo)志被設(shè)置,該幀還包含這次傳輸?shù)臄?shù)據(jù)的總長度;

流數(shù)據(jù)本身。

第二個字段(偏移量)是必須的,因為QUIC幀是通過UDP傳輸?shù)?,而UDP并不能保證數(shù)據(jù)包將以發(fā)送順序到達。與TCP不同,接收方可以丟棄亂序的數(shù)據(jù),但是,QUIC接收方必須提供一個緩沖區(qū)來保存亂序數(shù)據(jù),以便在丟棄前綴(missing prefix)到達時使用。

當(dāng)流的一方(通常是客戶端)決定不再進行傳輸時,將發(fā)送RESET_STREAM幀。語義上,它意味著不會再有數(shù)據(jù)朝這個方向發(fā)送。例如,如果瀏覽器用戶單擊停止按鈕以取消發(fā)送數(shù)據(jù),則可以發(fā)送這種幀。但是,即使服務(wù)器接收到RESET_STREAM,它仍然可能會處理該請求:例如,服務(wù)器可能在RESET_STREAM到達之前就已經(jīng)開始處理了。在這種情況下,該協(xié)議允許通過流來發(fā)送響應(yīng)。

RESET_STREAM幀包含三個有意義的字段,分別是:

流ID;

錯誤代碼,這不是我們感興趣的;

到此刻為止流中所發(fā)送的字節(jié)總數(shù)。

接收方應(yīng)使用后一個字段來檢查RESET_STREAM是否是在所有流數(shù)據(jù)發(fā)送完畢后發(fā)送的。如果它確實擁有所有的數(shù)據(jù),它可以將RESET_STREAM解釋為一個設(shè)置了FIN標(biāo)志的、長度為零的STREAM幀;也就是說,它可能會表現(xiàn)得好像流是完全發(fā)送的,而沒有中止。

漏洞詳情

本節(jié)及以下有關(guān)H2O的信息,僅對d1f0f65269及以前的版本有效。這個版本并不是對這里描述的漏洞的修復(fù),而是對此前的一個漏洞的修復(fù);之所以使用這個版本,是因為我的測試環(huán)境中使用的就是這個版本。關(guān)于本文所述漏洞的修復(fù),請見a68cabaeb1版本。

正如我之前提到的,H2O使用一個單獨的庫來處理QUIC協(xié)議,這個庫叫做Quicly。與其他實現(xiàn)不同的是,Quicly并沒有提供緩沖區(qū)來處理亂序的數(shù)據(jù);相反,它從用戶那里觸發(fā)一個回調(diào)函數(shù),接收數(shù)據(jù)偏移量作為參數(shù),同時接收數(shù)據(jù)大小和數(shù)據(jù)本身。因此,使用Quicly的應(yīng)用程序(在我們的例子中是H2O本身)需要保存那些比前面的一些字節(jié)早到而不能使用的字節(jié)。

然而,Quicly不僅會存儲已經(jīng)收到的字節(jié)的位置(開始和結(jié)束偏移量),并提供了一個接口來訪問這些范圍內(nèi)的位置。特別是,它定義了quicly_recvstate_bytes_available函數(shù),用于返回連續(xù)前綴的總長度。從應(yīng)用程序的角度來看,這相當(dāng)于在假定正確存儲了以前通過回調(diào)發(fā)送的所有數(shù)據(jù)的情況下已經(jīng)可以使用的字節(jié)數(shù)。

另一方面,負責(zé)存儲已經(jīng)到達的數(shù)據(jù)的H2O并不將其保存在塊中。相反,它使用一個連續(xù)的緩沖區(qū)(即st_h2o_http3_server_stream_t->recvbuf),并將所有分段存儲在那里,并必要時調(diào)整緩沖區(qū)的大小。因此,對應(yīng)于尚未到達的數(shù)據(jù)的字節(jié)來說,它們在這個緩沖區(qū)中是未初始化的。

這里要注意的是,H2O當(dāng)然不會存儲它已經(jīng)處理過的數(shù)據(jù)。相反,它會移動對應(yīng)于緩沖區(qū)開始位置的偏移量,并從收到的STREAM幀的偏移量中減去相同的值。這個細節(jié)對我們沒有任何影響,在此給出是為了說明:如果一個實現(xiàn)存儲所有的流字節(jié),包括那些它不再需要的字節(jié),那將帶來巨大的隱患。

實際上,這種行為已經(jīng)暗地里為我們提供了分配包含大量未初始化數(shù)據(jù)的緩沖區(qū)的方法:我們可以直接發(fā)送一個STREAM幀,并使其偏移量與之前發(fā)送的最后一個字節(jié)之間留一個間隙。現(xiàn)在,我們需要讓H2O使用它,這時候,RESET_STREAM幀就派上用場了。

RESET_STREAM的處理方式如下所示:

如果它已經(jīng)知道流的總長度(要么來自帶FIN標(biāo)志的STREAM幀,要么來自先前收到的RESET_STREAM幀),它將檢查幀內(nèi)“total size”字段中的值是否與已知的值一致;

丟棄所有關(guān)于先前收到的字節(jié)范圍的信息。

后者意味著:在處理過一個RESET_STREAM幀之后,H2O就無法區(qū)分stream->recvbuf中哪些字節(jié)是由客戶的數(shù)據(jù)初始化的,哪些是原封不動的。更令人興奮的是,調(diào)用quicly_recvstate_bytes_available函數(shù)會返回流的總長度,就好像所有留在緩沖區(qū)的數(shù)據(jù)都是之前被STREAM幀設(shè)置的一樣。

要想成功利用這個漏洞,還必須滿足一個要求:發(fā)送RESET_STREAM幀之后,設(shè)法讓系統(tǒng)將recvbuf內(nèi)容發(fā)送到上游。這個問題很棘手,取決于H2O如何將請求代理給上游。特別是,我們需要知道請求的正文(body)是如何被緩存的。

當(dāng)H2O收到一個HTTP/3請求后,如果頭部信息表明它包含一個正文,那么,它并不會立即開始處理該請求。相反,它將緩沖正文,直到它達到一個特定的限制,默認為10240字節(jié)。在收到第10240字節(jié)的正文后,它將切換到流模式(在這個模式下,Transfer-Encoding:chunked將被發(fā)送給HTTP/1.1上游),并繼續(xù)處理請求。

任何合理的代理實現(xiàn),都必須處理上游讀取數(shù)據(jù)的速度比客戶端發(fā)送數(shù)據(jù)的速度慢的情況。其中,一種方法是在每次向上游發(fā)送數(shù)據(jù)后,檢查客戶端的緩沖區(qū)是否包含更多字節(jié),而這正是H2O采取的方法。因此,如果我們能使RESET_STREAM幀在請求開始被發(fā)送到上游后,但在所有可用數(shù)據(jù)被發(fā)送之前被處理,H2O將檢查stream->recvbuf是否含有更多的數(shù)據(jù)。由于quicly_recvstate_bytes_available函數(shù)的返回值是不正確的,檢查會成功通過,H2O會繼續(xù)轉(zhuǎn)發(fā)數(shù)據(jù)到上游,而不知道這些數(shù)據(jù)是未初始化的。幸運的是,這里并不會檢查流是否已經(jīng)在代碼路徑上被RESET_STREAM撤銷了。

為了總結(jié)上述內(nèi)容并給出源代碼參考,讓我們重復(fù)一下我們想要組合使用的、Quicly/H2O中的三個漏洞:

1.當(dāng)收到RESET_FRAME時,位于deps/quicly/lib/quicly.c中的quicly_recvstate_reset只是清除了st_quicly_recvstate_t結(jié)構(gòu)的接收范圍。隨后對quicly_recvstate_bytes_available的調(diào)用將返回流中仍然剩余的字節(jié)總數(shù)(無論它們是否真的被接收)。

2.lib/http3/server.c中的函數(shù)handle_buffered_input并沒有檢查一個流是否被撤銷(quicly_stop_requested函數(shù)只檢查發(fā)送部分是否被取消,而沒有檢查接收部分)。因此,它將繼續(xù)處理stream->recvbuf中未初始化的部分,就像它們是客戶端發(fā)送的一樣。

3.lib/http3/server.c中的proceed_request_streaming函數(shù)會調(diào)用handle_buffered_input,并且沒有檢查流是否被撤銷。這個函數(shù)被設(shè)置為回調(diào)函數(shù),當(dāng)反向代理請求在流模式下發(fā)送更多數(shù)據(jù)時,這個函數(shù)將被調(diào)用。

漏洞利用思路

鑒于上面的描述,我們可以得出以下的漏洞利用思路:

攻擊者建立一個與H2O實例的連接,并進行QUIC握手。

攻擊者向?qū)嵗l(fā)送請求頭部,然后是10239字節(jié)的請求正文。同時,H2O會接收它們并進行確認。

漏洞利用代碼發(fā)送一個特制的數(shù)據(jù)報,其中包含三個QUIC幀。

——STREAM幀,位于正文的第10240字節(jié)處(即offset字段設(shè)置為headers長度+10239,數(shù)據(jù)長度為1)。

——STREAM幀,位于正文的第30000字節(jié)處,同時也設(shè)置了FIN標(biāo)志(即偏移字段設(shè)置為headers長度+29999,數(shù)據(jù)長度為1,設(shè)置FIN標(biāo)志,最終size字段設(shè)置為報頭長度+30000)。

——RESET_STREAM幀,最終size字段等于報頭長度+30000

這里的值30000是任意選擇的。我們將為這個值泄露30000-10240=19760字節(jié)的未初始化數(shù)據(jù)。在實際的漏洞利用過程中,我為這個參數(shù)嘗試了多個不同的值,并泄露了不同類型的數(shù)據(jù)。

H2O處理數(shù)據(jù)報時,將執(zhí)行以下步驟:

——當(dāng)處理第一個STREAM幀時,將HTTP請求切換到流模式,并發(fā)起與上游的連接;

——在處理了位于接收到的正文的第30000個字節(jié)處的幀后,它將stream->recvbuf擴大到30000,并寫入其中的最后一個字節(jié),而其余部分并未進行初始化。它還將eos值設(shè)置為30000,因為幀中的FIN標(biāo)志已被設(shè)置。

——在調(diào)用quicly_recvstate_reset中接收到RESET_STREAM幀后,并沒有關(guān)注stream->recvbuf的哪些字節(jié)被初始化,哪些字節(jié)沒有被初始化。

H2O反向代理模塊向上游發(fā)送前10240字節(jié),并通過調(diào)用proceed_request_streaming請求更多數(shù)據(jù)。后面的函數(shù)將剩下的19760字節(jié)轉(zhuǎn)發(fā)給上游,其中只有最后一個字節(jié)被設(shè)置過,其余的都是泄露的未初始化的數(shù)據(jù)。

在上面的描述中完全忽略了一件事,即HTTP/3級幀的處理。實際上,在QUIC流上傳輸?shù)臄?shù)據(jù)被打包成HTTP/3幀,這些幀不同于QUIC幀,而類似于HTTP/2幀。對于HTTP/3級幀來說,除了需要調(diào)整底層QUIC流中的偏移量之外,并不會給攻擊增加任何其他困擾。

漏洞利用過程

為了執(zhí)行上面的計劃,我們需要精心構(gòu)造的QUIC幀。剛開始,我打算從頭開始創(chuàng)建一個簡單的QUIC實現(xiàn),經(jīng)過幾次嘗試后我就放棄了,轉(zhuǎn)而開始轉(zhuǎn)向quic-go。實際上,我們只需要重命名internal目錄,就可以構(gòu)造自己的幀,同時,我們還可以為sendStream結(jié)構(gòu)添加一個函數(shù),用于將我們的幀直接添加到隊列中以供發(fā)送。

經(jīng)過幾輪調(diào)試,它在我的本地H2O設(shè)置中成功實現(xiàn)了漏洞的利用。我的本地Web服務(wù)器收到了一些看起來像未初始化的內(nèi)存數(shù)據(jù):如內(nèi)存地址、可讀字符串等。我使用exploit對Fastly發(fā)動攻擊,第一次嘗試就得手了!在上游服務(wù)器中,我收到了一個來自Fastly的請求,其正文包含一個HTTP響應(yīng),該響應(yīng)顯然是要發(fā)送給另一個用戶的(與使用Fastly的另一個網(wǎng)站相關(guān)),并與二進制數(shù)據(jù)混在一起。我運行了幾次,收到了不同的東西:圖像、Fastly內(nèi)部統(tǒng)計數(shù)據(jù)的轉(zhuǎn)儲、cookie以及其他有趣的東西。

披露時間線

我已經(jīng)向Fastly披露了這個安全漏洞,他們很快做出了反應(yīng):他們在幾天內(nèi)就對生產(chǎn)版本進行了熱修復(fù),然后,我們協(xié)調(diào)了披露事項(例如,這篇文章)。整個時間表如下所示:

11月23日(2021年):報告該漏洞;

12月1日:在一個實例中部署了熱修復(fù)程序;

12月8日:該安全問題得到完全解決;

1月31日:公開披露。

這個H2O中的漏洞的編號為CVE-2021-43848。

小結(jié)

在本文中,我們?yōu)樽x者詳細介紹了通過模糊測試在HTTP/3和QUIC發(fā)現(xiàn)的一個內(nèi)存漏洞,及其利用方法;我期待著將來能夠在開源的QUIC實現(xiàn)中找到更多令人興奮的安全漏洞。

本文翻譯自:https://medium.com/ emil.lerner/leaking-uninitialized-memory-from-fastly-83327bcbee1f

THEEND

最新評論(評論僅代表用戶觀點)

更多
暫無評論