보안정보
전문화된 보안 관련 자료, 보안 트렌드를 엿볼 수 있는
차세대 통합보안관리 기업 이글루코퍼레이션 보안정보입니다.
Prototype-Pollution 취약점을 이용한 공격사례 분석 및 대응방안
2024.04.30
2,459
01. Prototype-Pollution 개요
Prototype-Pollution(프로토타입 오염 공격)은 JavaScript의 프로토타입 기반 상속 모델에 의해 발생하며, 공격자가 전역 객체(Object) 프로토타입에 임의의 속성과 메서드를 도입하거나 수정할 수 있는 취약점이다. 따라서 Prototype-Pollution의 개념을 이해하는 것은 JavaScript 애플리케이션 보안의 기본을 이해하는 거라고 할 수 있다.
Prototype-Pollution은 독립형 취약점으로 악용된 사례는 적지만 이를 통해 공격자가 접근할 수 없는 개체의 속성을 제어할 수 있다. 애플리케이션이 이후에 공격자가 제어하는 속성을 안전하지 않은 방식으로 처리하는 경우 이는 잠재적으로 다른 취약점과 연결될 가능성이 존재한다. 클라이언트 측에서는 주로 DOM XSS로 발현되며 서버 측에서는 원격 코드 실행(RCE)로 이어지는 결과를 초래할 수 있다.
JavaScript 애플리케이션의 복잡성이 계속 증가함에 Prototype-Pollution의 가능성은 여전히 중요한 우려 사항으로 남아 있다. JavaScript의 동적 특성과 타사 라이브러리의 광범위한 사용으로 인해 공격 표면이 증가하는 중이다. 이에 따라 이번 호에서는 prototype-pollution이 발생하는 원인과 그로 인한 애플리케이션의 영향도 및 대응방안에 대해 알아보고자 한다.
02. JavaScript에서 상속과 Prototype
JavaScript는 객체 지향 언어이다. 즉, Java나 Python과 같은 여타 프로그래밍 언어와 같이 상속 기능이 존재한다는 의미이다. ‘class’의 개념으로 상속을 구현하는 타 언어와는 다르게 ‘class’의 개념이 존재하지 않는 JavaScript는 상속의 개념을 prototype를 이용하여 구현한다. 모든 객체는 최상위 객체를 원형(prototype)으로 삼고 이를 참조하는 방식을 통해 상속과 비슷한 효과를 가지게 되는 것이다.
프로토타입의 개념을 시각화한 [그림 2]를 보면 생성자(constructor)는 원형(prototype)을 속성으로 가지게 된다. 생성자가 new 키워드와 결합하게 되면 인스턴스(instance)가 생성된다. 이 과정에서 인스턴스는 자동으로 proto 속성을 가지게 되며, proto 속성은 생성자의 프로토타입을 참조하게 된다. 즉, 모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가지게 되는 것이다.
03. Prototype chain과 특수 속성
1) 프로토타입 체인(prototype chain)
앞서 언급했듯이 JavaScript에서는 객체의 원형인 프로토타입을 이용하여 새로운 객체를 만들게 되는데 새롭게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있다. 이를 프로토타입 체인(prototype chain)이라 부르며 프로토타입 체인을 통해 객체가 어떠한 속성에 접근하고자 할 때 그 객체의 속성 뿐만 아니라 객체의 프로토타입으로 이동하며 속성을 탐색한다. 이러한 연결은 프로토타입으로 null을 가진 객체에 도달할 때까지 지속된다.
프로토타입 객체는 생성자 함수의 prototype property에 할당함으로써 정의되며 정의된 프로토타입 객체에 메서드나 property를 추가하면, 해당 생성자 함수를 통해 생성된 모든 객체가 이를 상속하게 된다. [그림 3]은 프로토타입 체인을 이해하기 위한 예제 코드이다.
[그림 3]의 예제 코드 출력 결과(25~26라인) myCat 객체에는 sound와 meow 속성이 없음에도 ‘undefined’가 출력되지 않은 것을 확인 할 수 있다. 프로토타입 체인 덕분에 myCat 객체가 sound 메서드와 meow 메서드를 모두 사용할 수 있기 때문이다.
[그림 4]는 [그림 3]의 예제 코드의 프로토타입 체인을 시각화한 것이다. 이를 통해 프로토타입 체인에 대해 좀 더 자세히 살펴보고자 한다.
[그림 4]에서 확인할 수 있듯이 myCat 객체는 Cat 생성자 함수의 인스턴스이므로 먼저 Cat.prototype을 확인한다. 만약 해당 메서드나 속성을 찾지 못하면, 다시 Cat.prototype의 프로토타입인 Animal.prototype에서 검색하게 된다. 이렇게 계속해서 부모 프로토타입으로 이동하며 탐색한다. 결과적으로 prototype chain 으로 인해 myCat 객체는 Cat 생성자 함수와 그 부모인 Animal 생성자 함수의 프로토타입 메서드를 모두 사용할 수 있게 되는 것이다.
특정 객체가 가지고 있지 않은 속성을 찾을 때까지 상위 프로토타입을 탐색하며 속성 값을 리턴한다. 최상위인 Object인 prototype object까지 도달했음에도 속성 값을 찾지 못하면 undefined를 리턴한다. 이렇게 proto속성을 통해 상위 프로토타입과 연결되어 있는 형태를 프로토타입 체인이라고 한다.
2) 특수 속성
앞서 설명한 프로토타입 체인 구조 때문에 모든 객체는 Object의 자식이라 불리고, Object prototype Object에 있는 모든 속성을 사용할 수 있다. Object 프로토타입에는 기본적으로 많은 속성이 존재하지만 ‘constructor’와 ‘prototype’ 그리고 'proto' 속성에 대해 좀 더 깊이 알아보고자 한다.
‘constructor’는 객체의 생성자 함수를 참조하는 속성이다. 생성자 함수는 객체 생성 시 객체 타입을 결정하거나 객체 초기화로 사용된다. 여기서 주목할 점은 클래스를 사용하여 객체 생성 시 ‘constructor’속성이 자동으로 설정되고, 해당 객체의 프로토타입을 가리키는 ‘prototype’ 속성이 프로토타입 체인을 사용하여 관리된다는 것이다.
해당 예제는 객체의 생성자 함수, 프로토타입 그리고 프로토타입에 정의된 메서드가 어떻게 작용하는지에 대한 기본적인 이해를 얻을 수 있다.
'proto'는 객체 클래스의 'Prototype'을 반환하는 속성이다. 'proto' 속성은 JavaScript 언어의 표준은 아니지만 NodeJS 환경에서는 지원하고 있다. proto속성이 getter/setter 속성으로 구현되었으며, 읽기 /쓰기 시 getPrototypeOf/setPrototypeOf를 호출하는 특징이 있다.
즉, 'proto' 속성에 접근하거나 값을 설정할 경우 프로토타입에 대한 명시적인 작업이 수행된다는 것이다. 또한, 'proto' 속성에 새 값을 할당하더라도 해당 속성을 가리키는 값이 아닌 상속된 프로토타입 값이 변경되게 된다. 그렇기 때문에 'Object.defineProperty'를 사용하여 'proto' 속성을 직접 정의해야 한다.
04. 사용자 조작으로 일어나는 Prototype-Pollution의 예
1) 안전하지 않은 객체의 재귀 병합
재귀 병합 함수는 주로 객체를 합병(merge)하거나 병합된 객체를 생성하는 데 사용된다. 그러나 재귀 병합 함수는 사용자로부터의 입력이나 외부 데이터를 받아들일 때 안전하지 않게 구현되면 prototype-pollution에 취약해질 가능성이 높다. 주로 사용자 입력에 의해 생성된 객체가 프로토타입을 오염시키는 경우에 발생하며, 사용자가 입력한 객체에 proto를 통해 프로토타입을 지정할 경우 지정된 객체가 병합되거나 복제될 때 prototype-pollution이 발생하는 것이다. [그림 7]은 안전하지 않은 객체의 재귀 병합을 악용한 예이다.
재귀 병합 함수 merge를 사용하여 source 객체의 property를 target 객체로 병합할 때 재귀적으로 객체의 property를 확인하고 병합한다. 5라인의 target[key] = merge({}, source[key]);가 16라인 사용자 입력의 proto 부분을 새로운 빈 객체로 병합하는데, 이로 인해 18라인 unsafeObject의 프로토타입이 오염되어 prototype-pollution이 발생된다. 결과적으로 빈 객체인 {}의 프로토타입에 isAdmin이라는 property가 추가되고 [그림 7] 코드를 실행하면 true를 반환하는 것이다.
2) 경로별 속성 정의
[그림 8]은 주어진 경로(path)에 따라 객체(object)의 property 값을 설정하는 함수이다. 그러나 사용자가 제어할 수 있는 입력에 대한 검증이 적절히 이뤄지지 않을 경우 사용자가 ‘proto’ 경로를 임의로 지정하여 prototype-pollution을 유도할 수 있다. 다음은 [그림 9]를 이용한 사용자 입력이 어떻게 악용될 수 있는지를 보여주는 코드이다.
[그림 9] 속 코드를 살펴보면 5라인 for문에서 경로를 따라가면서 객체 생성 및 기존 객체를 찾아 가는 것을 확인할 수 있다. 12라인을 보면 경로 마지막 부분에 해당하는 property에 사용자가 제공한 값(value)을 할당한다. [그림 9] 코드의 17번째 userInput 객체에는 proto라는 property가 포함되어 있다. proto property를 경로로 설정하여 theFunction(safeObject, userInput)을 호출할 경우 안전한 객체(safeObject)에 사용자 입력 값의 proto에 해당하는 property가 추가되게 된다. 결과적으로 {} 객체의 프로토타입에 isAdmin이라는 property가 추가되어 {}.isAdmin이 true를 반환하게 되는 것이다.
3) 객체 복제(clone())
객체를 복제하는 역할을 하는 clone 함수와 여러 객체를 병합하는 merge 함수가 사용자 입력을 처리 시 충분한 검증 없이 동작한다면 악의적인 사용자가 입력 값을 조작하여 prototype-pollution 공격을 수행할 수 있다. [그림 10]은 악의적으로 clone 함수를 사용한 예이다.
[그림 10] 속 예제코드를 살펴보면 10번라인에 clone 함수를 사용하여 빈 객체 {}를 복사한다. 다음 12라인에서는 악의적인 사용자가 proto를 이용하여 프로토타입을 오염시키려고 시도하는 것을 확인할 수 있다. 14라인을 보면 clone 함수를 사용하여 userInput을 안전한 객체에 복제한 것을 확인할 수 있다. 이로 인해 prototype-pollution이 발생하게 된다. unsafeObject의 프로토타입에 isAdmin이라는 property가 추가되어 해당 예제를 실행할 경우 true를 반환하게 되는 것이다.
즉, 복제 함수(clone)가 재귀적인 방식으로 빈 객체에 속성과 병합할 때 소스 객체에 프로토타입 오염이 가능한 특수한 속성(constructor, prototype, proto)이 존재한다면, 복제된 객체의 프로토타입에도 해당 속성이 추가될 수 있다는 것이다.
05. Prototype-Pollution을 이용한 공격 시나리오
1) CVE-2018-16487 취약한 환경 구성
CVE-2018-16487는 npm(node package manager)의 인기있는 라이브러리인 lodash에서 발견된 취약점이다. lodash는 문자열, 숫자, 배열, 함수 및 개체 프로그래밍을 단순화하여 프로그래머가 JavaScript 코드를 보다 효율적으로 작성하고 유지/관리하는데 유용한 도구가 포함되어 있어 많은 사람들이 사용 중이다.
CVE-2018-16487는 lodash 4.17.5 ~ 4.17.11 버전이 영향을 받으며, ‘_.merge()’ 함수가 객체 병합 시 적절한 검증을 수행하지 않아 악의적인 사용자가 예상치 못한 속성을 추가하거나 기존 속성을 변경할 수 있는 취약점이 존재한다. 공격자는 이 취약점을 활용하여 응용 프로그램 내에 개체 동작에 따라 권한 상승 또는 잠재적으로 RCE 공격을 초래할 수 있는 개체 프로토타입 속성을 추가할 수 있다.
lodash 4.17.5 ~ 4.17.11 버전 내 ‘.merge’함수를 객체 프로토타입의 속성을 추가하거나 수정할 수 있다. 사용자는 이를 악용하여 ‘{constructor: {prototype: {…}}}’ 형태의 개체를 제작하여 ‘.merge’ 함수에 전송할 경우 대상 객체인 ‘message’와 사용자가 제공하는 ‘req.body.message’ 객체가 병합하면서 prototype-pollution이 발생하게 된다.
2) CVE-2018-16487 취약점 공격 시연
1.공격을 하기 앞서 테스트로 새 메시지를 게시하는 페이로드를 전송한다.
2.canDelete 값을 true로 변경시켜 관리자만 사용할 수 있는 기능을 획득한다.
[그림 14]에서 전송한 페이로드를 살펴보면 proto 속성을 사용하여 ‘canDelete’ 속성 변경을 시도하고 있다. ‘proto’ 속성은 객체의 프로토타입을 가리키며 프로토타입 체인으로 인해 ‘message’ 객체에 ‘canDelete’ 속성을 추가할 수 있는 것이다. 새롭게 추가된 ‘canDelete’ 속성은 ‘message’ 객체에 상속되어 접근할 수 있게 된 것이다. [그림 16]를 통해 좀 더 자세히 살펴 보고자 한다.
[그림 16]에서 ‘_.merge’ 함수를 이용하여 사용자가 입력한 post body 값과 ‘message’ 객체가 병합하는 것을 확인 할 수 있다. 이는 공격자가 입력한 요청으로 ‘message.proto’속성 값이 추가되거나 변경될 수 있다는 의미이며, ‘message.proto’속성 값에 ‘{'canDelete':true}’가 입력되었다는 것이다. 해당 코드에서 const message = {}; 는 const message = new Object();와 동일하므로 ‘message’ 객체의 상위 클래스는 ‘Object’가 된다.
[그림 17]의 ‘user’ 객체 또한 Object()가 상위 클래스이므로, ‘user’ 객체와 ‘message’ 객체에 ‘canDelete’ 값이 지정되어 있지 않을 경우 상위 prototype 검색을 통한 속성 값을 반환하게 된다.
그렇기 때문에 ‘message’ 객체에 ‘canDelete’ 속성을 지정해 주었음에도 불구하고, 다른 객체인 ‘user’의 ‘canDelete’ 까지 영향을 미치게 되는 것이다. 이를 통해 ‘user.canDelete’ 속성이 존재하지 않아 ‘Object’ 객체의 canDelete 값이 상속되어 [그림 15]에서 user.canDelete 값이 true로 반환된다.
3.앞서 획득한 기능으로 관리자 자격 증명 없이 메시지를 삭제하는 페이로드를 전송한다.
취약한 ‘_.merge’ 함수에 {prototype: {…}}} 형식의 개체를 전달함으로써 최상위 객체인 ‘Object’ 객체에 canDelete 값을 true로 추가하였다. 이를 통해 ‘canDelete’ 값이 존재하지 않는 user 객체는 프로토타입체인에 의해 ‘Object’ 객체의 canDelete 속성을 상속 받아 [그림 19]과 같이 별도의 관리자 기능 없이 관리자만 사용 가능한 기능인 메시지 삭제 기능을 정상적으로 사용할 수 있다.
06. Prototype-Pollution 취약점 대응방안
Prototype-Pollution은 중요한 보안 문제이다. 기술적 복잡성을 이해하고 사전 예방적 완화 전략을 채택함으로써 개발자는 악용 위험을 크게 줄이고 애플리케이션의 전반적인 보안 상태를 강화 해야 한다.
1) 사용자 입력 값 검증
사용자의 입력 값 검증을 수행하기 위해서는 사용자로 입력 받은 후 해당 입력을 검증하는 코드를 추가하거나 수정해야 한다.
[그림 20]은 ‘Object.prototype’와 ‘proto’와 같은 특정한 키워드를 필터링하고 안전한 데이터만 허용하는 예시 코드이다. 22라인의 ‘validateInput’ 함수는 사용자 입력을 받아 지정된 키워드를 필터링하고 안전한 입력만 반환한다. [그림 17] 은 특정 키워드를 필터링하는 코드이며 각 환경을 고려하여 이 외에 데이터 타입이나 길이 등을 확인하는 추가적인 검증 규칙을 적용하는 것을 권고한다.
2) Object.freeze()
Object.freeze()메서드는 객체를 동결하여 더 이상 변경이 불가하도록 한다. 동결된 객체는 새로운 속성 추가, 존재하는 속성을 제거하는 것을 방지하여 속성의 불변성이 변경되는 것을 방지한다. 또한, 동결 객체는 프로토타입이 변경되는 것을 방지한다.
[그림 21]과 같이 ‘Object.freeze(obj)’는 전달된 객체를 동결시킨다. 다음에 ‘obj.prop = 33;’로 객체의 속성을 변경하려고 시도하여도 ‘Object.freeze()’로 인해 작업이 무시되며 속성 변경이 이루어지지 않는다.
[그림 22]는 [그림 18]를 실행한 결과 값이다. 속성 변경이 이루어지지 않고 처음 선언되었던 42가 출력되는 것을 확인 할 수 있다.
3) Object.create(null)
Object.create()메서드는 지정된 프로토타입 객체 및 속성(property)을 갖는 새 객체를 만든다. 이때 프로토타입이 null인 객체를 생성할 경우 기본 프로토타입을 상속 받지 않는다. 기본적으로 javascript에서 객체 생성 시 ‘object’를 프로토 타입으로 상속하여 메서드와 속성을 사용할 때 프로토타입 체인을 통해 해당 객체, Object.prototyp의 프로토타입 체인을 따라가게 되지만 Object.prototype(null)를 사용 할 경우 기본적인 프로토타입 체인에서 벗어나게 된다.
[그림 23]과 같이 일반 객체를 생성할 경우 기본적으로 Object.prototype을 상속하므로 toString 메서드가 존재하게 된다. [그림 24]을 실행한 결과값인 [그림 23]를 보면 toString 메서드가 존재하는 것을 확인할 수 있다.
반면에 [그림 25]은 Object.create(null)을 사용한 경우의 예시이다.
[그림 26]와 같이 Object.create(null)를 사용하여 프로토타입이 없는 객체이므로 toString 메서드가 상속되지 않아 undefined가 출력되는 것을 확인 할 수 있다.
4) npm 모듈 최신화
npm(node package manager)는 JavaScript및 Node.js 환경에서 패키지를 관리하고 공유하는 도구로 JavaScript 생태계에서 널리 사용되고 있다. npm모듈을 최신화 함으로써 Prototype-Pollution등 보안 위협 및 버그를 방지할 수 있다. npm 모듈의 최신 버전은 GitHub 레포지토리, npm 공식 웹사이트, 또는 npm 명령어를 통해 확인이 가능하다.
06. 참고자료
1. https://github.com/HoLyVieR/prototype-pollution-nsec18
2.https://github.com/Kirill89/prototype-pollution-explained
3.https://b3cl4ssy.tistory.com/212
4.https://blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/
5.https://blog.sonatype.com/cve-2018-16487-lodash-rce-prototype-pollution
6.https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
7.https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67
8.https://portswigger.net/web-security/prototype-pollution