获取中...

-

Just a minute...

在学习URLDNS链以及之后的cc链之前 我们得先掌握java反序列化的基础知识

序列化和反序列化

序列化
把一个java对象转换成字节序列以便存储或者通过网络传输
反序列化
把字节序列还原成java对象

1
2
序列化:对象->字符串
反序列化:字符串->对象

为什么要进行序列化和反序列化

  • 保存对象状态:将对象存入磁盘/缓存 以便之后恢复
  • 网络传输:客户端和服务端通信的时候 对象通过socket(套接字)传输
  • 对象复制/传递:在不同模块之间传递对象需要序列化

JAVA函数(writeObject/readObject)实现序列化和反序列化

通过下面一个例子就可以理解这两个函数在序列化和反序列化之中的利用
构造恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package java_Serializable;  

import java.io.Serializable;

public class User implements Serializable{ //注意这个Serialiazble接口 这是序列化标志
private static final long serialVersionUID = 1L;

private String username;
private int age;

public User(String username,int age){
this.username = username;
this.age = age;
}

public String toString(){
return "User{name=" +username + ", age=" + age + "}";

}
}

构造序列化类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package java_Serializable;  

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String[] args)throws Exception{
User user=new User("admin",18);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.obj"));
oos.writeObject(user);
oos.close();

System.out.println("序列化成功 user.obj已生成");
}
}

构造反序列化类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package java_Serializable;  

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class UnSerializeDemo {
public static void main(String[] args)throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("user.obj"));
Object obj=ois.readObject();
ois.close();

System.out.println("反序列化成功");
System.out.println(obj);
}
}

实例分析

接下来我们来解释一下导入的这几个包以及代码的运行逻辑

1
2
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

java.io.FileOutputStream:文件字节输出流 负责把数据写入到磁盘文件
java.io.ObjectOutputStream:对象输出流 可以提供把java对象转换成字节流需要的框架 并配合前者写入文件

1
2
import java.io.FileInputStream;
import java.io.ObjectInputStream;

java.io.FileInputStream:文件字节输入流 从磁盘文件读取字节数据
java,io,ObjectInputStream:对象输入流 提供将java对象从字节流之中恢复的结构 配合前者使用

重头戏就是这一段代码

1
2
3
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.obj"));  
oos.writeObject(user);
oos.close();

new FileOutputStream("user.obj")这一段是建立一个和文件user.obj连接的字节流 类似C语言之中的fopen 将其嵌套于new ObjectOutputStream之中 此时这个字节流就可以传递java的序列化内容 但是此时就像开放了两国外交关系 也开放了商道 但是商品还没有生产出来
此时调用writeObject()将对象转换成序列化内容 就好比生产了商品 此时就可以贸易往来 也就是将这一段序列化内容传入user.obj之中
最后需要将这一段字节流关闭

同样的道理 反序列化过程也是如此

1
2
3
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("user.obj"));  
Object obj=ois.readObject();
ois.close();

此处不再过多解释
值得一提的是readObject的递归性 这也是后续cc链的核心基础:反序列化一个对象的时候 其引用的子对象也会被递归反序列化 并且每个子对象的readObject钩子都会被执行

反序列化漏洞利用

虽然我们前面学习了如何序列化和反序列化 但是实际上如果只是简单的再恶意代码之中加入我们想要执行的恶意代码是无法成功的
像这样

1
2
3
4
5
6
class EvilObject implements Serializable {
private String cmd;
public EvilObject(String cmd) {
this.cmd = cmd;
Runtime.getRuntime().exec(cmd);
}

这里的exec是无法执行的
这是因为反序列化不是重新执行构造的过程
当你正常new一个对象的时候

1
EvilObject obj = new EvilObject("calc");

JVM会执行构造方法 如果构造方法里面有exec 就会执行 整个逻辑大概像下面这样:

1
2
3
4
5
1. JVM 在堆上分配内存空间
2. 调用构造方法 EvilObject(String cmd)
└── 执行 this.cmd = cmd
└── 执行 Runtime.getRuntime().exec(cmd) ← 这里会执行!
3. 返回对象引用

但是反序列化完全绕过构造方法 直接通过JVM内部的底层机制分配一块内存 然后把从字节流之中读出的字段值粘贴到这块内存的对应位置上 写成一个对象 整个过程没有任何java层面的方法被主动调用

1
Object obj = ois.readObject();

整个运行逻辑大概是这样的

1
2
3
4
5
6
7
8
1. JVM 从字节流中读取类描述信息(类名 字段名 字段值等)
2. JVM 在堆上分配内存空间(注意此时还没有调用任何构造方法)
3. JVM 通过反射,直接从字节流中读取字段值,写入内存中的对应位置
└── 相当于:field.set(obj, valueFromStream)
4. JVM 检查该类是否定义了 private void readObject(ObjectInputStream) 方法
├── 如果有:反射调用它
└── 如果没有:什么都不做 直接返回
5. 返回对象引用

所以上面我们写的哪个恶意类如果直接反序列化 其中的恶意代码根本不会被执行

那么我们应该通过反序列化执行我们的恶意代码呢
我们来看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package java_Serializable;  

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class EvilObject implements Serializable {
private static final long serialVersionUID = 1L;

private String cmd;

public EvilObject(String cmd) {
this.cmd = cmd;
}

// 重写readObject,反序列化时自动执行
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 让反序列化按照默认方法执行读取
Runtime.getRuntime().exec(cmd); // 此处就有潜在RCE风险
}
}

