본문 바로가기

Web Security/Technology

Prototype Pollution

Object oriented Language : JavaScript

JavaScript는 Java, Python과 같이 객체지향언어지만, 클래스(class)라는 개념이 없습니다. 대신에 기존의 객체를 복사하여 객체를 생성하는 프로토타입(prototype)을 제공합니다. 따라서, JavaScript는 프로토타입 기반의 언어입니다.

Prototype

JavaScript의 모든 객체는 최소한 하나 이상의 다른 객체로부터 속성(property)과 메서드(method)를 상속(inheritanc)을 받으며, 상속되는 정보를 제공하는 객체를 프로토타입(prototype)이라고 합니다. [1] 단, Object.prototype 객체는 전역 범위를 가진 Root Prototype로 어떠한 프로토타입(prototype)도 가지지 않으며, 아무런 속성(Property)도 상속받지 않습니다.   

Prototype Pollution

Prototype Pollution 공격은 2018년 Olivier Arteau에 의해 논문[2]으로 발표되었으며, 지금까지도 관련 CVE들이 계속해서 나오고 있습니다. 

 

앞에서 언급했듯, 모든 객체는 Object.prototype 객체를 프로토타입으로 상속받습니다. Prototype Pollution 공격은 이러한 점을 활용하여 JavaScript, Node.js와 같은 prototype-based language에 영향을 주는 위험한 취약점입니다. Prototype Pollution이 발생하면, Denial of Service (DoS), privilege escalation, Remote Code Execution (RCE) 같은 공격으로 이어질 수 있습니다. [3]

 

https://payatu.com/blog/prototype-pollution/

 

공격자는 런타임(Runtime)에서 객체의 루트 프로토타입(object's root prototype)에 속성(properties)을 주입하고 이후 코드 가젯의 실행시킬 수 있습니다. 즉, 공격자가 루트 프로토타입(root prototype)을 변조한다면, 런타임에서 여러 객체들 또한 변조됩니다. 

const o1 = {}; 
const o2 = new Object(); 
o1.__proto__.x = 42; 
console.log(o2.x); // 42

예를 들어, 두 변수가 객체를 생성하였다고 가정해 봅시다. o1과 o2 객체 모두 Object.prototype으로부터 상속되어 o1 객체의 프로토타입에 x 속성을 새로 추가하면 o2 객체 또한 영향을 미치게 됩니다. 

Prototype pollution example

function entryPoint (arg1, arg2, arg3) { 
    const obj = {}; 
    const p = obj[arg1]; 
    p[arg2] = arg3; 
    return p ; 
}

공격자가 arg1에 __proto__을 넘기면, 변수 p는 Object.prototype이 저장되어 arg2와 arg3를 활용하여 속성(property)에 값을 쓸 수 있게 됩니다. 예를 들어, 공격자가 entryPoint("__proto__", "toString", 1);을 실행시키면, Objetc.prototype.toString = 1이 되어 toString() 호출 시 Crash가 발생하게 됩니다. 

Gadget example

const { execSync } = require("child_process"); 
function gadget(args, options) { 
   const cmd = options.cmd || "cmd.exe /k"; 
   return execSync('${cmd} ${args}'); 
} 
const args = ...; 
gadget(args, {});

위 예제는 options.cmd가 undefined이기 때문에 cmd.exe 프로그램을 실행하는 코드입니다. 하지만, 공격자는 options gadget을 활용하여 다른 프로그램을 실행시킬 수 있습니다. 

 

즉, 공격자는 entryPoint("__proto__", "cmd", "calc.exe&"); 코드를 실행시켜, options이 가진 프로토타입의 cmd 속성(property)을 calc.exe&로 변조하여 공격자가 원하는 명령을 실행시킬 수 있게 됩니다. 

Property Injection

function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}

// Function vulnerable to 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;
  }
}

const obj1 = {};
setValue(obj1, "__proto__.polluted", 1);
const obj2 = {};
console.log(obj2.polluted); // 1

obj1의 프로토타입인 Object.prototype 객체에 접근하여 polluted 속성(property) 값을 1로 설정합니다. 이후, obj2 객체를 새로 생성하여 obj2.polluted 값을 출력해 보면 1이 나오게 됩니다. 

 

그 이유는 Object.prototype.polluted 값이 1로 설정되어 obj2 객체 생성 시 Object.prototype으로부터 상속이 이루어져 obj.polluted 값이 1이 됩니다. 

Merge Objects [4]

function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}

// vulnerable merge function
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;
}
 
function clone(obj) {
  return merge({}, obj);
}
 
const express = require('express');
const app = express();
app.use(express.json());
app.post('/', (req, res) => {
  const obj = clone(req.body);
  const obj2 = {};
  // obj2 status pollution
  const status = obj2.status ? obj2.status: 'NG';
  res.send(status)
});
app.listen(1234);

JSON 형태로 POST Request를 받아 clone() 함수를 통해 객체를 복사하는 코드입니다. 

 

const http = require('http');
const client = http.request({
  host: 'localhost',
  port: 1234,
  method: 'POST'
}, (res) => {
  res.on('data', (chunk) => {
    console.log(chunk.toString());
  });
});
const data = '{"__proto__":{"status":"polluted"}}';
client.setHeader('content-type', 'application/json');
client.end(data);

공격자가 merge 함수에서 prototype pollution을 발생시켜 Object.prototype.status 값을 polluted로 설정하여 obj2 객체 생성 시 status 속성 값을 상속받아 status 값을 polluted로 변조하는 Payload입니다.

 

Prototype Pollution Prevention [5]

1. Keyword filter

merge() function 호출 전에, key 값에 __proto__ 키워드가 포함되어 있는지 확인하여 필터링 하는 방식입니다.

2. Object freeze

Object.freeze(Object.prototype);

Object.freeze()를 사용하여 객체가 가진 값을 수정할 수 없으며, 속성 또한 추가할 수 없도록 막는 방식입니다. (Object.seal() 함수는 존재하는 객체 값 수정은 가능하며, 속성 추가가 불가능합니다.) 

3. Object create

let myObject = Object.create(null);
Object.getPrototypeOf(myObject);    // null

모든 객체는 Object.prototype으로부터 상속받음으로써 문제가 발생합니다. 이를 해결하기 위해, Object.create()를 사용하여 prototype이 null인 객체를 생성하여 상속으로 인한 값 변조를 막는 방식입니다. 

4. Use Map function

Object.prototype.evil = 'polluted';
let options = new Map();
options.set('transport_url', 'https://normal-website.com');

options.evil;                    // 'polluted'
options.get('evil');             // undefined
options.get('transport_url');    // 'https://normal-website.com'

객체의 옵션을 정의할 때, Map() 객체를 생성 후 get()을 사용하면 이를 막을 수 있습니다. Map은 프로토타입의 속성(property) 값이 변조되면 이를 상속받긴 하지만, get() 함수는 오직 Map에 정의된 속성(property) 값만 반환하기 때문에 변조된 속성 값이 undefined로 식별됩니다. 

참고 자료

[1] http://www.tcpschool.com/javascript/js_object_prototype

[2] https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf

[3] https://www.usenix.org/system/files/sec23summer_432-shcherbakov-prepub.pdf

[4] https://book.hacktricks.xyz/pentesting-web/deserialization/nodejs-proto-prototype-pollution/prototype-pollution-to-rce

[5] https://portswigger.net/web-security/prototype-pollution/preventing

'Web Security > Technology' 카테고리의 다른 글

[CVE-2017-5941] node-serialize  (2) 2023.08.11
Cheat Sheet - SQL Injection  (2) 2023.06.19