HackThisSite - Extended Basic Mission 8

Challenge

Bill Gates wrote a Perl script that grants access to the company records; it has a security flaw that lets everyone in. Fix the flaw.

Bill Gates 写了个 Perl 脚本,用来校验访问者有没有权限读取公司记录;这段脚本存在安全缺陷,任何人都能拿到记录。要求把缺陷修掉。

关卡把整段脚本放在 <pre> 里,正文说明是 Fix the flaw for him!

1
2
3
4
5
6
7
8
#!/usr/bin/perl
chomp ( my $User = `/usr/bin/whoami` ) ;
print "Checking your access level...\n" ;
if ( $User == 'BillGates' ) {
print "Authorized! Here are the company records:\n" . `cat /home/BillGates/CompanyRecords.db` ;
die ( "Closing...\n" ) ;
}
die ( "You're not authorized!\n" ) ;

页面底部的提交表单指向 /missions/extbasic/template.php,字段是 formkey(每次加载关卡页都换值)、lvl=8pass

1
2
3
4
5
<form action="/missions/extbasic/template.php" method="post">
<input type="hidden" name="formkey" value="6vX9LfE8AxRiKYWUjM1Osc2S9GWvU7DLd9fenDQUW" />
<input type="hidden" name="lvl" value="8" />
<input type="text" name="pass" /><input type="submit" value="check" />
</form>

Solution

脚本的逻辑很短:用反引号执行 /usr/bin/whoami 拿到当前用户名存进 $User,然后只有一个 if 决定是否读取 /home/BillGates/CompanyRecords.db。要修好缺陷,改动点必然落在这个比较上;其它行(chompprintdie)都只是输出,不参与授权判断。

Perl 有两套比较运算符,不能混用:

  • ==!=<>数值比较,会先把两侧操作数转成数字;
  • eqneltgt字符串比较,逐字符对比。

脚本用的是 $User == 'BillGates'。两侧都是字符串,却在走数值比较。用 Perl 直接复现这个语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/perl
# Local reproduction of the ExtBasic 8 flaw (run against our own Perl, no
# network). A non-privileged user name stands in for the whoami output.
use strict;
use warnings;

my $User = "some_user"; # what `/usr/bin/whoami` would return

my $target = "BillGates";

print "user : $User\n";
print "target : $target\n";
print "num(user) : ", $User + 0, "\n";
print "num(target): ", $target + 0, "\n";
printf "flawed \$User == \$target -> %s\n", ($User == $target) ? "TRUE" : "FALSE";
printf "fixed \$User eq \$target -> %s\n", ($User eq $target) ? "TRUE" : "FALSE";
1
2
3
4
5
6
7
8
9
$ perl -w perl_demo.pl
Argument "some_user" isn't numeric in addition (+) at perl_demo.pl line 13.
Argument "BillGates" isn't numeric in addition (+) at perl_demo.pl line 14.
user : some_user
target : BillGates
num(user) : 0
num(target): 0
flawed $User == $target -> TRUE
fixed $User eq $target -> FALSE

