It’s clam’s newest javascript Calculator-as-a-Service: the CaaSio Please Stop Edition! no but actually please stop I hate jsjails js isn’t a good language stop putting one in every ctf I don’t want to look at another jsjail because if I do I might vomit from how much I hate js and js quirks aren’t even cool or funny or quirky they’re just painful because why would you design a language like this ahhhhhhhhhhhhhhhhhhhhh
It’s just a JavaScript eval jail.
#!/usr/local/bin/node
// flag in ./flag.txt
const vm = require("vm");
const readline = require("readline");
const interface = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
interface.question(
"Welcome to CaaSio: Please Stop Edition! Enter your calculation:\n",
function (input) {
interface.close();
if (
input.length < 215 &&
/^[\x20-\x7e]+$/.test(input) &&
!/[.\[\]{}\s;`'"\\_<>?:]/.test(input) &&
!input.toLowerCase().includes("import")
) {
try {
const val = vm.runInNewContext(input, {});
console.log("Result:");
console.log(val);
console.log(
"See, isn't the calculator so much nicer when you're not trying to hack it?"
);
} catch (e) {
console.log("your tried");
}
} else {
console.log(
"Third time really is the charm! I've finally created an unhackable system!"
);
}
}
);
There is a meager attempt at sandboxing with node’s vm, but the documentation makes clear that that isn’t meant to be secure:
The vm module is not a security mechanism. Do not use it to run untrusted code.
We don’t need to be very creative and can just Google stuff like “vm.runInNewContext ctf” to find articles like Sandboxing NodeJS is hard, here’s why, which explain that something like the following code can escape the vm
easily.
The main intrigue of this jail lies in the spicy regex check: !/[.\[\]{}\s;`'"\\_<>?:]/.test(input)
. Before we think more about that, let’s look at what kind of code we want to run in the first place. I looked around and found it somewhat annoying to pop a shell with standard node libraries, but we can just read the file directly:
With our vm.runInNewContext
jailbreak, it would look like:
Unfortunately, we are not allowed to use… a lot of characters. The main sources of annoyance are the banning of .
and all the quotes, including '
and "
and the more modern `
; secondarily, the lack of square brackets cuts off a Plan B for attribute access. Things we do have access to include all the alphanumerics, parentheses, commas, and the arithmetic operators.
For a bit, I tried looking at other JavaScript jail writeups for inspiration, and found the very impressive /[a-z().]/, which only allows the characters in that regex. Still, the .
in that challenge is very powerful because it granted access to attributes like .length
and .constructor
. In our challenge, we don’t even have, for example, String.fromCharCode
.
Well, let’s look at what standard built-in objects and Node global objects we have access to:
eval
is obviously good.decodeURI
seems quite promising; the%
used for URI escapes is not banned by the regex.- But the most interesting built-in I was reminded of by these lists was
RegExp
, even though directly calling it is not as useful.
Of course! JavaScript supports regexp literals, which are delimited by /
, which is not banned. The only remaining annoyance is that, when you convert a regexp to a string, it still has the /
s — so if you eval
a regexp, it’s still going to have the /
s and will typically just evaluate to itself.
Fortunately, because this is JavaScript, you can add an integer to a regexp, and it converts both to a string:
What’s more, if you have code that’s a legal expression and you include 1+
and +1
on the sides, you’ll get another legal expression after it’s surrounded by 1/
and /1
:
The final exploit
Short and sweet:
This prints the flag (and some 1
s that are easy to ignore):
actf{omg_js_is_like_so_quirky_haha}
Alternate approaches
Quite a few alternative approaches were discussed in the official Discord and in Huli’s writeup:
- There are many other ways to get rid of the regex
/
s, for example by adding URI-escaped characters that turn them into harmless comments. - It’s also possible to use a snappier payload,
require('repl').start()
, which pops a Node REPL instead of a shell (after which you can pop other things from the REPL). - The most different approach that was discussed, which was the author’s intended solution, is using the
with
statement heavily as a substitute for attribute access. Roughly, inside awith(foo)
block, a bare namebar
is equivalent tofoo.bar
; soString.fromCharCode
is suddenly viable again. Thoughwith
is obscure, I actually saw it in JS Safe 2.0 from Google CTF 2018.