通常在渗透的过程中会遇到很多 Weblogic 服务器,但通过 IIOP 协议进行发送序列化恶意代码包时,会面临着无法建立通讯请求发送恶意序列化失败。最近参与了一场在成都举办的《FreeTalk 2020 成都站》,有幸分享了关于 Weblogic IIOP 协议 NAT 绕过的几种方式。
PPT 下载地址:《Weblogic IIOP 协议NAT 网络绕过》
Goby 工具中关于 Weblogic 基本都是用了 IIOP 协议绕过的方案,比较有代表性的漏洞为 CVE-2020-2551 漏洞。
内部工具 weblogic-framework
使用了多项核心技术来进行优雅的测试 Weblogic,其中也使用了 IIOP 协议的绕过方案。
在开始之前,非常有比较提及一下以下这些协议相关的内容:
RMI: 远程方法调用,本质上是 RPC 服务的 JAVA 实现,底层实现是 JRMP 协议,主要场景是分布式系统。
CORBA:跨语言(C ++、Java等)的通信体系结构,通常在 IIOP 协议中使用。
GIOP:主要提供标准的传输语法以及 ORB 通信的信息格式标准。
IIOP:CORBA 对象之间交流的协议,GIOP 的实现。
RMI-IIOP:解决 RMI 和 CORBA/IIOP 无法同时使用的技术方案。
Weblogic IIOP:Weblogic 自实现的 RMI-IIOP。
T3:WebLogic Server 中的 RMI 通信使用 T3 协议传输 WebLogic Server 和其他 Java 程序,包括客户端及其他 WebLogic Server 实例之间 数据。
T3 协议本质上 RMI 中传输数据使用的协议,但通过上面我们看到 RMI-IIOP 是可以兼容 RMI 和 IIOP 的,所以在 Weblogic 中只要可以通过 T3 序列化恶意代码的都可以通过 IIOP 协议进行序列化,正是因为这种情况,我进入 Weblogic 第三季度深度贡献者名单。
一般 IIOP 序列化攻击的大致流程主要为首先构建恶意序列化代码,然后初始化上下文实例,最后通过 bind/rebind
进行发送恶意序列化代码,下图为关键代码。
初始化上下文通过攻击流程中的 new InitialContext(env)
进行构建,最终的入口点通过 getInitialContext
方法进行构建,最终是进行流入到 InitialContextFactoryImpl.getInitialContext
进行初始化上下文。
在流入 InitialContextFactoryImpl.getInitialContext
之后会通过 obj = ORBHelper.getORBHelper().getORBReference
来进行获取 NameService
,然后将获取到的 NameService
进行实例化创建上下文实例,提供后续的执行操作。
执行 rebind
流程中,首先会通过 this.getContext
方法进行获取前面所讲的上下对象,然后通过 rebind_any
进行发发送序列化代码,当前在此之前已经通过经过序列化的了。
在 rebind_any
中,首先会通过 this._request
进行发送 rebind_any
建立 Socket 通讯,最后通过 this._invoke
方法进行执行最终的操作发送序列化代码。
所以最终大致的执行流程是如下图,获取 NameService
,基于获取的信息进行创建上下文实例(获取实际连接信息),然后进行发起 request
请求,最后进行执行 rebind_any
操作。
NAT 网络构建通过 http://vulfocus.fofa.so/
进行搭建构造。
在后续的调试以及研究中我们所使用的版本为 12.1.3.0 版本,漏洞为 CVE-2020-2555 漏洞,以下为漏洞 POC,以及漏洞执行链,在这里不多讲该漏洞,有兴趣的可以移步漫谈 Weblogic CVE-2020-2555。
执行链:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
LimitFilter.toString()
ChainedExtractor.extract()
ReflectionExtractor.extract()
Method.invoke()
Class.getMethod()
ReflectionExtractor.extract()
Method.invoke()
Runtime.getRuntime()
ReflectionExtractor.extract()
Method.invoke()
Runtime.exec()
当我们可以与 Weblogic 所处同一网段并且可达的时,可以看到是成功执行系统命令弹出计算器。
通过 Wireshark 进行抓包可以,可以看到一共通讯了 2 次,第一次发送 LocateRequest
类型的 LocateRequest
的通讯操作获取 NameService
,第二次发送 Request
类型的 rebind_any
操作进行发送序列化代码。
而进行测试公网中的靶机时抛出 Operation time out
异常,具体信息如下图。
而在 Wireshark 中可以看到,与第一次获取NameService
中的内网 IP、端口进行了 Socket 通信。
而在执行的流程中停留在了 createEndPoint
方法中,所以通信问题大概率是在此方法中引发的。
在 createEndPoint
方法中,最后通过 MuxableSocketIIOP.createConnection
方法进行建立 Socket 通信,此时的通信变为了 Weblogic 运行的内网 IP 和端口。
所以大体的情况为如下图,问题出现在发起 request
时调用的 createEndPoint
方法中,由于 createEndPoint
无法正常建立 Socket 通信导致后续的操作无法正常秩序。
其实,我们也可以在 Weblogic 启动日志中也可以看到 Weblogic 关于端口和协议分配的情况,基本分配都是内网网卡的IP和端口同时会进行监听 0.0.0.0:7001
来处理协议的请求操作,那么现在问题来了,公网中的 Weblogic 服务器 99% 分配的都是内网 IP 和端口。
由于问题发生在响应的 Weblogic 在获取 NameService 时,响应的 IP和端口为内网中的端口,导致在后续 createEndPoint
建立 Socket 通信,所以我们可以进行在建立 Socket 通信之前修改为正确的 IP和端口(公网中的IP和端口)。
当我们与服务器所处同一网段时,可以看到一共通讯了 2 次,第一次发送 LocateRequest
类型的 LocateRequest
的通讯操作获取 NameService
,第二次发送 Request
类型的 rebind_any
操作进行发送序列化代码。
所以我们根据 Wireshark 中的信息,可以进行构建极简的 GIOP 实现,大体如下:
GIOP 协议大致由 Header 和 Message Type 进行构成,在 Header 包含了 Magic、Version、Message Type、Message Size。
Message Type 的类型如下:
消息类型 | 始发方 |
---|---|
Request | Client |
Request | Server |
CancelRequest | Client |
LocateRequest | Client |
LocateReply | Server |
CloseConnection | Server |
MessageError | Both |
Fragment | Both |
获取 NameService 请求代码实现:
执行 rebind_any 操作代码实现:
最终效果:
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库,它使 Java 程序能够在运行时定义一个新类,并在 JVM 加载时修改类文件。
读取类
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
创建类
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
继承
cc.setSuperclass(pool.get("test.Point"));
写入
cc.writeFile();
cc.toClass();
需要注意的是 toClass()
会把当前修改的 Class 加入到当前的 ClassLoader
中。
创建方法
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
修改方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
ctMethod.setBody("System.out.println(\"set body\");");
ctMethod.insertBefore("System.out.println(\"set before\");");
ctMethod.insertAfter("System.out.println(\"set after\");");
通过 Javassist 进行实现时,可以通过修改建立 Socket 通信之前的方法,将 ip、端口替换为正常的 IP 和端口,在这里选取的是 newSocket
方法,在第一个参数为 host
,第二个参数为 port
。
最终修改的如下图:
在实现的过程中,仅需要在执行到 newSocket
方法时,将连接到 IP 和端口设置为正确的 IP 和 端口,核心代码如下图:
最终效果:
在执行的流程中最终执行到了 createEndPoint
方法中,从执行流程来看主要如下图所示。
在执行 rebind
方法发送序列化代码时,可以看到在此时已经变成了 Weblogic 内网中运行的 IP 和 端口,直到程序执行到 createEndPoint
抛出异常。
而执行到 getInvocationIOR
方法时,会调用 IIOPRemoteRef.locateIORForRequest
方法来进行寻找 IOR
,并且将寻找到的 IOR
设置为当作当前 IOR
进行返回提供使用。
在进入 locateIORForRequest
方法之后会通过 EndPointManager.findOrCreateEndPoint
来进行寻找或创建结束切点,可以看到此时 IOR
的 host
、port
变成了内网中的 IP 和端口。
在进入 EndPointManager.findOrCreateEndPoint
最终会执行到 createEndPoint
方法中来进行建立 Socket 通信,在这里由于是内网的 IP 和端口无法成功建立通信,导致后续的利用也无法继续进行。
大致的问题点已经确认的清楚的情况下,我们可以通过修改原始代码的方式来进行实现绕过,大体思路为:
weblogic.corba.j2ee.naming.ContextImpl
类中的 rebind
方法weblogic.iiop.IIOPRemoteRef
类中 locateIORForRequest
方法的 ior 参数,确保正常调用 findOrCreateEndPoint
创建结束切点首先修改 weblogic.corba.j2ee.naming.ContextImpl
类中 rebind
方法将正确的连接 IP 和端口加入到系统环境变量中。
最后在 locateIORForRequest
方法读取系统环境变量中正确的 IP 和端口并且修改 ior
变量中相关的连接信息。使之能够正常的执行 findOrCreatePoint
方法创建结束切点
最终效果: