flask debug配合文件上传getshell

前言

在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)
image.png

问题出在这个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函数。


image.png

跟进run_simple函数


image.png

image.png

跟进DebuggedApplication类
image.png

跟进__init__函数,可以看到两个跟pin相关的参数


image.png

可以看到_get_pin函数钟有对pin赋值的地方。
image.png

跟进get_pin_and_cppkie_name函数
image.png

看到注释,可以肯定这个函数就是跟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等操作算出来的。


image.png

看看debug调试结果


image.png

分析出六个值分别位:

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是从注册表中取一个值。
image.png

下面改脚本,在本地计算出pin值


image.png

回到题目本身

由于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路径下的值确实是空的。

image.png
image.png

image.png

推荐阅读更多精彩内容