상상해보세요: 마음에 드는 온라인 쇼핑몰을 탐색하고 있습니다. 반드시 필요한 상품들을 장바구니에 담는 중에 갑자기 해커가 쇼핑 파티를 망치기로 결정합니다. 그러나 신용카드 정보를 훔치거나 주문에 천 개의 고무오리를 추가하는 대신, 그들은 당신의 쇼핑 카트의 프로토타입을 오염시킨다고 합니다! 이상한 과학 소설 플롯 같죠? 그러나, 프로토타입 오염의 세계로 오신 것을 환영합니다. 해커들이 JavaScript 객체를 자신들의 개인 놀이터로 바꿀 수 있는 곳입니다. 이 블로그 글에서는 취약한 전자 상거래 사이트를 사용하여 프로토타입 오염에 대해 자세히 살펴볼 예정입니다. 그러면, 해커 후드를 쓰고 간단한 쇼핑 카트가 해커의 낙원으로 변하는 과정을 탐험해보세요!
참고: JS를 해킹하는 데에 숙련되신 후에, 저희 레포지토리에서 실력을 테스트하고 별을 주시면 어떨까요?
🌟 Github에서 별 주기! 🌟
하지만 해킹을 하기 전에 자바스크립트 객체와 프로토타입에 대해 간단히 상기시켜 드리겠습니다.
자바스크립트에서 프로토타입이란
자바스크립트에서 거의 모든 것은 객체이며, 모든 객체에는 프로토타입이라는 내장 속성이 있습니다. 프로토타입 자체는 객체이며, 속성 및 메소드의 후퇴 소스로 작동합니다. 이것은 즉, 자바스크립트 런타임이 객체에서 속성을 찾지 못할 때 해당 속성을 객체의 프로토타입에서 확인한다는 것을 의미합니다. 여러 객체 간에 공유 동작이 필요한 경우, 모든 객체에서 해당 동작을 정의하는 대신 생성자 함수의 프로토타입에 그 동작을 정의합니다. 다음은 예시입니다:
function Product(name, price) {
this.name = name;
this.price = price;
}
Product.prototype.display = function () {
console.log(`${this.name} -> ${this.price}`);
};
let product_one = new Product("헤드폰", 1000);
let product_two = new Product("마이크", 400);
product_one.display();
product_two.display();
위의 예제에서 product_one과 product_two 객체는 생성자 함수 Product에서 파생되었습니다. 객체들에 display() 함수를 호출하면 JavaScript runtime은 먼저 객체가 display 메서드를 포함하고 있는지 확인합니다. 객체 자체에 없다면 runtime은 prototype(proto) 객체에서 이 메서드를 찾습니다. 객체의 proto 속성은 그 객체를 함수 생성자의 프로토타입 속성에 연결합니다. 따라서 Product.prototype === product_one.__proto__입니다.
이것은 또한 제품을 기록하고 그들의 프로토타입 객체를 확인하여 확인할 수 있습니다.
우리는 object product_one 자체에 display라는 메서드가 없지만 그 프로토타입에는 있음을 볼 수 있습니다. 또한 product_one의 프로토타입 안에 또 다른 프로토타입이 있음을 알 수 있습니다. 여기서 약간 흥미로운 것이 발생하며 이 기능이 잠재적인 약점으로 이어지는 문을 열게 됩니다. 다음으로 이를 자세히 살펴보겠습니다!
프로토타입 체이닝과 프로토타입 오염
자바스크립트에서는 객체를 사용하여 상속을 구현합니다. 각 객체는 자체의 프로토타입 객체에 대한 내부 링크가 있으며 해당 프로토타입도 자체의 프로토타입을 갖고 있습니다. 이러한 과정이 계속되어 null을 프로토타입으로 갖는 객체에 도달합니다.
여기서 보이는 중첩된 프로토타입은 사실 대부분의 JavaScript 객체에 대한 프로토타입 체인의 맨 꼭대기에 있는 Object의 프로토타입입니다. 이는 product_one이나 해당 즉시 프로토타입에서 속성을 찾을 수 없을 경우, JavaScript가 체인을 따라 계속 검색하며 결국 Object.prototype에 도달하게 됨을 의미합니다.
예를 들어, product_one.hasOwnProperty("name")을 실행할 때 JavaScript는 이 조회 체인을 따릅니다:
- 먼저, product_one 객체 자체에서 hasOwnProperty를 찾습니다. 거기에서 찾을 수 없습니다.
- 다음으로, product_one.__proto__를 확인합니다. 이것은 Product.prototype를 가리킵니다. 여기에 hasOwnProperty 메서드가 정의되어 있지 않습니다.
- 그런 다음, 프로토타입 체인을 올라가 Product.prototype.__proto__로 이동합니다. 이것은 Object.prototype와 동일합니다. 여기에서 드디어 hasOwnProperty 메서드를 찾습니다.
따라서 Object.prototype의 동작을 어떤 방식으로든 조작한다면, Object.prototype과 연결된 모든 객체의 동작을 제어할 수 있게 됩니다. 이를 '프로토타입 오염(Prototype Pollution)'이라고 합니다. 이를 Product의 이전 예제로 살펴보겠습니다.
이 코드는 프로토타입 오염의 전형적인 예입니다. 첫 번째 줄은 Object.prototype에 새로운 속성 new_property를 추가하여 프로토타입 체인을 수정합니다. 높은 수준의 이 프로토타입에 new_property를 설정함으로써, 변경 사항은 Object.prototype을 상속하는 모든 객체에 영향을 미칩니다. 코드의 두 번째 부분은 새로운 빈 객체 obj를 생성하고, 그 객체에서 new_property에 액세스하려고 시도합니다. obj에 이 속성이 직접 정의되지 않았지만, Object.prototype에서 오염된 속성을 상속했기 때문에 "polluted"가 여전히 출력됩니다. 이는 프로토타입 오염이 어떻게 예상치 못한 객체에 영향을 미치고, 응용 프로그램에서 보안 취약점이나 의도치 않은 동작으로 이어질 수 있는지를 보여줍니다.
좋아요, 이론은 여기까지. 해킹을 시작해봅시다!
우리가 좋아하는 전자 상거래 사이트에서 우리에게 100% 할인 특별 혜택이 있어요🤯. 하지만 불행하게도 우리는 쿠폰 코드를 가지고 있지 않아요🥹. 만약 그 코드를 어떻게든 획득할 수 있다면 얼마나 좋을까요? 어쩌면 그 코드가 보이지 않게 숨겨져 있을지도 모르겠어요. 웹 사이트의 소스에 하드코딩되어 있을지도 몰라요.
방법 1: 단순한 방법
신뢰할 만한 개발 도구를 브라우저에서 열고 페이지의 코드를 검사하기 위해 확인해봅니다. 스크립트 섹션으로 이동하여 쿠폰 코드를 찾을 수 있는지 확인해봅니다.
DISCOUNT_COUPON_HASH과 hashValue 해싱 함수가 있는 것을 볼 수 있습니다. 더 내려가보면 applyCoupon 함수를 찾을 수 있습니다. 이 함수는 할인 코드 텍스트 상자의 값을 가져와 hashValue 함수를 사용하여 해싱한 다음 DISCOUNT_COUPON_HASH와 비교합니다. 유감스럽게도 해싱 함수의 특성상 해시 값은 되돌릴 수 없습니다. 그러므로 DISCOUNT_COUPON_HASH로 해싱된 쿠폰 코드의 값을 어떤 방법으로도 얻을 수 없습니다.
과거의 접근 방식이 성과를 거두지 못했으니 이 사이트를 더 탐색해봅시다.
접근 방식 2: proto 쿼리 매개변수를 사용한 악성 URL 작성하기
가게에는 카트 상태를 URL에 저장하는 매우 편리한 기능이 하나 더 있습니다. 이렇게하면 카트를 다른 사람과 공유하거나 나중에 돌아와서 카트를 확인할 수 있습니다. 일반적으로 프로토 타입 오염 공격은 악성 URL을 조작하여 쿼리 매개변수가 파싱되는 방식을 이용합니다. Object.Prototype을 오염시키기 위한 악성 URL을 작성해 보겠습니다.
https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1}&__proto__.hack=hacked
https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1}&__proto__[hack]=hacked
위의 URL을 사용하여 전역 객체의 프로토 타입에 속성 hack:hacked
를 삽입하려고 시도합니다. 이렇게 하면 카트를 복구하는 데 사용되는 URL 파싱이 입력을 정리하지 않는다는 가정입니다. 그러나 위의 URL은 예상대로 작동하지 않습니다.
더 자세히 코드를 살펴봐야 URL 구문 분석이 어떻게 이루어지는지 이해할 수 있습니다.
코드 더 조사하기
코드를 읽어보면 페이지 로드 시 URL에서 장바구니를 복원하는 loadCartFromURL 함수를 찾을 수 있습니다.
Query parameters로부터 URLSearchParams 객체를 만들고 cart
쿼리 매개변수를 가져와 JSON.parse를 사용해 json으로 구문 분석합니다. 객체가 json으로 구문 분석된 후에는 merge 함수가 cart 객체와 updateObj를 재귀적으로 병합합니다. 이제 이는 오용될 수 있는 것처럼 보입니다.
JSON.parse는 객체의 모든 키를 임의의 문자열로 처리합니다. 그러므로 이제 __proto__의 새로운 쿼리 매개변수를 생성하는 대신에 이를 cart 객체 자체에 주입할 것입니다.
접근 방법 3: cart 쿼리 매개변수에 proto 주입하기
다음 URL을 시도해 봅시다:
https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1},"__proto__":{"hack":"hacked"}
와! 우리는 성공적으로 전역 객체 프로토타입을 오염시켰어요. 하지만 정확히 무엇이 일어났을까요? 왜 이 형식이 마술처럼 작동하고 다른 것들은 그렇지 않았을까요🤔.
우리가 일어나고 있는 일을 이해하기 위해 내부를 살펴봐봅시다.
JSON.parse는 모든 키를 임의의 문자열로 간주하기 때문에 JSON.parse(params.get("cart"))는 아래와 같은 객체를 생성할 것입니다:
const updateObj = {
"items": {
"2": 3,
"3": 1,
},
"__proto__": {
"hack": "hacked",
},
};
여기서 __proto__는 그냥 임의의 문자열이며 원형 객체 Object.prototype을 가리키지 않습니다. 그런 다음 장바구니 객체와 updateObj를 재귀적으로 병합하는 작업을 진행합니다.
재귀적으로 병합하는 중간 과정에서 함수는 target["proto"]["hack"] = "hacked"를 할당할 것입니다. 이 할당 중에 자바스크립트 실행 시점에서 ["proto"]를 Object의 원형 속성에 대한 getter로 간주합니다. 따라서 이 할당은 Object.prototype["hack"] = "hacked"와 동등합니다. 이제 Object() 생성자 함수를 사용하여 만든 모든 객체가 속성 'hack'에 액세스할 수 있게 됩니다.
'hack' 속성을 주입하는 것은 우리에게 상당히 쓸모가 없으므로, 그 감자스러운 100% 할인을 얻을 수 있는 더 유용한 속성을 찾아보도록 합시다😍.
코드 탐색
이제 이 방법을 사용하여 덮어쓰거나 주입할 수 있는 일부 기능 또는 속성을 찾아야 합니다. 이렇게 하면 할인 혜택을 받을 수 있습니다.
우리는 calculateTotal 함수가 할인 객체에 "진리" 속성으로 discountCodeValid가 있는지 확인하고 100% 할인을 적용하는 것을 볼 수 있습니다. 아하! Object.prototype에 discountCodeValid 속성을 주입하면 모든 즐겨찾는 제품을 무료로 구매할 수 있습니다!!!
100% 할인 적용 🥳
https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1},"__proto__":{"discountCodeValid":true}
위 URL은 다음의 자바스크립트 호출을 유발합니다:
cart["__proto__"]["discountCodeValid"] = true
이로써 discountCodeValid 속성이 Object.prototype 객체에 삽입됩니다. 함수 calculateTotal이 호출되면, (discount.discountCodeValid) 부분의 if 문으로 제어가 이동합니다. 자바스크립트는 prototype chaining의 원리에 따라 Object.prototype에서 discountCodeValid 속성을 찾게 되고, 총 비용이 0으로 설정됩니다.
장바구니에 표시된 총 비용이 0이고 Total: $0.00 (100% 할인 적용)🎊로 나옵니다.
할인이 적용된 구매 버튼을 클릭하면 특별한 서프라이즈를 받을 수 있어요😉.
원시 가공법에서 발생하는 취약점
최근 몇 년간 프로토타입 가공법으로 인한 실제 취약점이 많이 발견되었습니다. 다양한 자바스크립트 프레임워크와 라이브러리가 영향을 받았습니다.
- jQuery (CVE-2019-11358): 2019년 jQuery에서 프로토타입 가공법 취약점이 발견되었습니다. 가장 널리 사용되는 자바스크립트 라이브러리 중 하나인 jQuery의 3.4.0 버전 이전이 영향을 받았으며, 수백만 개의 웹 사이트에 영향을 줄 수 있습니다.
- minimist (CVE-2020-7598): Node.js를 위한 널리 사용되는 매개변수 분석 라이브러리인 minimist가 2020년 초에 취약점이 발견되어, 수많은 Node.js 애플리케이션 및 CLI 도구가 영향을 받았습니다.
- object-path (CVE-2020-15256): 2020년 후반에는 객체의 깊은 속성에 접근하는 object-path 라이브러리가 프로토타입 가공법 공격을 당할 수 있다는 것이 밝혀졌습니다.
- Lodash (CVE-2019-10744): 2019년 7월, 가장 널리 사용되는 JavaScript 유틸리티 라이브러리인 Lodash에서 중요한 프로토타입 가공법 취약점이 발견되었습니다. 이 취약점은 4.17.12 버전 이전의 모든 버전에 영향을 미쳤으며, 수백만 개의 프로젝트에 영향을 줄 수 있습니다.
프로토타입 가공법 안전 코드 작성 방법
쇼핑 사이트를 해킹했던 것처럼, 우리 앱도 마찬가지로 해킹 당할 수 있어요😥. 이를 방지하기 위해 웹사이트에 발생하지 않도록 코딩 관행을 채택해야 해요. Middleware에서 코드를 쓸 때 원형 오염에 저항하는 코드를 작성하는 데 사용하는 주요 전략 몇 가지를 소개할게요:
- Object.create(null): 신뢰할 수 없는 소스에서 객체를 저장할 때 사용하는 방법이에요. 이렇게 하면 원형이 없는 객체를 만들어 원형 오염의 위험을 제거할 수 있어요.
const safeObj = Object.create(null)
- 키의 살균화: 원형 오염을 방지하는 가장 명백한 방법일 것이에요. 하지만 종종 결함이 있는 살균화 구현은 공격자로 하여금 여전히 생성자를 통해 원형을 오염시키거나 키의 값을 약간 변경하여 살균회피를 우회할 수 있게 할 수 있어요. 쇼핑 사이트에서 사용된 병합 함수를 업데이트하여 이러한 유형의 공격을 방지하는 방법을 살펴볼게요:
function merge(target, source) {
for (let key in source) {
if (Object.hasOwn(source, key) && key !== '__proto__' && key !== 'constructor') {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
- Object.freeze() : 객체 프로토타입 변조를 방지하는 또 다른 방법은 Object.Freeze를 사용하는 것입니다. 객체를 동결하면 확장이 금지되고 기존 속성은 쓰기 및 구성할 수 없게 됩니다. 동결된 객체는 더 이상 변경할 수 없습니다.
Object.freeze(Object.prototype)
obj = {}
obj.__proto__.evil = "evil"
"evil" in obj // false
- Map() : 내장된 보호 기능을 제공하는 Map과 같은 객체를 사용할 수 있습니다. Map은 여전히 악의적인 속성을 상속할 수 있지만, 직접적으로 정의된 속성만 반환하는 내장된 get() 메서드가 있습니다.
Object.prototype.hacked = "polluted"
let safeObj = new Map()
safeObj.set("name", "John")
safeObj.hacked === "polluted" // true
safeObj.get("hacked") // undefined
safeObj.get("name") // John
- 의존성 보안: 코드를 작성할 때 모든 조치를 취할 수 있지만 취약한 라이브러리 하나만 있으면 모든 것이 깨질 수 있습니다. 따라서 안전한 라이브러리만 사용하는 것이 매우 중요합니다. 다행히 npm은 프로젝트에서 알려진 취약점을 스캔하는 npm-audit라는 내장 명령을 제공합니다.
npm audit
npm audit fix
최종 생각들
그럼 여기까지입니다! 쇼핑 카트를 우리만의 해킹 놀이터로 성공적으로 변신시켰습니다. 하지만 기억해 주세요, 위대한 능력에는 큰 책임이 따르며(그리고 아마 몹시 혼란스러운 개발자들도 있을 겁니다).
아마존을 만들고 싶은지, 아니면 자바스크립트 객체를 정리하려는지에 상관없이 이것을 기억하세요. 결국, 사용자들이 모든 것에 100% 할인을 받는 것은 원치 않을 거예요, 맞죠? (혹은 원한다면, 우린 친구가 될 수 있을까요?)
이제 이 프로토 타입 오염 능력을 갖고 있으니, 왜 이제서야 스킬을 테스트해 보지 않으세요? 이 작은 모험 이후 핫숏 해커인 줄 아시나요? 그렇다면, 우리가 도전 과제를 제공하겠습니다!
⚡️ Middleware 레포지토리를 깨뜨려 보세요!
자유롭게 시도해 보세요 🚀.
JavaScript 세계에서 안전하게 머무르고, 우리 객체가 오염되지 않게 합시다! (물론, 앱을 깨려고 하는 거라면, 도전해 보세요! 😛)