java反序列化之RMI~
一.概念
RMI全称是Remote Method Invocation,远程⽅法调⽤,就是让某个java虚拟机上的对象去调用另一个java虚拟机中对象上的方法
一个RMI过程有以下三个参与者:
RMI Registry
RMI Server
RMI Client
他们之间的关系

文字叙述过程:
- 客户端首先去连接服务端的注册中心RMI Registry,然后在其中寻找name是某个方法(下文中示例是Hello)的对象,这个过程对应数据流中的Call信息
- 然后RMI Registry接收到之后,会返回一个序列化之后的数据,里面包含找到的Hello的对象,这个对应的是数据流中的ReturnData的信息
- 然后客户端接收到后反序列化该消息,里面包含了远程对象的地址号和端口,然后与该地址建立连接,在这个链接中才真正的实现远程方法调用
下面两个是示例代码:
#rmi_server
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMI_Server {
public interface IRemoteHelloWorld extends Remote{
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{
protected RemoteHelloWorld() throws RemoteException{
super();
}
public String hello() throws RemoteException{
System.out.println("call from");
return "hello world";
}
}
private void start() throws Exception{
RemoteHelloWorld h=new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello",h);
}
public static void main(String[] args)throws Exception{
new RMI_Server().start();
}
}
#rmi_client
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RMI_Client {
public static void main(String[] args )throws Exception{
RMI_Server.IRemoteHelloWorld HELLO=(RMI_Server.IRemoteHelloWorld)Naming.lookup("rmi://ip:1099/Hello");
String ret= HELLO.hello();
System.out.println(ret);
}
}
二.RMI带来的安全问题
1.既然RMI Registry服务我们可以访问,那么我们如何对其攻击呢?
用p神的理解来说,RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的“后台”。我们可以尝试直接访问“后台”功能,比如修改远程服务器上Hello对应的对象:
RemoteHelloWorld h = new RemoteHelloWorld();
Naming.rebind("rmi://192.168.135.142:1099/Hello", h);
但是却会报错,

报错原因是Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法。
这时候就出现了新的方法,list和lookup方法可以远程调用
● list方法可以列出目标上所有绑定的对象:
String[] s = Naming.list("rmi://192.168.135.142:1099");
● lookup作用就是获得某个远程对象。
2.历史漏洞RMI利用codebase执行任意代码
在过去有段时间,有个applet标签,可以指定一个codebase属性,进而导致任意代码执行漏洞
而在RMI远程加载的过程中,也会涉及到codebase
首先
codebase是什么东西:(这里引用p神的文章,讲的很通俗易懂了)
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则 Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为 Example类的字节码。
RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻 找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在 本地没有找到这个类,就会去远程加载codebase中的类。
进而
所以这时候,当codebase被控制,我们就可以加载恶意类,但是只有满足以下条件的RMI服务器才能被攻击:
● 安装并配置了SecurityManager
● Java版本低于7u21、6u45,或者设置了java.rmi.server.useCodebaseOnly=false 其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:
● https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
● https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
备注:
官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。
在 java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase ,不再支持从RMI请求中获取
RMI_demo演示
rmiserver创建注册中心+绑定
#IRemoteObj
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
#RemoteObjImpl:
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
//因为最后是调用的服务端上的代码,所以要有一个真正的实现类
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
//UnicastRemoteObject是rmi的通用远程对象,如果说想要把服务端绑定在rmi服务上就必须要继承这个类
public RemoteObjImpl() throws RemoteException{
}
@Override
//就是实现一个字母大写的功能
public String sayHello(String keywords){
String upKeywords=keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
#RMIServer:
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args)throws RemoteException, AlreadyBoundException{
IRemoteObj remoteObj=new RemoteObjImpl();
//服务端创建远程对象,实际上这时候客户端已经可以和服务端通信,但是由于端口不确定,所以需要创建注册中心
Registry r= LocateRegistry.createRegistry(1099);
//创建注册中心,默认端口1099
r.bind("remoteObj",remoteObj);
//利用创建的注册中心和创建的远程对象绑定,这时候客户端通过连接注册中心即可获得服务端远程对象开启的端口
}
}

