前言
最近看到某平台上有一篇关于SSTI的文章,之前也没了解过SSTI的漏洞,因此决定写篇文章记录学习过程。
模板引擎
要了解SSTI漏洞,首先要对模板引擎有所了解。下面是模板引擎的几个相关概念。
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档。
模板引擎的本质是将模板文件和数据通过模板引擎生成最终的HTML代码。
模板引擎不属于特定技术领域,它是跨领域跨平台的概念。
模板引擎的出现是为了解决前后端分离的问题,拿JSP的举个栗子,JSP本身也算是一种模板引擎,在JSP访问的过程中编译器会识别JSP的标签,如果是JSP的内容则动态的提取并将执行结果替换,如果是HTML的内容则原样输出。
xxx.jsp
1 | <!DOCTYPE html> |
上面的代码经过JSP引擎编译后,HTML部分直接输出,而使用JSP标签部分则是经过了解析后的结果。
1 | out.write("<!DOCTYPE html>\r\n"); |
既然JSP已经是一个模板引擎了为什么后面还要推出其他的模板引擎?
动态资源和静态资源全部耦合在一起,还是需要在
JSP文件中写一些后端代码,这其实比较尴尬,所以导致很多JAVA开发不能专注于JAVA开发还需要写一些前端代码。第一次请求jsp,必须要在web服务器中编译成servlet,第一次运行会较慢。
每次请求jsp都是访问servlet再用输出流输出的html页面,效率没有直接使用html高。
如果jsp中的内容很多,页面响应会很慢,因为是同步加载。
jsp只能运行在web容器中,无法运行在nginx这样的高效的http服务上。
使用模板引擎的好处是什么?
模板设计好后可以直接填充数据使用,不需要重新设计页面,增强了代码的复用性
Thymeleaf
Thymeleaf是众多模板引擎的一种和其他的模板引擎相比,它有如下优势:
- Thymeleaf使用html通过一些特定标签语法代表其含义,但并未破坏html结构,即使无网络、不通过后端渲染也能在浏览器成功打开,大大方便界面的测试和修改。
- Thymeleaf提供标准和Spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改JSTL、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
- Springboot官方大力推荐和支持,Springboot官方做了很多默认配置,开发者只需编写对应html即可,大大减轻了上手难度和配置复杂度。
语法
既然Thymeleaf也使用的html,那么如何区分哪些是Thymeleaf的html?
在Thymeleaf的html中首先要加上下面的标识。
1 | <html xmlns:th="http://www.thymeleaf.org"> |
标签
Thymeleaf提供了一些内置标签,通过标签来实现特定的功能。
| 标签 | 作用 | 示例 |
|---|---|---|
| th:id | 替换id | <input th:id="${user.id}"/> |
| th:text | 文本替换 | <p text:="${user.name}">bigsai</p> |
| th:utext | 支持html的文本替换 | <p utext:="${htmlcontent}">content</p> |
| th:object | 替换对象 | <div th:object="${user}"></div> |
| th:value | 替换值 | <input th:value="${user.name}" > |
| th:each | 迭代 | <tr th:each="student:${user}" > |
| th:href | 替换超链接 | <a th:href="@{index.html}">超链接</a> |
| th:src | 替换资源 | <script type="text/javascript" th:src="@{index.js}"></script> |
链接表达式
在Thymeleaf 中,如果想引入链接比如link,href,src,需要使用@{资源地址}引入资源。引入的地址可以在static目录下,也可以司互联网中的资源。
1 | <link rel="stylesheet" th:href="@{index.css}"> |
变量表达式
可以通过${…}在model中取值,如果在Model中存储字符串,则可以通过${对象名}直接取值。
1 | public String getindex(Model model)//对应函数 |
1 | <td th:text="'我的名字是:'+${name}"></td> |
取JavaBean对象使用${对象名.对象属性}或者${对象名['对象属性']}来取值。如果JavaBean写了get方法也可以通过${对象.get方法名}取值。
1 | public String getindex(Model model)//对应函数 |
1 | <td th:text="${user.name}"></td> |
取Map对象使用${Map名['key']}或${Map名.key}。
1 | //页面的url地址 |
1 | <td th:text="${map.get('place')}"></td> |
取List集合:List集合是一个有序列表,需要使用each遍历赋值,<tr th:each="item:${userlist}">
1 | //页面的url地址 |
1 | <tr th:each="item:${userlist}"> |
选择变量表达式
变量表达式也可以写为*{...}。星号语法对选定对象而不是整个上下文评估表达式。也就是说,只要没有选定的对象,美元(${…})和星号(*{...})的语法就完全一样。
1 | <div th:object="${user}"> |
消息表达式
文本外部化是从模板文件中提取模板代码的片段,以便可以将它们保存在单独的文件(通常是.properties文件)中,文本的外部化片段通常称为“消息”。通俗易懂的来说#{…}语法就是用来读取配置文件中数据的。
片段表达式
片段表达式 ~{...}可以用于引用公共的目标片段,比如可以在一个template/footer.html中定义下面的片段,并在另一个template中引用。
1 | <div th:fragment="copy"> |
1 | <div th:insert="~{footer :: copy}"></div> |
Demo
为了能快速对Thymeleaf上手,我们可以先写一个Demo直观的看到Thymeleaf的使用效果。
首先创建一个SpringBoot项目,在模板处选择Thymeleaf。

