文章转载整理于深入研究preg_replace与代码执行
0x01 前言
该文章主要研究preg_replace /e模式下的RCE中出现的一些小坑,以及利用$_GET
替换非法字符、${}
可变变量 这两个知识点来构造RCE的payload
以下内容基于题目[BJDCTF2020]ZJCTF,不过如此
0x02 踩坑
先附上[BJDCTF2020]ZJCTF,不过如此
这道题有关preg_replace的源码:
<?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']);
}
先给出这道题的payload模板:
payload 1: /?.*={${phpinfo()}}
payload 2: ?\S*={${phpinfo()}}或${phpinfo()}
这里payload1无法执行成功,payload2可以执行成功
结合源码我们思考这几个问题:
preg_replace('/(' . $re . ')/ei','strtolower("\\1")',$str);
中第二个参数已经固定为'strtolower("\\1")'
,那我们怎么还能构造自己执行的代码呢?- 为什么payload中的
.*
不行而\S
可以? - 为什么要用
${phpinfo()}
或{${phpinfo()}}
这样的格式来实现代码的执行呢?为什么不能直接是phpinfo?
问题1
上面的命令执行相当于eval('strtolower("\\1");')
的结果,而\\1
是实际上是\1
,而\1
在PCRE中有特定的含义:
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
关于反向引用,这里不做过多解释,想要深入了解的可以看这两篇文章
也就是说这里的\1
实际上是第一个子匹配项,而在匹配模式中/(' . $re . ')/ei
正好有用()括起来的子匹配项,因此可以使用反向引用,至此,我们可以知道在preg_replace中代码执行的逻辑是/(' . $re . ')/ei
匹配到$str
(传参进去也就是${phpinfo()}
)的内容,但是这个内容并不能直接作为代码执行,preg_replace是第二个参数(replacement)才能作为代码执行,因此第二个参数'strtolower("\\1");'
反向引用/(' . $re . ')/ei
匹配到的$str
,这样就可以实现$str
的代码执行了
问题2
这里涉及$_GET对非法参数的处理问题,先看下面这个例子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hUiJJPSZ-1599572343887)(C:\Users\86188\AppData\Roaming\Typora\typora-user-images\image-20200908203401678.png)]
这里传入参数?.*=abc
后,在php中.
被替换为_
下划线,因此$_GET
接收参数时会把非法字符转化为下划线
这里fuzz一波哪些字符串会被替换
import requests
for i in range(0,256):
url='http://10.11.184.39/test5.php?ha'+chr(i)+'ha=abc'
r=requests.get(url)
if '_' in r.text:
print(str(i)+':'+chr(i))
以上字符被替换了。但是需要注意:
- +在开头,例如?+haha=123,这时+会被url解释为空格,然后传入参数中开头的空格会被去掉,只剩下
haha
- 以上测试都是将特殊字符放在字符串中间,而在开头只有
.
会变成下划线,其余字符不会变成下划线
通过上面的测试分析,我们知道了.*
传入后实际上变为了_*
这也就使得我们的贪婪匹配失效了,因此我们需要替换这一匹配规则,就有了payload2中的\S*
(\S匹配所有非空白字符,\s匹配所有空白字符)因此\S*
可以完全匹配到我们$str传入的代码
问题3
最后说说为什么要匹配到${xx}
或{${xx}}
这样的格式才能执行其中的函数。实际上这是php可变变量的原因。在php双引号包裹的字符串中可以解析变量,${phpinfo()}
中的 phpinfo()
会被当做变量先执行,执行后变为 ${1}
(phpinfo执行完返回true)。简单理解的话,就是在preg_replace中,匹配到了${xx}
先会执行${}
中的内容。
0x03 总结
这是做题中的一些思考,也算是提升了自己对于正则的理解能力和涨姿势了吧。