客户端请求注册中心

#IRemoteObj:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
#RMIClient:
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args)throws RemoteException, NotBoundException{
Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
//客户端连接服务端注册中心
IRemoteObj remoteObj= (IRemoteObj) registry.lookup("remoteObj");
//连接上注册中心后查找与其绑定的远程对象并获取
remoteObj.sayHello("hello");
//利用获取的远程对象执行客户端的sayHello方法
}
}
步骤:
服务端运行后,每运行一次客户端client,服务端就会回显一次HELLO

问题:
为什么客户端和服务端都要有IRemoteObj接口,但是服务端要实现该接口?
这里客户端要有IRemoteObj接口是因为当获取服务端的远程对象的时候要调用服务端的sayhello方法,服务端sayhello方法是实现的IRemoteObj接口方法
服务端要实现该接口是因为最终调用的是服务端的上代码即对应的方法
idea跟方法:
1.创建远程服务
这一步我们就是分析远程对象是如何发布到网络上去的
断点下在如图:

然后跟进去步入,发现到了

然后跟到UnicastRemoteObject(继承的类即当前类的父类)的构造方法里面
远程对象设置随机端口
注意不是注册中心的端口,而是远程对象的随机端口

继续跟exportObject

obj就是远程对象调用的方法(即这里的sayhello),UnicastServerRef是处理端口网络请求的
同时我们发现exportObject是一个静态函数,负责的就是将远程对象发布到网络上
当我们不继承UnicastRemoteObject这个类的时候,就得手动调用这个函数
继续跟到UnicastServerRef里面

发现new了一个LiveRef(port)(其实就是一个网络的引用类)
跟进去这个方法LiveRef,在跟到this方法里面


第一个参数objid就是调用的方法id,然后着重看一下TCPEndpoint,跟进去看一下

构造函数发现他就是传进去一个ip和端口就能进行网络请求
然后继续跟LiveRef 的构造函数 this 里面

然后看赋值,发现endpoint下面就有ip和远程对象的随机端口号,并且endpoint是封装在一个liveref里面,并且也只有这一个liveref(始终存储着ip和端口号)

然后这个liveref就跟的差不多了,知道这个他始终存储着ip和端口号就行了
然后一直步过F8,直到走到exportObject里面,发现有个stub的创建

tips:
什么是stub?
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
这行代码的作用是 创建一个代理对象(Proxy),通常用于封装某个实现类(implClass),并结合客户端引用(getClientRef())及是否强制使用存根(forceStubUse)的配置
简单理解stub可以认为就是封装了一个代理
那这里为什么要新建一个stub呢?(网上薅的图)

● RMI 就是在 Remote Service 的地方创建一个 Stub,再把 Stub 传到 RMI Registry(注册中心)中,最后让 RMI Client 去获取 Stub
然后我们跟到createProxy里面:

往下看就能看到一个明显的类加载的地方
第一个参数是 AppClassLoader,第二个参数是一个远程接口,第三个参数是调用处理器,调用处理器里面有一个ref(也就是上文中的有且只有一个)

然后stub代理就创建好了
然后createProxy出来之后,跟到target里面

跟进来看看参数

发现disp和stub和ref都是同一个ref(验证了上面的ref有且仅有一个)

看了看没啥用,出来继续跟,再看看exportObject是如何处理target的

这里面套了两三个exportObject,一直跟直到碰到synchronized

好了,找到了真正处理网络请求的了,跟进去listen

但是我们发现这时候这个端口还是空的

然后我我们更到newServerSocket方法里面,然后在最后有一个当检查到port=0的时候就会赋一个随机端口


走到下面发现就有另一个port
继续走,他会走到AcceptLoop方法里面


