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; |
开启服务端:
import java.rmi.AlreadyBoundException; |
2、客户端
实现Remote的对象接口:
import java.rmi.Remote; |
客户端进行远程调用:
import java.rmi.NotBoundException; |
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结点信息,然后再调用sref
的exportObject
导出对象操作
进入UnicastServerRef
的导出操作中,通过反射获取到远程调用类,再创建stub
存根(服务端创建stub
存根,发送给注册中心,客户端从注册中心拿到stub
存根,使用stub存根跟远程服务器进行通信),并对stub
创建动态代理,代理handler
处理还是由liveref
处理请求。
最后将上面的信息通过target
方法进行整合到一个对象上来,存放了stub
、impl远程服务对象
、UnicastServerRef
等信息。
其中UnicastServerRef
的通信对象和stub
的通信对象为同一个liveref
,表示引用同一个网络通信
创建好target
后,最后再将target
通过exportObj
方法导出来
即调用liveref
的exportObj
,其中的ep
为TCPEndpoint
,再调用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
对象的localport
在createServerSocket
创建服务socket server
的时候生成的,接着跟,下面的图都是调用情况,直接跟进
一直到PlainSocketlmpl.bind
方法,此时impl
为DualStackPlainSocketlmpl
对象,当前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
对象和绑定名称放入bindings
的hashtable
表中进行存储
到这里,注册中心的创建和绑定就完成
3、客户端调用注册中心
连接注册中心下断点
跟进getRegistry
函数
在客户端本地生成了liveRef
,将ObjID
和TCPEndpoint
进行封装,host
、port
为输入指定的
然后将liveRef
传入UnicastRef
方法中,最后传递给ref
对象,相当于再进行了一层封装
返回中调用createProxy
创建注册代理,跟进createProxy
这里跟注册中心创建stub
的步骤一样,通过反射调用实例化RegistryImpl_Stub
创建的stub
到这Registry
对象获取完成,接下来就是在注册中心lookup
查找远程对象名
跟进lookup
方法,先是通过newCall
方法对代理stub
获取远程连接,然后对远程连接流写入,写入对象为传进来的远程对象名称(目的是通过序列化传给注册中心我要查找的远程对象名是什么,注册中心再通过反序列化读取客户端传过来的数据)
写入过后,对call
对象执行invoke
方法操作,跟进
调用call.executeCall()
方法,executeCall
方法客户端对远程调用通信处理的核心,对获取的数据进行读取,包括读取远程对象的liveRef
的ObjID
再将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
方法
通过id
和transport
,在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
已经获取到远程调用对象的liverefresult
从lookup
方法中获取,impl
的lookup
方法也是从bindings
的hashtable
中获取远程对象名,返回给result
注册器处理客户端的大体的流程就到这完成
5、注册中心处理服务端调用
这里注册中心处理方法大体跟步骤4处理客户端的相同,不重复分析了,只是最后走进的分支不同,bind
走进0分支
注:不同的是这实现的步骤是基于服务端和注册端不在一起的情况下,在一起的情况下,服务端bind
直接就对注册中心的bind
的hashtable
进行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
也存在自己的stub
和skel
,执行位置也很相似,skel
监听线程中通过dispatch
方法执行分支DGCImpl_Stub
主要实现两个功能:clean
、dirty
clean
的作用是清除回收远程连接对象,创建call
连接流,将ObjId
、长度
等序列化写入,然后调用invoke
方法
在调用invoke
方法时,在客户端调用注册中心的时候说过会最后执行executeCall
方法对数据进行反序列化读取
dirty
方法的作用是客户端调用服务器远程引用时,使用dirty
来注册一个临时的远程引用,后续还想使用该远程引用,就再次使用dirty
方法去续租
同样该方法也会调用invoke
反序列化读取信息
然后DGC服务端的主要方法dispatch
,可以看到和注册中心的执行模式非常相似
也主要有两个分支分别针对clean
和dirty
针对不同的方法调用都显示反序列化获取stub
客户端先写入的信息ObjId
、租赁信息
等,最后再通过DGCImpl
的clean/dirty
方法对远程引用进行清除和租赁。
由于Skel/stub
都存在反序列化点,因此针对该DGC的攻击被称为JRMP攻击。
四、流程总结
直接引用java安全rmi文章总结,写的很清晰了
RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:
RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)。
Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。
RemoteCall序列化RMI服务名称、Remote对象。
RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式(传输层)传输到RMI服务端的远程引用层。
RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel.dispatch)。
Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
RMI客户端反序列化服务端结果,获取远程对象的引用。
RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
RMI客户端反序列化RMI远程方法调用结果。
五、RMI利用
1、攻击服务端Server
客户端通过注册中心获取到通信服务器端的stub,客户端直接向服务端进行通信,服务端通过获取客户端的参数进行反序列化结合组件触发漏洞
示例环境:java版本8u66,服务器使用cc3.1组件,服务端存在Object参数传入
RMIObject:
public interface RMIObject extends Remote { |
RMIObjectImpl:
public class RMIObjectImpl extends UnicastRemoteObject implements RMIObject { |
客户端调用cc1链对象传入
public class RMIClient { |
运行结果:
2、攻击注册中心Registry
在底层调试分析注册中心处理客户端和服务端请求的时候提到,根据不同的分支进入对应的处理,其中客户端使用的lookup
方法参数为字符串,因此不能通过lookup
方法传递Object
对象攻击注册中心,但服务端/客户端可调用bind
、rebind
等方法绑定对象,将Object
对象传入,注册中心再接收到服务端/客户端的bind
对象进行反序列化读取时,触发漏洞
服务端:
public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception { |
客户端向注册中心绑定恶意Bind
:
由于bind
对象是需要Remote
对象,因此调用的链最后需要将返回的对象添加一个动态代理返回给Remote
对象即可,在cc1链上最后返回值加工一下即可
public static void main(String[] args) throws RemoteException, NotBoundException,Exception { |
实现效果:
3、攻击客户端Client
客户端向注册中心请求会返回stub
时,会反序列化解析数据导致漏洞,也可以在服务端放置恶意的方法返回对象,客户端调用服务端的方法时解析放回来的数据时导致漏洞。
原理相同这里以服务器端放置恶意方法返回给客户端进行解析触发漏洞举例
服务端:
RMIObject:
public interface RMIObject extends Remote { |
RMIObjectImpl
放置恶意方法:
public class RMIObjectImpl extends UnicastRemoteObject implements RMIObject { |
客户端调用:
public static void main(String[] args) throws RemoteException, NotBoundException,Exception { |
执行效果:
0x02、LDAP
LDAP描述
引用:
LDAP是轻量目录访问协议(LightweightDirectory Access Protocol),是一种轻型目录访问协议,主要用于目录中资源的搜索和查询,是X.500的一种简便的实现。
目录是一个为查询、浏览和搜索而优化的数据库,是树状结构组织数据,通过TCP/IP传输服务运行。
LDAP作用是类似文件目录,而不是实际的数据库,功能作用比喻就是电话簿、地址簿。
LDAP服务常见端口:LDAP:389 LDAPS:636
引用文章中的原理图:
这里直接引用相关概念,写的很清楚了
以及引用概念:
目录树:
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目。
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)。
- 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来。
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。
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 { |
JNDIClient:
public static void main(String[] args) throws RemoteException, NotBoundException,Exception { |
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 { |
恶意远程地址:
恶意类:
public class EvilPayload { |
客户端执行:
public static void main(String[] args) throws RemoteException, NotBoundException,Exception { |
3、References-ldap注入(核心)
利用流程很简单:通过客户端lookup查询远程ldap服务恶意类触发漏洞。
恶意地址类:
客户端启动ldap服务,将恶意地址类进行绑定:
客户端进行查询ldap服务,触发漏洞:
三、源码分析
1、jndi-rmi原生调用分析
这里其实流程不多的,前面提到了该调用方式,顺带学习下流程分析
JNDI-RMI服务端:
public static void main(String[] agrs) throws RemoteException, AlreadyBoundException,Exception { |
JNDI-RMI客户端调用:
public static void main(String[] args) throws RemoteException, NotBoundException,Exception { |
在调用lookup
处下断点
跟进InitialContext.lookup
方法
通过getURLOrDefaultInitCtx
方法获取字符串上下文,传递的为rmi路径字符串,判断字符串协议,获取到rmiURLContext
上下文表(实际是一个hashtable
)
返回rmiURLContext
上下文表后调用,rmiURLContext.lookup
方法,实际调用rmiURLContext
父类GenericURLContext
的lookup
方法,继续跟进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 |
在该方法中,由于构建器未创建因此builder
为null
,同时该动态代理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 { |
JNDI-rmi客户端:
public static void main(String[] args) throws RemoteException, NotBoundException,Exception { |
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
方法,继续跟进
这里同样显示查看是否存在构造器,由于未创建,因此builder
为null
,再判断远程对象是否属于引用类(Reference
),属于,然后将引用对象信息赋值给ref
变量
接着ref
已经被赋值,因此进入判断体,变量f
为获取远程对象的工厂类名,ref
存在、f
存在,通过getObjectFactoryFromReference
方法从引用中获取对象工厂,跟进getObjectFactoryFromReference
方法
getObjectFactoryFromReference
方法通过类加载直接加载工厂名
通过本地类加载器去加载工厂类
由于是远程地址的工厂,本地加载器查询不到的远程的工厂,因此cla
为null
接着调用ref.getFactoryClassLocation()
查看工厂类地址,返回远程地址
然后再调用类加载去加载codebase
远程地址
这里通过调用URLClassLoader
类加载器去创建出FactoryURLClassLoader
工厂URL
类加载器,然后通过这个工厂URL
类加载器去远程加载恶意工厂
获取到加载器后,就调用加载器实例化newInstance()
,此时恶意工厂的构造函数被执行,触发漏洞
调用链
由上面分析步骤得到JNDI-rmi的Reference注入调用链:
RegistryContext.DecodeObject() |
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
方法,跟进
获取到远程ldap
的entry
后赋值给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