java代码审计-Thymeleaf注入漏洞研究与复现
Thymeleaf 模板安全分析
1、Thymeleaf简介
Thymeleaf是用于Web和独立环境的现代服务器端Java模板引擎。类似与python web开发中的jinja模板引擎。顺便说一句,Thymeleaf是spring boot的推荐引擎。
为啥用 Thymeleaf
Thymeleaf是SpringBoot中的一个模版引擎,个人认为有点类似于Python中的Jinja2,负责渲染前端页面。
之前写JavaWeb和SSM的时候,前端页面可能会用JSP写,但是因为之前项目都是war包部署,而SpringBoot都是jar包且内嵌tomcat,所以是不支持解析jsp文件的。
但是如果是编写纯静态的html就很不方便,那么这时候就需要一个模版引擎类似于Jinja2可以通过表达式帮我们把动态的变量渲染到前端页面,我们只需要写一个template即可。这也就是SpringBoot为什么推荐要使用Thymeleaf处理前端页面了。
2、 基础知识
Spring Boot 本身就 Spring MVC 的简化版本。是在 Spring MVC 的基础上实现了自动配置,简化了开发人员开发过程。Spring MVC 是通过一个叫 DispatcherServlet 前端控制器的来拦截请求的。而在 Spring Boot 中 使用自动配置把 DispatcherServlet 前端控制器自动配置到框架中。
例如,我们来解析 /users 这个请求
- DispatcherServlet 前端控制器拦截请求 /users
- servlet 决定使用哪个 handler 处理
- Spring 检测哪个控制器匹配 /users,Spring 从 @RquestMapping 中查找出需要的信息
- Spring 找到正确的 Controller 方法后,开始执行 Controller 方法
- 返回 users 对象列表
- 根据与客户端交互需要返回 Json 或者 Xml 格式
spring boot 相关注解
- @Controller 处理 Http 请求
- @RestController @Controller 的衍生注解– @RestController 是 @Controller 和 @ResponseBody 两个注解的结合体。
- @RequestMapping 路由请求 可以设置各种操作方法
- @GetMapping GET 方法的路由
- @PostMapping POST 方法的路由
- @PutMapping PUT 方法的路由
- @DeleteMapping DELETE 方法的路由
- @PathVariable 处理请求 url 路径中的参数 /user/{id}
- @RequestParam 处理问号后面的参数
- @RequestBody 请求参数以json格式提交
- @ResponseBody 返回 json 格式
Controller注解
@Controller 一般应用在有返回界面的应用场景下.例如,管理后台使用了 thymeleaf 作为模板开发,需要从后台直接返回 Model 对象到前台,那么这时候就需要使用 @Controller 来注解。
@Controller 包括了 @RestController。@RestController 是 Spring4 后新加的注解,从 RestController 类源码可以看出 @RestController 是 @Controller 和 @ResponseBody 两个注解的结合体。
@Controller=@RestController+@ResponseBody
如下 @RestController 的源码可以看出他们的关系
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
java
@Controller 与 @RestController应用场景
- @Controller 一般应用在有返回界面的应用场景下.例如,管理后台使用了 thymeleaf 作为模板开发,需要从后台直接返回 Model 对象到前台,那么这时候就需要使用 @Controller 来注解。
- @RestController 如果只是接口,那么就用 RestController 来注解.例如前端页面全部使用了 Html、Jquery来开发,通过 Ajax 请求服务端接口,那么接口就使用 @RestController 统一注解。
@RequestMapping 说明
首先我们来看看 @RequestMapping 的源码,
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
//指定请求的实际地址
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
//指定请求的method类型, GET、POST、PUT、DELETE等
RequestMethod[] method() default {};
//指定request中必须包含某些参数值是,才让该方法处理。
String[] params() default {};
//指定request中必须包含某些指定的header值,才能让该方法处理请求。
String[] headers() default {};
//指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
String[] consumes() default {};
//指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;
String[] produces() default {};
}
java
示例说明:
示例 |
说明 |
@RequestMapping(“/index”) |
默认为 GET 方法的路由 /index |
@RequestMapping(value=“/index”,method = RequestMethod.GET) |
同上面一条 |
@RequestMapping(value=“/add”,method = RequestMethod.POST) |
路由为 /add 的 POST 请求 |
@RequestMapping(value=“/add”,method = RequestMethod.POST),consumes=“application/json” |
路由为 /add 的 POST 请求,但仅仅处理 application/json 的请求 |
@RequestMapping(value=“/add”,method = RequestMethod.POST),produces=“application/json” |
路由为 /add 的 POST 请求,强调返回为 JSON 格式 |
@RequestMapping(value=“/add”,method = RequestMethod.POST),params=“myParam=xyz” |
路由为 /add 的 POST 请求,但仅仅处理头部包括 myParam=xyz 的请求 |
@RequestMapping(value=“/add”,method = RequestMethod.POST),headers=“Referer=http://www.xyz.com/” |
路由为 /add 的 POST 请求,但仅仅处理 来源为 www.xyz.com 的请求 |
@Controller 和 @RestController 示例
本章节,将对两个注解配合其他注解编写一系列示例,为了演示 @Controller 返回对应页面功能,我们在示例中引入了 thymeleaf 模板。具体在 pom.xml 中有说明。
编号 |
路由 |
Http方法 |
方法说明 |
1 |
/user/index |
GET |
获取用户列表并返回列表页面 |
1 |
/user/add |
GET |
用户新增页面 |
1 |
/user/save |
POST |
新增用户的api |
1 |
/user/edit |
GET |
用户编辑的页面 |
1 |
/user/update |
POST |
编辑用户的api |
1 |
/user/del |
GET |
删除用户页面 |
1 |
/user/deleted |
POST |
删除用户页面的api |
3、 Thymeleaf 介绍
片段表达式
Thymeleaf中的表达式有好几种
- 变量表达式: ${...}
- 选择变量表达式: *{...}
- 消息表达: #{...}
- 链接 URL 表达式: @{...}
- 片段表达式: ~{...}
片段表达式
片段表达式(FragmentExpression): ~{...},片段表达式可以用于引用公共的目标片段比如footer或者header
比如在/WEB-INF/templates/footer.html定义一个片段,名为copy。<div th:fragment="copy">
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>
html
在另一template中引用该片段<div th:insert="~{footer :: copy}"></div>
<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
html
片段表达式语法:
- ~{templatename::selector},会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment,如上面的~{footer :: copy}
- ~{templatename},引用整个templatename模版文件作为fragment
- ~{::selector} 或 ~{this::selector},引用来自同一模版文件名为selector的fragmnt
其中selector可以是通过th:fragment定义的片段,也可以是类选择器、ID选择器等。
当~{}片段表达式中出现::,则::后需要有值,也就是selector。
预处理
语法:__${expression}__
官方文档对其的解释:
除了所有这些用于表达式处理的功能外,Thymeleaf 还具有预处理表达式的功能。
预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式。
预处理的表达式与普通表达式完全一样,但被双下划线符号(如__${expression}__)包围。
个人感觉这是出现SSTI最关键的一个地方,预处理也可以解析执行表达式,也就是说找到一个可以控制预处理表达式的地方,让其解析执行我们的payload即可达到任意代码执行
4.特点:
Thymeleaf 模板引擎具有以下特点:
- 动静结合:Thymeleaf 既可以直接使用浏览器打开,查看页面的静态效果,也可以通过 Web 应用程序进行访问,查看动态页面效果。
- 开箱即用:Thymeleaf 提供了 Spring 标准方言以及一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
- 多方言支持:它提供了 Thymeleaf 标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式;必要时,开发人员也可以扩展和创建自定义的方言。
- 与 SpringBoot 完美整合:SpringBoot 为 Thymeleaf 提供了的默认配置,并且还为 Thymeleaf 设置了视图解析器,因此 Thymeleaf 可以与 Spring Boot 完美整合。
Thymeleaf 语法规则
在使用 Thymeleaf 之前,首先要在页面的 html 标签中声明名称空间,就可以对th语法进行高亮显示。
xmlns:th="http://www.thymeleaf.org"
Thymeleaf 作为一种模板引擎,它拥有自己的语法规则。Thymeleaf 语法分为以下 2 类:
- 标准表达式语法
- th 属性
标准表达式语法
Thymeleaf 模板引擎支持多种表达式:
- 变量表达式:${...}
- 选择变量表达式:*{...}
- 链接表达式:@{...}
- 国际化表达式:#{...}
- 片段引用表达式:~{...}
变量表达式
使用 ${} 包裹的表达式被称为变量表达式,该表达式具有以下功能:
- 获取对象的属性和方法
- 使用内置的基本对象
- 使用内置的工具对象
获取对象的属性和方法
使用变量表达式可以获取对象的属性和方法,例如,获取 person 对象的 lastName 属性,表达式形式如下:
${person.name}
使用内置的基本对象
使用变量表达式还可以使用内置基本对象,获取内置对象的属性,调用内置对象的方法。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 等。
使用内置工具对象 strings 的 equals 方法,来判断字符串与对象的某个属性是否相等
${#strings.equals('张三',name)}
选择变量表达式
选择变量表达式与变量表达式功能基本一致,只是在变量表达式的基础上增加了与 th:object 的配合使用。当使用 th:object 存储一个对象后,我们可以在其后代中使用选择变量表达式(*{...})获取该对象中的属性,其中 ,`* ````即代表该对象。
<div th:object="${session.user}" >
<p th:text="*{name}">name</p>
</div>
链接表达式
不管是静态资源的引用,还是 form 表单的请求,凡是链接都可以用链接表达式 (@{...})
链接表达式的形式结构如下:
- 无参请求:@{/xxx}
- 有参请求:@{/xxx(k1=v1,k2=v2)}
<link href="asserts/css/login.css" th:href="@{/asserts/css/login.css}" rel="stylesheet">
国际化表达式
消息表达式一般用于国际化的场景。
th:text="#{msg}"
片段引用表达式
片段引用表达式用于在模板页面中引用其他的模板片段
- ~{templatename::fragmentname}
- ~{templatename::#id}
以上语法结构说明如下:
- templatename:模版名,Thymeleaf 会根据模版名解析完整路径:/resources/templates/templatename.html,要注意文件的路径。
- fragmentname:片段名,Thymeleaf 通过 th:fragment 声明定义代码块,即:th:fragment="fragmentname"
- id:HTML 的 id 选择器,使用时要在前面加上 # 号,不支持 class 选择器。
th 属性
Thymeleaf 还提供了大量的 th 属性,这些属性可以直接在 HTML 标签中使用
5、模板注入demo
我们以spring boot + Thymeleaf模板创建一个带有漏洞的项目。核心代码如下
@GetMapping("/path")
public String path(@RequestParam String lang) {
return lang ; //template path is tainted
}
代码含义如下:用户请求的url为path,参数名称为lang,则服务器通过Thymeleaf模板,去查找相关的模板文件。
例如,用户通过get请求/path?lang=en,则服务器去自动拼接待查找的模板文件名,为resources/templates/en.html,并返回给用户的浏览器。
上面的代码存在两个问题: 1. 是不是存在任意文件读取? 2. 是不是存在诸如模板注入的漏洞???
PS: pom.xml文件如下所示,最新的版本该问题已经修复
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--latest-->
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
xml
正常访问URL
正常访问该url,出现http500 ,因为后台没有对应的en模板,所以报错,这个正常。
异常注入访问
当访问的URL为 /path2?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x
发现出现了模板注入的情况
调试看看后台是否如何变化的
1、在org.springframework.web.servlet.DispatcherServlet#doDispatch 下个断点
DispatcherServlet的作用
看下DispatcherServlet在Spring MVC中的位置,所有的请求经过DispatcherServlet进行分发,然后根据不通过的对象通过对应的处理方法进行处理:
初始化的过程:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
java
DispatcherServlet 处理请求的规则:
- 在请求中查找并绑定 WebApplicationContext,它可以作为参数被控制器中的方法使用。 默认绑定到 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 对应的值。
- 区域解析器 (LocaleResolver) 也绑定到请求上,它可以在请求解析、呈现视图、准备数据等过程中将信息解析为当前的区域环境。如果无需解析这些信息,可以不用管它。
- 主题解析器用来决定使用哪个主题。 如果你不使用主题,可以忽略掉它。
- 如果在应用中声明了 multipart file resolver,则会对请求进行 multipart 检查;如果发现了 multiparts,请求会被包装成 MultipartHttpServlet 来进行处理。
- 如果返回模型,则会解析并返回视图。 如果没有返回模型(由于其他处理程序拦截了请求,可能出于安全原因),则不会返回视图,因为可能已经有响应返回给客户端了。
WebApplicationContext 中声明的 HandlerExceptionResolver bean 可以解析请求处理时抛出的异常。 可以给异常解析器进行特定的配置来解决特定的异常。
DispatcherServlet 还支持返回最后修改日期。 DispatcherServlet 扫描注册的映射关系并,判断找到的处理程序是否实现了 LastModified 接口。 如果实现了,则将 LastModified 接口的 long getLastModified(request)方法的返回值返回给客户端。
2、org.springframework.web.servlet.DispatcherServlet#doDispatch doDispatch 方法
在DispatcherServlet 的doDispatch 中通过HandlerAdapter 的handle 方法来处理请求
3、在HandlerAdapter 中通过调用 ModelAndView handle方法进行处理
4、接下来调用RequestMappingHandlerAdapter#invokeHandlerMethod 通过反射的方法来处理请求
此时传入的request为 前台请的URI
在invokeHandlerMethod 方法中接着通过invokeAndHandle 方法处理请求的传入的URL参数
5、接下来通过调用org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle来处理
ServletInvocableHandlerMethod 调用该方法并通过其中一个配置的HandlerMethodReturnValueHandler处理返回值,传入参数:
Params:
webRequest – the current request mavContainer – the ModelAndViewContainer for this request providedArgs – “given” arguments matched by type (not resolved)
通过invokeForRequest 方法获取对应的请求的URL参数
6、获取到returnValue的值后,通过调用returnValueHandlers.handleReturnValue 先来获取用来处理returnValue值得handle方法
然后在handleReturnValue 中调用invokeHanderMethod方法来进行处理,接着调用RequestMappingHandlerAdapter的 getModelAndView 来处理,处理完成后返回对应的ModelAndView实例
7、RequestMappingHandlerAdapter 的handleInternal 方法中更新对应的ModelAndView 的view内容,即对应模板的名称值
8、ModelAndView mv 通过mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 获取对应当view内容
上述在handle中调用的一连串的方法,也仅仅是为了获取对应的view内容。
9、获取到ModelAndView 对应的view内容后,在DispatchServlet中通过 processDispatchResult 来进行下一步的处理
在返回来看下这个DispatchServlet的处理内容大概就比较有感觉了,上述处理在第九步的,返回了view的内容。
- DispatcherServlet 通过render 渲染9步骤返回的view
- 在render中调用,org.thymeleaf.spring5.view.ThymeleafView#renderFragment,并且通过pareseExpression处理viewTemplateName 模板的名称,并且给viewTemplateName 的名称加上了~{ }表达式的处理标志符号
- 在StandardExpressionParser中进行对应的表达式的处理,通过SpringEl表达式处理该模板的名称
13、后续就不在分析了,后面的处理逻辑就是通render发现 该名称的模板不存在,就报错返回到前台
14、至此整体的流程分析完毕。
6、修复方案
0x01 配置 @ResponseBody 或者 @RestController
这样 spring 框架就不会将其解析为视图名,而是直接返回, 不再调用模板解析。
@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
}
java
0x02 在返回值前面加上 “redirect:”
这样不再由 Spring ThymeleafView来进行解析,而是由 RedirectView 来进行解析。
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //FP as redirects are not resolved as expressions
}
java
0x03 在方法参数中加上 HttpServletResponse 参数
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。
@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document); //FP
}
java
新版本的修改:
1、在org.thymeleaf.spring5.view.ThymeleafView#renderFragment 中添加了SpringRequestUtils.checkViewNameNotInRequest(viewTemplateName, request);
public final class SpringRequestUtils {
public static void checkViewNameNotInRequest(String viewName, HttpServletRequest request) {
String vn = StringUtils.pack(viewName);
if (containsExpression(vn)) {
boolean found = false;
String requestURI = StringUtils.pack(UriEscape.unescapeUriPath(request.getRequestURI()));
if (requestURI != null && containsExpression(requestURI)) {
found = true;
}
if (!found) {
Enumeration<String> paramNames = request.getParameterNames();
while(!found && paramNames.hasMoreElements()) {
String[] paramValues = request.getParameterValues((String)paramNames.nextElement());
for(int i = 0; !found && i < paramValues.length; ++i) {
String paramValue = StringUtils.pack(paramValues[i]);
if (paramValue != null && containsExpression(paramValue) && vn.contains(paramValue)) {
found = true;
}
}
}
}
if (found) {
throw new TemplateProcessingException("View name contains an expression and so does either the URL path or one of the request parameters. This is forbidden in order to reduce the possibilities that direct user input is executed as a part of the view name.");
}
}
}
java
判断如果存在注入则抛出异常
在老版本该地方并不存在校验
参考:
1、java 安全开发之 spring boot Thymeleaf 模板注入 (seebug.org)
2、Spring Boot Web 开发@Controller @RestController 使用教程 - fishpro - 博客园 (cnblogs.com)
- 点赞
- 收藏
- 关注作者
评论(0)