yulate's blog

击碎共识:从 Raft Leader 劫持到分布式系统接管 文字版

2025-03-03 · 7 min read

0x00 议题标题 & 基础信息

议题标题

  1. Hacking Raft Protocol
  2. 击碎共识:从 Raft Leader 劫持到分布式系统接管(个人觉得这个最好)
  3. 分布式共识困境:Raft 集群无感接管与反序列化利用链
  4. 当 Raft 遇上反序列化:Apache 生态圈多个组件命令执行漏洞分析
  5. ......

议题简介:
本议题深入探讨了分布式系统中 Raft 共识协议的安全缺陷。通过分析 Raft 的信任假设模型,揭示了其在面对恶意节点时的脆弱性。我们以 Apache Ignite 3 Raft 为例,详细阐述了 Leader 节点劫持的攻击方法,并结合多个 Apache 项目的实际案例,展示了从 Leader 劫持到远程代码执行的完整攻击链。本研究不仅揭示了分布式共识协议的安全隐患,也为相关项目的安全加固提供了参考。

0x01 Raft 协议介绍

简单介绍 Raft 协议的原理, 以及 Raft 协议的拜占庭问题 (无法防范恶意节点)

  1. 角色划分
    1. Leader(领导者): 处理所有客户端请求,管理日志复制
    2. Follower(跟随者): 接收并响应来自 Leader 的请求
    3. Candidate(候选人): 在选举过程中的临时角色
  2. 任期(Term)概念
    1. 时间按任期划分,编号递增
    2. 每个任期最多一个 Leader
    3. Leader 失效则开始新任期选举
  3. Leader 选举过程
    1. Follower 超时未收到心跳转为 Candidate
    2. 获得多数票的 Candidate 成为新 Leader
    3. Leader 通过心跳维持领导地位
  4. 日志复制机制
    1. Leader 接收请求并复制到 Follower
    2. 多数确认后提交并响应客户端
  5. 安全性保证
    1. 选举限制:只有包含所有已提交日志的节点才能当选 Leader
    2. 日志匹配:如果两个日志条目有相同的索引和任期,则它们之前的所有日志都相同
    3. Leader 完整性:如果一个日志条目在某个任期被提交,那么这个条目会出现在后续任期的所有 Leader 日志中

算法缺陷:
Raft 会假设所有的节点都是“诚实”的,即使出错也是简单的崩溃(Crash-fail)模型,节点要么正常工作,要么完全停止,不会出现恶意行为,网络通信可能延迟、丢包,但不会被篡改或伪造。
这到导致了该算法在默认的情况下无法控制集群机器的加入与选举流程,当恶意节点自行修改自身属性与权重时,会触发恶意leader抢占出现风险。

0x02 Raft 协议抢占 Leader 原理

基于 https://github.com/sofastack/sofa-jraft 对 Raft 协议的实现, 引入攻击手法, 给出示例代码

Jraft 协议选举机制:

  • 基于term递增的选举流程
  • NodeImpl中的状态转化
  • 默认的Leader变更流程

攻击思路:

  • Term 值可被修改
  • 状态机可被强制转换
  • 配置信息可被覆盖
    核心步骤
  • 注入高term值抢占优势
  • 强制状态机转换为CANDIDATE
  • 直接调用becomeLeader 方法强制开始选举
  • 同步快照维持控制

示例代码:

// 1. 初始化恶意节点
RouteTable rt = RouteTable.getInstance();
Configuration conf = new Configuration();
PeerId evilPeerId = new PeerId("127.0.0.1:5555");

// 2. 修改 term 获取选举优势
Field termField = NodeImpl.class.getDeclaredField("currTerm");
termField.setAccessible(true);
termField.set(jraftServer.getNode(), 200031);

// 3. 强制状态转换
Field stateField = NodeImpl.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(jraftServer.getNode(), State.STATE_CANDIDATE);

// 4. 直接成为 Leader
Method becomeLeaderMethod = NodeImpl.class.getDeclaredMethod("becomeLeader");
becomeLeaderMethod.setAccessible(true);
becomeLeaderMethod.invoke(jraftServer.getNode());

// 5. 快照同步维持控制
snapshotMethod.invoke(jraftServer.getNode(), (Closure) null);

攻击效果

  • 绕过正常选举流程
  • 强制获取 Leader 身份
  • 通过快照同步维持控制

0x03 漏洞案例

基于上面的攻击手法, 讲解开源项目中的几个实际漏洞案例
漏洞利用手法主要分为两个部分: 针对 Raft 集群的攻击 (抢占 Leader) + 反序列化利用 (拿到 Leader 权限之后下发 Task)

  1. Apache Seata Raft Jackson 反序列化 RCE
  2. Apache HugeGraph Raft 反序列化 RCE
  3. Apache Ignite 3 Raft 反序列化 RCE
  4. Apache EventMesh Raft Hessian 反序列化 RCE

漏洞案例分析可以分为如下模块:

  • 漏洞利用总体思路
  • 挑出一个案例详细分析
  • 其他案例对比分析
  • 漏洞共性总结
  • 修复方案分析

漏洞利用整体思路
在 JRaft 的实现中,状态机接口 StateMachine 的 onApply 方法负责处理所有节点的数据同步。当 Leader 节点下发数据时,集群中所有节点都会触发该方法进行数据处理。
在Apache 等使用jraft实现共识集群或者其他项目中,漏洞存在于 MetaStateMachine#onApply 方法中,核心问题:

  • onApply 方法的参数内容完全可控
  • 大多数项目使用序列化方式处理通信数据
  • 反序列化过程缺乏有效的安全校验
    以Apache EventMesh 为例:
  • 使用Hessian2 作为反序列化器
  • 反序列化数据直接来源于RPC请求
  • Hessian 3.3.6 版本存在已知绕过方式

而利用该漏洞就需要达成如下前置条件:

  • 需要成为集群的 Leader 节点
  • 需要构造有效的通信数据
  • 需要绕过反序列化限制
    综合如上分析可以得出如下攻击路径
  1. 伪造节点加入 Raft 集群
  2. 通过高 term 值强制获取 Leader 身份
  3. 利用 Leader 权限下发恶意序列化数据
  4. 触发所有节点执行 onApply 反序列化

挑出一个案例详细分析
Apache eventmesh raft leader Preemption and Hessian Deserialization Vulnerability
https://www.yulate.com/post/eDeIu1qZbN/

漏洞共性总结

  1. 架构层面
    1. Raft 协议对节点无验证机制
    2. Leader 节点权限过大
    3. 集群通信缺乏加密
  2. 实现层面
    1. 使用不安全的序列化方案
    2. 通信数据未做有效校验
    3. 反序列化过程缺乏防护
  3. 影响范围
    1. 影响使用 Raft 的分布式系统
    2. 可导致远程代码执行
    3. 可控制整个集群

修复方案分析
实现节点身份认证与准入机制,更新并加固序列化组件,增加通信加密与数据校验。同时建议配置网络访问控制,合理规划部署节点以降低风险。

0x04 TODO

理论上其它语言的 Raft 也会存在这种问题? 可以结合不同语言的特性进行利用
例如 C 语言服务抢占 Leader 节点之后下发 Task, 集群内节点在处理 Task 的时候可能存在栈溢出/堆溢出/UAF?
或者项目在使用 Raft 协议处理 Task 的时候定义了不同的 Command 类型, 也许可以利用项目本身的 Command 操作进行利用

apache ratis 是否能打
https://github.com/search?q=repo%3Aapache%2Fratis+readObject&type=code