在java之中为了满足一些需求(字段验证等)提供了一个“钩子” :如果类中定义了 private void readObject(ObjectInputStream in) 那么在反序列化恢复字段值之后/之前 就会调用这个钩子 这就是我们说的重写readObject

1
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

当你重写的时候 在反序列化类之中再次调用readObject的时候

1
2
ObjectInputStream ois = new ObjectInputStream(...);
Object obj = ois.readObject(); // 假设 obj 的实际类型是 EvilObject

代码的执行逻辑是

  1. 从输入流之中读取一个类的描述信息
  2. 确认当前JVM之中是否存在这个类 不存在就尝试加载
  3. 通过反射检查这个类是否定义了一个名未readObject参数未ObjectInputStream修饰符未private的方法
  4. 找到之后 框架会在恢复完字段值之后 反射调用这个方法
  5. 未找到 只执行默认的字段恢复逻辑 不调用任何额外方法

所以

  • 没有重写 = 框架执行默认行为(只恢复字段)。

  • 重写了 = 框架执行默认行为 + 额外调用你写的 readObject 方法(因为你给了它一个明确的钩子)

1
2
3
4
5
6
7
8
9
10
11
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
// 自定义序列化逻辑
out.defaultWriteObject(); // 可选,先执行默认序列化
// 然后做点别的事...
}

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
// 自定义反序列化逻辑
in.defaultReadObject(); // 可选,先执行默认反序列化
// 然后做点别的事...
}

defaultReadObject/defaultWriteObject讲解

在这里我们还要说明一下这两个函数
我们前文之中说到 这两个方法是让JVM以默认的方式将对象存储成字节流或从字节流之中恢复对象 就以我们重写的readObject举例子吧

假设我们不写这个defaultReadObject

1
2
3
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
Runtime.getRuntime().exec(cmd); // 此处就有潜在RCE风险
}

那么此时的cmd指尚未被恢复 还是字节流 根本没有实际的值让我们将其作为参数执行exec 于是就导致了出错 让AI生成了一个流程图大概就是这样

1
2
3
4
5
6
7
8
9
10
1. JVM 分配内存(对象诞生,此时 cmd = null)
2. JVM 反射调用你写的 readObject(ObjectInputStream in) ← 你进入方法了!
|-- 此时,cmd 仍然为 null
|-- [关键点] 如果你现在直接执行 Runtime.getRuntime().exec(cmd),会报错(空指针)
|
|-- 执行 in.defaultReadObject(); ← 这一行代码执行完,JVM 才从流里读出数据,赋给 cmd
| (此时 cmd 才变成流里保存的 "calc.exe")
|
|-- 执行 Runtime.getRuntime().exec(cmd); ← 此时 cmd 有值了,成功执行!
3. 方法结束,返回对象

在开发者的设计初衷之中 这个方法是用于数据校验和迁移的 比如说下列场景

1
2
3
4
5
private void readObject(ObjectInputStream in) throws ... {
in.defaultReadObject();
//数据恢复后进行校验 如果年龄是负数 强行改成 18
if (this.age < 0) { this.age = 18; }
}

但是在我们的恶意攻击之中 就可以被我们用来恢复字段

那么现在对于这些知识都了解的差不多了 可以尝试攻击
我们将序列化类改成以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package java_Serializable;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String[] args) throws Exception {
// 改为创建 EvilObject
EvilObject evil = new EvilObject("calc.exe"); // Windows 弹计算器
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("evil.obj"));
oos.writeObject(evil);
oos.close();

System.out.println("序列化成功 evil.obj 生成");
}
}

然后修改一下反序列化类 不用大改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package java_Serializable;  

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class UnSerializeDemo {
public static void main(String[] args) throws Exception {
// 文件名改成 evil.obj ObjectInputStream ois = new ObjectInputStream(new FileInputStream("evil.obj"));
Object obj = ois.readObject();
ois.close();

System.out.println("反序列化成功");
}
}

执行二者之后成功弹出计算器

补充知识点:transient

这里还需要补充一个知识点就是如果一个类的某个字段不想被序列化 可以使用transient修饰 反序列化之后该字段改为默认值(null,0,false)

