RMI基本要素
RMIServer构成
- 一个继承了Remote类的接口,定义远程调用的函数。
- 一个实现该接口的类
- 一个主类来创建Registry,并将上面的实现了接口的类实例化后绑定一个地址,得到Server
Client
- 只需要调用Naming.lookup()来找远程注册的对象
Java是强类型语言
Naming.lookup()
是 Java RMI 提供的一个方法,用于从远程 RMI 注册表中查找远程对象。该方法返回的是一个通用的
Remote
类型的对象,因为Remote
是所有远程接口的父接口(标记接口)。也就是说,Naming.lookup()
不知道它具体返回的是什么类型的远程对象,只能返回一个通用的Remote
对象。因此,
Naming.lookup("rmi://localhost:1099/hello")
返回的对象是Remote
类型,但这只是一个父类类型的引用,实际的对象类型可能是你定义的远程接口类型,例如RMIServer.IRemoteHello
。
通信过程
- RMI Registry
- RMI Server
- RMI Client
代码实现相关
多重继承
java不允许多重继承,但是允许一个类继承一个类,然后实现多个接口。
就像Server里面实现的接口的类,其签名为:
1 | public class RemoteHello extends UnicastRemoteObject implements IRemoteHello |
同时接口IRemoteHello则继承RMI的父类Remote
1 | public interface IRemoteHello extends Remote |
可以看到Remote实际是一个标记接口(marker interface),它不包含任何方法,但是通过继承 Remote
,Java RMI 系统知道这个接口中的方法可以通过网络进行调用。
所以继承Remote接口的意义就是 IRemoteHello
成为一个 远程接口。所有继承自 Remote
的接口表示该接口中的方法可以通过 RMI 在远程调用。
攻击方法
攻击RMI Registry
限制:只能localhost源才能访问rebind,bind,unbind方法。
但是能用的是:list 和 lookup方法
1 | String[] s = Naming.list("rmi://localhost:1099"); |
lookup作用就是获得某个远程对象。
其实就是这段
那么,只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用
当然肯定不会这么简单
RMI利用codebase执行任意代码
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的
CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则
Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为
Example类的字节码。
这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗?
对,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去
CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。
不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:
安装并配置了SecurityManager
Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false
其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:
https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在
java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的
codebase ,不再支持从RMI请求中获取。
查看example.com的日志,可见收到了来自Java的请求 /RMIClient$Payload.class 。因为我们还没
有实际放置这个类文件,所以上面出现了异常:
我们只需要编译一个恶意类,将其class文件放置在Web服务器的 /RMIClient$Payload.class 即可。
在java序列化流里加入codebase
通过查看序列化流数据
1 | STREAM_MAGIC - 0xac ed |
这是一个 java.lang.reflect.Proxy 对象,其中有一段数据储存在 objectAnnotation 中:
0x000a556e6963617374526566000e3134302e3233382e33342e3231360000fa00276c0508063e8d45a
4462ec50000016d8d8d6357800101 ,记录了RMI Server的地址和端口。
再看RMI Call回来的数据包
可以看出我们的 codebase 是通过 [Ljava.rmi.server.ObjID; 的 classAnnotations 传递的。
所以,即使我们没有RMI的客户端,只需要修改 classAnnotations 的值,就能控制codebase,使其
指向攻击者的恶意网站。
classAnnotations(类注释
众所周知,在序列化Java类的时候用到了一个类,叫 ObjectOutputStream 。这个类内部有一个方法annotateClass
, ObjectOutputStream
的子类有需要向序列化后
的数据
里放任何内容,都可以重写这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。
比如,我们RMI的类 MarshalOutputStream 就将当前的 codebase 写入:https://github.com/JetBrains/jdk8u_jdk/blob/8db9d62a1cfe07fd4260b83ae86e39f80c0a9ff2/
src/share/classes/java/rmi/server/RMIClassLoader.java#L657
https://github.com/JetBrains/jdk8u_jdk/blob/8db9d62a1c/src/share/classes/sun/rmi/server/
LoaderHandler.java#L282
所以,我们在分析序列化数据时看到的 classAnnotations ,实际上就是 annotateClass 方法写入的
内容。
Java 序列化流格式详解
Java 序列化机制允许将对象的状态转换为字节流,以便保存到文件、数据库或通过网络传输。为了实现序列化,Java 定义了一套特定的二进制流格式,称为 序列化流格式(Serialization Stream Format)。理解这个格式对于深入理解 Java 序列化机制、调试序列化问题以及防范反序列化漏洞都有重要意义。
本文将详细解释序列化流格式中的各个组成部分,包括各个标记(Token)和数据结构。
一、序列化流的整体结构
序列化流由以下几个主要部分组成:
- Stream Header(流头部):标识序列化流的开始,包括魔数和版本号。
- Contents(内容):序列化的对象和数据。
1. Stream Header(流头部)
- Magic(魔数):前两个字节,固定值
0xAC ED
,用于标识这是一个 Java 序列化流。 - Version(版本号):接下来的两个字节,通常为
0x00 05
,表示序列化流的版本。
示例:
1 | Magic: 0xAC ED |
2. Contents(内容)
Contents 包含一个或多个 Content,每个 Content 可以是一个对象、块数据或控制标记。
二、详细结构解析
Contents
- Contents ::= Content [Contents Content]
也就是说,Contents 是由多个 Content 组成的序列。
Content
- Content ::= Object | BlockData
Content 可以是一个对象或者块数据。
Object
- Object ::= newObject | newClass | newArray | newString | newEnum | newClassDesc | prevObject | nullReference | exception | TC_RESET
Object 可以是以下类型之一:
- newObject:一个新的对象实例。
- newClass:一个新的类对象。
- newArray:一个新的数组对象。
- newString:一个新的字符串对象。
- newEnum:一个新的枚举对象。
- newClassDesc:一个新的类描述符。
- prevObject:对先前对象的引用。
- nullReference:空引用。
- exception:异常对象。
- TC_RESET:重置流上下文。
BlockData
- BlockData ::= blockdataShort | blockdataLong
BlockData 是序列化流中的块数据,可以是短块数据或长块数据。
三、各个标记和结构详解
1. Magic(魔数)和 Version(版本号)
- Magic:
0xAC ED
(16 进制),表示序列化流的开始。 - Version:
0x00 05
,表示序列化协议的版本。
2. TC_ 标记*
序列化流使用了一系列的标记(Token),以 TC_
开头,用于标识不同的类型和操作。
3. Object 类型详解
(1) newObject
- 语法:
TC_OBJECT classDesc newHandle classdata[]
- 解释:
- TC_OBJECT:标记,值为
0x73
,表示一个新对象的开始。 - classDesc:类描述符,描述对象的类信息。
- newHandle:新对象的句柄,用于引用此对象。
- **classdata[]**:对象的数据,包括父类的数据。
- TC_OBJECT:标记,值为
(2) newClass
- 语法:
TC_CLASS classDesc newHandle
- 解释:
- TC_CLASS:标记,值为
0x76
,表示一个新类对象。 - classDesc:类描述符。
- newHandle:新对象的句柄。
- TC_CLASS:标记,值为
(3) newArray
- 语法:
TC_ARRAY classDesc newHandle (int)length values[]
- 解释:
- TC_ARRAY:标记,值为
0x75
,表示一个新数组对象。 - classDesc:数组的类描述符。
- newHandle:新对象的句柄。
- length:数组长度。
- **values[]**:数组元素。
- TC_ARRAY:标记,值为
(4) newString
- 语法:
- 短字符串:
TC_STRING newHandle (utf)
- TC_STRING:标记,值为
0x74
。 - **(utf)**:使用 UTF-8 编码的字符串。
- TC_STRING:标记,值为
- 长字符串:
TC_LONGSTRING newHandle (long-utf)
- TC_LONGSTRING:标记,值为
0x7C
。 - **(long-utf)**:使用长 UTF-8 编码的字符串。
- TC_LONGSTRING:标记,值为
- 短字符串:
(5) newEnum
- 语法:
TC_ENUM classDesc newHandle enumConstantName
- 解释:
- TC_ENUM:标记,值为
0x7E
,表示一个新的枚举对象。 - enumConstantName:枚举常量的名称。
- TC_ENUM:标记,值为
(6) newClassDesc
- 语法:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo
- 解释:
- TC_CLASSDESC:标记,值为
0x72
,表示一个新的类描述符。 - className:类的完全限定名。
- serialVersionUID:类的序列化版本号。
- classDescInfo:类描述符信息。
- TC_CLASSDESC:标记,值为
(7) prevObject
- 语法:
TC_REFERENCE (int)handle
- 解释:
- TC_REFERENCE:标记,值为
0x71
,表示对先前对象的引用。 - handle:先前对象的句柄。
- TC_REFERENCE:标记,值为
(8) nullReference
- 语法:
TC_NULL
- 解释:
- TC_NULL:标记,值为
0x70
,表示一个空引用。
- TC_NULL:标记,值为
(9) exception
- 语法:
TC_EXCEPTION exceptionObject
- 解释:
- TC_EXCEPTION:标记,值为
0x7D
,表示一个异常对象。 - exceptionObject:异常对象的序列化数据。
- TC_EXCEPTION:标记,值为
(10) TC_RESET
- 语法:
TC_RESET
- 解释:
- TC_RESET:标记,值为
0x79
,表示重置流上下文。
- TC_RESET:标记,值为
4. BlockData 类型详解
(1) blockdataShort
- 语法:
TC_BLOCKDATA (unsigned byte)size (byte)[size]
- 解释:
- TC_BLOCKDATA:标记,值为
0x77
,表示块数据的开始。 - size:后续数据的长度,范围为 0 到 255。
- **(byte)[size]**:实际的数据字节。
- TC_BLOCKDATA:标记,值为
(2) blockdataLong
- 语法:
TC_BLOCKDATALONG (int)size (byte)[size]
- 解释:
- TC_BLOCKDATALONG:标记,值为
0x7A
,用于长度超过 255 的块数据。 - size:后续数据的长度,范围为 0 到 2^32-1。
- **(byte)[size]**:实际的数据字节。
- TC_BLOCKDATALONG:标记,值为
四、序列化过程中的句柄(Handle)
在序列化过程中,每个新创建的对象都会分配一个句柄,用于引用该对象。这有助于处理对象共享和循环引用。
- 句柄值的起始值:
0x7E0000
- 句柄值:每创建一个新对象,句柄值递增。
五、标记(Token)及其对应的十六进制值
标记名 | 值(十六进制) | 描述 |
---|---|---|
TC_NULL | 0x70 |
空引用 |
TC_REFERENCE | 0x71 |
对先前对象的引用 |
TC_CLASSDESC | 0x72 |
类描述符 |
TC_OBJECT | 0x73 |
新对象 |
TC_STRING | 0x74 |
新字符串(短字符串) |
TC_ARRAY | 0x75 |
新数组 |
TC_CLASS | 0x76 |
新类对象 |
TC_BLOCKDATA | 0x77 |
块数据(短) |
TC_ENDBLOCKDATA | 0x78 |
块数据结束 |
TC_RESET | 0x79 |
重置流上下文 |
TC_BLOCKDATALONG | 0x7A |
块数据(长) |
TC_EXCEPTION | 0x7D |
异常对象 |
TC_LONGSTRING | 0x7C |
新字符串(长字符串) |
TC_PROXYCLASSDESC | 0x7D |
代理类描述符 |
TC_ENUM | 0x7E |
新枚举对象 |
六、具体示例解析
1. 序列化一个字符串
当序列化一个字符串时,可能会使用 TC_STRING
或 TC_LONGSTRING
,取决于字符串的长度。
(1) 短字符串
- 语法:
TC_STRING newHandle (utf)
- 步骤:
- TC_STRING:
0x74
- newHandle:为字符串分配的新句柄。
- **(utf)**:字符串的 UTF-8 编码。
- TC_STRING:
(2) 长字符串
- 语法:
TC_LONGSTRING newHandle (long-utf)
- 步骤:
- TC_LONGSTRING:
0x7C
- newHandle:为字符串分配的新句柄。
- **(long-utf)**:字符串的长 UTF-8 编码。
- TC_LONGSTRING:
注意:当字符串长度超过 65535 个字节时,需要使用 TC_LONGSTRING
。
2. 序列化一个新对象
- 语法:
TC_OBJECT classDesc newHandle classdata[]
- 步骤:
- TC_OBJECT:
0x73
- classDesc:类的描述符,包括类名、序列化版本 UID、字段信息等。
- newHandle:为对象分配的新句柄。
- **classdata[]**:对象的实际数据,根据类的字段顺序写入。
- TC_OBJECT:
七、序列化流中的异常处理
1. exception
- 语法:
TC_EXCEPTION exceptionObject
- 解释:
- 当序列化过程中发生异常时,可以使用
TC_EXCEPTION
标记,后跟异常对象的序列化数据。
- 当序列化过程中发生异常时,可以使用
2. TC_RESET
- 语法:
TC_RESET
- 解释:
TC_RESET
用于重置序列化流的上下文,包括清除对象句柄表。这在需要重新开始对象引用计数时非常有用。
八、特殊标记的作用
1. TC_ENDBLOCKDATA
- 值:
0x78
- 作用:标记块数据的结束。在读取对象数据时,如果遇到
TC_ENDBLOCKDATA
,表示当前块数据或对象的结束。
2. TC_PROXYCLASSDESC
- 值:
0x7D
- 作用:表示一个代理类描述符,用于序列化实现了
java.lang.reflect.Proxy
的动态代理类。
九、序列化类描述符(ClassDesc)
在序列化对象时,需要先序列化其类描述符,包含以下信息:
- 类名(className):以 UTF-8 编码的完全限定类名。
- 序列化版本 UID(serialVersionUID):用于版本控制的长整型值。
- 类描述符信息(classDescInfo):
- 类标志(classFlags):指示类的序列化特性,如是否有
writeObject
方法。 - 字段描述符(fieldDesc[]):类的字段信息。
- 方法数据:可选,包括
writeObject
和readObject
方法的数据。
- 类标志(classFlags):指示类的序列化特性,如是否有
十、反序列化时的注意事项
句柄引用:在反序列化过程中,需要维护对象句柄表,以正确处理对象引用和共享。
类的加载:反序列化时,JVM 会尝试加载对应的类。如果类不存在,或版本不匹配,可能会导致异常。
安全风险:反序列化不可信数据可能导致安全漏洞,如反序列化漏洞。因此,反序列化时应确保数据的可信性。
十二、参考资料
- 官方文档:Java 序列化规范
About this Post
This post is written by void2eye, licensed under CC BY-NC 4.0.