创建好的目录结构如下,可以在templates中创建html模板文件。

编写Controller
1 |
|
在templates下创建模板文件index.html
1 |
|
启动程序访问/index

SpringMVC 视图解析过程分析
视图解析的过程是发生在Controller处理后,Controller处理结束后会将返回的结果封装为ModelAndView对象,再通过视图解析器ViewResovler得到对应的视图并返回。分析的栗子使用上面的Demo。
封装ModelAndView对象
在ServletInvocableHandlerMethod#invokeAndHandle中,做了如下操作:
invokeForRequest调用Controller后获取返回值到returnValue中- 判断
returnValue是否为空,如果是则继续判断0RequestHandled是否为True,都满足的话设置requestHandled为true - 通过
handleReturnValue根据返回值的类型和返回值将不同的属性设置到ModelAndViewContainer中。
1 | public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { |
下面分析handleReturnValue方法。
selectHandler根据返回值和类型找到不同的HandlerMethodReturnValueHandler,这里得到了ViewNameMethodReturnValueHandler,具体怎么得到的就不分析了。- 调用
handler.handleReturnValue,这里得到不同的HandlerMethodReturnValueHandler处理的方式也不相同。
1 | public void handleReturnValue( Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { |
ViewNameMethodReturnValueHandler#handleReturnValue
- 判断返回值类型是否为字符型,设置
mavContainer.viewName - 判断返回值是否以
redirect:开头,如果是的话则设置重定向的属性
1 | public void handleReturnValue( Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { |
通过上面的操作,将返回值设置为mavContainer.viewName,执行上述操作后返回到RequestMappingHandlerAdapter#invokeHandlerMethod中。通过getModelAndView获取ModelAndView对象。

1 | protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { |
getModelAndView根据viewName和model创建ModelAndView对象并返回。
1 | private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { |
获取视图
获取ModelAndView后,通过DispatcherServlet#render获取视图解析器并渲染。
1 | protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { |
获取视图解析器在DispatcherServlet#resolveViewName中完成,循环遍历所有视图解析器解析视图,解析成功则返回。
1 | protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { |
在Demo中有5个视图解析器。

本以为会在ThymeleafViewResolver中获取视图,实际调试发现ContentNegotiatingViewResolver中已经获取到了视图。

ContentNegotiatingViewResolver视图解析器允许使用同样的数据获取不同的View。支持下面三种方式。
使用扩展名
http://localhost:8080/employees/nego/Jack.xml 返回结果为XML
http://localhost:8080/employees/nego/Jack.json 返回结果为JSON
http://localhost:8080/employees/nego/Jack 使用默认view呈现,比如JSPHTTP Request Header中的Accept,Accept 分别是 text/jsp, text/pdf, text/xml, text/json, 无Accept 请求头
使用参数
http://localhost:8080/employees/nego/Jack?format=xml 返回结果为XML
http://localhost:8080/employees/nego/Jack?format=json 返回结果为JSON
ContentNegotiatingViewResolver#resolveViewName
getCandidateViews循环调用所有的ViewResolver解析视图,解析成功放到视图列表中返回。同样也会根据Accept头得到后缀并通过ViewResolver解析视图。getBestView根据Accept头获取最优的视图返回。
1 | public View resolveViewName(String viewName, Locale locale) throws Exception { |
视图渲染
得到View后,调用render方法渲染,也就是ThymleafView#render渲染。render方法中又通过调用renderFragment完成实际的渲染工作。
漏洞复现
我这里使用**spring-view-manipulation**项目来做漏洞复现。
templatename
漏洞代码
1 |
|
POC
1 | __$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x |
漏洞原理
在renderFragment渲染的过程中,存在如下代码。
- 当TemplateName中不包含
::则将viewTemplateName赋值给templateName。 - 如果包含
::则代表是一个片段表达式,则需要解析templateName和markupSelectors。
1 | protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { |
比如当viewTemplateName为welcome :: header则会将welcome解析为templateName,将header解析为markupSelectors。

上面只是分析了为什么要根据::做不同的处理,并不涉及到漏洞,但是当视图名中包含::会执行下面的代码。
1 | fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}"); |
在StandardExpressionParser#parseExpression中会通过preprocess进行预处理,预处理根据该正则\\_\\_(.*?)\\_\\_提取__xx__间的内容,获取expression并执行execute方法。
1 | private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\\_\\_(.*?)\\_\\_", 32); |
execute经过层层调用最终通过SPEL执行表达式的内容。


也就是说这个漏洞本质上是SPEL表达式执行。
URI PATH
下面的情况也可以触发漏洞,这个可能很多师傅和我一样都觉得很奇怪,这个并没有返回值,理论上是不会执行的。
1 |
|
前面我们分析了SpingMVC视图解析的过程,在解析视图首先获取返回值并封装为ModleAndView,而在当前当前环境中并没有返回值,按理说ModelAndView应该为空,为什么还能正常得到ModleAndView呢?
原因主要在DispatcherServlet#doDispatch中,获取ModleAndView后还会执行applyDefaultViewName方法。
1 | protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { |
applyDefaultViewName中判断当ModelAndView为空,则通过getDefaultViewName 获取请求路径作为ViewName。这也是在urlPath中传入Payload可以执行的原因。
1 | private void applyDefaultViewName(HttpServletRequest request, ModelAndView mv) throws Exception { |
但是需要注意的是如果要在urlPath中传入payload,则不能有返回值,否则就不会调用applyDefaultViewName设置了。下面的方式将不会导致代码执行。
1 |
|
回显失败问题分析
当在URL PATH中使用下面的POC会拿不到结果。
1 | /doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x |
经过分析问题主要是在StandardExpressionParser#parseExpression,在preprocess预处理结束后还会通过Expression.parse进行一次解析,这里如果解析失败则不会回显。
1 | static IStandardExpression parseExpression(IExpressionContext context, String input, boolean preprocess) { |
使用上面的POC,parse的内容如下,这里可以看到::后没有内容,因此这里肯定是会失败的。

而在templatename那个Demo中,parse内容如下是::后是有内容的。所以能否回显的关键就是Expression.parse能否正常执行。

但是我们在URL PATH的POC中也设置了::.x为什么会被去掉呢?
在分析URL PATH这种方式能获取ModelAndView的原因时,我们分析过会在applyDefaultViewName中获取URL Path作为ModelAndView的name,这个操作在getViewName中完成,getLookupPathForRequest仅仅获取了请求的地址并没有对后面的.x做处理,处理主要是在transformPath中完成的。
1 | public String getViewName(HttpServletRequest request) { |

transformPath中通过stripFilenameExtension去除后缀,是这部分导致了.x后内容为空。
1 | protected String transformPath(String lookupPath) { |
stripFilenameExtension去除最后一个.后的内容,所以可以通过下面的方式绕过。
1 | /doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()%7d__::assadasd.asdas |
漏洞修复
配置 @ResponseBody
1 |
|
配置了ResponseBody注解确实无法触发,经过调试在applyDefaultViewName中ModelAndView是Null而非ModelAndView对象,所以hasView()会导致异常,不会设置视图名。

所以我们要分析创建ModelAndView对象的方法,也就是getModelAndView,这里requestHandled设置为True时会返回Null,而不会创建视图。

当我们设置了ResponseBody注解后,handler返回的是RequestResponseBodyMethodProcesser,所以这里会调用它的handleReturnValue,设置了RequestHandled属性为True。


配置RestController修复和这种方式类似,也是由于使用RequestResponseBodyMethodProcesser设置了RequestHandled属性导致不能得到ModelAndView对象了。

有小伙伴可能要问,上面只是讲的URL PATH中的修复,templatename中这种方式也能修复嘛?答案是肯定的,根本原因在设置了RequestHandled属性后,ModelAndView一定会返回Null。

通过redirect:
根据spring boot定义,如果名称以
redirect:开头,则不再调用ThymeleafView解析,调用RedirectView去解析controller的返回值
所以配置redirect:主要影响的是获取视图的部分。在ThymeleafViewResolver#createView中,如果视图名以redirect:开头,则会创建RedirectView并返回。所以不会使用ThymeleafView解析。

方法参数中设置HttpServletResponse 参数
1 |
|
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。
首先声明下这种方式只对返回值为空的情况下有效,也就是URL PATH的方式,下面我会解释一下原因。
设置了HttpServletResponse后也是设置requestHandled设置为True导致在applyDefaultViewName无法设置默认的ViewName。
但是它的设置是在ServletInvocableHandlerMethod#invokeAndHandle中。由于mavContainer.isRequestHandled()被设置为True,所以进入到IF语句中设置了requestHandled属性,但是这里的前提条件是returnValue为空,所以这种修复方法只有在返回值为空的情况下才有效。

requestHandled的属性设置在HandlerMethodArgumentResolverComposite#resolveArgument解析参数时,这里不同的传参方式获得的ArgumentResolver是不同的,比如没加HttpServletResponse 时得到的是PathVariableMethodArgumentResolver。

加上后会对HttpServletResponse也进行参数解析,解析后的结果为ServletResponseMethodArgumentResolver,在它的resolveArgument方法中,会设置requestHandled属性。


总结
Thymeleaf 模板注入和我理解的不太一样,之前以为这种模板注入应该是解析特定标签时候导致的问题。 从修复的角度来讲使用@ResponseBody或者@RestController更容易修复漏洞,而设置HttpServletResponse 有一定的局限性,对templatename的方式无用。