HackThisSite - JavaScript Mission 6

Challenge

Fiftysixer decided to try his hand at javascript! All was going well until he realized that he forgot to remove the unused code, which resulted in a confusing mess. He didn't mind, in fact, he did his best to make it even MORE confusing!

Find the password:

索引名 go go away .js,难度 Weird。主页面只留一段自相矛盾的误导脚本,真正的比较藏在外部文件 checkpass.js 里。

Solution

  • 关卡页 https://www.hackthissite.org/missions/javascript/6/ 通过 <script src="/missions/javascript/6/checkpass.js"></script> 引入外部脚本。
  • 页面内联脚本故意留下大量未使用且自相矛盾的代码:check() 里出现 "hack_this_site" 字面量和跳向 about:blank 的分支,这些都被真链路旁置。
  • 表单真正调用的是内联的 checkpassw(this.value),它把输入写进 RawrRawr 再调用外部脚本里的 checkpass()

拉取主页面与外部脚本:

1
2
3
4
5
6
$ curl -s -b "$HTS_COOKIE" \
-H "Referer: https://www.hackthissite.org/missions/javascript/6/" \
"https://www.hackthissite.org/missions/javascript/6/" -o lvl6.html
$ curl -s -b "$HTS_COOKIE" \
-H "Referer: https://www.hackthissite.org/missions/javascript/6/" \
"https://www.hackthissite.org/missions/javascript/6/checkpass.js" -o checkpass.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RawrRawr = "moo";
function check(x)
{
"+RawrRawr+" == "hack_this_site"
if (x == ""+RawrRawr+"")
{
alert("Rawr! win!");
window.location = "about:blank";
} else {
alert("Rawr, nope, try again!");
}
}

function checkpassw(moo)
{
RawrRawr = moo;
checkpass(RawrRawr);
}

check() 里那条 "+RawrRawr+" == "hack_this_site" 是一条无副作用的裸表达式语句,about:blank 分支也永远不会给站点送出密码:表单并不调用 check()。真正的入口是 checkpassw(moo)checkpass(RawrRawr),而 checkpass 定义在外部文件里。这正是题面说的忘了删掉没用的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dairycow="moo";
moo = "pwns";
rawr = "moo";

function checkpass(pass)
{
if(pass == rawr+" "+moo)
{
alert("How did you do that??? Good job!");
window.location = "../../../missions/javascript/6/?lvl_password="+pass;
} else {
alert("Nope, try again");
}
}

决定性的比较是 pass == rawr+" "+moorawr="moo"moo="pwns",中间夹一个空格字面量 " "dairycow="moo" 在本文件里没被使用,和主页面的 check() 一样是烟雾弹。

从抓下来的 checkpass.js 里把变量逐个读出来再按比较式拼接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
"""Recover the JavaScript Mission 6 password from the external script.

index.php loads /missions/javascript/6/checkpass.js; the winning comparison
there is pass == rawr+" "+moo with dairycow="moo", rawr="moo", moo="pwns".

The inline check() (RawrRawr / "hack_this_site" / about:blank) is dead code;
the form calls checkpassw() which forwards to the external checkpass().
The password is exactly "rawr + ' ' + moo" -- the single space included.
"""
import re
import urllib.parse

js = open("checkpass.js", encoding="utf-8", errors="replace").read()
vals = dict(re.findall(r'(\w+)\s*=\s*"([^"]*)"', js))
password = "%s %s" % (vals["rawr"], vals["moo"])
print("rawr :", vals["rawr"])
print("moo :", vals["moo"])
print("password :", repr(password))
print("query : lvl_password=" + urllib.parse.quote(password))
1
2
3
4
5
$ uv run python decode6.py
rawr : moo
moo : pwns
password : 'moo pwns'
query : lvl_password=moo%20pwns

密码里带一个空格,所以回跳 URL 中它会被编码成 moo%20pwnswindow.location 赋值时浏览器也会自动做这一步)。

机制与第 5 关一致:客户端比较通过后 window.location = ".../6/?lvl_password="+pass 回跳,由服务端在这一次请求上记账。curl 复现必须带关卡 Referer,URL 里的空格写成 %20

1
2
3
4
$ curl -s -o /dev/null -w '%{http_code}\n' -b "$HTS_COOKIE" \
-H "Referer: https://www.hackthissite.org/missions/javascript/6/" \
"https://www.hackthissite.org/missions/javascript/6/?lvl_password=moo%20pwns"
200

接受证据:个人资料列出 Javascript: (1) (2) (3) (4) (5) (6) (7),积分为通关前的 6501 升到 Master (6669 Points)

1
2
3
$ curl -s -b "$HTS_COOKIE" "https://www.hackthissite.org/user/view/<account>/" -o prof.html
$ grep -aoE "Rank: [A-Za-z]+ \([0-9]+ Points\)" prof.html
Rank: Master (6669 Points)

状态:verified(live 通关并已计入 profile)。

Vulnerabilities

密码逻辑分散在主页面的内联脚本和外部 checkpass.js 两处,但两处都在客户端、都无条件下发。把校验拆到多个脚本、再掺入大量死代码和自相矛盾的语句,只是混淆而不是保护:只要能读到全部脚本(它们本来就是交给浏览器的),把执行路径顺着 <script> 标签走一遍就能还原真比较。修复方向依旧是服务端校验:客户端脚本可以被任意读取、改写和重放,任何留在里面的常量都等于公开。

moo pwns