freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

thymeleaf模板注入学习与研究--1漏洞原理
怕瓦落地 2022-10-08 10:40:58 146269
所属地 广东省

Thymeleaf是Java模板引擎,广泛运用与springboot中

模板的意义就是将参数动态地嵌入到html中,让前端页面活起来,不写死

环境搭建

package com.thymeleaf.jack.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


/**
 * Author:Jack @Date:2022.09.28
 */
@Controller
public class demo {
    private static final Logger logger = LogManager.getLogger(demo.class);
    @RequestMapping("/path")
    public String path(@RequestParam String lang) {

        return lang;

    }
<?xml 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.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.Thymeleaf</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ThymeleafVulnDemo</name>
    <description>Demo project for Spring Thymeleaf Vuln</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>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>

        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>



漏洞原理

当我们请求 /path?lang=jack,spring会自动去查找的模板文件名,为resources/templates/jack.html,并返回给用户的浏览器。

让我们看看最普通的poc

GET /path?lang=$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d::.x HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1659928725
Connection: close

意思就是去查找名字为

${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}::.x的模板文件

由于thymeleaf本身 支持 spel orgl 等表达式的 ${....}中间的部分就得以解析

spel 表达式在不指定EvaluationContext的情 况下默认采⽤的是StandardEvaluationContext。能够执行spel表达式中的系统命令

因此就成就了这个很实验室的漏洞

org.thymeleaf.standard.expression.Expression

image

variableExpressionEvaluator 用什么表达式的计算器

final IStandardVariableExpressionEvaluator variableExpressionEvaluator =
                StandardExpressions.getVariableExpressionEvaluator(context.getConfiguration());

debug发现结果是 spel 表达式

在不指定EvaluationContext的情 况下默认采⽤的是StandardEvaluationContext。了解spel表达式注入的同学们轻易地知道这是支持命令执行


基础知识

片段表达式 是什么

Thymeleaf模版存在很多表达式,感觉和jsp模板里的表达式差不多。不过功能更强大

比如以下

变量表达式: ${...} 选择变量表达式: *{...} 消息表达: #{...} 链接 URL 表达式: @{...} 片段表达式: ~{...} 这里主要关注片段表达式,这个功能呢就是可以将其他模板的部分片段插入到本模板中。这个在每个模板中插入footer时经常用到

比如你在/WEB-INF/templates/footer.html中定义了这么一个片段

<!DOCTYPE html>

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

  <body>

    <div th:fragment="aaa">
      copyright 2022
    </div>

  </body>

</html>

现在要在另一个模板中引用该片段,则可以使用片段表达式


<body>

  ...

  <div th:insert="~{footer :: aaa}"></div>

</body>

片段表达式的语法有三种形式:

~{templatename::selector},这种呢前面是被选的模板名,后面是片段名 ~{templatename},这种的话是直接引用被选模板的全部片段 ~{::selector} 或 ~{this::selector},这种意思就是选择本模板下名为selector的片段名 PS:注意如果片段表达式中出现::,那么后面必须跟片段名。否则会报错

Thymeleaf 预处理 Thymeleaf模版引擎有一个特性叫做表达式预处理(Expression PreProcessing),置于**...**之中会被预处理,预处理的结果再作为表达式的一部分继续处理。举例如下

~{user/__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x/whoami}

会被预先处理为如下,然后再解析片段表达式

~{user/rerce::.x/whoami}

spring框架的基础知识

image

org.springframework.web.servlet.DispatcherServlet#doDispatch

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	/**

   * 声明变量 HttpServletRequest HandlerExecutionChain Handler执行链包含和最后执行的Handler
     	 */
      HttpServletRequest processedRequest = request;
      HandlerExecutionChain mappedHandler = null;
         	//是不是一个多组件请求
      boolean multipartRequestParsed = false;
         	//异步管理器
      WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        try {
            //定义模型与视图
            ModelAndView mv = null;
            //异常
            Object dispatchException = null;

            try {
                /**
			 	* 检查是否上传请求
			 	*/
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                /**
                *1 根据请求processedRequest获取handler执行链 mapperHandler
                *(mapper句柄,处理器执行链HandlerExecutionChain)
                *其中包含了适配的handler以及interceptor
                */
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    /**
				 	* 如果mappedHandler为空就返回404
				 	*/
                    this.noHandlerFound(processedRequest, response);
                    return;
                }
                /**2 通过mapperHandler 获得处理器适配器HandlerAdapter
				* 确定当前请求的处理程序适配器
				*/
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                /**
			 	* 获取请求方法
			 	* 处理last-modified 请求头
			 	*/
				// Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    //获取最近修改时间,缓存
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                /**
			 	* 3.预处理,执行interceptor拦截器等
			 	*/
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                /**
                * 4.通过 HandlerAdapter处理器适配器与控制器交互,获得 ModelAndView对象 mv
			 	* 执行Controller中(Handler)的方法,返回ModelAndView视图
			 	*/
				// Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    /**
				 	* 判断 是不是异步请求,是就返回了
				 	*/
                    return;
                }

