java io
主要分为两个部分,节点流(低级I/O)和包装流(高级I/O)
节点流是直接从数据源读取或写入数据,核心类是InputStream
和OutputStream
,它们是所有节点流的基类。
- InputStream:所有字节输入流的基类,提供了基本的读取字节的功能。
- OutputStream:所有字节输出流的基类,提供了基本的写入字节的功能。
节点流的一些常见子类包括:
- FileInputStream:从文件中读取字节。
- FileOutputStream:向文件写入字节。
- ByteArrayInputStream:从字节数组中读取字节。
- ByteArrayOutputStream:向字节数组写入字节。
包装流,也称为高级流或过滤流,是建立在节点流的基础上的,提供了更高级的功能,如自动处理字符编码和解码、提供缓冲等。包装流的核心类是Reader
和Writer
。
序列化和反序列化基本概念
序列化:java对象——>字节序列,输出到java外部,依靠java的输出,依靠 java.io.ObjectOutputStream 类的 writeObject()
方法,序列化之前会检查是否实现了Serializable接口
transient 标识的对象成员变量不参与序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException{
// FileOutputStream 负责将数据写入文件中
// ObjectOutputStream 是 Java 的一个类,用于将对象序列化为字节流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa", 25);
System.out.println(person);
serialize(person);
}
}
|
反序列化:字节序列——>java对象,输入到java内部,依靠java的输入,依靠java.io.ObjectInputStream 类的 readObject
方法。被反序列化的类必须存在,serialVersionUID 值必须一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package org.example;
import java.io.*;
public class UnserializationTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object obj=ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
|
为什么要序列化?
程序运行结束之后,程序里的对象都会被删除,并且对象构成表复杂,不方便传输,因此想到要将这些对象进行序列化成一串数据,方便传输和数据持久化。
php中提供了序列化和反序列化的api:serialize和unserialize。但是java中没有这种api,需要自己写过程。
想要自定义对象的序列化/反序列化过程,可以重写writeObject/readObject方法:
1
2
3
4
5
6
7
8
9
10
|
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 调用默认的反序列化机制
ois.defaultReadObject();
// 自定义反序列化逻辑
// 例如,执行打开计算器程序命令
Runtime.getRuntime().exec("calc");
Runtime.getRuntime().exec("open -a Calculator");
}
}
|
java.io.Serializable 接口
序列化在 Java 中是通过 java.io.Serializable
接口来实现的,该接口没有任何方法,是一个空接口,只用于标识该类可以被序列化。
实现此接口的类都应生成一个 serialVersionUID常量。
serialVersionUID常量:每个可序列化的类在序列化的时候都会关联一个版本号,即serialVersionUID,在反序列化过程中,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出序列化版本不一致的异常InvalidCastException。
serialVersionUID 属性必须通过 static final long 修饰符来修饰,官方建议显式声明,如果未显式声明,序列化时会自动计算
Externalizable接口是继承自Serializable接口的接口,使用起来相对比较麻烦——需要手动编写 writeExternal()方法和readExternal()方法 , 这两个方法将取代定制好的 writeObject()方法和 readObject()方法。
那什么时候会使用 Externalizable 接口呢 ? 当我们仅需要序列化类中的某个属性 , 此时就可以通过 Externalizable 接口中的 writeExternal() 方法来指定想要序列化的属性。同理,如果想让某个属性被反序列化,通过 readExternal() 方法来指定该属性就可以了。
一般还是使用Serializable接口。
要使一个类可序列化,需要让该类实现 java.io.Serializable 接口,告诉 Java 编译器这个类可以被序列化,例如:
1
2
3
4
5
|
import java.io.Serializable;
public class Se implements Serializable {
}
|
反序列化漏洞成因
要服务端反序列化数据,readObject中代码会自动执行,这就给予了攻击者在服务器上运行代码的能力。
当用户能控制序列化数据的时候,服务端又有反序列化操作时,就会引发反序列化漏洞。
可能的形式:
-
readObject直接调用危险方法,现实中几乎不可能。通常是要找到一条gadget,通过构造,最终在某个重写的readObject中执行命令。

