浅谈Java RMI 反序列化安全问题

阅读量    221663 | 评论 2

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

本文讲述了Java RMI Registry 远程bind对象所带来的反序列化问题,包括两个level,具体见下面

 

0x01 原理

Java RMI流程可参考1,出问题的位置在于:

从Client接收到的bind或rebind的remote obj,将由sun/rmi/registry/RegistryImpl_Skel.java#dispatch处理

5599

可以看到获取到的序列化数据直接调用了readObject函数,导致了常规的Java反序列化漏洞的触发。

这里我们只需要写一个bind或rebind的操作,即可攻击到RMI Registry。

 

0x02 Level 1

Level1 使用的是jdk8u111,该版本下的RMI Registry接收的remote obj只要继承了Remote类即可(原理见2),没有其他的限制。

ysoserial工具中的ysoserial.exploit.RMIRegisterExploit采用了代理Remote类的方式来解决这个限制。

使用如下命令即可

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit RMIRegisterHost RMIRegisterPort CommonsCollections7 "open /System/Applications/Calculator.app"

简单看一下ysoserial.exploit.RMIRegisterExploit的原理

根据前面文章中的原理,我们传过去的对象必须要是一个继承了java.rmi.Remote接口的对象。这里ysoserial工具则直接利用动态代理的原理,对Remote类做代理,其处理的handler用了CommonsCollections中常用的AnnotationInvocationHandler。但其触发点变为handler的memberValues属性被反序列化所执行的利用链。

4688

接下来,远程bind对象将构造好的remote对象传过去即可,来看一下这个代码

6319

 

0x03 Level 2

Level2采用的是修复后的版本jdk8u121,修复后的版本里,对反序列化的类做了白名单限制,见sun/rmi/registry/RegistryImpl.java#registryFilter

这个白名单包括:

if (String.class == clazz
        || java.lang.Number.class.isAssignableFrom(clazz)
        || Remote.class.isAssignableFrom(clazz)
        || java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
        || UnicastRef.class.isAssignableFrom(clazz)
        || RMIClientSocketFactory.class.isAssignableFrom(clazz)
        || RMIServerSocketFactory.class.isAssignableFrom(clazz)
        || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
        || java.rmi.server.UID.class.isAssignableFrom(clazz)) {
    return ObjectInputFilter.Status.ALLOWED;
} else {
    return ObjectInputFilter.Status.REJECTED;
}

Level1中发送的对象是代理后的AnnotationInvocationHandler对象,并不在上述允许的类里面,这导致原先ysoserial工具中的ysoserial.exploit.RMIRegisterExploit无法利用。这里我们参考3的方法来达成利用。

首先思路比较明确的是,如果要绕过这个限制,我们需要在上述的白名单里找到可以利用的对象。

在白名单里我们注意一下两个特殊对象Remote对象和UnicastRef对象

1. UnicastRef对象

我们都知道RMI过程中存在客户端与服务器端之间的交互,那么在代码层面,我们需要怎么去创造这样一个链接呢?

由于漏洞发生点为向远程服务器注册对象的引用。回顾一下,我们在bind时,会先去获得一个Registry,类似下面

Registry registry = LocateRegistry.getRegistry("192.168.98.80");

跟进java/rmi/registry/LocateRegistry.java#getRegistry

8839

注意到这样一段代码,通过TCPEndpoint注册服务端的host、端口等信息,以UnicastRef封装liveRef.在下面createProxy时使用了RemoteObjectInvocationHandler作为UnicastRef动态代理的处理类

2156

最终,我们将以客户端的身份去链接,所以这里的Registry会是sun/rmi/registry/RegistryImpl_Stub.java#bind向远程RMI Registry注册。

5987

newCall发起连接,并将需要绑定的对象发送过去。

到这里就结束了向远程Registry发起绑定的操作。这个过程中我们用到了UnicastRef对象,那么这里想象一下,如果我们可以控制UnicastRef对象里LiveRef的host和port,那么我们就能发起任意的RMI连接。这里就是ysoserial中JRMPClient的原理,来看一下这个payload

是不是很熟悉XD,使用的方法就是前面绑定过程中的代码。而在白名单里UnicastRef对象是允许被反序列化的。

2. RemoteObjectInvocationHandler对象

前面分析到UnicastRef可被用于发起RMI连接,但是为了符合发送的条件,仍然需要满足实现Remote接口的条件。而UnicastRef并没有实现Remote接口,这就意味着直接传UnicastRef是不行的,我们还需要再多做一步,这里有两种方法:

  1. 跟RMIRegisterExploit一样,使用Proxy反射来实现,其handler继承于Remote并处理了构造好的UnicastRef,这里用到的就是RemoteObjectInvocationHandler
  2. 找到一个可以封装UnicastRef的对象,并且该对象还实现了Remote对象

