HackThisSite - JavaScript Mission 5

Challenge

Javascript Mission 5: Uhm, faith spelled runescape wrong?

索引名为 Escape!,难度 easy。页面上只有一个密码框和 Check Password 按钮,密码被一层百分号编码包着,用 unescape() 在浏览器里解出来。

Solution

  • 关卡页 https://www.hackthissite.org/missions/javascript/5/,唯一业务脚本是页面内联的一段;另有 jQuery 的 CDN 标签(data.htscdn.org/js/jquery-1.8.1.min.js)与本题无关。
  • 页面没有任何把密码送去服务端校验的请求:比对全在客户端的 check() 里完成,命中后用 window.location = "../../../missions/javascript/5/?lvl_password="+x 回跳。
  • 所以只要把源码里那个百分号编码串解开就行。

拉取页面源码:

1
2
3
$ curl -s -b "$HTS_COOKIE" \
-H "Referer: https://www.hackthissite.org/missions/javascript/5/" \
"https://www.hackthissite.org/missions/javascript/5/" -o lvl5.html

页面内联脚本(业务部分全文):

1
2
3
4
5
6
7
8
9
10
11
moo = unescape('%69%6C%6F%76%65%6D%6F%6F');
function check (x) {
if (x == moo)
{
alert("Ahh.. so that's what she means");
window.location = "../../../missions/javascript/5/?lvl_password="+x;
}
else {
alert("Nope... try again!");
}
}

moo 直接由 unescape(...) 得到,check(x) 只做一次相等比较,相等就带着 x 回跳。没有第二因子,也没有隐藏分支。

unescape() 是早期的百分号解码器,%XX 中的 XX 是字符的十六进制 ASCII 码。逐个翻译:

  • %69 = 0x69 = 105 = i
  • %6C = 0x6C = 108 = l
  • %6F = 0x6F = 111 = o
  • %76 = 0x76 = 118 = v
  • %65 = 0x65 = 101 = e
  • %6D = 0x6D = 109 = m
  • %6F = 0x6F = 111 = o
  • %6F = 0x6F = 111 = o

为避免手抄出错,直接从抓下来的 lvl5.html 里正则取出该串再解码:

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

The page assigns the expected value at load time:

moo = unescape('%69%6C%6F%76%65%6D%6F%6F');

`unescape()` is the legacy percent-decoder, so the password is just that
literal decoded once. urllib.parse.unquote is the modern equivalent.
"""
import re
import urllib.parse

src = open("lvl5.html", encoding="utf-8", errors="replace").read()
encoded = re.search(r"unescape\('([^']*)'\)", src).group(1)
password = urllib.parse.unquote(encoded)
print("encoded :", encoded)
print("decoded :", repr(password))
1
2
3
$ uv run python decode5.py
encoded : %69%6C%6F%76%65%6D%6F%6F
decoded : 'ilovemoo'

浏览器控制台里 unescape('%69%6C%6F%76%65%6D%6F%6F')decodeURIComponent('%69%6C%6F%76%65%6D%6F%6F') 结果相同,都是 "ilovemoo"

命中后页面回跳到 .../5/?lvl_password=ilovemoo。密码全是常规字符,无需额外编码。浏览器里直接在输入框填 ilovemooCheck Password 即可,请求会天然带上关卡目录 Referer。

用 curl 复现时必须补上 Referer: https://www.hackthissite.org/missions/javascript/5/:实测同一条 URL 不带 Referer 会被服务端当作跨站请求丢弃,profile 不记账;带上后立即计入完成。

1
2
3
4
$ curl -s -o /dev/null -w '%{http_code}\n' -b "$HTS_COOKIE" \
-H "Referer: https://www.hackthissite.org/missions/javascript/5/" \
"https://www.hackthissite.org/missions/javascript/5/?lvl_password=ilovemoo"
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

整关校验全在客户端:密码以 unescape(...) 的常量形式写在页面里,unescape() 只做一次百分号解码,没有密钥、也没有服务端参与。任何在客户端做校验的设计都等价于把答案连同校验算法一起交给用户;无论密码是明文、编码还是运行时计算出来的,逆回来只是时间问题。修复方向是把校验搬到服务端,客户端只提交凭据,不持有比较逻辑。

ilovemoo