LitCTF-TriH-WP(MISC、CRYPTO)
LitCTF-TriH-WP(MISC、CRYPTO)
Crypto
1. lit_xor_two_story
题目分析
题目给了两段密文:
c1 = 5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31bf14ab7a0a82ccc108f8476f75c98a28
c2 = 5f70a847ce123cc153283ca710ae7f042b8490a238eb2228970fad6a2694f2985dc5557e69e5f474
同时脚本中给出第二段明文:
M2_KNOWN = b"litctf2026_xor_keystream_reuse_40bytes!!"
加密逻辑为:
c1 = m1 ^ k
c2 = m2 ^ k
这里同一段 OTP 密钥流 k 被复用了。由异或性质可得:
m1 = c1 ^ c2 ^ m2
解题脚本
c1 = bytes.fromhex(
"5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31"
"bf14ab7a0a82ccc108f8476f75c98a28"
)
c2 = bytes.fromhex(
"5f70a847ce123cc153283ca710ae7f042b8490a238eb222897"
"0fad6a2694f2985dc5557e69e5f474"
)
m2 = b"litctf2026_xor_keystream_reuse_40bytes!!"
flag = bytes(a ^ b ^ c for a, b, c in zip(c1, c2, m2))
print(flag)
Flag
litctf{otp_reuse_never_twice_same_key__}
2. lit_elgamal_handshake
题目分析
题目是 ElGamal 加密,公钥和密文如下:
p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
g = 3
y = 269130883529708333054320571854006406481346665463416017026083074488011546059928157925990665431751017523964760326934454181952822744463714981243407307134357
c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627
c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654
题目还在 debug 日志中泄露了长期私钥:
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884
ElGamal 加密形式为:
c1 = g^k mod p
c2 = m * y^k mod p
由于 y = g^x mod p,所以:
y^k = (g^k)^x = c1^x mod p
因此可以直接解密:
s = c1^x mod p
m = c2 * s^{-1} mod p
解题脚本
from Crypto.Util.number import long_to_bytes
p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627
c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884
s = pow(c1, x, p)
m = c2 * pow(s, -1, p) % p
print(long_to_bytes(m))
Flag
litctf{elgamal_leak_makes_happy_decrypt}
3. lit_rsa_neighbor
题目分析
题目给出 RSA 参数:
n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911
c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429
e = 65537
脚本:
p = getPrime(512)
q = p
for _ in range(NEXT_PRIME_STEPS):
q = int(gmpy2.next_prime(q))
n = p * q
也就是说,q 是从 p 连续取 next prime 得到的。虽然中间隔了若干个素数,但两个数仍然很接近,适合使用费马分解。
费马分解基于:
n = p * q = a^2 - b^2 = (a - b)(a + b)
当 p 和 q 接近时,a = ceil(sqrt(n)) 附近很快就能找到平方差。
解题脚本
from math import isqrt
from Crypto.Util.number import long_to_bytes
n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911
c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429
e = 65537
a = isqrt(n)
if a * a < n:
a += 1
while True:
b2 = a * a - n
b = isqrt(b2)
if b * b == b2:
break
a += 1
p = a - b
q = a + b
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
Flag
litctf{rsa_fermat_finds_close_primes}
4. lit_tiny_key_aes
题目分析
题目使用 AES-128-ECB,密钥前 13 字节固定:
KEY_PREFIX = b"LitCTF2026!!!"
AES-128 密钥总长度为 16 字节,因此未知部分只有 3 字节:
key = KEY_PREFIX + UNKNOWN_KEY_SUFFIX
密文为:
c = b"\x0c\xdb'`\xc91\xf7\x05\x91+\x0fM\xed\xbc\x9b\xf1\xd8D\xcd\xfd\x0c\xb9\xb6\xb2J<\x86\x19\x06K\xb3\xa2\xa4\x18\x87<v\xac\x1bbu#\xaa\xb5I\x7f\xd8\xd3"
未知密钥空间为:
2^24 = 16777216
这个范围可以直接离线爆破。解密后检查明文是否以 litctf 开头,并验证 PKCS#7 padding。
解题脚本
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
c = bytes.fromhex(
"0cdb2760c931f705912b0f4dedbc9bf1"
"d844cdfd0cb9b6b24a3c8619064bb3a2"
"a418873c76ac1b627523aab5497fd8d3"
)
prefix = b"LitCTF2026!!!"
for i in range(1 << 24):
suffix = i.to_bytes(3, "big")
key = prefix + suffix
pt = AES.new(key, AES.MODE_ECB).decrypt(c)
if pt.startswith(b"litctf"):
try:
print("suffix =", suffix)
print(unpad(pt, 16))
break
except ValueError:
pass
爆破得到:
suffix = b"7\xa2\x01"
Flag
litctf{aes_tiny_brut3_for_the_win!}
Misc
lit_lsb_base64_clean
关于lsb隐写
把图片拖入随波逐流,发现疑似base64编码的东西

解码得到

LitCTF{lsb_1s_fun_w1th_b4s3_64}
lit_rush_qr_clean
叫豆包补全二维码定位符

扫码得到flag
lit_sstv_clean
知道这是sstv,直接叫ai写脚本