这里JRMPClient使用的RemoteObjectInvocationHandler就是第一种方法,我们将AnnotationInvocationHandler替换成RemoteObjectInvocationHandler。在反序列化时会调用RemoteObjectInvocationHandler的父类RemoteObject的readObject函数

5786

这里的ref就是我们传进去的UnicastRef,调用其readExternal函数,这里介绍一下readExternal

Java默认的序列化机制非常简单,而且序列化后的对象不需要再次调用构造器重新生成,但是在实际中,我们可以会希望对象的某一部分不需要被序列化,或者说一个对象被还原之后,其内部的某些子对象需要重新创建,从而不必将该子对象序列化。 在这些情况下,我们可以考虑实现Externalizable接口从而代替Serializable接口来对序列化过程进行控制。
Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。

https://xz.aliyun.com/t/5392

这里的readExternal就是重新创建一个tcp连接

5735

继续往下跟

0019

重新生成一个LiveRef对象后,将存储到当前的ConnectionInputStream上。后续该stream会继续调用registerRefs函数

0690

最终由DGCClient发起连接,下图中的loopup函数

3670

到这里后面就是JRMPListener反序列化的东西了,这里在最后进行分析。

3. RMIConnectionImpl_Stub对象

前面提到了两种思路,还有一种就是找到一个实现了Remote接口并且封装了ref的对象,这里RMIConnectionImpl_Stub对象就是

RemoteObjectInvocationHandler后续的触发主要依靠RemoteObject对象的readObject函数的重新填充,而RMIConnectionImpl_Stub对象也继承了RemoteObject所以后面的一些调用过程跟第二个对象一样,不再叙述。

其实顺着思路找还能发现DGCImpl_StubRMIServerImpl_StubRegistryImpl_Stub都可以

 

0x04 后续

其实到最新版的jdk8仍然没有处理这个绕过的问题,是不是很开心?

虽然没有解决rmi服务端的反序列化,但是在发起新的RMI连接时,sun/rmi/transport/DGCImpl_Stub.java#leaseFilter

4042

新添加了对于返回的序列化对象的过滤条件,只允许上面的几种类型,而现有的反序列化利用链中HashSet、HashTable等类都是通不过的。也就意味着JRMP这种利用方式在jdk8u232_b09及以上的版本都是不行的(在调用dirty的时候注册了leaseFilter)。

ps: 其实上诉的过滤很早就有了,甚至还跟严格,但是他的生效仅在jdk8u232_b09上,见http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/523d48606333#l4.38

 

0x05 番外:ysoserial中的JRMPListener与JRMPClient

看了上面可能你会疑惑,为什么server端发起一个RMI的连接就会触发java反序列化?

前文我们将的是RMI Registry在接收远程绑定对象时所发生的反序列化漏洞,那么RMI Client在接收Server端的数据时是否也会发生反序列化漏洞呢?答案是肯定的,毕竟RMI交互过程中的数据采用的是序列化的数据,也就意味着存在着一个反序列化的过程。

看一下JRMPListener的代码,简单来说,其实现了与RMI Client的交互流程。这里我们直接看重点的代码

9982

在完成前面的一些交互步骤后,Listener会向Client发送一个ExceptionalReturn的状态,并将序列化的payload填充到BadAttributeValueExpException的val属性。这里用的BadAttributeValueExpException并不是我们以前分析时做的toString触发点,而是仅作为payload的一个载体,在反序列化BadAttributeValueExpException的val属性时同样反序列化了我们的payload。

而位于Client在接收到ExceptionalReturn时的处理方式见sun/rmi/transport/StreamRemoteCall.java#executeCall前面的分析都省略了

4429

在这里我们看到了熟悉的readObject函数,其用于将前面的Exception进行反序列化。

到这里就可以串起来了,Client发起RMI连接,连接到我们的恶意Listener上面。而我们的Listener将返回一个构造好的Exception,旨在Client接收到ExceptionalReturn的信号时进行还原,从而造成了RMI Client端也受到反序列化漏洞的攻击。

 

0x06 总结

前文主要讲诉了RMI的相关反序列化问题,包括RMI Registry和RMI Client接收到反序列化数据时产生的反序列化漏洞。

全文所使用的JDK版本为JDK8,在分析过程中,发现在最近的JDK8u版本上,已无法使用JRMPListener的这种方式来利用了,但其在JDK8u232_b09之前的版本还是可以的。

本文主要攻击的是RMI Registry,而RMI的攻击面不单单文中提到的这种方式,还存在

1.针对codebase的攻击见codebase-attack,加载我们构造好的class。当然现在这种情况比较少了。
2.针对绑定的危险obj的攻击,我们可以通过list所有绑定的obj,查找危险的绑定地址进行攻击。这个暂时还没找到案例…

ps: 文中提到的RMIExploit都更新到了github

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多