前言
其实本章的内容已经和 JAVA RMI
没有太大关系了 . 但是由于 Java
反序列化技术大量应用于 JRMI( Java远程方法调用 )
, JMX( Java管理扩展 )
, JMS( Java消息服务 )
中 , 因此 Java RMI
服务也算是 Java 反序列化漏洞的高发地 .
2015年1月28日 , Gabriel Lawrence 和 Chris Frohoff( Ysoserial
的作者 ) 两位前辈在 AppSecCali 2015 上发表了 Marshalling-Pickles 一文 , 其中就提到了反序列化漏洞 , 并且发布了 Ysoserial 等多种反序列化利用工具 , 拉开了 Java 反序列化漏洞的序幕 . 但是在当时并没有太多人注意到这个漏洞 .
同年11月6日 , FoxGlove Security
安全团队的 breenmachine 前辈发表 What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability 一文 , 文中强调了反序列化漏洞这个概念 . 并且阐述了如何利用 Java
反序列化漏洞和 Commons Collections
库实现远程代码执行 .
此外 , breenmachine 前辈还指出了 Java反序列化漏洞产生的原因 , 详细说明了该漏洞的辉煌战绩 --- 横扫当时市面上大部分主流的 JavaWeb
框架(WebLogic
, WebSphere
, JBoss
, Jenkins
, OpenNMS
) , 最后给出了漏洞修复方案 --- 删除存在漏洞的的文件 .
本文将从 Java 反序列化基础内容说起 , 直到理解这个传奇级别的 ACC(Apache Commons Collections
) 利用链 .
Java 序列化与反序列化
什么是 Java 序列化 / 反序列化
Java 序列化就是把一个 Java Object
变成一个二进制字节数组 , 即 byte[]
.
Java 反序列化就是把一个二进制字节数组(byte[]
) 变回 Java 对象 , 即 Java Object
.
-
在很多应用中 , 为了减轻内存压力或长期保存数据 , 会将部分对象进行序列化操作 , 使其脱离内存空间进入物理磁盘 . 当需要使用这些对象时 , 会将存储的字节数组进行反序列化操作 , 重构出对象.
-
在很多服务中( 例如 Java RMI 远程方法调用 ) , 客户端和服务端之间传输的是" 对象 " 而不是基本数据类型 . 此时会将对象进行序列化操作后 , 将字节数组传输给对方 . 由对方通过反序列化操作获取原对象 .
-
... ... ... ...
总而言之 , Java 序列化/反序列化的目的无非就是用于 " 数据存储 " 或 " 数据传输 " .
如何实现 Java 序列化 / 反序列化 .
Serializable
接口 / Externalizable
接口
-
Serializable
如果一个类要实现序列化操作 , 则必须实现
Serializable
接口 , 并且可以在类中设置serialVersionUID
属性 .Serializable
接口中没有方法和属性字段 , 仅用于标识序列化的语义 , 代表该类可以进行序列化/反序列化操作 .如果在序列化时发现某个对象不能被序列化( 例如在遍历某些图形对象时 ) , 则会抛出
NotSerializableException
异常并标识不可序列化对象的类 .在序列化或反序列化过程中需要进行特殊处理的类要实现下面三个方法 :
每个可序列化的类在序列化时都会关联一个版本号 , 这个版本号就是
serialVersionUID
属性 .在反序列化过程中会根据这个版本号来判断序列化对象的发送者和接收着是否有与该序列化/反序列化过程兼容的类 .( 简单的说就是序列化过程和反序列化过程都需要被序列化的类 , 通过
serialVersionUID
属性来判断这两个类的版本是否相同 , 是否是同一个类 ) . 如果不相同 , 则会抛出InvalidClassException
异常serialVersionUID
属性必须通过static final long
修饰符来修饰 .如果可序列化的类未声明
serialVersionUID
属性 , 则 Java 序列化时会根据类的各种信息来计算默认的serialVersionUID
值 . 但是 Oracle 官方文档强烈建议所有可序列化的类都显示声明serialVersionUID
值 .文档给出的原因是 : 默认的
serialVersionUID
计算会对类详细信息高度敏感 , 而类详细信息可能会根据编译器的实现而有所不同 . 因此可能在反序列化期间抛出意外的InvalidClassExceptions
异常 .
-
Externalizable
不仅可以通过继承
Serializable
接口来标识某个类是可序列化的 , 还可以通过继承Externalizable
接口来标识某个类是可序列化的 . 事实上 ,Externalizable
接口继承了Serializable
接口 .通过
Externalizable
接口实现序列化和反序列化操作会相对麻烦一点 , 因为我们需要手动编写writeExternal()
方法和readExternal()
方法 , 这两个方法将取代定制好的writeObject()
方法和readObject()
方法 .那什么时候会使用
Externalizable
接口呢 ? 当我们仅需要序列化类中的某个属性 , 此时就可以通过Externalizable
接口中的writeExternal()
方法来指定想要序列化的属性 . 同理 , 如果想让某个属性被反序列化 , 通过readExternal()
方法来指定该属性就可以了.此外 ,
Externalizable
序列化/反序列化还有一些其他特性 , 比如readExternal()
方法在反序列化时会调用默认构造函数 , 实现Externalizable
接口的类必须要提供一个Public
修饰的无参构造函数等等在 Externalizable 与 Serializable 一文中 , 简单的总结了
Serializable
和Externalizable
两个接口的区别及优劣 .
writeObject()
与 readObject()
在最简单的情况下 , 开发人员会通过继承 Serializable
类来实现序列化与反序列化 . 这种方法离不开 writeObject()
和 readObject()
两个方法.
从官方文档可以看出 , 在序列化和反序列化时需要实现上述两个方法 , 这两个方法的参数都是 ObjectOutputStream
类型的 . 来简单介绍下这个类 .
-
java.io.ObjectOutputStream
ObjectOutputStream
类会将支持java.io.Serializable
接口的 Java对象( 包括部分图形对象 ) 的原始数据类型写入OutputStream
中 . 然后使用ObjectInputStream
类读取( 重构 )对象 .可以通过使用 " 文件流 " 来实现对象的持久存储 , 也可以使用 " 网络套接字流 " 来传输对象 .
值得一提的是 , 这里
Output
和Input
都是针对 " 内存 " 来说的 .Output
即将 " 内存 " 中的 Java对象 传输到 " 文件流 " 或者 " 网络套接字流 " 中 , 而Input
则是将 " 文件流 " 或 " 网络套接字流 " 中的数据加载到 " 内存 " 中 , 这个点初学者容易搞混 , 需要重点关注 .java.io.ObjectOutputStream
类会通过writeObject()
方法将 Java 对象写入到数据流中 .'
-
java.io.ObjectOutputStream.writeObject( ObjectOutputStream stream )
writeObject()
方法会将所有 对象的类 , 类签名 , 非瞬态和非静态字段的值 写入到数据流中1. 什么是类签名 ?
在开发 JNI( Java Native Interface , Java 本地接口 ) 时需要调用 Java 层的方法或创建引用 , 此时就会用到 Java 签名机制 . 比如基本数据类型的签名如下所示 :
还有像
Ljava/lang/Class;
,Ljava/lang/String;
等都是类签名 , 这些字符串在解析 Java 序列化数据时会用到 . 详细内容可以参考 java的数据类型的签名 一文 .2. 什么是非瞬态 ?
瞬态变量(
Transient
) 是一个 Java 关键词 , 它用于标记类的成员变量在持久化到字节流时不要被序列化 ; 在通过网络套接字流传输字节流时 ,transient
关键词标记的成员变量不会被序列化 .因此 , 如果仅想序列化某个类中部分变量 , 除了可以通过继承
Externalizable
接口来指定需要序列化的成员变量 ; 还可以将其他变量添加transient
关键词 , 使得变量不被序列化 .写个测试 Demo , 内容如下所示 :
-
java.io.ObjectInputStream
通过
ObjectOutputStream
类可以将对象写入数据流中 , 而通过ObjectInputStream
类可以将数据流中的字节数组重构成对象 .ObjectInputStream
类在重构对象时会从本地 JVM 虚拟机中加载对应的类 , 以确保重构时使用的类与被序列化的类是同一个 . 也就是说 : 反序列化进程的 JVM 虚拟机中必须加载被序列化的类 .java.io.ObjectInputStream
类会通过readObject()
方法将数据流中的序列化字符串重构成 Java 对象.'
-
java.io.ObjectInputStream.readObject( ObjectInputStream stream )
readObject()
方法将读取序列化数据中各个字段的数据并分配给新对象的相应字段来恢复状态 . 需要注意的是 :readObject()
方法仅会反序列化 非静态变量 和 非瞬态变量 . 当读取到一个用transient
修饰符修饰的变量时 , 将直接丢弃该变量 , 不再进行后续操作 .此外 , 反序列化过程中 , 需要将重构的对象强制转换成预期的类型 , 比如
String
型变量就需要通过(String)
修饰符强制转换成原来的类型 .例如这里将
test.ser
中的序列化字符串进行反序列化操作 .可以看到 ,
String
对象被重构了 , 对象的值 "epicccal
" 也被还原了 .
完整的 Java 序列化 / 反序列化流程
通过上面的内容 , 已经可以得到一个完整的 Java 序列化 / 反序列化 流程 .
-
序列化过程
Man 类中存在变量
str
和方法 prt()
. 该类继承了Serializable
接口 , 表明该类可以被序列化 .ser
类用于执行 Java 序列化过程 . 将Man
类的实例对象序列化后输出到文件流中 , 并保存在/tmp/test.ser
文件中 .通过
xxd
工具查看/tmp/test.ser
文件 , 可以看到其中存在 Java 反序列化数据 , 反序列化过程执行成功 . -
反序列化过程
unSer
类打开文件流 , 读取/tmp/test.ser
文件的内容 , 并进行反序列化操作 . 注意要进行强制转换 .重构对象后 , 输出
Man
类str
变量的值 , 并且执行Man
类的prt()
函数 . 全部执行成功 , 说明Man
对象的相关字段全部被恢复了 .
至此 , 一个最简单的 序列化 / 反序列化 过程就结束了 .
Java 反序列化漏洞
那到底什么是 Java 反序列化漏洞呢 ? 其实问题就出在 Java 反序列化过程的关键函数 readObject()
上 . 这里需要仔细看一下 readObject()
方法 .
图中已经说的比较清楚了 , 官方允许用户在被序列化的类中重写 readObject()
方法 , 重写后的方法将负责在反序列化时重构当前类对象 .
用户只需要在重写的 readObject()
方法中实现 defaultReadObject()
方法 , 就可以确保反序列化过程正常执行 . 至于添加的代码内容 , 官方没有做任何限制 .
写个例子
可以看到 , 在不修改其他代码的情况下 , 通过重写 readObject()
方法时添加其他代码 , 可以使得被添加的代码在反序列化过程中被执行
也就是说 , 如果我们在其中添加恶意代码 , 那么恶意代码也将被反序列化并执行 . 比如下面这个 反弹Shell 的demo .
我们指定的恶意代码被成功执行 , 这就是 Java 反序列化漏洞一个最简单的案例 .
Java 序列化数据分析
您一定对 Java 序列化数据的含义非常感兴趣 , 这里也简单说明一下
通过 SerializationDumper 项目可以很方便的分析 Java 序列化数据 . 如下所示 :
-
首先通过一个
Py
获取到序列化数据的十六进制字符串 .aced0005737200034d616e00000000000000010200014c00037374727400124c6a6176612f6c616e672f537472696e673b787074000868656c6c6f202120
-
然后通过
SerializationDumper
工具解析十六进制字符串.
下面简单说一下 SerializationDumper
的输出结果是什么意思 .
-
ACED0005
0xAC
0xED
为 Java 序列化字符串魔术幻数 , 可以看作是 Java 序列化字符串的特征值 , 每次 Java 序列化字符串被创建时 , 都会添加该特征值 .0x00
0x05
为 Java 序列化版本号 , 一般来说版本号都为 :5
. 很少看到其他数字/ -
7372
0x73
即TC_OBJECT
, 代表下面内容是一个新的对象 .0x72
即TC_CLASSDESC
, 类描述符 , 代表一个新类的开始这些属性的定义位于
java.io.ObjectStreamConstants
类中 , 可以参考 Source for java.io.ObjectStreamConstants . -
00034D616E
这里会先输出类名的长度 , 然后才是类名的值 .
例如
0x0003
代表类名长度为 3 个字符 , 然后0x4D 0x61 0x6E
代表类名为Man
. -
0000000000000001
然后是
serialVersionUID
属性的值 . 在编写代码时我将它指定为一个长整型数字" 1L " , 因此这里显示为0x000000000001
-
0002
这里是 Java 序列化属性标识符 , 即
classDescFlags
, 该属性字段的值为 5 个标记的算数和SC_WRITE_METHOD = 0x01
: 如果被序列化的类中重写了writeObject()
方法 , 则设置该标志 , 并使用TC_ENDBLOCKDATA
标记来终止该类的数据 .SC_BLOCK_DATA = 0x08
: 如果使用STREAM_PROTOCOL_2
将Externalizable
类写入流中 , 则设置该标志 .
这里用的不多 , 内容也比较复杂 , 关于Stream Protocol
以及BLOCK DATA
的内容可以参考 Object Serialization Stream Protocol , 这里不详细说明了 .SC_SERIALIZABLE = 0x02
: 如果被序列化的类继承了Serializable
接口 , 则设置该标志 , 并在反序列化时调用的类也需要继承Serializable
接口 , 并使用默认的反序列化机制 .SC_EXTERNALIZABLE = 0x04
: 如果被序列化的类继承了Externalizable
接口 , 则设置该标志 , 并在反序列化时调用的类也需要继承Externalizable
接口 . 并且在序列化/反序列化过程中要使用readExternal()
方法和writeExternal()
方法处理序列化数据 .SC_ENUM = 0x10
: 如果被序列化的类是枚举类型 , 则设置该标志 . 同时反序列化时调用的类也必须是枚举类型 .
这里用的不多 , 内容也比较复杂 , 关于枚举类型常量的序列化可以参考 Serialization of Enum Constants, 这里不详细说明了 . -
0001
这里是类中字段的个数 . 在 Java 序列化时 , 只有类中的属性字段会被序列化, 而 方法/函数 是不会被序列化的 .
由于
Man
类仅有一个str
字段 , 所以这里的值为 " 1 " . -
4c
这里是 " 类类签名 " . 即字段类型的类签名 . 注意 , 这里非常容易搞混 ! 是字段类型的类签名 , 不是字段值的类签名 .
也就是说 , 这里填写的是类类签名 , 而不是类签名(
实在有点绕 , 您多理解下.)弄明白之后就好理解了 , 这里需要填写 "
L
" , 即0x4C
. -
0003737472
这里就是类中属性字段的长度和值了 .
0x0003
为属性字段str
的长度 , 为 " 3 " .0x73 0x74 0x72
为属性字段str
的值 , 这个没啥好说的 . -
7400124c6a6176612f6c616e672f537472696e673b
有上面的基础 , 这里就放一起说了 .
0x74
标识下面将出现一个 String 类型 , 这也是在java.io.ObjectStreamConstants
类中定义的 .0x12
代表String
类型标准签名的长度 . 即 "Ljava/lang/String;
"的长度 , 一共 18 个字节 , 注意不要忘记 类类签名 "L
" 和 分号 ";
" .0x4C 0x6A 0x61 0x76 0x61 0x2F 0x6C 0x61 0x6E 0x67 0x2F 0x53 0x74 0x72 0x69 0x6E 0x67 0x3B
: 即 "Ljava/lang/String;
" . -
7870
0x78
: 为Block_data
的结束表示符号 .可以理解为一个原始数据块的结束 .0x70
: 用于在流中指定 Null 引用 . 因为这里字段的字段代表 父类的类描述符号 , 而Man
类仅继承了Serializable
接口 , 并未继承其他类 , 所以这里应指定为Null
. -
000868656c6c6f202120
0x0008
: 属性字段值的长度 , 即 "hello !
" 的长度 , 为 " 8 " .0x68 0x65 0x6C 0x6C 0x6F 0x20 0x21 0x20
: 即属性值 "hello !
" .
至此 , 这段序列化数据就分析完了 , 其实整体来看并不难 , 但是细化后还是有一些点存在疑问 . 这个之后会补充说明 . 个人认为可以根据 SerializationDumper
工具输出的层级关系来分析 , 会更加清楚 .
总结
篇幅原因 , 本章的内容就到这了 , 本来是准备研究一下 Apache Commons Collections
利用链的 , 现在看来只能拖到下一章了 .
整体的序列化流程不算太难 , 通过 SerializationDumper
工具也可以比较清晰的知道每一部分序列化数据分别对应什么内容 . 不过部分细节还需要深入研究下 .
有兴趣的师傅可以看一下 解析Serializable原理 一文 , 讲的挺好的 .
本章算是为 Java 反序列化开了个头 , 下一章将结合 Ysoserial 工具来谈一谈 Apache Commons Collections POP
利用链 .