温锦文欧普厨电及净水器总代理 发表于 2024-5-17 05:05:55

Thymeleaf SSTI模板注入分析

情况搭建

先搭建一个SpringMVC项目,参考这篇文章,或者参考我以前的spring内存马分析那篇文章
https://blog.csdn.net/weixin_65287123/article/details/136648903
SpringMVC路由

简单写个servlet
package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Base64;

@Controller
public class TestController {
    @GetMapping("/")
    public String Welcome(String type) throws UnsupportedEncodingException {
      System.out.println(type);
      if(!type.equals("")) {
            return "hello";
      }
      return "index";
    }
    @ResponseBody
    @RequestMapping("/readobject")
    public String frontdoor(String payload) throws IOException, ClassNotFoundException {
      byte[] base64decodedBytes = Base64.getDecoder().decode(payload);
      ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
      ObjectInputStream ois = new ObjectInputStream(bais);
      ois.readObject();
      ois.close();
      return "right";
    }
}这样就是访问到index.jsp
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412132054018-1368045809.png#height=142&id=vaZdx&originHeight=173&originWidth=826&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=676.7142944335938
路由剖析流程主要就是Model和View以及最后Render。return处打个断点,看怎么处置惩罚的
先进入invokeAndHandle,调用invokeForRequest方法,这个操作会获取到我们传进去的视图名称
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412133314652-254681078.png#height=229&id=d23k8&originHeight=538&originWidth=1644&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=699.7142944335938https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412133735595-363106890.png#id=QQ1K5&originHeight=53&originWidth=401&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
往下走,这个mavContainer就是一个ModelAndViewContainer容器对象
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412133952639-1006449979.png#height=192&id=ya9Cn&originHeight=409&originWidth=1528&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=715.7142944335938
进入handleReturnValue方法
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412134055188-1494338936.png#height=179&id=MYhwi&originHeight=410&originWidth=1633&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=714.7142944335938
跟进另一个handleReturnValuehttps://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412134138899-1356808521.png#height=297&id=QDyID&originHeight=671&originWidth=1617&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=716.7142944335938
略过dispatch的调试环节,直接定位到render处
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412134718428-832434454.png#height=326&id=QkToV&originHeight=722&originWidth=1591&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=717.7142944335938
往下走进入ThymeleafView的render方法,然后走到这
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412135323982-988604182.png#height=317&id=f18Sa&originHeight=713&originWidth=1632&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=724.7142944335938
这个方法是重点,背面会说到,退出这个方法后,流程就结束了
Thymeleaf模板注入成因

