PHP利用PCRE回溯次数限制绕过某些安全限制

学习博客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); ...
}

正则表达式描述的正则语言,恰好就是有限状态自动机能识别的语言。其实是一个语言类

有限自动机本质就是:只有有限个状态,读一个字符就切换一次状态;读完字符串,如果停在 “接受状态”,就认为这个字符串匹配成功。

常见的正则匹配有两个类别DFS(确定性)和NFA(非确定性)

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

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

回溯过程

所以,我们题目中的正则<\?.*[(;?>].*,假设匹配的输入是<?php phpinfo();//aaaaa`,实际执行流程是这样的:

image.png

见上图,可见第4步的时候,因为第一个.*可以匹配任何字符,所以最终匹配到了输入串的结尾,也就是//aaaaa。但此时显然是不对的,因为正则显示.*后面还应该有一个字符[(;?>]`。

所以NFA就开始回溯,先吐出一个a,输入变成第5步显示的//aaaa,但仍然匹配不上正则,继续吐出a,变成//aaa,仍然匹配不上……

最终直到吐出;,输入变成第12步显示的<?php phpinfo(),此时,.*匹配的是php phpinfo(),而后面的;则匹配上[(;?>]这个结果满足了正则表达式,于是不再回溯。13步开始向后匹配;,14步匹配.*,第二个.*匹配到了匹配到了字符串末尾,最后结束匹配。

查看回溯次数

image.png

然后发现有8次

PHP的pcre.backtrack_limit限制利用

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

image.png

可见,回溯次数上限默认是100万。那么,假设我们的回溯次数超过了100万,会出现什么现象呢?比如:

image.png

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

preg_match函数返回false表示此次执行失败了,我们可以调用var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR);,发现失败的原因的确是回溯次数超出了限制:

image.png

所以,这道题的答案就是通过发送超长字符串的方式,使正则执行失败,最后绕过目标对PHP语言的限制。

对应的POC如下:

1
2
3
4
5
import requests
from io import BytesIO

files = {
'file': BytesIO(b'aaa<?php

PHP利用PCRE回溯次数限制绕过某些安全限制
https://camellia72-h.github.io/2026/09/19/php-pcre-backtrack-limit-bypass/
作者
Camellia72-H
发布于
2026年9月19日
许可协议