Javascript Prototype Pollution
프로토타입(prototype) 개념
여기서는 프로토타입을 자세하게 설명하기보다는 Prototype Pollution을 이해하기 위한 간단한 지식만을 설명하겠습니다.
자세하게 알고 싶은 분은 하단의 링크를 참고하세요!
프로토타입(prototype) 이란?
일반적인 의미로 프로토타입이란 시제품, 견본 등과 같은 의미로 원래의 형태 또는 표준이라는 의미를 가집니다.
즉, 자바스크립트에서 프로토타입은 자신을 만들어낸 객체의 원형을 프로토타입이라고 하고 Java나 php에서 말하는 class가 바로 자바스크립트의 프로토타입과 동일한 의미일 것입니다.
프로토타입(prototype)
이해를 돕기 위해 게임에서 사용하는 다음과 같은 world 함수가 있다고 가정하겠습니다.
function world(name,joincode){
this.name=name;
this.code=joincode;
}
저희는 다음과 같이 월드1과 월드2라는 객체를 생성하고 동일한 크기의 중력을 추가하려고 합니다.
* wd1은 월드1 , wd2는 월드2 입니다 *
var wd1 = new world("admin",100);
var wd2 = new world("dori",10);
wd1.gravity = 10;
wd2.gravity = 10;
이때 월드1과 월드2 객체는 gravity라는 동일한 속성을 가지고 있습니다.
월드1과 월드2 객체에 각각 gravity라는 속성을 새로 생성하여 값을 삽입하기 때문에 서로 다른 메모리 공간을 차지하여 메모리를 2배로 사용하게 됩니다. 서로 다른 메모리 공간을 사용하기 때문에 월드1의 gravity를 수정해도
월드2의 gravity는 변하지 않습니다.
wd1.gravity = 20;
wd2.gravity;
>> 10
그러면 "wd1과 wd2가 같은 속성을 가지게 만들게 할 수 없을까?"라는 의문이 들 수 있습니다.
이런 경우는 prototype을 사용하면 됩니다. 다음 코드를 통해서 살펴보겠습니다. 우리는 size 속성을 추가하기 우해 다음과 같은 코드를 실행했습니다.
wd1.__proto__.size = "100px"
우리는 wd1에만 size라는 속성을 추가했습니다. 그러면 wd2에서 size를 검색하면 어떻게 나올까요?
wd2.size;
>> 100px
우리는 wd2에 size를 생성한 적 없었는데 wd2에 size가 들어간 것을 확인할 수 있습니다.
이 뿐만 아니라 다음과 같이 새로 생성하는 객체들에서 size가 들어있는 것을 확인할 수 있습니다.
var wd3 = new world("dummy",30);
wd3.size;
>> 100px
위와 같은 현상이 발생하는 이유는 자바스크립트 객체에서 어떤 속성을 찾기 위해서는 객체를 생성할 때
prototype까지 쭉 거슬러 올라가서 속성을 찾기 때문에 발생하는 현상입니다. __proto__를 통해 처음 생성한 world함수의 프로토타입에 접근할 수 있습니다. 따라서 우리가 이미 만들어져 있거나 또는 새로 생성한 모든 곳에서 size를 확인할 수 있던 것입니다.
프로토타입에 대해서 더 자세하게 알고 싶은 사람은 다음 글을 참고하길 바랍니다.
프로토타입 오염(prototype pollution)
prototype pollution은 말 그대로 앞서 설명한 prototype 특성을 이용하여 다른 객체를 오염시키는 공격입니다.
여러분의 이해를 돕기 위해 아래 문제를 만들었습니다. 다음 코드를 보면서 프로토타입 오염 공격을 설명하겠습니다.
( 프로토타입 특성을 이용하여 스스로 플래그를 구해보세요!! 못 풀더라도 스스로 시도해보시고 해설을 보시는 것을 추천드립니다!)
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let input = []
rl.on("line", function(line){
input = line.split(' ').map((el) => el);
rl.close();
})
let admin_world={"admin":"secret_world_join_code"}
let world={}
rl.on("close", function(){
let name = input[0];
let size = input[1];
let world_code = input[2];
if(world[name] === undefined){
console.log("your group is not exist!");
console.log("only admin can access this world");
process.exit();
}else world[name][size] = world_code;
for(var id in admin_world){
if(admin_world[id]==world_code){
console.log("hello admin");
console.log("flag{ohoh_prototype_pollution!!}");
}
}
})
우선 위 코드에 대한 간단한 설명을 드리겠습니다.
우리가 입력한 3개의 인자가 name, size, world_code에 들어갑니다. 18번줄과 22번 줄을 통해 우리가 검색한 user에 우리가 입력한 키가 존재하는지 비교하고 키에 해당하는 값이 존재하지 않으면 "your group is not exist!"를 띄워주고, 존재하면 해당 name에 size 딕셔너리를 추가하고 world_code를 넣어줍니다. 그다음 admin_world 딕셔너리를 돌면서 우리가 입력한 world_code와 일치하는 값이 존재한다면 플래그를 던져주는 문제입니다.
if(world[name] === undefined){
console.log("your group is not exist!");
console.log("only admin can access this world");
process.exit();
}else world[name][size] = world_code;
위 코드가 존재하는 22번 줄에서 prototype pollution 취약점이 발생합니다.
우리가 인자로 a b c 를 넘겨주었다고 가정해보겠습니다.
world[name] === undefined
위에서 말한 인자를 넘겨주면 world라는 딕셔너리에 a가 존재하지 않으므로 if 조건에 의해 종료됩니다.
우리는 a가 아닌 다른 값을 넣어서 해당 조건을 거짓으로 만들어줘야 하는데 world에는 어떤 값도 들어있지 않습니다.
여기선 a 대신 __proto__를 넣어주면 해당 조건을 거짓으로 만들 수 있습니다. __proto__를 넣어주게 되면 users['__proto__']가 되고 users.__proto__와 같은 의미가 됩니다. 이는 자신의 프로토타입에 접근하는 방법입니다.
그 결과 __proto__를 입력하면 world의 프로토타입에 접근하게 되므로 undefined와 비교시 거짓이 됩니다. 다음 사진은 console.log(users[name]);을 코드 중간에 삽입한 결과와 나온 결과를 undefined와 비교하는 사진입니다.
만약 우리가 인자로 다음과 같이 넘겨준다고 생각해보겠습니다.
첫번째 인자로 __proto__를 넣어주었으므로 18번째 if문을 통과하고 다음 구문을 실행합니다.
world[name][size] = world_code;
위 구문에 우리가 넣은 인자를 대입해보면 world['__proto__']['dori'] = 'dori'; 와 같이 되고 이는 world.__proto__.size = 'dori'와 같은 의미를 가집니다. 이게 서버에서 실행되게 된다면 world의 프로토타입 즉 딕셔너리에 {'dori':'best'}라는 속성이 추가됩니다. 이것뿐만 아닌 admin_world 역시 딕셔너리로 생성된 객체이므로 admin.dori 역시 best라는 값을 가진다. 다음은 중간에 console.log(admin_world[size]);를 삽입한 결과입니다.
마지막 반복문에서 id에는 admin_world의 키값을 가지고 있는데 프로토타입 오염 공격으로 admin_world에도 dori라를 키를 가지는 값이 들어있으므로 admin_world ['dori']==best라는 조건이 참이 되어 플래그를 얻을 수 있습니다.
* dori best 는 마음대로 적어둔 말이지만 이 2개 인자 대신에 어떤 값을 넣어도 상관없습니다 ㅎㅎ