其实就是上面的renderFragment函数。这里直接讲3.0.12版本后的方法,因为3.0.12后加了一层check,需要绕过,之前版本的就是直接SPEL表达式就可以RCE,__${T%20(java.lang.Runtime).getRuntime().exec(%22calc%22)}__::.x
poc如上,接下来我们将一步步解释为什么poc是上述形式,先改一下controller
@GetMapping("/")
    public String Welcome(String type) throws UnsupportedEncodingException {
      System.out.println(type);
      if(!type.equals("")) {
            return "hello/"+type+"/challenge";
      }
      return "index";
    }type传入我们的payload,renderFragment方法里获取我们的payload
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412140349563-2093002901.png#id=GgzmI&originHeight=876&originWidth=1667&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
往下走,这里会判断viewTemplateName是否包含::https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412140523013-197908702.png#id=yNLBJ&originHeight=345&originWidth=774&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
这里需要介绍一个东西
Thymeleaf 是与 java 配合使用的一款服务端模板引擎,也是 Spring 官方支持的一款服务端模板引擎。而 SSTI 最初是由 (https://portswigger.net/research/server-side-template-injection) 提出研究,(https://github.com/epinna/tplmap) 对他的研究进行了补充,不过这些作者都没有对 Thymeleaf 进行 SSTI 相关的漏洞研究工作,后来 Aleksei Tiurin 在 ACUNETIX 的官方博客上发表了关于 Thymeleaf SSTI 的[文章](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/),因此 Thymeleaf SSTI 逐渐被安全研究者关注。
为了更方便读者理解这个 Bypass,因此在这里简单说一遍一些基础性的内容,如果了解的,可以直接跳到 0x03 的内容。
Thymeleaf 表达式可以有以下类型:

- ${...}:变量表达式 —— 通常在实际应用,一般是OGNL表达式或者是 Spring EL,如果集成了Spring的话,可以在上下文变量(context variables )中执行
- *{...}: 选择表达式 —— 类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行。
- #{...}: Message (i18n) 表达式 —— 允许从外部源(比如.properties文件)检索特定于语言环境的消息
- @{...}: 链接 (URL) 表达式 —— 一般用在应用程序中设置正确的 URL/路径(URL重写)。
- ~{...}:片段表达式 —— Thymeleaf 3.x 版本新增的内容,分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法。 正是由于这些表达式,片段可以被复制,或者作为参数传递给其他模板等等

实际上,Thymeleaf 出现 SSTI 问题的主要原因也正是因为这个片段表达式,我们知道片段表达式语法如下:

1. ~{templatename::selector},会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment重点是片段表达式。假如有一个html代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>© 2021 ThreeDream yyds
</body>
</html>我们需要在另一个template模板文件引用上述的fragment
这就是片段表达式,片段表达式背面必须要有一个名字,这也对应payload中的.x,这个.x就是名称,那个.也可以去掉改为恣意的字符串.
继续往下走,fragmentExpression处进行了一个拼接,刚好是片段表达式形式的拼接
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412140939411-435870038.png#id=K1izE&originHeight=730&originWidth=1592&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
跟进parser.parseExpression这个方法https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412141157527-1826913937.png#id=Vcola&originHeight=876&originWidth=1558&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
继续跟进,这里进入preprocess函数https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412141326380-1688343107.png#id=hH6Tz&originHeight=709&originWidth=1595&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
注意上方的Pattern,Pattern.compile("\\_\\_(.*?)\\_\\_", 32);
这个刚好就能辨认payload的形式,然后由于是片段表达式,所以有最后的.x
https://img2024.cnblogs.com/blog/2746479/202404/2746479-20240412141513029-323567748.png#id=smDzq&originHeight=1001&originWidth=1614&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=
往下走进入execute方法剖析匹配到的payload,剖析过程就不说了,就是正常的SPEL表达式剖析,说一下3.12版本后的一个checker
public static boolean containsSpELInstantiationOrStatic(final String expression) {

      /*
         * Checks whether the expression contains instantiation of objects ("new SomeClass") or makes use of
         * static methods ("T(SomeClass)") as both are forbidden in certain contexts in restricted mode.
         */

      final int explen = expression.length();
      int n = explen;
      int ni = 0; // index for computing position in the NEW_ARRAY
      int si = -1;
      char c;
      while (n-- != 0) {

            c = expression.charAt(n);

            // When checking for the "new" keyword, we need to identify that it is not a part of a larger
            // identifier, i.e. there is whitespace after it and no character that might be a part of an
            // identifier before it.
            if (ni < NEW_LEN
                  && c == NEW_ARRAY
                  && (ni > 0 || ((n + 1 < explen) && Character.isWhitespace(expression.charAt(n + 1))))) {
                ni++;
                if (ni == NEW_LEN && (n == 0 || !Character.isJavaIdentifierPart(expression.charAt(n - 1)))) {
                  return true; // we found an object instantiation
                }
                continue;
            }

            if (ni > 0) {
                // We 'restart' the matching counter just in case we had a partial match
                n += ni;
                ni = 0;
                if (si < n) {
                  // This has to be restarted too
                  si = -1;
                }
                continue;
            }

            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;
            } else if (si > n && !(Character.isJavaIdentifierPart(c) || c == '.')) {
                si = -1;
            }

      }

      return false;

    }进入这个方法,他会辨认new关键字,不答应存在new关键字,并且不答应存在T(.*)这种形式的字符串,因此就得bypass了,而方法也很简单,fuzz一下就知道是T ()加一个空格就行了。后续的一系列利用都是针对sepl表达式的研究了

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Thymeleaf SSTI模板注入分析