caasio-ce
challenge
caasio.js
#!/usr/local/bin/node
const readline = require("readline/promises");
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
console.log("Welcome to CaaSio: Contrived Edition!");
console.log("What \"math\" expression would you like to evaluate?");
rl.question("> ")
.then(inp => {
if (inp.length > 200) {
console.log("That's so much math; what do you think I am, a calculator???");
throw new Error();
}
const banned = new Set(".,:;(){}<>`pxu");
for (const char of inp) {
const c = char.charCodeAt(0);
if (c < 0x20 || c > 0x7e || banned.has(char)) {
console.log("I have arbitrarily deemed your input to not be math.");
throw new Error();
}
}
return inp;
})
.then(inp => console.log(Function("return " + inp)()))
.catch((e) => console.log("Bye!", e))
.finally(() => rl.close());
Dockerfile
FROM pwn.red/jail
COPY --from=node:22-bookworm-slim / /srv
COPY --chmod=755 caasio.js /srv/app/run
COPY flag.txt /srv/app/flag.txt
solution
we cannot call functions, create objects, or create anonymous functions. or can we ? (vsauce music starts)
the first thing i noticed about this challenge is that we can get arbitrary strings, even one that have blocked chars, by using octals like "console\056log\050var\051"
(this is equal to "console.log(var)"
). so, if we can get eval, we can easily win.
first off, we can call some functions/classes like Object
with new Object
because they are constructors.
but, we cannot call eval or anything so easily. to call functions with one argument, we can overwrite the hasInstance attribute (see here) of an object and then use instanceof
keyword with it
however, to do multiple statements, we can use a weird trick where setting attributes with =
isnt considered a statement, but rather, an expression (i think).
so, [a][[b][c]]
will evaluate expression a
, then expression b
, then expression c
, and return undefined
. this doesn’t raise an error because invalid array indicies return undefined instead of throwing errors
we can create an object, modify the Symbol.hasInstance to be eval, and then call eval with our string, but then how do we write the hasInstance attributes without .
? because we cannot do q=new Object
and q.hasInstance = "test"
well, object attributes can be set in two ways in js. you can either set it with obj.attr = val
or with obj["attr"] = val
so, we can do [ q=new Object ][[ q[Symbol.hasInstance] = eval ][ "string" instanceof q ]]
. now we can eval arbitrary strings.
now comes the hardest part of the challenge (imo), which is trying to escape to a shell or something.
if we try to use require
, we will get that it is undefined. so, my first attempt was to use process.create_binding
or something to get an internal module which we could use to escape. but that would be too many chars.
however, we can use process.mainModule.require
and that exists for some reason even though process.mainModule appears as below:
Module {
id: '.',
path: '/mnt/c/Users/quasar/Downloads',
exports: {},
filename: '/mnt/c/Users/quasar/Downloads/test.js',
loaded: false,
children: [],
paths: [
'/mnt/c/Users/quasar/Downloads/node_modules',
'/mnt/c/Users/quasar/node_modules',
'/mnt/c/Users/node_modules',
'/mnt/c/node_modules',
'/mnt/node_modules',
'/node_modules'
]
}
for some reason, require still exists there idk why
so you can easily just use [q=new Object][[q[Symbol["hasInstance"]]=eval]["throw process.mainModule.require('fs').readFileSync('flag.txt')" instanceof q]]
, encoding the eval’d string with octals to bypass the restrictions