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;
input
和converter
这两个参数我们完全可控,意味着我们能调用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
颖奇L'Amore原创文章,转载请注明作者和文章链接
本文链接地址:https://www.gem-love.com/ctf/2494.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示