直接来看一下我们的绕过方式,我们传入的数据为一串超长字符串(在php代码之后拼接了一百万个a字符):
让我们看看匹配的效果,发现is_php函数并没有按照作者预想中的情况返回1,而是返回了false,导致走到了else语句,达成绕过效果:
为什么这样的字符串会导致preg_match返回false呢?我们一步一步探索一下。
回溯和Redos
首先我们需要知道作者会这样写判断语句的原因,因为他觉得preg_match的执行结果只有1和0两种结果,但实际上,正如我们所见,preg_match还可能会返回false:
在分析正则匹配的流程之前,我们需要先了解一下正则引擎。
正则引擎
传统正则表达式引擎分为两类,分别是 NFA(非确定性有限状态自动机)和 DFA(确定性有限状态自动机)。
-
DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
-
NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态
由于NFA的执行过程存在回溯,所以其性能会劣于DFA,但它支持更多功能。大多数程序语言都使用了NFA作为正则引擎,其中也包括PHP使用的PCRE库。
回溯过程
结合上面的正则表达式,我们来看下回溯的过程:
-
因为第一个.*可以匹配任何字符,所以在第四步的时候,匹配到了字符串的末尾,准备开始进行子表达式[(`;?>]的匹配:
-
这里子表达式显示在第一个.*后面还有一个字符[(`;?>],所以NFA开始进行回溯,回溯了8次之后匹配到了分号,然后继续向后匹配
ReDos
那么回溯会导致什么问题呢?如果回溯次数过多,计算次数也会急剧增多,严重可导致ReDos(正则表达式拒绝服务攻击)。
-
2019年七月,Cloudflare 曾经全球中断服务,原因是为了改进内联JavaScript屏蔽,部署了一条正则表达式组成的WAF防御规则,耗尽了CPU资源,导致全球大量网站访问出现了502。
举个栗子,我们在python中使用这个正则做一个测试:
可以看到程序执行的时间随着a的个数增多,呈现出一个指数爆炸的增长趋势:
pcre.backtrack_limit
因此PHP为了防止回溯次数过多,限制了回溯的次数上限,这个次数对应着php_ini中的pcre.backtrack_limit:
可见,当前环境回溯次数上限默认是100万。而超过100万次之后,preg_match就会执行错误返回false,这也就是为什么上面的超长字符串可以绕过is_php函数的原因。
修复
明白了为什么被绕过之后,修复这个问题也会变得很简单。其实,PHP官方文档对preg_match的警告中已经给了答案:
建议我们使用===全等号来判断preg_match函数的返回值:
参考链接
-
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html?page=1#reply-list
-
https://www.php.net/manual/en/function.preg-match
原文始发于微信公众号(陌陌安全):ReDos与preg_match某些场景下的绕过