HackThisSite - Extended Basic Mission 09

Challenge

Captain Kirk has coded this Perl script for all his fellow-captains to automate their logging. This way they don't have to record their logs on tape, but they can type them in and archive them. But this log only seems to log one log?! It automatically deletes all previous logs! Fix the script for him, so they can keep their logs again! Captain Kirk 给同僚写了一个自动记日志的 Perl 脚本,但每次只留下一条日志, 之前的全被删掉;把它修好,让日志能留存下来。

关卡页给出完整脚本:

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
#!/usr/bin/perl
# Captain Kirk has coded this Perl script for all his fellow-captains
# to automate their logging.
# This way they don't have to record their logs on tape, but they can type them in
# and archive them. But this log only seems to log one log?!
# It automatically deletes all previous logs! Fix the script for him,
# so they can keep their logs again!
print '> Hello Captain ' . $ENV{'USER'} . '.' . "\n";
open(STARTREKLOG, '>/var/log/startrek');
print '> Please enter your log data here, end with a "." on a single line.' . "\n";
my $LogText;
print '> ';
while (<STDIN>) {
unless ($_ ne '.' . "\n") {
last;
}
$LogText .= $_;
print '> ';
}
print '> Log is being saved to /var/log/startrek' . "\n";
$DateTime = localtime();
print STARTREKLOG ' -- START OF LOG -- ' . "\n";
print STARTREKLOG 'Date/Time: ' . $DateTime . "\n";
print STARTREKLOG 'Log : ' . $LogText;
print STARTREKLOG ' -- END OF LOG -- ' . "\n";
die('> Log saved! Now exiting.' . "\n");

页面只有一处输入:隐藏的 formkey、隐藏的 lvl,以及文本框 pass。底部另有全局提醒 All missions are case sensitive. I tried to keep them lowercase however.。答案大小写敏感。

Solution

脚本把交互内容累加进 $LogText,最后用四条 print STARTREKLOG 落盘:

1
2
3
4
print STARTREKLOG ' -- START OF LOG -- ' . "\n";
print STARTREKLOG 'Date/Time: ' . $DateTime . "\n";
print STARTREKLOG 'Log : ' . $LogText;
print STARTREKLOG ' -- END OF LOG -- ' . "\n";

写入语句本身没有问题,本次要记的内容也完整。决定上一次的日志还在不在的是更早的那一行:

1
open(STARTREKLOG, '>/var/log/startrek');

Perl 的两参数 open 把模式写在文件名前面:< 读、> 写、>> 追加。>截断写:打开时先把文件长度清零,文件指针回到开头,于是每次运行都从空文件开始,上一次的内容在这次运行的第一条 print 之前就没了。题面说的 It automatically deletes all previous logs 正是这个行为。

把模式从 > 改成 >>

1
open(STARTREKLOG, '>>/var/log/startrek');

>> 打开时把文件指针定位到末尾(文件不存在则创建),后续的 print STARTREKLOG 只在尾部追加,旧日志原样保留。脚本其余部分一行都不用动:while 循环、$DateTime = localtime()die 结尾都保持原样,所以这是最小修复。

关卡页只有一个文本框,看不出比对粒度。实测只提交 >> 会被静默拒绝:模板页不返回任何错误文案,响应正文就是关卡页本身(与提交前逐行对比,差异只有 onion-location、每小时格言、logout nonce、formkey 和页脚时间这些每请求噪声),翻牌与否只能从个人资料徽章看出来。按题面 Fix the script 的粒度提交修正后的整行才会通过:

1
open(STARTREKLOG, '>>/var/log/startrek');

空格照抄清单:文件句柄后是 ,(逗号加一个空格),) 前没有空格。整行同样大小写敏感,openSTARTREKLOG 保持原样。

formkey 每次加载关卡页都会重新生成,取页面和 POST 必须在同一次运行里完成;Referer 必须指向该关卡页,否则模板页不计分。cookie 只从环境变量读入,不落盘。

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
import argparse
import hashlib
import os
import re
import urllib.parse
import urllib.request

UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36")
BASE = "https://www.hackthissite.org"
LEVEL = BASE + "/missions/playit/extbasic/9/"
TEMPLATE = BASE + "/missions/extbasic/template.php"
PROFILE = BASE + "/user/view/{user}/"
COOKIE = os.environ.get("HTS_COOKIE", "")
USER = os.environ.get("HTS_USER", "")

def fetch(url, referer=None):
headers = {"Cookie": COOKIE, "User-Agent": UA}
if referer:
headers["Referer"] = referer
req = urllib.request.Request(url, headers=headers)
return urllib.request.urlopen(req, timeout=30).read().decode("utf-8", "replace")

def formkey_of(page):
match = re.search(r'name="formkey"\s+value="([^"]+)"', page)
if not match:
raise SystemExit("formkey not found (session expired?)")
return match.group(1)

def profile_extbasic():
if not USER:
return []
page = fetch(PROFILE.format(user=USER))
match = re.search("<b>Extbasic:</b></font>(.*?)<br />", page, re.S)
return re.findall(r"\((\d+)\)", match.group(1)) if match else []

def main():
ap = argparse.ArgumentParser()
ap.add_argument("--answer", required=True)
args = ap.parse_args()
if not COOKIE:
raise SystemExit("HTS_COOKIE env var required")

page = fetch(LEVEL)
payload = urllib.parse.urlencode(
{"formkey": formkey_of(page), "lvl": "9", "pass": args.answer}).encode()
req = urllib.request.Request(
TEMPLATE, data=payload,
headers={"Cookie": COOKIE, "User-Agent": UA,
"Content-Type": "application/x-www-form-urlencoded",
"Referer": LEVEL})
resp = urllib.request.urlopen(req, timeout=30).read().decode("utf-8", "replace")

print("submit response bytes:", len(resp),
"sha256:", hashlib.sha256(resp.encode()).hexdigest()[:16])
print("go-on link:", "/missions/playit/extbasic/10" in resp)
print("profile Extbasic:", profile_extbasic())

if __name__ == "__main__":
main()

调用方式:

1
2
3
4
5
$ export HTS_COOKIE='HackThisSite=<value>'
$ uv run python submit_and_verify.py --answer "open(STARTREKLOG, '>>/var/log/startrek');"
submit response bytes: 21677 sha256: 57d1468b08b86c32
go-on link: True
profile Extbasic: ['1', '2', '3', '4', '5', '6', '7', '9']

判完成看 Extbasic: 徽章里出现 (9)。首次成功提交的响应页在任务单元末尾多出一条通往下一关的 go on 图片链接,可作为同一进程内的即时凭据:

1
<br /><a href='/missions/playit/extbasic/10'><img src='http://hackthissite.org/missions/GoOn.gif' alt='go on' title='go on' border='0'/></a>

Key points

  • > 写模式先截断再写>> 才是追加;日志只剩最后一条这类症状先查 open 的模式字符
  • 两参数 open(FH, '>/path') 把模式写在文件名前缀里,>>> 只差一个字符,语义完全不同
  • 只改模式不影响其余 print FH 语句,是最小且可控的修复
  • 答案按整行比对、大小写敏感:单填 >> 会被静默拒绝,模板页不给任何错误文案;判完成只能靠 profile 徽章,不能靠提交响应的文字
open(STARTREKLOG, >>/var/log/startrek);