1
private transient String password;

这个修饰符的设计初衷就是为了保护一些敏感信息
然而在攻击者的视角之中 这个修饰符只能保护JVM默认的序列化行为 却无法影响重写的writeObject/readObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SecureData implements Serializable {
private String data;
private transient String encryptedData; // 不会自动序列化

// 我们可以手动写入加密后的数据
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 正常写入 data
// 手动把加密后的数据写入流
String encrypted = encrypt(data);
out.writeUTF(encrypted);
}

// 手动读取并解密
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 正常恢复 data
// 手动读取加密数据并解密
String encrypted = in.readUTF();
this.encryptedData = decrypt(encrypted);
}
}

但是由于transient字段不会被自动序列化 所以攻击者无法直接控制transient字段的值 所以构造反序列化利用链的时候 攻击者会优先瞄准非transient字段攻击

小结

至此我们已经初步掌握了java反序列化的一些基本知识 核心性质有这些

  • 一个类必须实现java.io.Serializable标识接口 对象才能被序列化
  • 必须重写readObject方法才能植入恶意攻击
    还学习了一些细碎的知识点 比如transient可以阻止默认序列化
    我都下一篇文章将讲解URLDNS链 这是一个初步实际掌握java反序列化的好链子 也能帮助我们更好的理解后续的cc链 敬请期待

参考文章

相关文章
评论
分享
  • 社会工程学——cupp获取windows密码

    这其实本质是一个社会工程学问题至于为什么 在文章的最后想必大家也能理解假设现在有一个人短时间离开电脑之后并未锁屏 那么旁人就可以通过U盘拷贝重要密码文件的方式获取他的密码 密码文件获取windows并不会直接存储你的明文密码 它存的是...

    社会工程学——cupp获取windows密码
  • URLDNS链入门

    在一些测试环境之中 目标是否发生反序列化是无回显的 是否有一种办法能够测试目标是否能够发生反序列化呢?有的兄弟有的如果我们能让目标服务器通过反序列化发起一次DNS解析请求 那么在接收到请求的时候不就知道了吗那么这就是我们需要学习的UR...

    URLDNS链入门
  • 用租房视角学习动态代理

    关于这一部分的知识点 我们先从静态代理说起 静态代理什么是代理? 代理就是:不操作目标对象而是通过一个中间人(代理)来间接操作最常见的现实例子就是租房问题:你要租房子 不需要直接找到房东 而是通过租房中介 中介帮你去和房东沟通 那么...

    用租房视角学习动态代理
  • 动态加载字节码

    动态加载字节码是一种让JVM可以接收由程序在运行时动态产生的字节数组并将其装在的能力 本义是让程序变得更加灵活 但是也产生了一定的安全问题 核心方法在java.lang.ClassLoader类中有一个方法 1protected fi...

    动态加载字节码
  • 类加载机制漫谈

    解释java字节码为什么需要编译成class文件我们首先要理解JVMjava是是一种混合型语言有编译的步骤也有解释步骤我们编写的java文件需要被编译成class文件之后才能被JVM运行 什么是class文件传统编译型语言(C C++...

    类加载机制漫谈
  • 反射随笔

    反射概念在java之中 万物都是对象 包括“类本身也是一个对象”(属于java.lang.Class类的对象) 反射的本质就是在程序运行期间 通过操控这个Class对象 反向解析和操控它所代表的哪个类里面的内容(构造器,属性,方法) ...

    反射随笔
  • LangChain反序列化

    最近在打SCTF的时候遇到一个LangChain反序列化的题目 比较新颖 于是想着学习一下 LangChain介绍LangChain是一个用于构建大语言模型应用的开源框架 通过这个可以把常用的组件抽象成可串联的链(Chain)举个例...

    LangChain反序列化
  • filter-chain漏洞演示

    这个漏洞链被称为CNEXT 由安全员Charles Fol披露 核心思想是利用系统内置的看似无害的模块(流过滤器)的巧妙组合 在受限的web环境之中执行命令 原理讲解filter协议对于filter伪协议 我们肯定都不陌生这个伪协议允...

    filter-chain漏洞演示
  • 死亡绕过

    前言以及环境介绍本文是对p神文章谈一谈php://filter的妙用 | 离别歌知识点的学习 本文主要介绍使用filter伪协议进行死亡绕过的三种方式 测试环境:小皮面板测试代码:exit.php 12345<...

    死亡绕过
  • Phar反序列化漏洞演示

    前言我们在之前学习的反序列化漏洞 无一不是利用了unserialize()这个函数 但是我们没有去考虑 假设此时的题目并没有给我们这个函数 我们还能否实现反序列化呢 其实是可以的 在2018年的BlackHat 大会上 Sam Tho...

    Phar反序列化漏洞演示
Please check the parameter of comment in config.yml of hexo-theme-Annie!