-
入口类参数包含可控类,可控类里有危险方法,readObject时调用
-
入口类参数包含可控类,可控类调用其他含危险方法的类,readObject时调用
入口类,这里是指满足以下几个条件的类:继承Serializable;重写了readObject;参数类型宽泛;最好jdk自带
比如HashMap
- 构造函数/静态代码块等类加载时隐式执行,类加载的时候也有可能执行代码
POP Gadgets:带入序列化数据之后,经过一系列调用的代码链。POP,指的是Property-Oriented Programming,面向属性编程;Gadgets,是小工具的意思。当确定了可以带入序列化数据的入口之后,就要寻找对应的pop链。
执行类 sink:最重要的一部分,执行代码的地方,rce、ssrf、写文件等
一些基础库存在危险:
1
2
3
4
5
6
7
8
9
10
11
12
|
commons-fileupload 1.3.1
commons-io 2.4
commons-collections 3.1
commons-logging 1.2
commons-beanutils 1.9.2
org.slf4j:slf4j-api 1.7.21
com.mchange:mchange-commons-java 0.2.11
org.apache.commons:commons-collections 4.0
com.mchange:c3p0 0.9.5.2
org.beanshell:bsh 2.0b5
org.codehaus.groovy:groovy 2.3.9
org.springframework:spring-aop 4.1.4.RELEASE
|
如何发现反序列化漏洞?
白盒来看,可能出现的场景:导入模板文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘、或DB存储等业务场景
1、找反序列化输入点,可以检索源码中对反序列化函数的调用:
1
2
3
4
5
6
7
|
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject
|
2、检查应用的Class Path中是否包含危险基础库
3、检查涉及命令、代码执行的代码区域
4、如果包含危险库,使用ysoserial进行攻击复现
反射
反射:在运行状态中,对于任意一个类都能知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为java语言的反射机制。
正射:当需要用的某个类的时候,会先去了解这个类是干嘛的,然后再new对象,再进行操作。
反射:一开始并不知道要初始化的类对象,就无法用new来创建对象。照镜子,一照全出来了。作用:让java具有动态性,比如修改已有对象的属性、动态生成对象、动态调用方法、操作内部类和私有方法。
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
50
51
52
53
54
55
56
57
58
59
60
61
|
package org.example;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionTest {
public static void main(String[] args) throws Exception {
Person person = new Person();
// 0. 反射就是操作Class
Class c = person.getClass();
// 1. 从原型class里面实例化对象
Constructor personconstructor = c.getConstructor(String.class, int.class);
Person p = (Person) personconstructor.newInstance("John", 24);
System.out.println(p);
// 输出:Person{name='John',age=24}
// 2. 获取类的属性-Field
Field[] personfileds = c.getFields();
for (Field f : personfileds) {
System.out.println(f);
}
// 输出:public java.lang.String org.example.Person.name
System.out.println("-----------------");
// 加了Declared 可以获取私有的
Field[] personfileds_1 = c.getDeclaredFields();
for (Field f1 : personfileds_1) {
System.out.println(f1);
}
// 输出:public java.lang.String org.example.Person.name
// private int org.example.Person.age
// 修改属性
Field namefield = c.getField("name");
namefield.set(p, "XXXX");
System.out.println(p);
// 输出:Person{name='XXXX',age=24}
// 修改私有变量的属性
Field namefield1 = c.getDeclaredField("age");
// 给修改权限
namefield1.setAccessible(true);
namefield1.set(p,18);
System.out.println(p);
// 输出:Person{name='XXXX',age=18}
System.out.println("-----------------");
// 调用类里的方法
Method[] personmethod = c.getMethods();
for(Method m : personmethod) {
System.out.println(m);
}
// 需要传入接收参数的类型
Method personm = c.getMethod("action",String.class);
personm.invoke(p,"sdhsc");
}
}
|
要调用一个方法的steps:
1、获取方法的Method对象
2、invoke调用方法,传入对象和参数
invoke的作用是执行方法,它的第一个参数是:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类
反射在反序列化漏洞中的应用:
- 定制需要的对象
- 通过invoke调用除了同名函数以外的函数
- 通过Class类创建对象,引入不能序列化的类
动态代理
代理:是java中的一种设计模式,为其他对象提供代理来控制对这个对象的访问。代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行功能的附加与增强。小明要租房,遇见中介,中介就是代理。
代理类和被代理类应该共同实现一个接口,或是共同继承某个类。所有接口类型的变量是通过某个实例向上转型并赋值给接口类型变量的。如果想要不编写实现类,直接在运行期创建某个接口的实力的话,就需要用到动态代理。
静态:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 定义接口
public interface Hello {
void morning(String name);
}
// 编写实现类
public class HelloWorld implements Hello {
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}
// 创建实力
Hello hello = new HelloWorld();
hello.morning("Bob");
|
动态代理:先定义好接口,但不编写实现类,而是借助Proxy.newProxyInstance()
创建接口对象。需要传入的参数包括三部分:类加载器,要代理的接口,要做的事情。
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
|
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// 实现接口的方法调用
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
// 将返回的Object强制转型为接口
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口数组,至少一个
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
|