文章目录
- [1. SSTI概述](#1. SSTI概述)
- [2. 动态网页](#2. 动态网页)
- [3. 模板引擎](#3. 模板引擎)
- [4. 服务端模板注入漏洞](#4. 服务端模板注入漏洞)
-
- [4.1 模板拼接](#4.1 模板拼接)
- [4.2 表达式注入](#4.2 表达式注入)
- [5. 典型漏洞代码模式](#5. 典型漏洞代码模式)
- 参考
1. SSTI概述
SSTI(Server-Side Template Injection,服务器端模板注入) 是一种利用Web应用程序中模板引擎的安全漏洞,通过注入恶意代码来执行服务器端命令的攻击方式。
好,如果没有学过Web开发,不知道什么是MVC模型,上面这段话理解起来会很抽象。但有一点我们是可以明确的,只要是Web漏洞,都是前端/客户端提交请求数据包,通过HTTP协议发送,后端/服务端接收请求包,处理完后生成响应数据包返回。既然叫"服务端模板注入",顾名思义就是前端提交的数据注入到了后端的模板中,可后端不是一些PHP、Java、Python等语言程序文件吗?
要回答什么是模板,前端数据怎么注入到模板,注入模板怎么产生漏洞,那就需要看看后端是怎么动态生成HTML文件的,下面,我们就要尝试在有限的篇幅中描述清楚这一过程及其中的一些概念。
2. 动态网页
一个网页就是一个HTML文件,浏览器负责解析这个文件并展示。动态网页到底动态在哪?我们所有用户都可以登录CSDN,登录成功后返回给用户的HTML页面几乎是一致的,只是用户名、头像等略有差别。服务端不可能为每一个用户事先准备一个静态的HTML,分别为每一个用户返回,这样服务器存储会爆炸。
很明显,我们在CSDN登录成功后返回的HTML文件的骨架是固定的,只是在头像等区域换成各自用户的数据。后端就是根据这个固定的HTML骨架,并在相应位置插入用户的数据,形成最终返回给各自用户的HTML文件。

说起来很简单,要实现这个过程很繁琐。HTML文件本质就是一行行的字符串/文本,古早时候后端程序也确实是通过字符串拼接来动态生成HTML文件,这是一个怎样的过程呢?我们以Java为例子展示一下查询数据库获取数据后并生成HTML文件的过程,
java
public class DeptListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取应用的根路径
String contextPath = request.getContextPath();
// 设置响应的内容类型以及字符集。防止中文乱码问题。
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print(" <head>");
out.print(" <meta charset='utf-8'>");
out.print(" <title>部门列表页面</title>");
out.print("<script type='text/javascript'>");
out.print(" function del(dno){");
out.print(" if(window.confirm('亲,删了不可恢复哦!')){");
out.print(" document.location.href = '"+contextPath+"/dept/delete?deptno=' + dno");
out.print(" }");
out.print(" }");
out.print("</script>");
out.print(" </head>");
out.print(" <body>");
out.print(" <h1 align='center'>部门列表</h1>");
out.print(" <hr >");
out.print(" <table border='1px' align='center' width='50%'>");
out.print(" <tr>");
out.print(" <th>序号</th>");
out.print(" <th>部门编号</th>");
out.print(" <th>部门名称</th>");
out.print(" <th>操作</th>");
out.print(" </tr>");
/*========================上面一部分是固定不变的========================*/
// 连接数据库,查询所有的部门
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 获取连接
conn = DBUtil.getConnection();
// 获取预编译的数据库操作对象
String sql = "select deptno as a,dname,loc from dept";
ps = conn.prepareStatement(sql);
// 执行SQL语句
rs = ps.executeQuery();
// 处理结果集
int i = 0;
while(rs.next()){
String deptno = rs.getString("a");
String dname = rs.getString("dname");
String loc = rs.getString("loc");
out.print(" <tr>");
out.print(" <td>"+(++i)+"</td>");
out.print(" <td>"+deptno+"</td>");
out.print(" <td>"+dname+"</td>");
out.print(" <td>");
out.print(" <a href='javascript:void(0)' onclick='del("+deptno+")'>删除</a>");
out.print(" <a href='"+contextPath+"/dept/edit?deptno="+deptno+"'>修改</a>");
out.print(" <a href='"+contextPath+"/dept/detail?fdsafdsas="+deptno+"'>详情</a>");
out.print(" </td>");
out.print(" </tr>");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 释放资源
DBUtil.close(conn, ps, rs);
}
/*=====================下面一部分是固定不变的=========================*/
out.print(" </table>");
out.print(" <hr >");
out.print(" <a href='"+contextPath+"/add.html'>新增部门</a>");
out.print(" </body>");
out.print("</html>");
}
}
在上面这个查看部门列表的代码里,部门的数量关乎列表的行数,部门数量是通过数据库动态查询出来的,也就是说HTML文件里的表格里地数据和表格行数需要动态地生成。那么我们代码里就需要取数据+拼接HTML格式地标签字符串,程序的逻辑和视图混合在一起,不利于前后端的分工,也使得程序的开发很繁琐。
自然而然,人们的解决办法就是:提供一个视图模板文件(HTML文件),里面有关于数据渲染的占位符,提供一个包含占位符信息的数据对象,有一个软件库,可以把数据直接渲染进模板,这个软件就是模板引擎。
3. 模板引擎
我们来实现一个功能极其单一分模板引擎,帮助我理解"什么是模板"、"什么是数据"、"什么是模板引擎"。
javascript
<script>
var templateStr = '<h1>我在用{{lang}},实现一个简易的{{engine}}';
var data = {lang:'js', engine:'模板引擎'};
function render(templateStr, data){
return templateStr.replace(/\{\{(\w+)\}\}/g, function(findStr, $1){
return data[$1];
});
}
var result = render(templateStr, data);
console.log(result)
</script>
在浏览器控制台中运行,发现成功"将数据渲染到了模板"。

4. 服务端模板注入漏洞
4.1 模板拼接
传统的MVC框架开发,生成HTML文件这一步是在后端完成的。现代前后端分离结构,可以简单理解为将MVC中的"C(控制)"和"V(视图)"放到了前端。
后端的模板引擎提供的功能是将数据填充进模板占位符,模板中的占位符并不单单是一个静态标记,而是一套有严格形式的指令。这些模板指令定义了该从哪里取数据,如何渲染数据等。模板引擎解析模板时,遇到对应的指令会执行对应的操作。服务器端模板注入(SSTI)的根源不在于模板引擎本身,而在于错误的代码编写模式。
模板语法 vs 模板内容
安全的场景(数据层):
─────────────────────
模板: "Hello, ${name}"
数据: name = "<script>alert(1)</script>"
结果: Hello, <script>alert(1)</script>
↑ name 的值被当作"数据"输出,不会被二次解析为模板语法
危险的场景(代码层):
─────────────────────
模板字符串: "Hello, " + userInput ← userInput 被拼进了模板本身
userInput = "${7*7}"
模板引擎解析的字符串: "Hello, ${7*7}"
结果: Hello, 49
↑ 用户输入变成了"模板代码",被执行了
漏洞模式:将用户输入直接拼接进模板
4.2 表达式注入
SSTI广义上包括任何模板指令注入,因为模板引擎提供的动态能力主要是通过表达式语言实现,即表达式注入是其最常见形式,攻击者通过注入表达式来突破数据边界执行代码。JSP的EL注入就是向JSP模板注入EL表达式,Spring的SpEL注入在模板场景下也是向模板注入SpEL表达式。因此,对于这类漏洞,核心都是表达式注入,表达式的执行权限决定了漏洞的危害程度。
5. 典型漏洞代码模式
参考
1\] [Vue源码解析之mustache模板引擎](https://www.bilibili.com/video/BV1EV411h79m) \[2\] [JavaWeb-JSP原理深度解析](https://www.bilibili.com/video/BV1Z3411C7NZ?spm_id_from=333.788.videopod.episodes&vd_source=beeb967a84b38ad9b59e7206ed42a531&p=36) \[3\] 《TOMCAT与JAVA WEB 开发技术详解 第3版》 \[4\] [deepseek](https://chat.deepseek.com/) \[5\] [mimo](https://aistudio.xiaomimimo.com/#/chat)