上一篇记录了反序列化的一些利用链,而反序列化需要两个点,可控的反序列化输入点与存在利用链,本篇关注Java自带协议里的反序列化点...
RMI协议攻击
RMI基础
RPC远程过程调用在大型复杂应用是少不了的,Java实现的叫RMI(Remote Method Invocation),其底层可食用多种协议,本文只关注默认的JRMP协议实现。RMI由三部分组成,Server实现功能并把对象注册到Registry,Client从Registry查询到Server注册对象的位置与其交互,如下:

图里除了三个角色还有三种类,对于开发只需要定义类的接口,该接口需继承自Remote
接口,它也是空接口标记用,有了接口还需实现该接口的功能classImpl
,实现时为了能注册为远程对象以接收调用,它需要继承UnicastRemoteObject
类或者在注册前调用UnicastRemoteObject.exportObject()
封装它:
public interface HelloInterface extends java.rmi.Remote;
public class HelloImpl extends UnicastRemoteObject implements HelloInterface;
而classStub
和classSkel
是由RMIC
自动生成的,分别用于客户端和服务端作为代理去分发调用,其实在1.2后就不再生成Skel了,再之后Stub也不静态生成了,而都是使用动态代理生成,但是Registry和DGC还会使用旧的方式,之后会看到在服务分发时会有两条路径,只有这两个是走旧路径...
现在先来康康一些类或接口,它们大都在rt.jar!sun.rmi.*
下:
// 公共类
public interface Remote {}; // 空接口,标记用
public abstract class RemoteObject implements Remote, Serializable; // Object的Remote版
public abstract class RemoteServer extends RemoteObject; //
public class UnicastRemoteObject extends RemoteServer; // 用于导出一个使用JRMP的远程对象和与远程对象通信的Stub
abstract public class RemoteStub extends RemoteObject; // 静态生成的Stub的父类,用于提供远端通信
public interface Skeleton; // 静态生成的Skel的父类
// 引用,表示远端对象的句柄
public interface RemoteRef extends java.io.Externalizable; // 引用的通用接口,调用stub会作用到它的invoke方法上
public class UnicastRef implements RemoteRef; // 引用的一个具体实现,它内部使用了LiveRef
public class LiveRef implements Cloneable; // 它存储具体的引用,含Endpoint表示具体的地址(TCP的IP:PORT),ObjID表示一个远程对象的ID(可直接指或JVM分配)等信息
public interface ServerRef extends RemoteRef; // 服务端句柄
public class UnicastServerRef extends UnicastRef implements ServerRef, Dispatcher; // 服务端远程句柄实现
// 注册中心相关的类
public interface Registry extends Remote; // 注册中心的接口,注册中心是特殊的远程对象,可远程调用,因此也要像其他对象一样继承Remote
public class RegistryImpl extends RemoteServer implements Registry; // 注册中心的实现,这里并没有继承UnicastRemoteObject,但最终它在使用时会封装
public final class RegistryImpl_Skel implements Skeleton; // 注册中心的Skel
public final class RegistryImpl_Stub extends RemoteStub implements Registry, Remote; // 注册中心的Stub
RMI是从获取注册中心开始的,注册中心可能位于本地也可能为位于远端,Java提供了LocateRegistry
类用于获取注册中心实例或引用,它提供两类方法:
public static Registry createRegistry(int port); // 在本地创建/获取注册中心实例,返回为RegistryImpl对象
public static Registry getRegistry(String host, int port); // 在远程获取注册中心引用,返回为RegistryImpl_Stub对象
由于我们使用使只用它的方法(接口对就行),所以一般不会意识到它们的不同,实际上它们返回的对象不同,因此在对其进行操作时内部过程也会有区别,createRegistry
返回的会直接操作内部的map属性,而getRegistry
返回的需要使用Skel代理才能作用于真正的Registry,对一个远端访问,其分发流程如下:
public class UnicastServerRef extends UnicastRef implements ServerRef, Dispatcher{
public void dispatch(Remote obj, RemoteCall call) throws IOException {
try {
// read remote call header
ObjectInput in = call.getInputStream();
int num = in.readInt();
if (num >= 0) {
oldDispatch(obj, call, num); // 1.1(兼容模式)之前走的流程
return;
}
// 1.2之后走的流程
long op = in.readLong();
MarshalInputStream marshalStream = (MarshalInputStream) in;
marshalStream.skipDefaultResolveClass();
Method method = hashToMethod_Map.get(op); // 获取方法
Class[] types = method.getParameterTypes(); // 获取方法的参数类型
Object[] params = new Object[types.length];
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in); // 反序列化参数
}
Object result = method.invoke(obj, params); // 调用方法
ObjectOutput out = call.getResultStream(true); // 获取返回值
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out); // 序列化返回值
}
} catch (Throwable e) {
ObjectOutput out = call.getResultStream(false);
out.writeObject(e); // 若出现一场,将异常返回给调用者,这里可以回显结果,若能自定义异常内容的话,如能自定义类...
}
攻击注册中心
上面可见有两个流程,注册中心和分布式垃圾回收使用的是以前的方法,于是看oldDispatch
,如下:
public void oldDispatch(Remote obj, RemoteCall call, int op) throws IOException{
try {
ObjectInput in = call.getInputStream();
try {
Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel");
if (clazz.isAssignableFrom(skel.getClass())) {
((MarshalInputStream)in).useCodebaseOnly();
}
} catch (ClassNotFoundException ignore) { }
long hash = in.readLong();
unmarshalCustomCallData(in);
skel.dispatch(obj, call, op, hash); // 分发
} catch (Throwable e) {
ObjectOutput out = call.getResultStream(false);
out.writeObject(e);
}
}
接着还是要跟入具体的Skel,这里是RegistryImpl_Skel
,没有对应的源码,它是由rmic生成的,直接看反编译代码即可:
// 操作如下,
new Operation("void bind(java.lang.String, java.rmi.Remote)") // 0
new Operation("java.lang.String list()[]") // 1
new Operation("java.rmi.Remote lookup(java.lang.String)") // 2
new Operation("void rebind(java.lang.String, java.rmi.Remote)") // 3
new Operation("void unbind(java.lang.String)") // 4
public final class RegistryImpl_Skel implements Skeleton {
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
RegistryImpl var6 = (RegistryImpl)var1;
ObjectInput var8;
switch(var3) {
case 0:
RegistryImpl.checkAccess("Registry.bind"); // 是否有权访问,默认只能本地访问,之前没有或使用不正确
try {
ObjectInput var9 = var2.getInputStream();
String var7 = (String)var9.readObject();
Remote var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var80); // 注册
...
到这里看到了,注册中心提供很多功能,其中list
无参数只是列出也算有用,可以看看注册了哪些对象,没准可利用,其他的方法都是有参数的,而且它还挺懒的,像Name既然规定了是字符串这里就用字符串读嘛,偏要用对象读,所以这些点就是反序列化点,当然之后有很多修复,一个一个列:
访问权限检测
bind
/unbind
/rebind
会检查权限,默认只能本地访问,最初是反序列化后再检查,这样虽然不能操作成功但是能反序列化成功,后来在JDK8u141彻底修复了,但是lookup可以传参但没检查,因此它也可以搞,见RMIRegistryExploit...
反序列化过滤器
这是JEP290引入的(JDK8u121/JDK7u131/JDK6u141),如下:
public class RegistryImpl extends RemoteServer implements Registry {
public RegistryImpl(int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) throws RemoteException {
this(var1, var2, var3, RegistryImpl::registryFilter); // 实例化时注册了过滤器
}
private static Status registryFilter(FilterInfo var0) { // 过滤器如下,若未在参数或配置文件中指定,则使用默认的
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
// 默认支持如下类的子类...
return (String.class != var2 &&
!Number.class.isAssignableFrom(var2) &&
!Remote.class.isAssignableFrom(var2) &&
!Proxy.class.isAssignableFrom(var2) &&
!UnicastRef.class.isAssignableFrom(var2) &&
!RMIClientSocketFactory.class.isAssignableFrom(var2) &&
!RMIServerSocketFactory.class.isAssignableFrom(var2) &&
!ActivationID.class.isAssignableFrom(var2) &&
!UID.class.isAssignableFrom(var2)
? Status.REJECTED : Status.ALLOWED;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}
}
// 它会传给UnicastServerRef,而在之前的dispatch过程中,
public class UnicastServerRef extends UnicastRef implements ServerRef, Dispatcher{
public void dispatch(Remote obj, RemoteCall call) throws IOException {
unmarshalCustomCallData(in); // 它会在调用readObject前被调用
}
protected void unmarshalCustomCallData(ObjectInput var1) throws IOException, ClassNotFoundException {
if (this.filter != null && var1 instanceof ObjectInputStream) {
final ObjectInputStream var2 = (ObjectInputStream)var1;
Config.setObjectInputFilter(var2, UnicastServerRef.this.filter); // 设置了过滤器,拜拜...
}
}}
这时注册中心就没法反序列化之前如CC中的恶意类了,但是可以从白名单中找一些类,如LiveRef,它会在反序列化时注册到DGC里,DGC之后会调用makeDirtyCall,于是可以在这里返回恶意的序列化数据(见JRMPClient和JRMPClient),如下:
public class UnicastRef implements RemoteRef {
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
ref = LiveRef.read(in, false); // UnicastRef反序列化会构造LiveRef
...
public class LiveRef implements Cloneable {
public static LiveRef read(ObjectInput in, boolean useNewFormat) throws IOException, ClassNotFoundException {
Endpoint ep = TCPEndpoint.read(in);
...
LiveRef ref = new LiveRef(id, ep, false);
if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
stream.saveRef(ref); // 这里会把引用保存到stream的incomingRefTable
...
class ConnectionInputStream extends MarshalInputStream {
void saveRef(LiveRef ref) { // 保存引用,在所有参数/返回值被反序列完后,发送dirty call
Endpoint ep = ref.getEndpoint();
List<LiveRef> refList = incomingRefTable.get(ep); // 保存在了incomingRefTable里
if (refList == null) {
refList = new ArrayList<LiveRef>();
incomingRefTable.put(ep, refList);
}
refList.add(ref);
}
void registerRefs() throws IOException { // 将incomingRefTable里的所有数据添加到DGCC
if (!incomingRefTable.isEmpty()) {
for (Map.Entry<Endpoint, List<LiveRef>> entry :
incomingRefTable.entrySet()) {
DGCClient.registerRefs(entry.getKey(), entry.getValue());
...
public class StreamRemoteCall implements RemoteCall {
public void releaseInputStream() throws IOException {
in.registerRefs(); // 添加到DGC...
in.done(conn);
conn.releaseInputStream();
}
public void done() throws IOException {
releaseInputStream();
}
}
final class DGCClient {
static void registerRefs(Endpoint ep, List<LiveRef> refs) {
do {
EndpointEntry epEntry = EndpointEntry.lookup(ep);
} while (!epEntry.registerRefs(refs));
}
private static class EndpointEntry {
public boolean registerRefs(List<LiveRef> refs) {
// 要素过多,就是把EndPoint取出来放refsToDirty里,再调用makeDirtyCall
makeDirtyCall(refsToDirty, sequenceNum);
return true;
}
private void makeDirtyCall(Set<RefEntry> refEntries, long sequenceNum) {
Lease lease = dgc.dirty(ids, sequenceNum, new Lease(vmid, leaseValue)); // 这是个远程调用...
...
不过JDK8u231在dirty函数也加了过滤器,而且在出异常时直接把列表清空了不让调用dirty...
注:(1). 除了用本地利用链,RMI还支持指定codebase,就是客户端可为服务端指定,反之亦然,于是在反序列化本地不存在的类时将从codebase加载类,不过限制很多(JDK 6u45/7u21之后加的)基本不会出现不多说...
(2). 上面用到了DGC反连相当于攻击客户端,除此之外本来客户端和服务端可以相互攻击,一般都是打服务所以不说客户端了,思路一样...
攻击注册的对象
先用list看看有哪些,若对象的参数是非原始类型的,就会调用readObject,显然这个点没法用默认的过滤器防所以没有防,另外没准里面有些方法本来就是危险方法直接拿来用也很香啊(想🍑...
JNDI
JNDI基础
JNDI即Java命名和目录接口(Java Naming and Directory Interface),分开看就是名称服务和目录服务的接口,它对外提供了标准的API,而底层支持多种具体协议,用户也可使用SPI自己实现,架构图如下:

本文只关注RMI/LDAP,其实CORBA也出过,见Java CORBA与隐秘的角落 -- JDK CORBA 安全性研究(下),关于JNDI细节的可以看看JNDI Guide不多说。JNDI维护着上下文与绑定对象,绑定属于某个上下文,除初始上下文外其他上下文属于它的父级上下文,一般使用如下代码查询对象:
Context context = new InitialContext();
context.lookup(jndiName);
这里面的绑定的对象有两种存储形式:
- 序列化:按某种方式将其序列化为数据存储,也可使用Java原生的序列化实现,缺点是占空间,有些对象不能完全序列化
- 引用:存储对象的工厂类与属性信息,在查询时重新有工厂类根据属性信息生成对象实例
于是攻击面有两个,序列化与工厂类,下面分别描述。
利用引用
这是JNDI最有名的利用方式,也直接把它叫JNDI注入,简单的说能控制lookup的参数就能(远程)加载任意类,这是由它支持存引用决定的,引用形式需要类有匹配的工厂类,它在存储时调用getStateToBind
生成引用形式(可以不实现),在获取时调用getObjectInstance
生成对象,其类关系图如下:

当获取的引用时,需要根据引用里的工厂类和额外的信息构造处期望的对象,引入类定义如下:
public class Reference implements Cloneable, java.io.Serializable {
protected String className; // 对象的类全名
protected Vector<RefAddr> addrs = null; // 其他数据,存储实例的信息,用于工厂类重建对象实例
protected String classFactory = null; // 工厂类的名称,工厂根据引用数据创建对象
protected String classFactoryLocation = null; // 工厂类的位置
}
可以看到它竟然可以指定工厂类的位置,因此我们写个恶意的工厂类,在它类初始化/实例初始化/getObjectInstance
等位置插入恶意代码即可让受害者加载并执行它...
然而,这个点被修啦:
对象 | 版本 | 说明 |
---|---|---|
RMI/CORBA | JDK 6u141/7u131/8u121 | 增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI/CORBA协议使用远程codebase的选项 |
LDAP | JDK 6u211/7u201/8u191 | 增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项 |
虽然禁止加载远程的,但是本地的还是不限,最有名的就是Tomcat的BeanFactory
,它可以构造并调用[12]:
- 有public修饰的无参构造方法
- public修饰的只有一个String.class类型参数的方法,且该方法可以造成漏洞
这里面最出名的有Tomcat8内置(SpringBoot1.2也行,它内置的Tomcat也是8以上)的javax.el.ELProcessor#eval
和Groovy
的groovy.lang.GroovyShell#evaluate
,以前者为例它能利用引用里面的信息执行任意表达式:
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /Applications/Calculator.app']).start()\")"));
存在BeanFactory
时还能利用SnakeYaml
/XStream
反序列化漏洞,MVEL
执行表达式,NativeLibLoader
加载动态库。
而其他工厂类,如数据库连接的,可利用h2执行Java代码实现RCE,这些详见[12]。
利用本地利用链
它底层本身也有反序列化,若还存在利用链即可...
JMX
Java管理扩展用于管理/监视应用,它管理的是MBean对象,这类对象除了符合Bean(属性私有用getter/setter获取,存在无参构造器)外,实现自定义的接口,该接口名必须是类名+MBean
,JMX可远端访问,默认用的是RMI,攻击手段就多种多样了

利用存在的MBean
MLet
这是个特殊类,可用于从远端加载类,使用时有两个限制:
1.不能有用户鉴权,有认证时默认的权限是不支持该操作的
2.不能有安全管理器,有的话默认的策略是不支持该操作的
在没有这两个限制时,则可以编写一个恶意类与mlet文件将其放入http目录下,创建javax.management.loading.MLet
实例并调用getMBeansFromURL
加载恶意类,详见[6]/[10]
其他类
其他类的属性可能会泄露一些信息,如会话ID,或提供一些敏感操作,如修改日志的路径,或者方法参数是非原始类型,这时有了个反序列化输入点...
利用RMI
JMX会启动Registry并把自己注册为jmxrmi,因此之前攻击RMI的那套可以弄上来...
攻击鉴权过程
之前的鉴权是用HashMap传递鉴权信息,显然在鉴权前就要发生反序列化,这个已经修了(CVE-2016-3427)...
参考
[1] JAVA RMI 反序列化知识详解 -- 天融信阿尔法实验室
[2] 关于 Java 中的 RMI-IIOP -- Longofo@知道创宇404实验室
[3] Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上) -- Longofo@知道创宇404实验室
[4] Java 安全-RMI-学习总结 -- p1g3@D0g3
[5] Java 反序列化过程中 RMI JRMP 以及 JNDI 多种利用方式详解 -- Alpha@天融信阿尔法实验室
[6] attacking-rmi-based-jmx-services -- Hans-Martin Münch@h0ng10
[7] JNDI with RMI -- wh1t3p1g
[8] JAVA JNDI 注入知识详解 -- 天融信阿尔法实验室
[9] 浅谈Java RMI Registry安全问题 -- wh1t3p1g
[10] 攻击Java JMX-RMI -- wh1t3p1g
[11] JNDI with LDAP -- wh1t3p1g
[12] 探索高版本 JDK 下 JNDI 漏洞的利用方法 -- 浅蓝