• 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

    路由解析流程主要就是ModelView以及最后Render。return处打个断点,看怎么处理的
    先进入invokeAndHandle,调用invokeForRequest方法,这个操作会获取到我们传进去的视图名称

    往下走,这个mavContainer就是一个ModelAndViewContainer容器对象

    进入handleReturnValue方法

    跟进另一个handleReturnValue
    略过dispatch的调试环节,直接定位到render处

    往下走进入ThymeleafView的render方法,然后走到这

    这个方法是重点,后面会说到,退出这个方法后,流程就结束了

    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

    往下走,这里会判断viewTemplateName是否包含::
    这里需要介绍一个东西

    Thymeleaf 是与 java 配合使用的一款服务端模板引擎,也是 Spring 官方支持的一款服务端模板引擎。而 SSTI 最初是由 [James Kettle](https://portswigger.net/research/server-side-template-injection) 提出研究,[Emilio Pinna](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代码

     
    "http://www.thymeleaf.org"> 
     
    "banquan"> © 2021 ThreeDream yyds

    我们需要在另一个template模板文件引用上述的fragment

    "~{footer :: banquan}">

    这就是片段表达式,片段表达式后面必须要有一个名字,这也对应payload中的.x,这个.x就是名称,那个.也可以去掉改为任意的字符串.
    继续往下走,fragmentExpression处进行了一个拼接,刚好是片段表达式形式的拼接

    跟进parser.parseExpression这个方法
    继续跟进,这里进入preprocess函数
    注意上方的Pattern,Pattern.compile("\\_\\_(.*?)\\_\\_", 32);
    这个刚好就能识别payload的形式,然后由于是片段表达式,所以有最后的.x

    往下走进入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]
                        && (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表达式的研究了


    __EOF__

  • 本文作者: F12
  • 本文链接: https://www.cnblogs.com/f12-blog/p/18131128
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    c++(15)静态成员变量、静态成员函数、static占用大小
    泰国放宽MogaFX外汇经营商规则
    nginx(2)
    AcWing 505. 火柴排队(每日一题)
    SystemVerilog学习-10-验证量化和覆盖率
    更改linux开发板默认shell为bash
    Java23种设计模式-结构型模式之桥接模式
    猜一猜掌握哪个测试技能让你从容不破应对工作!
    Selenium 4.11 正式发布--再也不用手动更新chrome driver 了
    Linux命令记载
  • 原文地址:https://www.cnblogs.com/F12-blog/p/18131128