TSG CTF 2020 Beginner's Web Writeup

Author:颖奇L’Amore Blog:www.gem-love.com


签到题,签到失败,实在是丢人,四个web一个都不会~~~

题目给了源码:

const fastify = require('fastify');
const nunjucks = require('nunjucks');
const crypto = require('crypto');


const converters = {};

const flagConverter = (input, callback) => {
const flag = '*** CENSORED ***';
callback(null, flag);
};

const base64Converter = (input, callback) => {
try {
const result = Buffer.from(input).toString('base64');
callback(null, result)
} catch (error) {
callback(error);
}
};

const scryptConverter = (input, callback) => {
crypto.scrypt(input, 'I like sugar', 64, (error, key) => {
if (error) {
callback(error);
} else {
callback(null, key.toString('hex'));
}
});
};


const app = fastify();
app.register(require('point-of-view'), {engine: {nunjucks}});
app.register(require('fastify-formbody'));
app.register(require('fastify-cookie'));
app.register(require('fastify-session'), {secret: Math.random().toString(2), cookie: {secure: false}});

app.get('/', async (request, reply) => {
reply.view('index.html', {sessionId: request.session.sessionId});
});

app.post('/', async (request, reply) => {
if (request.body.converter.match(/[FLAG]/)) {
throw new Error("Don't be evil :)");
}

if (request.body.input.length < 10) {
throw new Error('Too short :(');
}

converters['base64'] = base64Converter;
converters['scrypt'] = scryptConverter;
converters[`FLAG_${request.session.sessionId}`] = flagConverter;

const result = await new Promise((resolve, reject) => {
converters[request.body.converter](request.body.input, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});

reply.view('index.html', {
input: request.body.input,
result,
sessionId: request.session.sessionId,
});
});

app.setErrorHandler((error, request, reply) => {
reply.view('index.html', {error, sessionId: request.session.sessionId});
});

app.listen(59101, '0.0.0.0');

看看题 题目是一个编码工具,支持两种编码,界面这样的:

阅读源码可知,首先是有一个flagConverter()能得到flag

const flagConverter = (input, callback) => {
const flag = '*** CENSORED ***';
callback(null, flag);
};

这个函数可以通过调用converters对象的FLAG_sessionid来触发

converters[`FLAG_${request.session.sessionId}`] = flagConverter;

inputconverter这两个参数我们完全可控,意味着我们能调用converters对象下的任何属性

const result = await new Promise((resolve, reject) => {
converters[request.body.converter](request.body.input, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});

当然题目不会这么简单,FLAG这四个字母是不被允许的,并且长度也有要求

if (request.body.converter.match(/[FLAG]/)) {
throw new Error("Don't be evil :)");
}

if (request.body.input.length < 10) {
throw new Error('Too short :(');
}

我最开始绕了好久这个正则,因为JavaScript内可以用\u \x \???来分别表示Unicode、hex、octal,然而这三种编码都绕不过去match()方法的正则匹配,因为这是JavaScript内置支持的编码形式,在match()前会被自动解码的。后面还尝试过ejs注入、调nunjucks、原型链污染等,也都无果。 切入点 因为converters[request.body.converter](request.body.input, (error, result) => { *** });可控的,本题目的关键是利用__defineSetter__,参考

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global\_Objects/Object/\_\_defineSetter\_\_

The __defineSetter__ method binds an object’s property to a function to be called when an attempt is made to set that property.

这个方法将属性绑定到一个函数上,它接收2个参数,第一个是属性(字符串类型)第二个就是函数了。 知道了这些还远远不够,因为本题目并不是最终通过回调或其他方式来调用flagConverter()函数,而是利用reject(error)来爆出源码进而得到flag。 先来看个JS小特性 我有一个f()函数接收两个参数并输出,如果只传给他一个参数会怎么样呢? C语言等语言会直接报错,但是像JavaScript或者PHP这类语言都是可以容错的(RCTF的swoole就需要用到PHP的这个特性),如果只穿一个参数给f()那么它会被认为是第一个参数,而第二册参数则是undefined

解题 刚刚说了__defineSetter__方法会把属性分配给一个函数,那么如果request.body.converter__defineSetter__就会把request.body.input分配给(error, result) => {if (error) {reject(error);} else {resolve(result);}}这个函数(这是Lambda写法,也称为箭头函数) 而converters现在有三个属性,分别是base64 scrypt FLAG_sessionid,为了本地调试方便,我们直接假设session_id为123123123,代码写成converters[`FLAG_123123123`] = flagConverter;,因为每次重新启动脚本就得重新弄session很麻烦。 所以我们就能直接把FLAG_123123123分配给那个箭头函数,虽然这个箭头函数接收两个参数,但是根据上面刚介绍的JS的函数容错特性,FLAG_123123123会被作为第一个参数也就是error这个参数,然后会被reject(error) 但是这样打过去是什么效果呢?

可以看到converters对象内的FLAG_123123123现在虽然成了Setter,证明__defineSetter__是成功的,但是HTTP请求卡住了,没有回包了 这是因为我们只通过__defineSetter__分配了函数并没有发送回任何结果,所以Promise((resolve, reject) => {***})没有完成,于是就一直在await 但是,当我们再发过去一个包的时候,这个包就是个普通的base64编码的包就好了,第二个包会执行converters[`FLAG_123123123`] = flagConverter;赋值操作,因为刚刚的__defineSetter__的缘故,flagConverter函数作为一个值被传进了分配给FLAG_123123123的function,也就是(error, result) => {}这个箭头函数,此时的error参数就是flagConverter,然后reject(error)也就是reject(flagConverter)就通过报错得到了flagConverter的源码,当然flag也包含其中

因为只有第二个包请求成功后才会返回第一个包的结果,因此需要使用Thread,当然用burp开两个repeater也行 Reinforce 为了确定是否是第二个包的converters[`FLAG_123123123`] = flagConverter;的赋值成为了得到flag的关键,我自定义了一个y1ng()函数

const y1ng = (input, callback) => {
const y2ng = 'test';
callback(null, y2ng);
};

并且让第二个包时将FLAG_123123123赋值为y1ng函数

converters['base64'] = base64Converter;
converters['scrypt'] = scryptConverter;
if (request.body.converter != 'base64') {
converters[`FLAG_123123123`] = flagConverter;
console.log("log:"+request.body.converter);
} else {
converters[`FLAG_123123123`] = y1ng;
console.log("base64 log:"+request.body.converter);
}

同样的payload再打过去,Error出的源码就是y1ng()的源码的了

这证明上面说的是没错的


References

https://github.com/TeamUnderdawgs/CTF-Docs/blob/master/TsgCTF2020/Web/Beginners-Web.md
https://gist.github.com/0xParrot/310b71266ca2a6bfcaf26b5419c91a0d

Author: Y1ng
Link: https://www.gem-love.com/2020/07/13/tsg-ctf-2020-beginners-web-writeup/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
【腾讯云】热门云产品首单特惠秒杀,2核2G云服务器45元/年    【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!