Thymeleaf与SSTI

Uncategorized
1.7k words

本科学的毕竟是软工,关于Thymeleaf多少还是了解的。就一模板引擎,和Jinja2是一个性质的东西。一些基本的使用和概念这里就不多赘述,直接开始分析。

环境准备

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?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.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

视图解析流程分析

封装ModelAndView对象

在Spring Boot中,ModelAndView是一个用于封装数据模型和视图名称的对象。它主要用于在控制器方法中传递数据给前端视图并指定要展示的视图。

ModelAndView对象由以下两个部分组成:

  1. Model:代表数据模型,用于存储要传递给前端视图的数据。通过在Model对象中添加属性键值对,可以将数据传递给前端。这些属性可以在视图中使用,以展示数据或进行操作。在控制器方法中,我们可以使用方法参数绑定或通过调用ModelAndView对象的getModel()方法来获取Model对象,并添加属性。
  2. View:代表要展示的视图名称。视图名称是指前端页面的名称或路径。Spring Boot会根据视图名称解析对应的模板文件,将数据模型应用到该视图上,并最终返回给客户端。可以通过设置视图名称来指定要加载的视图,可以是JSP、Thymeleaf或其他前端模板。

流程跟踪

咱们这里分析的流程发生在DispatcherServlet#doDispatch()这里,着重关注一下504行的ModelAndView对象封装和517行的视图渲染这里,首先跟进一下504行的mv封装

跳过一些不甚重要的步骤,跟踪ServletInvocableHandlerMethod#invokeAndHandle()方法,调用栈和方法源码如下。

invokeForRequest 从请求中解析出具体 Controller 方法的入参,并通过反射进行调用。

1
2
3
4
5
6
7
8
9
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {  
//获取请求参数,并赋值给args
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Arguments: " + Arrays.toString(args));
}
//调用相应的controller处理args
return this.doInvoke(args);
}

这里处理完的returnValue是返回了一串值为index的字符串,也就是我们所指定的视图名称。
缩略起没起到作用的if结构,接下来就会在67行根据returnValue的值来选择处理器进行处理,再根据处理结果设置mavContainer


跟进一下handlerReturnValue,在50行依据returnValuereturnType先选出了一个handler,这里用到的是ViewNameMethodReturnValueHandler这个handler,听名词也知道这里是和视图名称有关的处理器,我们接着跟进一下54行的handler.handleReturnValue()

没什么需要特别注意的地方,就是会根据传来的viewName来判定是不是需要为mavContainer开启重定向选项,这里因为viewName中不存在redirect:这个字符串,所以并未设置。整完这一套之后
相当于对mavcontainer进行了一个基础的设置(HttpStatusviewNamemodel),之后要做的就是从mavcontainer中获取mav对象,让我们出栈回到RequestMappingHandlerAdapter#invokeHandlerMethod() ,跟进一下this.getModelAndView这里。我们以上的操作都是553行RequestMappingHandlerAdapter#invokeHandlerMethod()搞得,上面忘提了。


接着跟进669行的new ModelAndView()这里,为了防止忘了,可以先看看这里的参数,代码就没必要看了,至此mav对象正式创建完毕。

视图层处理ModelAndView对象

获取ModelAndView后,回去跟DispatcherServlet#doDispatch部分的517行断点处(上面图有),这里就不一步一步跟了,最终是到了DispatcherServlet#render这里进行视图解析,我们跟进去看看。



从宏观的角度浅谈一下这段代码,先通过我们传入的ViewName拿到View对象,之后再根据View对象对我们所传入的数据进行渲染,先跟进一下resolveViewName这一块。


这里会对 viewResolvers 进行一个遍历,挨个尝试解析 viewName ,看看哪个viewResolver能给解析出来个 view 给返回来。我们直接跟着这个循环进viewResolver.resolveViewName()看返回的view



这里没什么好说的,viewResolver会先根据viewName匹配出来一个可解析的view列表,再把view列表传到getBestView里面挑个最合适的view对象,这里是第一次循环就给挑出来了,ThymeleafView,直接返回出栈。因为拿到了合适的view对象,我们已经可以来到view.render()这里了,在render处下个断点跟进几步,最终来到了ThymeleafView#renderFragment()这里,关键部分代码如下。


能够看到,这里是对我们传入的viewTemplateName进行了判断,如果包含::字符串,则进行188行的片段表达式解析,看着非常像SPEL注入的入口,顺便提一下,我们这里耀手动改一下viewTemplateName的值(前面忘改了),设其值为index:xxx。之后再跟进188行,一路跟到process()这里,来到我们的目的地。



这里的input就是我们前面经过拼接处理后得到的TemplateName,然后关键看我标断点处的代码,发现是对input进行了一个正则匹配,然后把匹配结果当作参数传给parserExperssion最后再经过execute来执行。现在,我们只需要去查一查这个正则的匹配规则,则完事备矣。查一下这里的PREPROCESS_EVAL_PATTERN,详情如下。

非常简单的一个非贪婪匹配,我们只需要在payload的前后分别加上两个_即可。
因为我们这里的是input是瞎填的,显示效果没有那么好,那假如把input这里改一改,又会发生什么呢?

1
::__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}_______________

It is amazing!

文末简单的总结一下利用条件,这里就直接套用别的师傅的了

  1. 用户传入的字符串拼接到了Controller方法的返回值中且返回的视图非重定向(前面流程可用知晓,重定向优先级最高),或URI路径拼接了用户的输入且Controller方法参数中不带有ServletResponse类型的参数;
  2. 视图引擎名称中需要包含::字符串;
  3. 被执行表达式字符串前后需要带有两个下划线,即__${EL}__
  4. 如果POC在URI中,由于URI格式化的原因且我们的POC中带有.符号,所以需要在URI末尾添加.

环境上的话,得Thymeleaf的版本 < 3.0.2