                /**
			 	* 如何返回的modelandview为空,则将URI path作为mav的值
			 	*/
                this.applyDefaultViewName(processedRequest, mv);
                /**
			 	* 5 拦截器后置处理
			 	*/
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }

            /**
		 	* 6 利用返回的mv进行页面渲染
		 	*/
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            /**
		 	* 最终对页面渲染完成调用拦截器中的AfterCompletion方法
		 	*/
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }

    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            //清除由多个部分组成的请求使用的所有资源
            this.cleanupMultipart(processedRequest);
        }

    }
}




漏洞跟进

如何获取modelandview对象


image

跟进org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
image

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
image

此处的 handlerMethod 中的 method就是需要去寻找的控制器

image

跟进invokeHandlerMethod

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

注释:Invoke the RequestMapping handler method preparing a ModelAndView if view resolution is required.

告诉我们这个方法的作用就是 调用mappring handler的方法 为 生成ModelandView对象做准备

先进行包装

将request 包装成了 ServletWebReques t webRequest

handlerMethod封装为一个ServletInvocableHandlerMethod对象,该对象用于对当前request的整体调用流程进行了封装*

将handlerMethod 包装成了 ServletInvocableHandlerMethod invocableMethod

image

image

其实这就是设计模式中的工程模式嘛 ,没必要每个对象都new出来,只要实例化工厂然后给对象工厂传递参数就好了

首先来解读一下这几个工厂吧

  • binderFactory 参数绑定的工厂: 获取容器中全局配置的InitBinder和当前HandlerMethod所对应的Controller中,配置的InitBinder,用于进行参数的绑定

image

  • modelFactory Model工厂 :
    851行 ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
    getModelFactory ()的结果 获取容器中全局配置的ModelAttribute和当前当前HandlerMethod所对应的Controller中配置的ModelAttribute,这些配置的方法将会在目标方法调用之前进行调用

  • mavContainer 就是modelandView的容器:

在865行 传入参数进行实例化

modelFactory.initModel(webRequest, mavContainer, invocableMethod);


image


继续向下invokeAndHandle

跟进org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

这个方法的意义: 对请求参数进行处理,调用目标HandlerMethod,并且将返回值封装为一个ModelAndView对象

image

跟进invokeAndHandle

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

我们先看看注释是什么意思?

我的理解是 对包装过的请求参数进行处理,去调用目标HandlerMethod(控制器中的方法),并且将返回值封装为一个ModelAndView对象
image

这里的invokeForRequest就很关键。其是根据url获取调用对应的controller,然后将返回值赋值给returnvalue。将returnvalue做为待查找的模板名,Thymeleaf会去查找对应的模板进行渲染

image

returnValue结果为

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

然后线下走 handleReturnValue,这个是根据returnValue的值填充ModelAndViewContainer

image

跟进handleReturnValue (看名字就知道处理返回值)

org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#handleReturnValue

迭代已注册的HandlerMethodReturnValueHandler并调用支持它的Handler
image

再次跟进handleReturnValue来到

org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler#handleReturnValue

判断返回值是否以redirect:开头,如果是的话则设置重定向的属性

image

好了mavContainer也处理完了,一路返回来到RequestMappingHandlerAdapter#invokeHandlerMethod

回到这里刚刚从这进去的

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

image

向下走到 getModelAndView 就是更新模板了

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getModelAndView
image

对封装的ModelAndView进行处理,主要是判断当前请求是否进行了重定向,如果进行了重定向, 还会判断是否需要将FlashAttributes封装到新的请求中


漏洞从processDispatchResult开始

* 注意:真正的漏洞促发点来了 *

