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

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

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

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

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

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

0x01 正則表達式是什麼

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

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

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

  • DFA: 從起始狀態開始,一個字符一個字符地讀取輸入串,並根據正則來一步步肯定至下一個轉移狀態,直到匹配不上或走完整個輸入
  • NFA:從起始狀態開始,一個字符一個字符地讀取輸入串,並與正則表達式進行匹配,若是匹配不上,則進行回溯,嘗試其餘狀態

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

0x02 回溯的過程是怎樣的

因此,咱們題目中的正則<\?.*[(`;?>].*,假設匹配的輸入是<?php phpinfo();//aaaaa,實際執行流程是這樣的:性能

 

 

 

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

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

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

在調試正則表達式的時候,咱們能夠查看當前回溯的次數:

 

 

 

這裏回溯了8次。

0x03 PHP的pcre.backtrack_limit限制利用

PHP爲了防止正則表達式的拒絕服務攻擊(reDOS),給pcre設定了一個回溯次數上限pcre.backtrack_limit。咱們能夠經過var_dump(ini_get('pcre.backtrack_limit'));的方式查看當前環境下的上限:

 

 

 

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

咱們應該以英文版爲參考。

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

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

preg_match函數返回false表示這次執行失敗了,咱們能夠調用var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_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語句  

  

 

來自p神文章:

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html?page=1#reply-list

相關文章
相關標籤/搜索