WHUCTF HappyGame

Posted on 2020-05-26,6 min read

F12有源码

const express = require('express');
const path = require('path');
const opn = require('opn');
const crypto = require('crypto');
const session = require("express-session");
const bodyParser = require('body-parser');
const stringRandom = require('string-random');
const app = express();
const FUNCFLAG = '_$$ND_FUNC$$_';
const serialize_banner = '{"banner":"Congratulations! 你是目前最高分!"}';
app.use(bodyParser());
app.use(bodyParser.json());

const logs={};
var highestScore = 400;


const serialize = function(obj) {
  var outputObj = {};
  var key;
  if (typeof obj === 'string') {
    return JSON.stringify(obj);
  }
  for(key in obj) {
    if(obj.hasOwnProperty(key)) {
      if(typeof obj[key] === 'function') {
        var funcStr = obj[key].toString();
        outputObj[key] = FUNCFLAG + funcStr;
      } else {
        outputObj[key] = obj[key];
      }
    }
  }
  return JSON.stringify(outputObj);
};

const validCode = function (func_code){
  let validInput = /process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base64|"|'|\[|\+|\*/ig;
  return !validInput.test(func_code);
};

const validInput = function (input){
  // filter bad input
  let validInput = /process|child_process|main|require|exec|this|function/ig;
  ins = serialize(input);
  return !validInput.test(ins);
};


// not safe
const unserialize = function(obj) {
  obj = JSON.parse(obj);
  if (typeof obj === 'string') {
    return obj;
  }
  var key;
  for(key in obj) {
    if(typeof obj[key] === 'string') {
      if(obj[key].indexOf(FUNCFLAG) === 0) {
        var func_code=obj[key].substring(FUNCFLAG.length);
        if (validCode(func_code)){
          var d = '(' + func_code + ')';
          obj[key] = eval(d);
        }
      }
    }
  }
  return obj;
};

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


const genSanbox = function (req){
  var content = stringRandom(32);
  var result = crypto.createHash('md5').update(content).digest("hex");
  req.session.sanbox = result;
  logs[result] = new Record();
  return result;
};

const getSanbox = function (req){
  return req.session.sanbox;
};

app.use(session({
  secret: 'hahahahahaha@@@@@@',
  name : 'sessionId',
  resave: false,
  saveUninitialized: false
}));

app.use(function(req, res, next){
  if(!getSanbox(req)){
    genSanbox(req);
  }
  if(validInput(req.body)) {
    next();
  } else{
    res.status(403).send('Hacker!!!');
  }
});

const clearEnvir = function (){
  for(key in Object()){delete {}.__proto__[key]};
};


function Record(){
  this.lastScore=0;
  this.maxScore=0;
  this.lastTime=null;
}

async function record (req, res, next){
  new Promise(function (resolve, reject) {
    var sanbox = getSanbox(req);
    var record = new Record();
    var score = req.body.score;
    var oldRecord = logs[sanbox];

    console.log(score);
    clearEnvir();
    if (score.length<5){
      merge(record, {
        lastScore: score,
        maxScore: parseInt(logs[sanbox].maxScore)>parseInt(score)?logs[sanbox].maxScore:score,
        lastTime: new Date().toString()
      });
      logs[sanbox] = record;
      oldRecord.maxScore = record.maxScore;
      highestScore = highestScore > parseInt(score)? highestScore: parseInt(score);
      if((score - highestScore)<0){
        var banner = "再接再厉,马上就要赶上最高分了!";
      }else{
        // var serialize_banner = req.params.data;
        var banner = unserialize(serialize_banner).banner;
      }
    }else{
      banner="我都打不了这么高, 你小子肯定作弊了";
    }

    clearEnvir();
    res.json({
      banner: banner,
      record: oldRecord
    });
  }).catch(function(err){
    next(err)
  })
}

app.post('/record', record);
app.get('/token', function(req, res){
  token = getSanbox(req);
  res.end(token);
});

代码很长。大致就是。
有一个unserialize操作。里面有eval执行函数。
但是会经过两个check。

const validCode = function (func_code){
  let validInput = /process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base64|"|'|\[|\+|\*/ig;
  return !validInput.test(func_code);
};

const validInput = function (input){
  // filter bad input
  let validInput = /process|child_process|main|require|exec|this|function/ig;
  ins = serialize(input);
  return !validInput.test(ins);
};

并且存在merge。
大致方向就是一个原型参数污染+反序列化RCE

首先。我们看可控点

async function record (req, res, next){
  new Promise(function (resolve, reject) {
    var sanbox = getSanbox(req);
    var record = new Record();
    var score = req.body.score;
    #这里。接受了我们传递的score
    var oldRecord = logs[sanbox];

    console.log(score);
    clearEnvir();
    #这里score的length必须小于5
    if (score.length<5){
      merge(record, {
        lastScore: score,
        maxScore: parseInt(logs[sanbox].maxScore)>parseInt(score)?logs[sanbox].maxScore:score,
        lastTime: new Date().toString()
      });
    #进行了merge操作。可进行原型链污染
      logs[sanbox] = record;
      oldRecord.maxScore = record.maxScore;
      highestScore = highestScore > parseInt(score)? highestScore: parseInt(score);
      if((score - highestScore)<0){
        var banner = "再接再厉,马上就要赶上最高分了!";
      }else{
       //进入unserialize。下一步利用反序列化RCE
        var banner = unserialize(serialize_banner).banner;
      }
    }else{
      banner="我都打不了这么高, 你小子肯定作弊了";
    }

    clearEnvir();
    res.json({
      banner: banner,
      record: oldRecord
    });
  }).catch(function(err){
    next(err)
  })
}

首先。进行参数污染
这边

function Record(){
  this.lastScore=0;
  this.maxScore=0;
  this.lastTime=null;
}

lastScore已经被设置为了0。一个__proto__。得到number
第二个__proto__得到function

然后就可以执行任意代码了
但是我们还得绕过
https://xz.aliyun.com/t/7184#toc-8
这是反序列RCE的payload。但是。我们发现。这里的function被过滤了。
那么我们可以通过``.constructor.constructor得到object对象。然后执行
由于。字符都被过滤。我们可以通过16进制绕
自行忽略背景~~~~

将我们的代码16进制一下。然后执行
现在就可以任意代码执行了。用文件读取flag。然后和字符串比较。如果比较正确。就继续。不正确就直接报错。作为一个盲注使用

下一篇: [GKCTF2020]EZ三剑客-EzNode(settime溢出+沙盒逃逸)→