前言
在8月25日的安恒杯月赛题出现了一道flask debug配合任意文件读取的题,当时没有搞出来,作为萌新没见过这种题目,所以赛后经过讲解,自己本地复现了一波,收益良多。
0x01
此漏洞主要是利用Flask在debug会生成一个pin码。
E:\1待处理\123\flaskfuxianaaa\venv\Scripts\python.exe -m flask run
* Serving Flask app "app.py" (lazy loading)
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 229-992-815
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
问题出在这个pin码的生成,在同一台机子上多次启动同一个Flask应用时,会发现这个pin码是固定的。
0x02
漏洞测试环境:
windows
python3.6
Flask 1.0.2
0x03
现在来分析pin码是如何生成的,本人是用pycharm单步debug,下面只给出重要代码段。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hellso World!'
if __name__ == '__main__':
app.run()
跟进app.run函数。
跟进run_simple函数
跟进DebuggedApplication类
跟进__init__函数,可以看到两个跟pin相关的参数
可以看到_get_pin函数钟有对pin赋值的地方。
跟进get_pin_and_cppkie_name函数
看到注释,可以肯定这个函数就是跟pin的生成有关,下面贴出源码。
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
可以看到,计算pin码时,是通过向本机取一下东西,经过md5等操作算出来的。
看看debug调试结果
分析出六个值分别位:
wuli丶Decade
flask.cli
DispatchingApp------------------>run函数所属的类
E:\\1待处理\\123\\flask11\\venv\\Lib\\site-packages\\flask\\cli.py------>根据报错信息得出路径
xxxxx5457141305 ------->某网卡的mac地址的十进制
xxxxxx-8fcb-44d5-be62-36049d2db881 分linux、windows、mac,Windows是从注册表中取一个值。
下面改脚本,在本地计算出pin值
回到题目本身
由于pycharm版本的原因,构造的六个值稍微有点不同,下面直接给出路径和值。
username ------>/etc/passwd
中间两个一般固定
路径(根据报错)
网卡的mac地址一般存在 /sys/class/net/网卡/address
linux---> /etc/machine-id or /proc/sys/kernel/random/boot_id
'ctf'
'flask.app'
'Flask'
'/usr/local/lib/python2.7/dist-packages/flask/app.pyc'
'2485377892354'
''
但是这里有两个坑
1、路径报错是pyc。
2、/etc/machine-id路径下的值确实是空的。