spring-boot-thymeleaf-ssti
spring-boot下的thymeleaf模板注入挺有趣的,本文尝试对该漏洞一探究竟。
https://github.com/veracode-research/spring-view-manipulation
本文使用该项目给出的demo进行调试分析,其中,spring-boot
版本为2.2.0.RELEASE
,
启动
自动装配
首先,我们知道,在配置好spring-boot情况下,加入如下thymeleaf的mvn依赖,就可以实现thymeleaf的自动配置。
1 | <dependency> |
thtmyleaf
的自动配置类为org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
排序viewResolvers
我们可以看到,在o.s.w.s.v.ContentNegotiatingViewResolver#initServletContext
方法中,对viewResolvers
进行初始化,初始化后包含了多个视图解析器(模板引擎),包括 BeanNameViewResolver
、ViewResolverComposite
、InternalResourceViewResolver
、ThymeleafViewResolver
。
查看ThymeleafViewResolver
源码,发现其order
值为Integer.MAX_VALUE
,通过sort
方法排序过后的结果如下图所示(BeanNameViewResolver
也是Integer.MAX_VALUE):
具体代码流可以参考这里的堆栈信息:
1 | getOrder:282, ThymeleafViewResolver (org.thymeleaf.spring5.view) |
视图解析
获得视图解析器
org.springframework.web.servlet.DispatcherServlet#render
:用户发起的请求触发的代码会走到这里获取视图解析器,随后从resolveViewName
获得最匹配的视图解析器。
org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName
:在该方法中,先是通过getCandidateViews
筛选出resolveViewName
方法返回值不为null(即有效的)的视图解析器;之后通过getBestView
方法选取“最优”的解,getBestView
中的逻辑简而概之,优先返回重定向的视图动作,然后就是根据用户HTTP请求的Accept:
头部字段与candidateViews
数组中视图解析器的排序获得最优解的视图解析器,而前面所讲到的viewResolvers
的排序正是参与决定了这一排序决策。
1 | resolveViewName:227, ContentNegotiatingViewResolver (org.springframework.web.servlet.view) |
获得视图解析器名称
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
:该方法为获取视图名称的关键点,其中首先会调用invokeAndHandle
(下面会讲该方法内的逻辑),之后返回getModelAnView
方法执行结果的返回值。
o.s.w.s.m.m.a.ServletInvocableHandlerMethod#invokeAndHandle
:在该方法中,会尝试获取前端控制器的return返回值,也就是说,如果前端Controller返回值中直接拼接了用户的输入,相当于控制了该视图名称;另外,当用户自定义的Controller方法的入参中添加了ServletResponse
,这里的invokeForRequest
中会触发ServletResponseMethodArgumentResolver#resolveArgument
将mavContainer
的requestHandled
设置为true
,而mavContainer.isRequestHandled()
为true
导致了getModelAndview(...)
返回值为null,也就不会有后面的漏洞触发流程。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getModelAndView
:如果mavContainer.isRequestHandled()
为true
,直接返回null。
org.springframework.web.servlet.DispatcherServlet#applyDefaultViewName
:另外,如果前端Controller的方法返回值为null,即void方法类型,前面的流程无法拿到视图名称,后面会调用applyDefaultViewName
方法将URI路径作为视图名称。
org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#transformPath
:在将URI设置为视图名称的代码流程中,调用了该方法对URI进行格式调整,其中包括去除URI扩展名称。
1 | ... |
使用视图解析器
org.thymeleaf.spring5.view.ThymeleafView#renderFragment
:这里是漏洞触发的关键逻辑点之一,如果用户的输入拼接到了视图名称中,即控制了viewTemplateName
变量。通过浏览代码,我们可以了解到,首先视图模板名称中需要包含::
字符串,否则不会走入表达式执行代码中。
上图的o.t.s.e.StandardExpressionParser#parseExpression
不重要,我们这里略过,其随后会走到org.thymeleaf.standard.expression.StandardExpressionPreprocessor#preprocess
方法:这里的input
变量就是上面viewTemplateName
前后分别拼接了~{
、}
后的字符串,随后这里使用PREPROCESS_EVAL_PATTERN
正则对input
进行匹配,正则内容为\_\_(.*?)\_\_
,随后获取正则命中后的元组内容,即非贪婪匹配的.*?
,随后讲该元组内容传入parseExpression
方法并在这里触发了EL表达式代码执行。
POC构造
由触发的代码流程梳理可以得出触发表达式的条件:
①用户传入的字符串拼接到了Controller方法的返回值中且返回的视图非重定向(前面流程可用知晓,重定向优先级最高),或URI路径拼接了用户的输入且Controller方法参数中不带有ServletResponse
类型的参数;
②视图引擎名称中需要包含::
字符串;
③被执行表达式字符串前后需要带有两个下划线,即__${EL}__
;
④如果POC在URI中,由于URI格式化的原因且我们的POC中带有.
符号,所以需要在URI末尾添加.
。
于是,我们可以构造出与作者有所差异的POC
1 | POST /path HTTP/1.1 |
如果我们要利用该漏洞干点啥,建议还是结合BCEL一类的方式来利用更加方便(不过JDK251后BCEL使用不了),win弹计算器:
1 | POST /path HTTP/1.1 |
结语
spring-boot的自动化配置为开发部署带来了极大的便捷,但这也对我们深入底层问题提搞了学习成本。而该模板注入问题十分巧妙,令人深思,另外笔者也为自研的AST代码扫描器无法扫描该种漏洞感到蛋疼。