搞安全的总是要赛博考古,找找被遗忘的角落。这两年的漏洞偶尔会出现CORBA这个东西,这玩意好像比RMI还老,得有二十多年历史了,但很多框架还支持它,简单分析一下。
CORBA也是用于RPC的,可以理解为和RMI差不多,但不像RMI是Java特有的,CORBA是一种跨语言的分布式调用结构,先写个demo。
Java CORBA Demo实现
首先,CORBA是跨语言的结构,所以先要用它自己的格式创建出对应文件,再编译成Java。CORBA自己的语言文件叫IDL,长这样:
1 | module HelloApp |
其实就是package为HelloApp的Hello接口,里面有sayHello方法。然后用jdk自带的idlj工具编译:
1 | idlj -fall hello.idl |
编译后多出来一个HelloApp包,里面有六个文件。如果参数改成-fclient/-fserver可以单独生成客户端和服务端的文件。这里列举一下。
客户端服务端都有:
HelloOperations
1 | public interface HelloOperations |
一个接口,定义了idl里定义的方法
Hello
1 | public interface Hello extends HelloOperations, org.omg.CORBA.Object, org.omg.CORBA.portable.IDLEntity |
其实就像RMI,需要两边都有远程接口的定义
服务端:
一个接口,名称是idl定义的接口,继承了几个接口。
HelloPOA
1 | public abstract class HelloPOA extends org.omg.PortableServer.Servant |
一个抽象类,叫做POA,实际是负责服务端处理调用请求的,可以类比为RMI里面的Skel。
客户端:
HelloHelper
1 | abstract public class HelloHelper |
辅助类,处理编码解码。
HelloHolder
1 | public final class HelloHolder implements org.omg.CORBA.portable.Streamable |
也是一个辅助类,处理参数的。
_HelloStub
1 | public class _HelloStub extends org.omg.CORBA.portable.ObjectImpl implements HelloApp.Hello |
看名字也知道了,客户端处理远程调用的代理,类比RMI中的stub。
有了这些自动生成的类以后,当然还要写自己的逻辑。和RMI类似CORBA也分为三部分:客户端、服务端、Naming Service。Naming Service和注册中心差不多,服务端把对象绑定上去,客户端去查询。因为CORBA是跨语言的,所以Naming Service不是用Java代码开启的,也是一个内置工具:
1 | orbd -ORBInitialPort 1050 -ORBInitialHost localhost |
然后创建服务端,做两件事:实例化Hello接口,绑定到Naming Service
CORBAServer,来自官网示例https://docs.oracle.com/javase/8/docs/technotes/guides/idl/jidlExample.html
1 | class HelloImpl extends HelloPOA { |
然后是客户端,获取stub,对stub调用远程方法,就照着RMI理解就行了
1 | public class CORBAClient { |
运行后客户端成功打印出Hello World。实际上服务端也调用了,可以自己sout一下。
总体来说和RMI很像,但是多了一步ORB的操作。RMI是getRegistry直接获创建注册中心Stub的,CORBA是创建一个ORB,用ORB获取Naming Service,再在Naming Service上面操作。
直觉上这个过程和RMI一样可能出现两种漏洞:
1、反序列化
2、动态类加载
分析一下调用流程,看看有没有存在风险的地方。由于代码实在很乱,所以直接在com.sun.corba.se包里所有调用readObject的点下断点了,省的遗漏。
先看服务端,大概做了几件事:
1、用ORB从Naming Service获取RootPOA。
2、用RootPOA将远程对象转化为stub
3、用ORB从Naming Service获取命名上下文NameService
4、绑定stub到NameService
5、开启监听
可以看到中间和Naming Service是有多次交互的,有可能有反序列化的问题。调试跟一下:
服务端调用流程
在ORB从Naming Service解析引用名时会调用ORBImpl#resolve_initial_references
1 | public org.omg.CORBA.Object resolve_initial_references( |
到CompositeResolverImpl#resolve
1 | public org.omg.CORBA.Object resolve( String name ) |
具体不分析了,第二次请求NamingService时会到BootstrapResolverImpl#resolve
1 | public org.omg.CORBA.Object resolve( String identifier ) |
对输入流调用read_Object,这里有好几层,最后到CDRInputStream_1_0#read_Object
1 | // ------------ RMI related methods -------------------------- |
默认进clz==null的分支,注意到获取了codebase,那就看看有没有远程加载。来到StubFactoryFactoryStaticImpl#createStubFactory
1 | public PresentationManager.StubFactory createStubFactory( |
全是函数,这代码看着真累。来到javax.rmi.CORBA.Util#loadClass
1 |
|
从注释里看到实际上是支持远程加载的,但是需要useCodebaseOnly为false。
最后到JDKBridge#loadClassM
1 | private static Class loadClassM (String className, |
如果useCodebaseOnly为false并且codebase不是空,会调用RMIClassLoader.loadClass。但实际上即使满足这两个条件,真正想用RMI进行远程类加载还需要允许配置SecurityManager。如果能本地加载最后就是调JDKClassLoader.loadClass,最后用Class.forName加载,后面进行了实例化,但实际上forName会触发初始化,不管是否实例化都已经能执行代码了。
当前就是本地加载了org.omg.CosNaming._NamingContextStub类,这个类其实相当于RMI中的RegistryImpl_Stub,获取到这个stub后调用了两次,先看第一处
_NamingContextStub#to_name
1 |
|
大致是创建输出流,写一个字符串,调用连接,然后接收了返回值。这不就是RMI里的newCall+invoke+反序列化吗。
和RMI不同,这个_invoke里没看到反序列化的地方。看看接收返回值的函数
NameHelper#read
1 | public static org.omg.CosNaming.NameComponent[] read (org.omg.CORBA.portable.InputStream istream) |
NameComponentHelper#read
1 | public static org.omg.CosNaming.NameComponent read (org.omg.CORBA.portable.InputStream istream) |
跟进去发现这两个read_string不是用反序列化实现的,而是创建了字符串,直接赋值网络流里读取的字符。catch块里的InvalidNameHelper.read也是一样的。意外的比RMI安全。
那么看下一处交互,绑定的地方
_NamingContextExtStub#rebind
1 | public void rebind (org.omg.CosNaming.NameComponent[] n, org.omg.CORBA.Object obj) throws org.omg.CosNaming.NamingContextPackage.NotFound, org.omg.CosNaming.NamingContextPackage.CannotProceed, org.omg.CosNaming.NamingContextPackage.InvalidName |
简单跟了下,没发现可控的反序列化点,catch里有一处调用了read_object但是参数不可控。
那么服务端绑定过程没有触发反序列化的点,有触发远程类加载的点。
客户端调用流程(客户端)
接下来看客户端调用的过程
前面和Naming Service交互的部分和服务端是一样的,看获取远程对象stub的部分:
_NamingContextExtStub#resolve_str
1 | public org.omg.CORBA.Object resolve_str (String sn) throws org.omg.CosNaming.NamingContextPackage.NotFound, org.omg.CosNaming.NamingContextPackage.CannotProceed, org.omg.CosNaming.NamingContextPackage.InvalidName |
和前面不同的是调用了ObjectHelper#read
1 | public static org.omg.CORBA.Object read (org.omg.CORBA.portable.InputStream istream) |
和前面的类似,最后也是来到CDRInputStream_1_0#read_object,那么这里也是一个动态类加载点。
获取到的是远程对象代理_HelloStub,和RMI用反序列化的方式获取stub不同,CORBA是直接进行类加载,客户端本来是有这个类的定义的,同时也支持远程类加载。
看客户端调用远程方法的部分,也就是helloImpl.sayHello这里,调用到_HelloStub#sayHello
1 | public String sayHello () |
这里接收调用结果时使用的read_string是安全的方法,但这时会想到如果返回值是Object的呢?或者如果有参数,又是怎么进行传递的呢?
CORBA的idl里,参数类型不是Java的类型,而是in、out、inout这三种,比如这样:
1 | module HelloApp2 |
在ibm的文档里的IDL数据类型里没看到支持Object类型,但是好像也能正常用。
重新看一下这个Hello接口的调用流程,_HelloStub#sayHello
1 | public org.omg.CORBA.Object sayHello (org.omg.CORBA.Object obj, String str2) |
此时的$result已经变成用ObjectHelper.read来获取了,这个函数前面分析过是存在动态类加载问题的。也就是说CORBA的客户端调用时,如果返回值是Object就会产生风险。别的类型是否有危险可能需要针对的去看,这里重载的方法太多了。
分析完了对返回值的处理,最后看看服务端对参数的处理。
客户端调用流程(服务端)
断点下在HelloPOA里,相当于RMI中的Skel
1 | public org.omg.CORBA.portable.OutputStream _invoke (String $method, |
可以看到有两个读取参数的操作,第一个已经见过好几回了,看下read_wstring这个函数,最后走到CDRInputStream_1_2#read_wstring
1 | public String read_wstring() { |
是没有问题的。这里实际上是根据不同的参数类型来调用不同的read_*方法,那干脆搜索一下到底有没有哪中参数类型是会调用反序列化处理的。最后找到了两处,都在IDLJavaSerializationInputStream里面:
1 | public String read_wstring() { |
这是另一个InputStream,前面一直用的都是CDRInputStream。那么如果能想办法把调用流程切换到这个IDLJavaSerializationInputStream里面,在读取特定数据类型时就有机会触发反序列化漏洞。这个输入流实际上是根据服务端返回的标记为来决定的,也就是恶意CORBA服务端可以攻击CORBA客户端。
但实际上,这种原生CORBA实在是太少见了,基本不会有这个攻击场景。Java中使用CORBA更常用的是RMI+IIOP的方式,具体的实现是结合JNDI,在JNDI那篇文章详细介绍。
参考链接
https://docs.oracle.com/javase/8/docs/technotes/guides/idl/jidlExample.html
https://docs.oracle.com/javase/7/docs/technotes/guides/rmi-iiop/rmi_iiop_pg.html