代码审计-通过preg_replace函数深入命令执行

代码审计day1

前言

最近开始学习代码审计,刚好mochazz学长的团队红日安全-代码审计小组正在做一个PHP代码审计的项目,该项目会对一道题目进行细致的分析,我觉得很适合新手学习,就跟进他们的项目,对项目中的题目写出我自己的想法。希望能有所进步。

题目1

这道题的名字叫做蜡烛

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

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

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

?>

代码很短,考察的是preg_replace/e模式执行任意代码,我们很清楚preg_replace函数是通过正则匹配出符合的字符串并对匹配出的字符串进行替换,而preg_replace/e模式则可以执行匹配出的字符串,这就导致了命令执行的漏洞。我们先来详细了解一下preg_replace函数

1
2
3
4
5
6
7
8
9
10
preg_replace:(PHP 5.5)

功能 : 函数执行一个正则表达式的搜索和替换

定义 : mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 如果匹配成功以 replacement 进行替换

$pattern 存在 /e 模式修正符,允许代码执行
/e 模式修正符,是 preg_replace() 将 $replacement 当做php代码来执行

这里使用了/e模式,输入的参数和对应的参数值分别对应于匹配的模式和用于正则匹配的字符串,这两个参数都可以通过GET方式进行控制,但是第二个参数写定了‘strtolower(“\1”)’,那么要如何执行代码呢

解析1

‘strtolower(“\1”)’其实涉及了正则的反向引用,我们可以看一下W3Cschool对它的解释:

1
2
3
反向引用

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

\1实际上就是1,即第一个匹配项。那么就很好办,payload已经大致出来了,我们只需要控制匹配模式为/(.*)/

,匹配的字符串为phpinfo(),就可以执行命令

现在本地测试一下

1
2
3

preg_replace('/(.*)/ie','strtolower("\1")','phpinfo()');
?>

没有成功执行,为什么呢,试着输出函数执行结果,结果为phpinfo()

其实,我们执行函数preg_replace/e,就是执行下面的过程

1
2
3
4
5
6
preg_match('/(.*)/i',$value,$match);
eval('strtolower("$match[0]");');
当我们输入$value = 'phpinfo()'
$match[0] = 'phpinfo()';
eval('strtolower("phpinfo()");');
执行结果自然是'phpinfo()'

因为preg_replace/e只执行一次代码,即strtolower函数,所以我们必须想办法让输入的phpinfo()自己执行,这就涉及到了php动态变量,根据原贴给出payload为{${phpinfo()}},我一开始也很困惑为什么,我们知道php变量名经过{}包裹后会将变量值输出,而这里phpinfo{}包裹后会首先执行phpinfo(),执行结果返回true,那么我们画一个等价的式子

1
{${phpinfo()}} == {$true} == {null} == ''

我们继续测试一下:

1
2
3

preg_replace('/(.*)/ie','strtolower("\1")','{${phpinfo()}}');
?>

成功执行了代码phpinfo()

解析2

那么payload是不是已经很明显了:/?.*={${phpinfo()}}

并没有成功执行,这是因为我们之前是将.*直接写入程序的正则表达式中,而本题我们是需要通过GET方式提交.*,而很明显我们通过GET提交是没有成功执行phpinfo(),我们可以用var_dump试着输出一下GET数组

我们可以看到点号.被替换成了下划线_,这是因为php自动将非法字符替换成了下划线,我们换个通用字符即可,因此payload:/?S*={${phpinfo()}}

成功执行代码

总结

命令执行的函数我们比较熟悉的是eval,assert,今天通过这个例子,学习了preg_replace/e函数执行任意代码,很感谢红日团队提供这么有趣的题目,之后将继续跟进红日团队的审计项目,学习代码审计。

最后附上参考文章:深入研究preg_replace与代码执行