HackThisSite - Programming Mission 1

Challenge

Level 1 — Unscramble the words

找出被打乱顺序的原始单词,单词是从官方 wordlist 里随机挑的,30 秒内把原始单词按列表顺序用逗号分隔提交。

Find the original (unscrambled) words, which were randomly taken from a wordlist. Send a comma separated list of the original words, in the same order as in the list below. You have 30 seconds time to send the solution.

每次访问实例页都会重新生成一组乱序单词,30 秒的限时决定了只能脚本化。

Solution

  • 实例页 https://www.hackthissite.org/missions/prog/1/ 里,乱序词逐个用 <br /> 分隔,前面是 List of scrambled words:,后面是 Answer:,解析时按这两句切段最省事。
  • 词表是公开的固定文件:https://www.hackthissite.org/missions/prog/1/wordlist.zip(不需要登录态,匿名 curl 也能下)。解开后是 wordlist.txt1274 行、CRLF 行尾
  • 词表里不只有普通英文单词,还混着数字串和带符号的词(1212126543218675309666666html:) 等)。所以题面里出现 888888 这种条目是完全正常的,它本身就是一个合法条目。
1
2
3
4
5
6
7
8
9
10
11
$ curl -s -o wordlist.zip https://www.hackthissite.org/missions/prog/1/wordlist.zip && unzip -o wordlist.zip >/dev/null
$ file wordlist.txt && wc -l wordlist.txt
wordlist.txt: ASCII text, with CRLF line terminators
1274 wordlist.txt
$ head -6 wordlist.txt
html:)
121212
131313
123123
654321
8675309

打乱只改变字符顺序、不改变字符频次,所以每个乱序词 w 与原文的排序后字符串(把字符排序后拼接)完全相等。建索引时用排序后的串当 key,一次查表即可;重复字母(aremedrrirmemto)也不会出错。

词表可能存在同一多重集对应多个词的极端情况,这里额外保底:命中不到时按长度过滤再比对排序串。

限时 30 秒,但真正的时间开销在解析上(字符串处理),网络只占两次往返:GET 实例页 + POST 答案。

完整脚本如下。脚本把 cookie 从 HTS_COOKIE 环境变量读取,依赖的 HTTP 与页面解析逻辑也一并包含在内;wordlist.txt 放在脚本同目录。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env python3
"""Solve HackThisSite Programming Mission 1."""

import html
import os
import re
from pathlib import Path

import requests

BASE = "https://www.hackthissite.org"
LEVEL_URL = f"{BASE}/missions/prog/1/"
WORDLIST = Path(__file__).with_name("wordlist.txt")
USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
)


def session():
cookie = os.environ.get("HTS_COOKIE", "").strip().strip("'\"")
if not cookie:
raise SystemExit("HTS_COOKIE is not set")

client = requests.Session()
client.headers.update(
{
"User-Agent": USER_AGENT,
"Cookie": cookie,
"Referer": f"{BASE}/missions/programming/",
"Accept-Language": "en-US,en;q=0.9",
}
)
return client


def body_text(source):
source = re.sub(r"<script.*?</script>", "", source, flags=re.S)
source = re.sub(r"<br\s*/?>", "\n", source)
source = re.sub(r"</(?:p|div|tr)>", "\n", source)
source = re.sub(r"<[^>]+>", " ", source)
source = html.unescape(source)
source = re.sub(r"[ \t]+", " ", source)
return re.sub(r"\n\s*\n+", "\n", source)


def load_wordlist(path):
table = {}
with path.open("r", encoding="utf-8", errors="replace") as fh:
for line in fh:
word = line.strip()
if word:
table.setdefault("".join(sorted(word)), word)
return table


def parse_scrambled(page_text):
marker = "List of scrambled words:"
end_marker = "Answer:"
if marker not in page_text or end_marker not in page_text:
raise ValueError("could not find the scrambled-word section")
chunk = page_text.split(marker, 1)[1].split(end_marker, 1)[0]
return [token for token in re.split(r"[\s,]+", chunk) if token]


def unscramble(words, table):
result = []
for word in words:
key = "".join(sorted(word))
candidates = [value for value in table.values() if "".join(sorted(value)) == key]
if not candidates:
result.append(word)
else:
result.append(candidates[0])
return result


def submit(client, answer):
response = client.post(
f"{LEVEL_URL}index.php",
data={"solution": answer, "submitbutton": "submit"},
headers={"Referer": LEVEL_URL},
timeout=30,
)
response.raise_for_status()
text = body_text(response.text).lower()
success_markers = (
"congratulation",
"you have completed",
"completed this",
"successfully",
"correct",
"well done",
"mission accomplished",
"complete!",
)
failure_markers = (
"wrong",
"incorrect",
"not correct",
"try again",
"failed",
"too late",
"time is up",
"sorry",
)
success = any(marker in text for marker in success_markers)
failure = any(marker in text for marker in failure_markers)
if success and not failure:
return True
if failure and not success:
return False
return None


def main():
client = session()
response = client.get(LEVEL_URL, timeout=20)
response.raise_for_status()
words = parse_scrambled(body_text(response.text))
table = load_wordlist(WORDLIST)
answer = ",".join(unscramble(words, table))
print("scrambled:", words)
print("answer :", answer)
print("verdict :", submit(client, answer))


if __name__ == "__main__":
main()

运行方式:

1
2
3
4
5
6
$ cd <hts-workspace>/challenges/hts-prog/1
$ export HTS_COOKIE='<mission-cookie>'
$ uv run --with requests python solve.py
scrambled: ['amtnar', 'plradnot', 'chyeok', 'ubeadtht', 'aremedr', 'getayaw', 'udsettn', 'n1j6o3h', 'cuatain', 'rirmemto']
answer : mantra,portland,hockey,butthead,dreamer,gateway,student,john316,nautica,mortimer
verdict : True