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

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/ctf/2494.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

分类: CTF

颖奇L'Amore

Most of the time is also called Y1ng. Cisco Certified Internetwork Expert - Routing and Switching. CTF player for team r3kapig. Forcus on Web Security. Islamic Scholar. Be good at sleeping and fishing in troubled waters.

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

在此处输入验证码 : *

Reload Image