Flask渗透1:debug执行任意代码

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

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

概述

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

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

需要pin码

3

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

漏洞产生分析

4

下断点

5

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

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

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

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

相关代码可以看到都调用了get_pin_and_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
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

该函数如上所示。

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

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

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

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

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

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

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

probably_public_bitsprivate_bits的生成来源于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()]
  • 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
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
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

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

PIN码生成

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

  • username 需要获取
  • modname
  • getattr(app, '__name__', getattr(app.__class__, '__name__'))
  • getattr(mod, '__file__', None)
  • machine_id
  • str(uuid.getnode())

未完待续


Flask渗透1:debug执行任意代码
https://qiuye.ink/2021/03/23/CTF/web/flask渗透-debug执行任意代码/
作者
Kagami
发布于
2021年3月23日
许可协议