走到run方法里面,然后走到

就是根据分配到端口进行了一个连接,因为在listen里面就创建了一个socket服务
然后listen主要操作就分析完了,走出listen,走到最后

就发现了port不再为0了,这时候大致流程就都跟完了,也就是创建远程服务的代码分析
然后最后的话就是把这些内容放到了一个hashmap里面,具体就不分析了,也没啥用(好吧,虽然这个过程本身就没啥用)
总结:
- 在这个过程中是并没有漏洞产生的,大致就是用 exportObject() 指定到发布的 IP 与端口,负责的就是整个过程中不断的封装(就当更好的理解java特性了)
- 在这个过程中有两次tcp连接,第一次是客户端连接注册中心,第二次是服务端发送数据到客户端
第一次连接过程中,先寻找remoteObj这个远程对象名称,找到之后注册中心返回远程对象的序列化数据
第二次连接,客户端反序列化注册中心发送回来的数据,发现是一个远程对象且包含远程对象地址和端口,然后与远程对象建立tcp连接,然后远程方法调用
所以rmi是一个基于序列化的java远程方法调用
2.创建注册中心+绑定

更到RegistryImpl,发现新建了一个ref以及创建了一个新的 UnicastServerRef,和创建远程服务差不多

更到setup方法看一下,发现也是调用exportObject方法

然后对比创建远程服务的

发现两个exportObject区别就是一个设置true,一个默认false,两者的区别就是true创建的是永久对象,而false创建的是临时对象
然后进到setup.exportObject里面,同样创建stub

和创建远程服务不同的就是,这里创建的时候走到了createStub(remoteClass, clientRef),remoteClass不为空,走到if里面了,而创建远程服务没走到if里面,而是直接创建的动态代理


然后到setup.(UnicastServerRef)exportObject下,继续跟,发现是检测到存在stub,就会执行setSkeleton方法

setSkeleton方法用来设置Skeleton,跟进去看一下

发现是forname创建的

然后到setup.(UnicastServerRef)exportObject,继续到往下跟,到了target,和创建远程服务一样就不在跟了,然后一直f8步过到super.exportobject

f7跟进去,有一个 putTarget() 方法,是将target封装的数据放进去

然后看看具体放了什么东西,一直f8直到target的里面的id值变了
我们点开objTable看一看里面放的数据,发现随机端口和注册中心端口都有了

但是我们发现这里的target是一个DGCImpl_Stub,并不是我们之前创建的(之后会讲)
绑定
然后大致创建注册中心的流程就结束了,然后就是绑定,很简单的一个过程,就是在buildings里面放了一个hasgtable,里面存着ip和端口,然后put把ip和端口放进去就结束了,很简单的一个过程(虽然但是没啥用hhhhhhhhhh)

走过绑定之后,buildings里面就有数据了

总结:
总而言之和创建远程中心差不多,就是唯一比较大的区别就是exportobject的那个对象,一个是永久的,一个是临时的
3.客户端请求注册中心之客户端:
断点三处直接都下,然后后服务端的代码要运行起来

然后跟到getRegistry()方法里面,发现也是new一个liveref和createproxy,和之前的流程差不多

