Author:颖奇L’Amore Blog:www.gem-love.com
签到题,签到失败,实在是丢人,四个web一个都不会~~~
题目给了源码:
const fastify = require('fastify'); |
看看题 题目是一个编码工具,支持两种编码,界面这样的:
阅读源码可知,首先是有一个flagConverter()
能得到flag
const flagConverter = (input, callback) => { |
这个函数可以通过调用converters
对象的FLAG_sessionid
来触发
converters[`FLAG_${request.session.sessionId}`] = flagConverter; |
input
和converter
这两个参数我们完全可控,意味着我们能调用converters
对象下的任何属性
const result = await new Promise((resolve, reject) => { |
当然题目不会这么简单,FLAG这四个字母是不被允许的,并且长度也有要求
if (request.body.converter.match(/[FLAG]/)) { |
我最开始绕了好久这个正则,因为JavaScript内可以用\u
\x
\???
来分别表示Unicode、hex、octal,然而这三种编码都绕不过去match()
方法的正则匹配,因为这是JavaScript内置支持的编码形式,在match()
前会被自动解码的。后面还尝试过ejs注入、调nunjucks、原型链污染等,也都无果。 切入点 因为converters[request.body.converter](request.body.input, (error, result) => { *** });
可控的,本题目的关键是利用__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) => { |
并且让第二个包时将FLAG_123123123
赋值为y1ng
函数
converters['base64'] = base64Converter; |
同样的payload再打过去,Error出的源码就是y1ng()
的源码的了
这证明上面说的是没错的
References
https://github.com/TeamUnderdawgs/CTF-Docs/blob/master/TsgCTF2020/Web/Beginners-Web.md
https://gist.github.com/0xParrot/310b71266ca2a6bfcaf26b5419c91a0d