preg_replace在/e模式下的代码执行
版本
- php5.5.0以下
前置知识
有如下代码:
<?php preg_replace("/test/e",$_GET["a"],"jutst test");?>
意为在 “this is a test” 字符串中找到 “test”,并将其替换为通过$_GET["a"]获取的代码执行结果
也就是说只要提交GET参数“a”的内容为php代码,即可实现远程代码执行,比如提交:
?a=phpinfo();
便会回显phpinfo页面
案例
找了一个很有意思的案例:
<?php
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
?>
如果能够代码执行,那么代码相当于:
eval('strtolower("\\1");')
在这里,\\1
在转义后即为\1
,其在正则中有自己的含义:
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
有点抽象,说人话就是,\几,就匹配第几个
这里的 \1 实际上指定的是第一个子匹配项,这里如果GET传参:
?.*={${phpinfo()}}
那么正则语句就变成了:
preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});
而我们都知道,这样传参的话,参数名中的.
是非法字符,会被替换为_
,所以我们要做的就是换一个正则表达式,让其匹配到 {${phpinfo()}} 即可实现代码执行,而\S恰好能够实现,所以最终实现代码执行的payload为:
?\S*={${phpinfo()}}
解释一下这个正则:
[\s]表示,只要出现空白就匹配
[\S]表示,非空白就匹配
那么它们的组合[\s\S],表示所有的都匹配
"."是不会匹配换行的,所有出现有换行匹配的时候,就习惯使用[\s\S]来完全通配模式。
这样是可以执行phpinfo()的
再解释一下为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数:
这是利用了 php可变变量的原因,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果,单引号中的变量不会被处理,所以这里的\1
可解析变成{${phpinfo()}}
在此情况下,花括号 {}
被用于指示 PHP 需要解析一个复杂或动态表达式,而通过使用 ${}
结构,代码试图动态地调用 phpinfo()
函数
而{${phpinfo()}} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)
接下来一步一步分析:
var_dump(phpinfo()); // 结果:布尔 true
var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'
var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
有了上面的例子,同理:
<?
function test($str)
{
}
echo preg_replace("/s*[php](.+?)[/php]s*/ies", 'test("\1")', $_GET["h"]);
?>
如果GET方法提交请求:
?h=[php]{${phpinfo()}}[/php]
phpinfo()也会被执行
总结
preg_replace \e 模式存在代码执行
如果 replacement中是双引号的,可引申出上面的漏洞
针对上面双引号引发的漏洞的防御方法也很简单,比如:
将'strtolower("\\1")'修改为"strtolower('\\1')"
将'test("\1")' 修改为"test('\1')"
这样{${phpinfo()}}或${phpinfo()}就会被当做一个普通的字符串处理(单引号中的变量不会被处理)