HackThisSite - JavaScript Mission 7

Challenge

JS Obfuscation. FTW!

Find the password:

索引名 JS Obfuscation. FTW!,难度 moderate。密码比较逻辑藏在一个运行时由十六进制字符串数组加 String.fromCharCode(...) 生成出来的 <button> 里。要先把混淆还原出来。

Solution

  • 关卡页 https://www.hackthissite.org/missions/javascript/7/,页面源码里只有一行被混淆的脚本(另有 jQuery 的 CDN 标签)。
  • 看不到可见的密码框逻辑:那个 Check Password 按钮本身是脚本用 document.write(...) 运行时写进页面的。
  • 那一行叠加了两类混淆:["\x66\x72..."] 这种 \xNN 字符串数组藏标识符名,String.fromCharCode(0x3c,0x62,...) 这种十六进制码点数组藏 HTML。

拉取页面源码:

1
2
3
$ curl -s -b "$HTS_COOKIE" \
-H "Referer: https://www.hackthissite.org/missions/javascript/7/" \
"https://www.hackthissite.org/missions/javascript/7/" -o lvl7.html
1
var _0x4e9d=["\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65","\x77\x72\x69\x74\x65"];document[_0x4e9d[0x1]](String[_0x4e9d[0x0]](0x3c,0x62,0x75,0x74,0x74,0x6f,0x6e,0x20,0x6f,0x6e,0x63,0x6c,0x69,0x63,0x6b,0x3d,0x27,0x6a,0x61,0x76,0x61,0x73,0x63,0x72,0x69,0x70,0x74,0x3a,0x69,0x66,0x20,0x28,0x64,0x6f,0x63,0x75,0x6d,0x65,0x6e,0x74,0x2e,0x67,0x65,0x74,0x45,0x6c,0x65,0x6d,0x65,0x6e,0x74,0x42,0x79,0x49,0x64,0x28,0x22,0x70,0x61,0x73,0x73,0x22,0x29,0x2e,0x76,0x61,0x6c,0x75,0x65,0x3d,0x3d,0x22,0x6a,0x30,0x30,0x77,0x31,0x6e,0x22,0x29,0x7b,0x61,0x6c,0x65,0x72,0x74,0x28,0x22,0x59,0x6f,0x75,0x20,0x57,0x49,0x4e,0x21,0x22,0x29,0x3b,0x77,0x69,0x6e,0x64,0x6f,0x77,0x2e,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x2b,0x3d,0x20,0x22,0x3f,0x6c,0x76,0x6c,0x5f,0x70,0x61,0x73,0x73,0x77,0x6f,0x72,0x64,0x3d,0x22,0x2b,0x64,0x6f,0x63,0x75,0x6d,0x65,0x6e,0x74,0x2e,0x67,0x65,0x74,0x45,0x6c,0x65,0x6d,0x65,0x6e,0x74,0x42,0x79,0x49,0x64,0x28,0x22,0x70,0x61,0x73,0x73,0x22,0x29,0x2e,0x76,0x61,0x6c,0x75,0x65,0x7d,0x65,0x6c,0x73,0x65,0x20,0x7b,0x61,0x6c,0x65,0x72,0x74,0x28,0x22,0x57,0x52,0x4f,0x4e,0x47,0x21,0x20,0x54,0x72,0x79,0x20,0x61,0x67,0x61,0x69,0x6e,0x21,0x22,0x29,0x7d,0x27,0x3e,0x43,0x68,0x65,0x63,0x6b,0x20,0x50,0x61,0x73,0x73,0x77,0x6f,0x72,0x64,0x3c,0x2f,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3e));

拆开看:

  • _0x4e9d 是一个字符串数组,两项都用 \xNN 转义写出:\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65 解出来是 fromCharCode\x77\x72\x69\x74\x65 解出来是 write
  • 于是 document[_0x4e9d[0x1]] 就是 document.writeString[_0x4e9d[0x0]] 就是 String.fromCharCode
  • String.fromCharCode(0x3c,0x62,...) 的那一串十六进制参数,正是被写进页面的 HTML 每个字符的码点。

把标识符数组和码点数组都换成真实形态,就能拿到 document.write 真正写出的按钮 HTML。脚本对三类字符串分别解码,最后把 HTML 里的比较字面量抽出来:

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
#!/usr/bin/env python3
"""Recover the JavaScript Mission 7 password from the obfuscated source.

The level ships two hex-string arrays and rebuilds a <button> at run time by
turning a numeric char-code list into HTML:

var _0x4e9d=["\\x66\\x72\\x6F\\x6D\\x43\\x68\\x61\\x72\\x43\\x6F\\x64\\x65",
"\\x77\\x72\\x69\\x74\\x65"];

Step 1 unescapes the \\xNN literals into identifier names
("fromCharCode","write"); step 2 turns the numeric char-code list into the
button HTML and reads the compared literal out of it -- the password.
"""
import re

src = open("lvl7.html", encoding="utf-8", errors="replace").read()

# Step 1: \xNN string literals -> identifier names
arr = re.search(r"_0x4e9d=\[(.*?)\]", src, re.S).group(1)
names = [bytes(b, "latin1").decode("unicode_escape")
for b in re.findall(r'"((?:\\x[0-9a-fA-F]{2})+)"', arr)]
print("identifiers:", names)

# Step 2: rebuild the button HTML from the String.fromCharCode() code list
call = re.search(r"String\[_0x4e9d\[0x0\]\]\(([^)]*)\)", src, re.S).group(1)
codes = [int(x, 16) for x in re.findall(r"0x([0-9a-fA-F]+)", call)]
button = "".join(chr(c) for c in codes)
print("button HTML:")
print(button)

# Step 3: the compared literal inside the button is the password
password = re.search(r'value=="([^"]+)"', button).group(1)
print("password:", password)
1
2
3
4
5
$ uv run python decode7.py
identifiers: ['fromCharCode', 'write']
button HTML:
<button onclick='javascript:if (document.getElementById("pass").value=="j00w1n"){alert("You WIN!");window.location += "?lvl_password="+document.getElementById("pass").value}else {alert("WRONG! Try again!")}'>Check Password</button>
password: j00w1n

还原出的按钮 HTML 里,成功分支是:

1
2
3
4
if (document.getElementById("pass").value=="j00w1n") {
alert("You WIN!");
window.location += "?lvl_password="+document.getElementById("pass").value
}

比较的字面量即密码(见文末 spoiler)。注意这一关用的是 window.location +=(在当前 URL 上追加 query),而 5、6 关是整体赋值。写法不同,效果一样,都是把密码拼进 lvl_password 参数。

curl 复现必须带关卡 Referer:

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

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

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

Vulnerabilities

这一关只做了混淆:\xNN 字符串数组隐藏标识符名,String.fromCharCode(...) 隐藏 HTML,但两者都是可逆的常量编码,不是加密:脚本在运行时无论如何都要把它们还原成明文才能 write 和比较,所以明文必然在同一份源码里可被还原。在浏览器里把那一行写入控制台、或在页面渲染完后查看 DOM,都能直接看到按钮 HTML 和 j00w1n。混淆只能拖慢人工阅读,挡不住任何自动化或一次性的还原脚本。修复方向:把比较放服务端,客户端最多提交不可逆校验值;如果必须客户端比较,也应使用服务端下发的随机挑战值,而不是一个可离线还原的静态常量。

j00w1n