上面获得了modelandview对象后回到 org.springframework.web.servlet.DispatcherServlet中来继续向下

1057行这里开始了视图的渲染

image

processDispatchResult 对页面渲染

org.springframework.web.servlet.DispatcherServlet#processDispatchResult的

render() 就是渲染 返回一个model and view对象

image

再次跟进 分段渲染 org.thymeleaf.spring5.view.ThymeleafView#render

image

跟进org.thymeleaf.spring5.view.ThymeleafView#renderFragment 解析片段

image

跟进 org.thymeleaf.standard.expression.FragmentExpression#executeFragmentExpression 执行表达式中的内容

image

跟进org.thymeleaf.standard.expression.Expression#execute(org.thymeleaf.context.IExpressionContext, org.thymeleaf.standard.expression.StandardExpressionExecutionContext)

image

跟进 org.thymeleaf.standard.expression.Expression#execute(org.thymeleaf.context.IExpressionContext, org.thymeleaf.standard.expression.Expression, org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator, org.thymeleaf.standard.expression.StandardExpressionExecutionContext)

image

注意此时 的expressionEvaluator 值为SpringEl spel表达式懂了吧 没有专门设定SimpleEvaluationContext 漏洞出来了

看看接口是如何定义的

IStandardVariableExpressionEvaluator expressionEvaluator 打开这个接口查看定义


image


image

这里就告诉我们了

Common interface for all objects in charge of executing variable expressions ({...}) inside Thymeleaf Standard Expressions. The basic implementation of this interface evaluates expressions using OGNL OGNLVariableExpressionEvaluator, but a SpringEL version also exists in the Thymeleaf + Spring integration package.

负责解析 ({...}) 之间的所有 即可解析 OGNL 表达式,又可以解析 SpringEL

好了我们继续跟进 省略一些吧 不是很重要的

org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator

image

跟进org.springframework.expression.spel.standard.SpelExpression#getValue() 来到了spel类中来了
image

org.springframework.expression.spel.ast.SpelNodeImpl#getValue(org.springframework.expression.spel.ExpressionState) 接下来就是解析spel表达式了

其实这部分不是thymeleaf模板注入的知识了 输入与spel表达式注入的原理了接下来的部分

image

跟进 org.springframework.expression.spel.ast.CompoundExpression#getValueInternal 解析内部的值

这样函数也好理解 就是获得表达式的值嘛 ,在获取(解析)表达式值的过程中执行了恶意的命令
image

再省略吧 没那么重要的

最终到达org.springframework.expression.spel.support.ReflectiveMethodExecutor#execute

image



invoke:498, Method (java.lang.reflect)
execute:129, ReflectiveMethodExecutor (org.springframework.expression.spel.support)
getValueInternal:139, MethodReference (org.springframework.expression.spel.ast)
getValueInternal:95, MethodReference (org.springframework.expression.spel.ast)
getValueRef:61, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
createNewInstance:114, ConstructorReference (org.springframework.expression.spel.ast)
getValueInternal:100, ConstructorReference (org.springframework.expression.spel.ast)
getValueRef:55, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:330, SpelExpression (org.springframework.expression.spel.standard)
evaluate:263, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
executeVariableExpression:166, VariableExpression (org.thymeleaf.standard.expression)
executeSimple:66, SimpleExpression (org.thymeleaf.standard.expression)
execute:109, Expression (org.thymeleaf.standard.expression)
execute:138, Expression (org.thymeleaf.standard.expression)
doCreateExecutedFragmentExpression:458, FragmentExpression (org.thymeleaf.standard.expression)
createExecutedFragmentExpression:426, FragmentExpression (org.thymeleaf.standard.expression)
renderFragment:284, ThymeleafView (org.thymeleaf.spring5.view)
render:189, ThymeleafView (org.thymeleaf.spring5.view)
render:1373, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1118, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1057, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:367, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:860, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1591, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
# 代码审计 # Java代码审计
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 怕瓦落地 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
怕瓦落地 LV.3
学习少就是多,慢就是快~
  • 5 文章数
  • 2 关注者
一份永远都不会中奖的Lottery合约
2023-10-17
对 Conic Finance攻击事件的解读
2023-07-30
一笔小损失的漏洞复现: 从tx交易分析到攻击者合约逆向
2023-07-06
文章目录