JNDI概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用。这些对象可以存储在不同的命名或目录服务中,例如RMI、CORBA、LDAP、DNS等。
JNDI包括以下提供者
- LDAP
轻量级目录协议
- RMI
远程对象注册表,具体内容可看RMI反序列化学习
- DNS
域名解析服务
- CORBA的COS服务
JNDI与RMI
ps :这个方法在jdk8u121后就被修复了,能碰到的话,评价是且用且珍惜
导入Demo
JndiRmiServer
1 | public class JNDIRMIServer { |
JndiRmiClient
1 | public class JNDIRMIClient { |
这里的话,就是通过lookup这个函数去加载rmi远程注册表上的Hello对象,假如这里是可控的话,我们显然可以加载自设的恶意RMI服务,从而实现反序列化任意对象的目的,但这个并不是传统意义上的Jndi注入
传统Jndi注入
解释:将远程对象设置为引用对象,关于引用对象的概念有点类似于代理模式
(当调用引用对象的时候【这里体现客户端获取引用对象?/调用引用对象方法?】,会自动调用设定的factory类里的代码)
下面是们要用到的一些demo源码
IRemote接口
1 | import java.rmi.Remote; |
Main(RMI注册)
1 | import org.example.HelloImpl; |
JndiRmiServer
1 | public class JNDIRMIServer { |
TestRef
1 | import java.io.IOException; |
客户端
JndiRmiClient
1 | public class JNDIRMIClient { |
以上例子本质上还是利用类加载机制来实现的RCE,仍然没有跳出我们之前的所学范围
而这里的具体利用,其实就是之前RMI反序列化部分学习过的codebase恶意类加载,没什么过多好说的
lookup跟踪调试
在客户端这里给lookup方法上个断点,跟进调试一下
首先进入了InitialContext#lookup
:
继续跟进一下来到了GenericURLContext
(这里是由前面的getURLOrDefaultInitCtx决定的,由于JNDI调用的是RMI服务,所以这里进的是GenericURLcontext类,对于不同的服务,进入的Context不同(具体详情可以自己跟进一下getURLDefaultInitCtx
),接着跟进 ctx.lookup
定位到了RegistryContext
这里,跟进断点处的lookup
这里的话是调用了注册中心的lookup方法,是本次重载的最后一层了,好像在这个方法这里也有一个相关的利用点,先不做深究了,这里执行一下lookup
可以看到从远程注册表那里取回了一个obj对象,但出乎意料的是对象的类型不是我们预料中的引用类型的对象,猜测上一层的lookup处进行了加密处理
(后来看了看别的博主,发现猜错了,是在服务端那里加的密)
( ==想问一下wrapper类型的对象到底是什么,感觉好多地方能看到他== )
这里跟进一下服务端,这里就不解释这么详细了,到registry之前的内容做下省略
最后是跟到了RegistryContext这里,能够看到他最终是重载了注册中心的rebind方法
比较关键的地方是这个encodeObject,光看名字就知道和我们的reference对象加密有关
(这个时候能够看到右下角的obj还是个引用对象)
跟进 encodeObject
方法
能够看到这里是用NamingManager.getStateToBind对obj进行了一次处理,再根据obj处理后的类型,将obj给打包起来,加密问题大概就是这样的,接着回到客户端
(怕忘了,把当时的进度再贴一遍)
跟进一下解密用的decodeObject函数
decodeObject这里调用了getObjectInstance函数,光是看名字就知道这位是重量级,得跟进
(顺便提一下,现在已经能看到obj被解析回引用类型了)
跟进结果如下,咳咳,原本一位是在第三个断点处返回的对象,但根本没进去,很麻,看了看别的师傅的博客,发现在factory.getObjectInstance就返回了,我们跟进一下getObjectFactory
这里又是个关键点,他会根据我们所传入的factoryname(服务端的第二个参数),尝试在本地实例化,如果没实例化成功,则通过codebase所记载的地址去加载factory对象
跟进一下最后的loadclass,发现了关键方法,newstance,通过这个方法拿到了我们的恶意类的类对象
拿到类对象后是在这里进行newInstance的,步过一下就会RCE
JNDI与LDAP
ps : 在8u141以下的版本可复现,工程师当年仅仅修复了RMI和CORBA的问题,把这个给忘
复现Demo
JndiServer
1 | public class JNDIRMIServer { |
JndiClient
1 | public class JNDIRMIClient { |
此时此刻的ApacheDirectoryStudio内部详情
流程分析
老样子,在Client处下个断点
进去之后还是老样子,根据调用的协议一步一步找重载lookup,这里省略省略,大体流程和之前类似
这里的c_lookup就已经是最后一层的lookup了,没必要跳了
仔细的分析一下里面的情况,查看了一下它的方法字段,发现多了一个obj的属性,估计这个就是我们从ldap服务上所夺取的对象,给他下个断点,后面重点关注一下
在这个方法处,我们得到了我们的ref对象,这里是通过attrs解密获得的,在obj.java这里找到了我们
的decodeReference方法,最后在这个方法里返回了我们的引用对象
回到LdapCtx这里继续分析,在下面这里通过DirectoryManager.getObjectInstance方法,获得工厂对象(这里就是我们的恶意类)
在这里获取引用对象所指向的工厂的类对象,跟进一下
唔,和之前是一样的,在本地没有找到相应类,所以这里就从codebase加载对象,这里要是深究一下里面的内容结构的话,会发现他是加载了一个UrlClassLoader(和rmi那个一样),不深究了,反正就是通过这个方法拿到了一个工厂对象的类对象(TestRef)
拿到类对象以后,实例化了一下,执行构造函数,完成RCE
JNDI的高版本RMI绕过
复现Demo
pom.xml
1 |
|
JNDIBPServer
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
JNDIBPClient
1 | import javax.naming.*; |
前情提要
这里在8u某个版本之后,jdk对于codebase加了一层处理,他会检查一下codebase中传入的url是不是可信任的url,如果是可信任的url就从中加载指定class文件,若不是可信任url则放弃执行
具体位置如下
这直接导致我们之前通过codebase加载恶意类的方法失效
无论是前面的ldap还是rmi在此都无法使用了
最后的解决方案是利用服务器上的Tomcat内置类 BeanFactory.getObjectInstance
实现RCE
(getObjectInstance为这里工厂对象默认会调的一个方法)。
总而言之,==这里的原理是利用将工厂对象设置为BeanFactory这个tomcat内置类来调用里面危险的getObjectInstance方法(里面有method.invoke)==
流程分析
前面的和之前的分析流程都是一样的,我们直接来到getObjectInstance
这里,跟进去看看里面的执行流程
熟悉的地方,熟悉的断点,再跟
来到了和之前流程都不一样的一个地方了,之前我们在本地都找不到facrory工厂类,必须得从codebase加载factory,但现在直接就能从本地查到,省去了codebase查找的流程,在这里就能直接实例化出factory的类对象,这里不看了,直接跟进一下facroty.getObjectInstance
这个方法
来到了BeanFactory里面
它这里先是依据factory的classname属性设定好了beanClassName
字段,用于一会儿得到类对象
再是通过ref.get方法,得到了forceString的值,这里不知道他这个forceString到底是个什么类型的东西,跟进去get看一看
好吧,只能说是类似于属性的东西吧,这个get方法就很常规的从列表里面取出值而已,没什么价值,我们步出回去
forced是我们的方法集合,这里要记住
然后value的值是从ra里面抠出来的,这里经过处理后是Sentiment=eval
对value进行一点小小的分离处理,paoname就是Sentiment=eval
等号后面的部分,param就是等号前面的部分,得到forced集合
最后,经过一系列比较复杂的操作(其实也还行,就是取出RefAddr里面不为scope/forceString/auth.factory的元素而已),从那个类似属性的集合里面抠出来了方法的参数(Runtime.getRuntime().exec()
)交给method.invoke执行,弹出计算器,至此结束