主要就是在这里面,新建一个ref,然后把注册中心ip和端口(stub)都封装到新建的ref里面,然后这里我们就相当于获取到了注册中心的stub
然后开始查找远程对象,这里就是存在漏洞的根源所在
IRemoteObj remoteObj= (IRemoteObj) registry.lookup("remoteObj");
客户端根据一个名字去向注册中心请求远程对象的ip和端口
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);
}
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);
}
}
其中下面这一段就是将我们要获取的远程对象的名称进行序列化然后写到输出流去
try {
// 获取一个 ObjectOutput 对象,该对象用于将对象写入输出流
ObjectOutput var3 = var2.getOutputStream();
// 将对象 var1 写入到输出流中,这会触发对象的序列化过程
var3.writeObject(var1);
} catch (IOException var18) {
// 如果在获取输出流或写入对象的过程中发生了 I/O 异常
// 则抛出一个新的 MarshalException 异常,并将原始的 IOException 作为其原因
throw new MarshalException("error marshalling arguments", var18);
}
既然有序列化那肯定就有反序列化(注册中心反序列化后将远程对象ip和端口(stub)再次序列化发送到客户端)
然后我们再往下看发现刚好就有一个反序列化的数据
try {
// 获取一个 ObjectInput 对象,该对象用于从输入流中读取对象
ObjectInput var6 = var2.getInputStream();
// 从输入流中读取一个对象,并将其强制转换为 Remote 类型,赋值给 var23,然后进行反序列化
var23 = (Remote)var6.readObject();
}
那这时候我们就知道了v23就是注册中心返回来的序列化数据(因为是class文件,所以变量赋值不好观看,更多的是理解数据的发送和接受流程)
那种这时候我们就知道客户端发送给网络请求到注册中心再到注册中心返回数据就是在这两段代码之间,那就直观明了了,就是super.ref.invoke(var2);有问题了

executeCall就是真正进行请求网络的地方
public void executeCall() throws Exception {
...
// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;
case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}
// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}
}
invoke() —> call.executeCall() —> out.getDGCAckHandler()
这里我们可以观察到当有一个异常为TransportConstants.ExceptionalReturn时,就会对注册中心发送回来的序列化数据进行一个反序列化,这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是当返回的是一个恶意的序列化数据,就会导致客户端受到攻击
tip:
in就是返回的数据流里面的东西

4.客户端请求服务端之客户端:
客户端第三句代码remoteObj.sayHello("hello");
这里你直接跟是无法跟进去的,因为你这里获取到的远程对象是一个动态代理,而动态代理在cc分析的时候也知道当他调用任意一个方法的时候,就会调用invoke方法,我们这里直接到RemoteObjectInvocationHandler里面给invoke下一个断点

然后执行到这然后一步一步f8(都是一些if判断异常,就不看了),直到下面有一个return,跟进去方法看看

然后看到ref.invoke,感觉有问题,跟进去看看

我们可以看到executeCall,上面的分析我们知道有这个就是会有网络请求

try {
// 从 call 对象中获取一个 ObjectOutput 类型的输出流,用于后续的对象序列化操作
ObjectOutput out = call.getOutputStream();
// 调用自定义方法 marshalCustomCallData,将自定义的调用数据进行序列化并写入到输出流 out 中
marshalCustomCallData(out);
// 获取要调用的方法的参数类型数组,该数组包含了方法每个参数的类型信息
Class<?>[] types = method.getParameterTypes();
// 遍历参数类型数组
for (int i = 0; i < types.length; i++) {
// 调用 marshalValue 方法,将每个参数的类型、参数值以及输出流作为参数传入
// 该方法会将参数值按照其类型进行序列化并写入到输出流中
marshalValue(types[i], params[i], out);
}
}
跟入marshalValue,发现就是序列化数据,而且参数是hello(就是调用远程对象执行sayhello方法要传入的参数)

既然有序列化,那肯定就有反序列化,下面就有unmarshalValue反序列化

而且因为我们传入的是字符串hello,所以上面那个if就不会进去,然后就会到unmarshalValue反序列化,然后把数据读取回来

然后再f8就会发现数据被读回来了

总结:
攻击存在的点就是注册中心返回序列化的数据到客户端时,入口类是call.executeCall(),当抛出固定异常的时候就会进行反序列化
客户端请求服务端同样存在,一个是call.executeCall(),另一个就是unmarshalValue
5.客户端请求注册中心之注册中心
上面我们分析的是客户端如何处理,这里分析一下注册中心如何处理,大致逻辑肯定是反序列化客户端发送的数据,经过处理后把要发送的数据序列化后发送给客户端
因为在客户端那里,我们操作的是一个stub,而服务端操作的是一个skel,而在之前的分析里我们知道skel会被存在target里面,所以断点下到服务端代码里的处理target的地方