lit_welcome_clean Writeup
1. 基础检查
- 文件:
welcome.png,5148 bytes,900×500 RGB - PNG 结构正常,仅 IHDR / IDAT / IEND 三个块
- 无隐藏文本块(tEXt/iTXt/zTXt)
2. 像素分析
from PIL import Image
import numpy as np
img = Image.open('welcome.png')
pixels = np.array(img)
# 统计唯一颜色
unique = np.unique(pixels.reshape(-1, 3), axis=0)
# 结果只有两个颜色:
# RGB(254, 255, 255) → #feffff (7318像素)
# RGB(255, 255, 255) → #ffffff (442682像素)
整张图只有两个颜色,差异仅在于 R 通道的 LSB(254 vs 255),肉眼完全无法分辨。
3. 提取二值图
# R=254 → 黑, R=255 → 白
binary = (pixels[:,:,0] == 254).astype(np.uint8) * 255
Image.fromarray(binary, 'L').save('reveal.png')
4. 结果
生成的 reveal.png 直接以 ASCII 艺术字显示了 flag。

lit_pyjail_reader
题目分析
连接后服务器运行一段 Python 脚本,核心逻辑如下:
- 验证码:生成 8 位随机大写字母,要求输入其反转串
- 第一次读文件:提示读取
/app/where_is_flag.txt(包含 flag 真实路径) - 第二次读文件:读取上一步获得的路径,拿到 flag
源码关键部分
def safe_read(path: str) -> str:
p = path.strip()
if not p or p.startswith("-") or "\x00" in p:
raise ValueError("invalid path")
with open(p, "r", errors="replace") as f:
return f.read(MAX_FILE)
校验逻辑只阻止了空路径、以 - 开头、含空字节的路径。没有路径遍历限制,也没有 eval/exec。题目描述明确写了"无 RCE",所以按照提示读文件即可。
解题步骤
- 连接并完成验证码
服务端发送:
Please enter the reverse of 'FEXCESXB' to continue:
提取单引号内字符串并反转:FEXCESXB → BXSECXEF,发送回去。
- 第一步读文件 — 获取 flag 路径
收到提示后发送 /app/where_is_flag.txt,服务端返回:
--- begin ---
/flag
--- end ---
File path (2/2):
提取得到 flag 路径为 /flag。
- 第二步读文件 — 获取 flag
发送 /flag,服务端返回 flag。
解题脚本
import re
import socket
HOST = "challenge.cyclens.tech"
PORT = 32690
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(30)
sock.connect((HOST, PORT))
# ---- 验证码 ----
data = b""
while b": " not in data:
data += sock.recv(1024)
prompt = data.decode(errors="replace")
m = re.search(r"'([A-Z]+)'", prompt)
challenge = m.group(1)
answer = challenge[::-1]
sock.sendall((answer + "\n").encode())
# ---- 等待 File path (1/2) 提示 ----
data = b""
while b"File path (1/2): " not in data:
data += sock.recv(1024)
# ---- Step 1: 读 flag 路径 ----
sock.sendall(b"/app/where_is_flag.txt\n")
data = b""
while b"File path (2/2): " not in data:
data += sock.recv(1024)
resp = data.decode(errors="replace")
begin = resp.find("--- begin ---")
end = resp.find("--- end ---")
flag_path = resp[begin + len("--- begin ---"):end].strip()
# ---- Step 2: 读 flag ----
sock.sendall((flag_path + "\n").encode())
data = b""
while True:
try:
chunk = sock.recv(1024)
if not chunk:
break
data += chunk
except socket.timeout:
break
flag = data.decode(errors="replace").strip()
print(f"Flag: {flag}")
sock.close()
Flag
flag{hxj1mkye-ilpb-4hg-84mo-ndfb4lfc2lshl}
lit_pyjail_unicode
沙箱逻辑
```
核心过滤:正则匹配 ASCII 关键字
BANNED = re.compile(
r"\bimport\b|\bexec\b|\beval\b|\bopen\b|\bcompile\b|\bglobals\b|\blocals\b|__|"
r"\bgetattr\b|\bsetattr\b|\bdelattr\b|\bvars\b|\bbreakpoint\b|\binput\b|"
r"\bsubprocess\b|\bpty\b|os.|sys.|\bposix\b",
re.IGNORECASE,
)
def banned(raw: str) -> bool:
if "\u" in raw or "\U" in raw or "\x" in raw:
return True
return BANNED.search(raw) is not None
三道防线:
- 禁止
\u、\U、\x转义序列(挡住 Unicode escape) - 正则
\b...\b词边界匹配 ASCII 关键字 - 检查对象是用户发送的原始源码字符串
执行路径
out = eval(line, {"__builtins__": __builtins__})
eval() 满权限,__builtins__ 全给。过滤本身不拦截具体操作,只拦截源码中的 ASCII 关键字。
矛盾点
| 阶段 | 处理对象 | 编码 |
|---|---|---|
banned() 检查 |
用户原始字节流 | 只看 ASCII |
eval() 编译 |
源码字符串 | NFKC 归一化后编译 |
关键知识:Python 3 在编译标识符前会做 NFKC 归一化。全角字母(Fullwidth Latin,U+FF01–U+FF5E)归一化后折叠为对应 ASCII 字母。
Unicode 等价表
| 全角字符 | 码位 | 归一化为 |
|---|---|---|
| o | U+FF4F | o |
| p | U+FF50 | p |
| e | U+FF45 | e |
| n | U+FF4E | n |
| r | U+FF52 | r |
| a | U+FF41 | a |
| d | U+FF44 | d |
Payload
open('/flag').read()
- 正则看到的是全角字符串
open,不是open→ 不触发\bopen\b \u等转义也未出现在源码中 → 通过第二道防线eval()编译时 NFKC 归一化:open→open,read→read→ 正常执行
连接
echo "open('/flag').read()" | nc challenge.cyclens.tech 31022
结果
flag{xmaxymff-6by8-4qi-8gy0-4kk1zuzy2bcda}
浙公网安备 33010602011771号