最近在学习审计的时候学习到了关于Spring下Thymeleaf模版注入的知识,随即来记录一下
Thymeleaf模版存在很多表达式,感觉和jsp模板里的表达式差不多。不过功能更强大
比如以下
${...}
*{...}
#{...}
@{...}
~{...}
这里主要关注片段表达式,这个功能呢就是可以将其他模板的部分片段插入到本模板中。这个在每个模板中插入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>
片段表达式的语法有三种形式:
PS:注意如果片段表达式中出现::,那么后面必须跟片段名。否则会报错
Thymeleaf模版引擎有一个特性叫做表达式预处理(Expression PreProcessing),置于__...__
之中会被预处理,预处理的结果再作为表达式的一部分继续处理。举例如下:
~{user/__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x/whoami}
会被预先处理为如下,然后再解析片段表达式
~{user/rerce::.x/whoami}
controller如下
return "index"意为返回用model渲染的index.html模板
index.html
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>titletitle>
head>
<body>
hello 第一个Thymeleaf程序
<div th:text="${name}">div>
body>
html>
访问index,成功获取到渲染后的页面
先来梳理以下Spring下模板渲染流程
之前讲到过Spring中的前端控制器DispatcherServlet,这个是根据请求派遣到对应controller处理然后对结果进行解析的核心类
现在重新梳理一遍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;
//根据请求processedRequest获取handler执行链 HandlerExecutionChain,其中包含了适配的handler以及interceptor
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
/**
* 如果mappedHandler为空就返回404
*/
this.noHandlerFound(processedRequest, response);
return;
}
// 确定当前请求的处理程序适配器
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;
}
}
/**
* 4.预处理,执行interceptor拦截器等
*/
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
/**
* 执行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);
/**
* 拦截器后置处理
*/
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
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);
}
}
}
下面走一遍流程
当我们访问127.0.0.1:8080/path?lang=aaa时候
直接从ha.handle开始说起,即已经获得到了处理该请求的handler
好了程序走到ha.handle,开始进行参数绑定和方法执行。参数绑定spring那篇文章已经讲过了。这里就不再赘述,重点关注方法调用和执行
跟进handle,来到handleInternal这个方法就是使用Handler处理request并获取ModelAndView,可以看到调用了invokeHandlerMethod,而参数handlerMethod中包含了要执行的方法path
跟进invokeHandlerMethod,可以看到对handlerMethod包装了一下转换为invocableMethod
直接来到554行invocableMethod调用invokeAndHandle
跟进invokeAndHandle,这里的invokeForRequest就很关键。其是根据url获取调用对应的controller,然后将返回值赋值给returnvalue。将returnvalue做为待查找的模板名,Thymeleaf会去查找对应的模板进行渲染
可以看到返回值为user/aaa/welcome
然后进入到handleReturnValue,这个是根据returnValue的值填充ModelAndViewContainer
首先获取returnValue的处理器handler,然后调用handleReturnValue对returnValue进行处理
mavContainer.viewName设置为returnValue
判断返回值是否以redirect:
开头,如果是的话则设置重定向的属性
好了mavContainer也处理完了,一路返回来到RequestMappingHandlerAdapter#invokeHandlerMethod
根据mavContainer获得modelandview对象
回到核心类,此时已经获取到了modelandview对象
获取到mv后,进入processDispatchResult进行视图渲染
跟进render,传入mv
首先获取视图解析器,然后调用解析的render渲染模板
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale();
response.setLocale(locale);
String viewName = mv.getViewName();
View view;
if (viewName != null) {
//获取视图解析器
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'");
}
} else {
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
}
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
//渲染
view.render(mv.getModelInternal(), request, response);
} catch (Exception var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Error rendering view [" + view + "]", var8);
}
throw var8;
}
}
可以看到获取的视图解析器为thymeleafview,跟进render
这里来到了关键位置,可以看到首先判断viewTemplateName是否包含::如果包含的话进入else分支,进行表达式预处理
我们更换payload为lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x
此时viewTemplateName就是user/lang=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x/welcome
当viewTemplateName中包含::时,thymeleaf会认为其是一个要处理的片段表达式,会给其加上~{}然后进行解析
来到109行,跟进parseExpression
首先会对片段表达式进行thymeleaf预处理
首先进行正则提取出__…__之间的东西
此时提取出的就是${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(“whoami”).getInputStream()).next()}
然后调用execute执行,跟进execute最终调用org/thymeleaf/standard/expression/VariableExpression#executeVariableExpression使用SpEL执行表达式,触发任意代码执行。
然后返回result,返回到ThymeleafView#renderFragment。可以看到我们controller返回的模板名被解析
user/lang=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x/welcome
被解析为了user/desktop-f0jqiou\rerce模板然后selector为.x/welcome
但是由于找不到user/desktop-f0jqiou\rerce模板,所以最终会返回404页面并携带出whoami结果
针对这个payload,有两种情况
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x
这种情况是通过uri path注入,前面讲到过如果controller没有return值则会在核心类中进入applyDefaultViewName,用uri path给viewname赋值,这里面会对后缀做一个清除,如果没有.的话会使得::被stipe掉,从而无法进入预处理导致无法执行任意代码
https://www.anquanke.com/post/id/254519#h3-12
payload各种变形
spel表达式不仅可以放在templatename位置,也可以放在selector位置,只不过一个有回显一个无回显
payload放在了templatename位置会以找不到模板名的方式回显回来
但如果payload放在selector位置,通过上面的分析其实也是可以触发命令执行的,只不过不会回显
各种场景下的payload变形可以参考如下两篇
https://xz.aliyun.com/t/8568
https://www.anquanke.com/post/id/254519#h3-12
可以说就是Thymeleaf在处理controller返回的templatename时,如果检测到其中包含::则会认为其是一个片段表达式会对其加上~{}进行解析,在解析之前会对该表达式预处理,该过程中通过正则取出两个横线之间的内容(如果没有就不预处理,直接return)即${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}
。然后调用标准解析器对其进行解析,因为最终是一个spel表达式,所以导致spel命令执行。将该执行结果替换到templatename上,所以最终templatename变为了~{user/desktop-f0jqiou\rerce::.x/welcome},然后再进行片段表达式解析,::前面的为模板名但又因为找不到user/desktop-f0jqiou\rerce这个模板,所以最终会以报错的方式将命令结果回显回来
@ResponseBody
或者 @RestController
这样 spring 框架就不会将其解析为视图名,而是直接返回, 不再调用模板解析。
这样不再由 Spring ThymeleafView来进行解析,而是由 RedirectView 来进行解析。
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。
https://www.anquanke.com/post/id/254519
https://xz.aliyun.com/t/10514
https://www.cnblogs.com/nice0e3/p/16212784.html
https://xz.aliyun.com/t/8568