先debug服务端的代码,然后在运行客户端的代码即可,如下图

然后往下走走到if判断里面,然后看看target里面

发现里面有一个stub,stub中是一个ref,对应的1099,然后过了if之后,走到了final Dispatcher disp = target.getDispatcher();,作用是将skel放到disp里面

然后继续往下走,就会调用disp.dispatch方法

进来之后,到了if判断,这里skel不为空,所以到oldDispatch()这里,然后下面就是一堆switch-case
而这些case分别代表的就是客户端和注册中心交互的几种方式
● 0->bind
● 1->list
● 2->lookup
● 3->rebind
● 4->unbind
如果这些交互方式里面对应着有反序列化的话,就可以实现客户端打注册中心
然后观察一下代码发现除了list都可以(只要有readobject就说明可以反序列化)
总结:
注册中心处理target,进行skel的生成和一系列的处理。
反序列化客户端发送来的数据之后对处理后的数据进行序列化后再发送给客户端,而攻击点就产生在注册中心反序列化客户端数据的时候,就可以实现客户端攻击注册中心的流程
6.客户端请求服务端之服务端
这个流程就较为简单了,要记得在客户端和服务端之间通信的时候,中间永远有一个注册中心作为传话人,在第五条中我们知道注册中心会生成一个skel,而得到的这个skel是一个$Proxy0类,在前面我们提过封装了三个target,这个就是其中之一
这里下两个断点

这里还有一个需要注意的就是打完这两个断点之后,得到的第一个target中的stub是DGCImpl,但这个是用来处理内存垃圾的,所以我们要按两下f9,直到看到proxy动态代理的stub为止

然后我们继续跟到dispatch里面,这是skel是空的,所以不会像注册中心的那时一样执行oldDispatch 方法

继续往下走,到metho这里,这时method就是我们之前写的sayhello方法

继续往下走,会发现执行到了一个循环里面的unmarshalValue

看到这个就知道这里是存在漏洞的了,流程就是把我们的hello序列化读进去,反序列化读出来
问题
那这里在解决一个疑问就是开头的那个DGC的stub(三个target中的一个)

断点需要下在ObjectTable 类的 putTarget() 方法里面(要把之前那两个去掉)然后运行到光标处f9

而DGC运行的原理就是将target放到一个静态表里面(这个就是之前说的objectTable里面封装了三个target)

put完之后我们发现是Proxy 这个动态代理的 Target 而非 DGC 的 Target被放进去了
而DGC的target是被封装到了static里面

创建的流程----->
在 DGC 这个类在调用静态变量的时候,就会完成类的初始化
类的初始化是由 DGCImpl 这个类完成的,所以我们到里面看看,看到一个作用于class initializer的static方法

然后就new了一堆target的属性,然后到createproxy()

createProxy() 方法进去,会看到一个 createStub() 方法,跟进去

然后判断尝试是否可以获取到DGCImpl_Stub,如果获取不到就抛出异常

然后一个DGCImpl_Stub 的服务就创建成功了
而在这个过程中存在漏洞的地方就是到 DGCImpl_Stub 这个类下有两个clean和dirty两个方法
而clean方法就存在反序列化漏洞

同样在DGCImpl_Skel 这个类下也存在

总结:
总而言之dgc就是一个自动创建的过程,用于清理内存
而漏洞点在客户端和服务端都有,因为都会清理内存,存在于skel和stub,也就是所谓的JRMP绕过(还没学,看其他博主博客学习的,之后会补充对应笔记)
大总结:
在整个rmi的分析过程中,虽然存在不少漏洞点,但实际上d单纯攻击rmi没啥用,因为在jdk8u1之后就都修复完毕了
一般是和fastjso和strust2等打组合拳较多

浙公网安备 33010602011771号