Perl 把非数字开头的字符串转成数字时取前导数字部分,没有前导数字就是 0(同时抛 isn't numeric 警告)。whoami 返回的是用户名,BillGates 也是裸字符串,两者的数值转换结果都是 0

授权条件退化成 0 == 0,恒为真。任何用户名(rootnobody、普通用户都一样)都会走进 if 分支,打印记录再 die("Closing...")。真正的字符串判定应该用 eq'some_user' eq 'BillGates' 为假,只有用户名恰好是 BillGates 时才通过。这正是标题里 VisualBasic 转 Perl 的讽刺:VB 里 = 依赖类型推断,搬到 Perl 用错运算符就成了全局绕过。

只改这一处比较运算符,其余原文照抄:

1
if ( $User eq 'BillGates' ) {

提交的是这一行修正后的语句(==eq)。取页面拿 formkey、POST、再读回关卡页与 profile 的完整脚本:

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
#!/usr/bin/env python3
"""ExtBasic 8 solver/verifier.

Fetches a fresh formkey from the level page, POSTs the candidate fix to
/missions/extbasic/template.php (with the mandatory Referer), then reloads the
level page and the profile to read the completion oracle.

Cookie is read from HTS_COOKIE only; the account name never appears in output
files (the profile HTML is stored as-is but is not quoted in the writeup).

Usage:
export HTS_COOKIE='HackThisSite=...'
uv run python solve.py --answer "if ($User eq 'BillGates')" [--tag label]
"""
import argparse
import hashlib
import html
import os
import re
import sys
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/8/"
TEMPLATE = BASE + "/missions/extbasic/template.php"
OUT = os.path.dirname(os.path.abspath(__file__))
CK = os.environ.get("HTS_COOKIE", "")
if not CK:
sys.exit("HTS_COOKIE env var required (never write the cookie to a file)")

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

def formkey(page):
m = re.search(r'name="formkey"\s+value="([^"]+)"', page)
if not m:
raise RuntimeError("no formkey on level page")
return m.group(1)

def save(name, text):
path = os.path.join(OUT, name)
with open(path, "w", encoding="utf-8") as fh:
fh.write(text)
print(" saved %s sha256=%s" % (name, hashlib.sha256(text.encode()).hexdigest()))
return path

def text_of(page):
t = re.sub(r"<script.*?</script>", " ", page, flags=re.S)
t = re.sub(r"<style.*?</style>", " ", t, flags=re.S)
t = html.unescape(re.sub(r"<[^>]+>", " ", t))
return re.sub(r"\s+", " ", t)

def completion_marker(page):
return "You have already done this mission." in page

def profile_levels(profile):
row = re.search(r"<b>Extbasic:</b></font>(.*?)<br />", profile, re.S)
return re.findall(r"\((\d+)\)", row.group(1)) if row else []

def main():
ap = argparse.ArgumentParser()
ap.add_argument("--answer", required=True)
ap.add_argument("--tag", default="cand")
a = ap.parse_args()

page = get(LEVEL)
key = formkey(page)
prof_url = re.search(r'href="(/user/view/[^"]+/)"', page).group(1)
body = urllib.parse.urlencode(
{"formkey": key, "lvl": "8", "pass": a.answer}).encode()
req = urllib.request.Request(
TEMPLATE, data=body,
headers={"Cookie": CK, "User-Agent": UA,
"Content-Type": "application/x-www-form-urlencoded",
"Referer": LEVEL})
with urllib.request.urlopen(req, timeout=30) as r:
resp = r.read().decode("utf-8", "replace")
save("submit_response_%s.html" % a.tag, resp)
print(" submit status/bytes:", len(resp))

after = get(LEVEL)
done = completion_marker(after)
save("level8_after_%s.html" % a.tag, after)
print(" level completed marker:", done)

profile = get(BASE + prof_url)
save("profile_after_%s.html" % a.tag, profile)
print(" profile extbasic levels:", profile_levels(profile))

snippet = text_of(after)
i = snippet.find("Partners")
print(" level cell:", snippet[i + 8:i + 200].strip() if i > 0 else "?")
print("RESULT", "ACCEPTED" if done else "not-accepted", "|", a.answer)

if __name__ == "__main__":
main()

运行输出:

1
2
3
4
5
$ cd <hts-workspace> && uv run python challenges/hts-playit/extbasic-8/solve.py --answer "if ($User eq 'BillGates')" --tag cand1
submit status/bytes: 17610
level completed marker: True
profile extbasic levels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
RESULT ACCEPTED | if ($User eq 'BillGates')

服务端对这条修正给出下一关的 go on 链接,这是被接受的判据:

1
<br /><a href='/missions/playit/extbasic/9'><img src='http://hackthissite.org/missions/GoOn.gif' alt='go on' title='go on' border='0'/></a><!--TY tormn for the go on pic--></td>

Key points

  • Perl 里数值比较与字符串比较是两套运算符:== / eq!= / ne,拿字符串去走 == 会触发类型强制转换
  • 非数字开头的字符串转数字得到 0 并抛 isn't numeric 警告;两个不同的裸字符串用 == 比较就退化成 0 == 0
  • 修复只动比较运算符:$User == 'BillGates'$User eq 'BillGates',其它行保持不变
  • 提交必须带 Referer: /missions/playit/extbasic/8/,且 formkey 每次加载页面都变,取页面与提交要在同一次运行内完成
if ($User eq BillGates)