P-代码审计点之利用回溯绕过正则表达式

PHP代码审计之利用回溯绕过正则表达式

第一篇:利用回溯绕过PHP中的正则表达式

背景介绍

首先,来看一段在waf上很常见的正则,

1
2
3
4
5
6
7
...
if(preg_match('/SELECT.+FROM.+/is', $_POST['sql'])){
die("SQL injection") //WAF
}else{
echo($_POST['sql']);
//mysql_query($db, $_POST['sql']); //查询
}

如何绕过?——相信师傅们肯定是方法多多。但今天主要详细说说其中的一种另类的绕过方式,利用正则回溯绕过正则判断

select_bypass

原理分析

要知道``preg_match`的返回值:除了0和1,还可能因为出错而返回false

image-20200731114158728

出错的原因大致有两个,1. 正则回溯次数超过限制 ; 2. 入参的类型不是字符串(数组类型)

我们主要关注第一种:利用正则回溯次数超过限制。回到一开始举的例子:

1
2
3
4
5
6
7
...
if(preg_match('/SELECT.+FROM.+/is', $_POST['sql'])){
die("SQL injection") //WAF
}else{
echo($_POST['sql']);
//mysql_query($db, $_POST['sql']); //查询
}

只看前几行判断入参是否为php代码的is_php()函数:

当输入的字符串中含有SELECTFROM,且两个词后分别跟有任意字符串,即视为满足匹配,preg_match就会返回1。其中的参数介绍如下

  • .+,“点 加号”,.表示匹配除了换行符\n之外的任意单个字符串,+ 匹配前面的子表达式1次或多次,两者合在一起,其实就表示匹配任意的字符串。

这段正则是有问题的——但问题在哪里呢?这就要从PHP中正则的匹配原理说起。

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit,我们可以在phpinfo里查看当前环境下的上限,默认为1000,000

image-20200731114823341

常见的正则引擎,可被细分为 DFA(确定性有限状态自动机)与 NFA(非确定性有限状态自动机)。大多数程序语言都使用了 NFA 作为正则引擎,其中也包括 PHP 使用的 PCRE 库。

NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行往回查找(回溯),尝试其他状态

那 NFA 自动机到底是怎么进行回溯的呢?我们以下面的字符和表达式来举例说明。

1
2
regex=/SELECT.+FROM.+/
param=select id from /*0123456789*/ test
  • 首先,拿到正则表达式的第一个匹配符S,于是去和字符串的字符进行比较,字符串的第一个字符是s,匹配(忽略大小写),换下一个,第二个是 E,和字符串的第二个字符e匹配,再换下一个,一直到SELECT与select匹配完毕
  • 读取到正则表达式匹配符的第二部分.+:任意字符串匹配1次以上,那么可以一次性匹配掉剩下的所有字符串,即正则拿到select id from /*0123456789*/ test,但此时正则表达式中的F是无法匹配上的,且拿到的字符串已到了末尾,于是开始回溯,从末尾开始往回匹配,首先尝试匹配末尾的t,当然无法匹配上F,于是匹配倒数第二个字符s,也不行,于是一步步回溯到字符串中的f,回溯次数一次又一次地增加着。。直到回溯次数超过预设值1000,000而发生错误,函数返回false

因此,我们可以通过发送超长字符串的方式,使正则执行失败,返回false,从而绕过目标对 PHP 语言的限制。

regex3

(动画中是正则调试器,来自:https://regex101.com/r/pf5Pa0/1/debugger)

开头的那种方法,便是这样绕过的,但其实有个前提,payload必须在POST的参数里,不能是GET的参数,因为RFC 2616里限制了GET的参数最长不能超过8K(8*1024),本地实测8178个字符,超过状态码即变成414

image-20200812164653626

这可能也是很多时候构造超长字符串,能绕过waf的原因之一(另一个原因是waf需要考虑性能)

利用方式

在某些情况下(POST参数+特殊的正则):可以利用正则回溯,使得preg_match函数返回false,进而绕过正则表达式的判断。代码审计时可以额外关注全局过滤处的正则表达式是否可用这种方法绕过。

之前出过maccms出过一个前台RCE,就是利用的这种方法来绕过全局过滤函数的,详情可参考maccmsV8前台RCE(preg_match绕过)

利用方式:发送超长字符串,可以用burp的intruder,payload类型选Character blocks

image-20200812173241780

也可以改编下面这个python脚本进行利用

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
#encoding: utf-8
import requests

NUM = 1000000;# 你想填充的字符串数
URL = "http://php.test/select.php" # 地址

param = "union select 1,2,3,4,5 /*{}*/ ".format("A"*NUM)
post_data = {"p":param}
resp = requests.post(url=URL, data=post_data)

print(resp.text)

结论(修复方案)

  1. 如果用preg_match对字符串进行匹配,一定要使用===全等号来判断返回值,如:
1
2
3
4
5
6
7
8
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

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

这样,即使正则执行失败返回false,也不会进入if语句。

  1. 再分享一个先知上很不错的黑白盒审计的思维导图,来源https://xz.aliyun.com/t/1720 img

  2. 最后,推荐一个网站,这个网站可以检查你写的正则表达式和对应的字符串匹配时是否有问题。

image-20200812170735197

reference