Ruby反序列化漏洞审计:Sidekiq Worker如何沦为RCE重灾区

发布时间:2026/10/21 6:03:38
Ruby反序列化漏洞审计:Sidekiq Worker如何沦为RCE重灾区 前阵子帮某团队做代码审计他们的核心服务是 Ruby 写的后台跑着一堆 Sidekiq Worker。刚开始我们的重点放在 Web 端的注入、越权这类常规问题上结果翻着翻着真正让人后背发凉的反而是 Worker 队列消费环节里的一个反序列化调用。标题里那句可导致系统完全沦陷不是夸张Worker 进程拿到命令执行权限之后后续的一切几乎都是顺理成章。这个内容适合谁看如果你是 Ruby / Rails 项目的开发者、运维或者正在做安全自查的安全工程师这篇文章会比较对胃口。我会把 Worker 为什么容易中招、Ruby 几种序列化 API 的危险程度、审计排查的完整思路、以及最终怎么修复加固一层层拆开讲。1. Worker进程为何成为反序列化漏洞的重灾区1.1 从架构矛盾说起Worker既要消化脏数据又拿着高权限大多数团队对 Worker 的定位是后台默默干活的角色。Web 进程负责接收用户请求把任务丢进队列Worker 再慢慢消费。这个分工带来一个天然的信任错位Web 层知道数据来自外部通常会做校验和过滤Worker 却默认队列里的东西是可信赖的内部数据很少再检查一遍。于是问题就来了。队列里的数据真的都可信吗未必。任务参数可能来自用户上传的配置快照、第三方 Webhook 回调、管理后台填写的 URL 地址甚至是某个内部服务被污染后写进队列的脏数据。这些数据一旦在 Worker 侧被反序列化处理风险就完全暴露了。我在实际项目里见过更离谱的情况同一个 Redis 实例既当缓存又当任务队列Redis 本身又没有做访问控制结果外部攻击者通过 Web 端的 SSRF 直接往队列里塞数据。类比一下仓库管理员收快递从来不拆开检查理由是发货方都是自己人。结果某天收到一个包裹里面不是货物而是一个在签收瞬间就会引爆的装置。Worker 就是这个管理员。1.2 反序列化的本质还原对象的瞬间就是执行逻辑的瞬间要理解这个漏洞有多致命得先想明白反序列化到底在做什么。序列化是把一个对象拍成一张X光片记录它的类型、属性、嵌套关系反序列化则是拿着这张片子在内存里重建出原来的对象。听起来很中性问题出在重建过程不是纯机械操作。Ruby 的 Marshal 机制在还原对象时会触发对象内部的回调方法比如_load、marshal_load还会逐个恢复实例变量。攻击者如果能控制序列化字节流就等于能控制这些回调的触发方式。如果他们选中的类里某个方法会调用系统命令或者能通过一系列既有类方法组合出一条调用链最终就能在 Worker 进程里执行任意命令。业界把这种组合链称为 gadget本质上是用程序自带的砖头盖成了一座通向外部的桥。很多人觉得反序列化漏洞听起来偏底层离业务很远。但从攻击者的角度看只要找到了一个能传入恶意序列化数据的人口后面的事情就跟开盲盒一样——绝大多数 Ruby 项目里都有现成的类方法可以被利用成功率并不低。1.3 容易被忽略的入口队列、缓存、回调地址都是输入面梳理 Worker 的输入面时我通常按四类去看任务队列的 Job 参数。这是最常见的一类。Sidekiq、Resque、Delayed Job 这类框架都允许把参数传到perform方法里如果这些参数是序列化数据危险程度直接拉满。从 Redis / Memcached 里读取缓存对象。很多团队在缓存里存的是Marshal.dump之后的对象取出来直接Marshal.load。缓存被污染时这一步就是入口。文件或 URL 内容。Worker 经常要处理用户上传的文件、抓取远程 URL如果文件内容是序列化格式又没有校验同样会踩雷。第三方依赖库内部反序列化。这是最隐蔽的业务代码里干干净净但某个 gem 的实现里用了危险 API一旦依赖方数据可控问题就从里面爆出来。每一条入口单独看都不起眼但叠加在一起,就构成了一张相当宽的攻击面。后面我会专门讲排查链路这里先记住一个判断标准任何可能被外部影响的字节流都不要轻易交给反序列化 API。2. Ruby三种序列化能力的安全分层别把Marshal当JSON用2.1 Marshal.load最危险还原整个对象图的代价是信任一切Ruby 原生的 Marshal 是全能型序列化工具。它能序列化绝大部分 Ruby 对象也能完整还原成对象图。问题是它太全反序列化过程中会实例化任意指定类还会触发类内各种回调。攻击者完全可以构造一个自定义类名让 Marshal 在还原时把某个类加载进来再通过类方法链跳到命令执行上。危险点在于Marshal.load 面对不可信数据时几乎没有防御能力。它不像 JSON 那样只产生基本数据类型它是你说要什么类我就给你创建什么类。攻击者可以在序列化数据里指定Gem::Installer、Gem::SpecFetcher这类 gem 自带的类利用它们内部的方法组合出命令调用链。这类链在公开资料里已经有很多现成案例所以实践中只要看到Marshal.load或它的别名Marshal.restore我基本都会直接标红。二进制格式也是个麻烦。Marshal 输出是一堆不可读的字节代码评审时很难直接看出数据长什么样。很多团队之所以迟迟没发现这类问题就是因为二进制数据不好肉眼检查也不容易被 IDE 的搜索高亮到。这里必须强调不可读不等于安全反而更容易藏东西。2.2 YAML.load不同版本默认行为变了老代码最容易踩雷YAML 的情况比 Marshal 稍微温和一点但也有明显的历史陷阱。在 Psych 4对应较新版本的 Ruby/psych gem之前YAML.load默认会实例化任意对象攻击者可以在 YAML 里声明自定义 class 标签反序列化时产生任意对象。换句话说老的YAML.load和 Marshal 的危险等级相当接近。Psych 4 之后官方把YAML.load的默认行为改成了安全模式相当于原来需要显式调用的YAML.safe_load。看起来问题解决了但新麻烦也来了有些老项目把 Ruby 从 2.x 升到 3.x 之后发现原有YAML.load报错或者行为变了开发者图省事直接改调YAML.unsafe_load。这是在重新打开潘多拉魔盒。还有一个细节YAML.safe_load有个permitted_classes参数允许你指定部分白名单类。如果开发者图方便把一些深层嵌套的类丢进白名单甚至直接放宽成permitted_classes: [Symbol, ...]安全边界就会逐步失守。白名单加得越多等价于把门缝开得越大这个参数不是拿来省事的。2.3 JSON.parse相对安全但也要警惕间接风险大多数场景下用 JSON 替代 Marshal 是最稳妥的选择。JSON.parse只会生成 Hash、Array、String、Numeric 这些基础容器和标量不会实例化任意业务类。这就在机制上卡死了绝大多数 gadget 链因为攻击者没法指定我要创建哪个类。但不能说 JSON 就完全没风险。至少有三个间接问题值得留意symbolize_keys这类递归转 Symbol 的操作可能导致大量 Symbol 无法被 GC 回收间接形成内存耗尽型拒绝服务虽不是 RCE但也不舒服。JSON 解析结果如果又被Marshal.dump之后传给某个Marshal.load那只是把危险推迟了一环该爆还是会爆。解析后的嵌套结构如果直接用于动态调用比如send、method_missing之类的模式仍可能被攻击者当成跳板。所以我的结论是JSON 本身安全但要看它处在整条数据流里什么位置。不要因为输入是 JSON就放松后续处理过程中的审计。2.4 三个API的安全对照这里整理一张表格方便做评审或自查时快速对照反序列化 API格式会实例化任意对象吗不可信数据可直接使用吗备注Marshal.load/Marshal.restore二进制是绝对不行可指定任意类名危险最高YAML.load旧版 Psych 3 及更早文本 YAML是不行升级到 Psych 4 后默认变安全YAML.unsafe_load文本 YAML是不行刻意绕开安全模式的危险 APIYAML.safe_load文本 YAML只实例化白名单类白名单之外不行白名单不宜随意放宽JSON.parseJSON否基本可以仍需留意间接风险表格背后是一句话格式的安全性是 API 设计出来的不是靠使用者的运气。选序列化方案时先问一句数据会不会被外人影响答案只要有一丝不确定就别选大批量实例化对象的那个。3. 审计复盘一条排查链路我是怎么找到问题点的3.1 用代码搜索框定全部可疑入口审计的第一步永远是代码搜索不要凭记忆。直接用grep扫一遍把危险 API 全部列出来grep -RIn --include*.rb -E Marshal\.(load|restore) app lib config grep -RIn --include*.rb -E YAML\.(unsafe_load|load)|Psych\.(unsafe_load|load) app lib config grep -RIn --include*.rb -E load\(|restore\( app/lib第三行是兜底搜索因为有些代码写得很含蓄比如public_send(:load, data)或者把Marshal封装到一个工具类之后到处调用。第一次搜索的结果往往是一堆代码位置不急着下结论先把这些点整理成清单逐个标记数据从哪来。这里有个经验搜索时务必把别名和相似写法一起搜。Marshal.restore是Marshal.load的同义词Psych.load在某些版本里也是危险用法漏掉一个别名就可能漏掉一个洞。我当时给某团队做审计时第一轮搜索只搜了Marshal.load漏了两个Marshal.restore的调用点第二轮补搜才看到。3.2 顺着数据流确认是否不可信搜索只是第一步关键是判断每个可疑点暴露给谁。我的做法是画一条数据流从反序列化调用点向上游找数据来源向下游找数据去向。以某个项目为例。某个 Worker 的perform方法里有这么一段逻辑接收一个config_payload参数先做 Base64 解码再Marshal.load。第一次看代码团队负责人解释说这是内部配置快照不会有外部数据进来。但我们往上游追了一层后发现这个config_payload最终来自一个开放的表单字段用户可以把任意字符串提交上来。这就直接坐实了不可信数据进入危险 API的完整链路。判断数据是否可信我习惯套用一个标准只要数据源头不是当前代码确定的常量或内部受信服务一律当作不可信。用户输入、Webhook、第三方回调、开放 API、缓存中可能被其它途径污染的键都属于不可信范围。不要因为目前只有内部系统调用就放松警惕攻击面是会移动的。3.3 从Job参数到命令执行危害性验证确认了入口之后下一步是评估危害到底有多大。这里要非常小心做安全验证时尽量在隔离的测试环境里进行不要在生产环境里尝试构造 payload。更稳妥的做法是先用静态分析判断可触达的类方法集合里有没有命令执行能力的类。具体来说我会做两件事列出反序列化点能触达的类。如果Marshal.load出现在某个 Job 的逻辑里那就看这个 Job 所在的应用加载了哪些 gem尤其是常见的基础库。在这些类里搜索是否有会调用system、exec、Open3、IO.popen、Kernel#spawn这类命令执行方法的方法以及它们的参数是否可能被对象属性控制。如果两者都命中那基本可以判定为高危。某团队当时的实际情况是他们引入的一个监控类库里恰好有根据传入配置拼接命令并执行的工具方法攻击者构造序列化对象把这个类加载出来再通过嵌套属性触发命令调用危害就落地了。整个验证过程并不需要真的跑 payload核心是证明数据流 类能力两端能接上。3.4 审计中的两个漏网之鱼这里分享两个容易漏掉的点都是我实际踩过的。第一个是 Rails 的ActiveSupport::MessageVerifier和MessageEncryptor。这两个类在较老版本里底层默认使用 Marshal 做序列化如果用它给用户下发 token、cookie 之类的内容一旦密钥泄露或者算法配置不当反序列化攻击就会顺着这条线进来。审计时要专门搜这两个类名不能只看字面上的load。第二个是依赖库内部的隐藏反序列化。业务代码干净不代表整条链路干净某些 gem 在做内部状态持久化时可能自行调用了Marshal.load。这类问题靠搜业务代码找不到必须靠bundle依赖清单加人工抽查。我那次审计最后就靠这个方式在某个序列化相关 gem 里挖出一个可疑调用点虽然那条链路当时还够不到外部输入但它已经暴露了如果数据源被污染会怎样的脆弱性。4. 攻击链拆解为什么最后的结果是系统完全沦陷4.1 五步链路从数据注入到主机控制把散乱的细节串起来攻击者的完整动作大致可以拆成五步每一步都环环相扣找到注入点。攻击者需要一条能把数据写进 Worker 消费来源的路径比如 Web 端的参数、Webhook、SSRF 打到的 Redis 队列。构造恶意序列化数据。用应用依赖的类库拼出能触发命令执行的对象图序列化成字节流或 YAML。投递数据。把构造好的数据通过注入点写进队列、缓存或文件。触发反序列化。Worker 消费任务时调用危险 API比如Marshal.load恶意对象被还原命令执行逻辑在进程内被调用。控制主机。拿到命令执行后攻击者会立刻尝试做权限维持比如写计划任务、放置 SSH 配置、下载持久化工具然后开始横向移动。很多人只把目光放在第 4 步觉得反正就是执行一条命令。但真正可怕的是第 5 步Worker 进程所在主机往往是整个内部网络里的熟面孔被攻破后可以用来访问数据库、内网管理接口、其它服务节点。4.2 Worker权限失控沦陷扩大化的根本原因为什么明明是一个后台任务最后能上升到系统完全沦陷核心原因是 Worker 的权限长期处于失控状态。以我见过的不少 Ruby 团队为例Worker 服务要么直接以 root 用户跑要么使用一个权限极大的服务账号数据库账号经常是 admin 角色容器里挂了 docker.sockRedis 还和主应用共享同一个实例。这样部署的原因倒也好理解方便、省事、不用做细粒度授权。可一旦 Worker 被攻破这些便利全部变成攻击者的跳板。权限失控带来的连锁效应很直观攻击者先通过 Worker 执行命令发现当前用户是 root直接读取整个文件系统找到数据库配置连上去拖走全量用户数据再从内网横向访问管理后台把一台台机器都打穿。这就是标题里完全沦陷的真实含义——一个反序列化点最终放大成对整个系统的控制。4.3 每个环节的检测机会虽然这条攻击链看起来很顺畅但每一步其实都有留下痕迹的机会。防御者如果能抓住其中任何一环都能打断链条攻击环节可用的检测手段说明数据注入队列队列写入审计、Redis 写操作监控、异常大对象检测突然出现 Base64 长字符串或复杂嵌套结构值得关注反序列化触发危险 API 调用日志、异常日志反序列化失败时往往留下格式错误或类加载异常日志命令执行HIDS 进程监控、命令执行审计、网络外连检测Worker 进程突然执行curl、wget或打开新 shell 都是强信号权限维持计划任务、启动项、SSH 配置变更监控新的 cron 或 SSH key 出现基本意味着已被入侵很多团队只做了最后一道防线也就是发现已经被控制之后再清理但中间环节的监控其实更值钱。我建议把前三类检测尽早落到基础设施里成本不高效果却很直接。5. 修复与加固让Worker从信任边界变成最小权限单元5.1 应用层首选用安全格式替代危险反序列化修复的第一步永远是消除问题源头。把所有不可信数据的反序列化调用点改成 JSON 这类安全格式并对字段做严格的类型校验。实际改造时代码可能从这样def perform(raw_config) config Marshal.load(Base64.decode64(raw_config)) process(config) end改成这样def perform(raw_config) config JSON.parse(Base64.decode64(raw_config)) job_id config.fetch(job_id) raise invalid job_id unless job_id.is_a?(String) job_id.match?(/\A[a-zA-Z0-9_-]\z/) process(config) end区别在哪里JSON.parse不会实例化任意对象攻击者没法通过指定某个 gem 类来触发命令执行。类型白名单校验进一步限制了字段值范围即使数据被篡改也只会导致业务处理失败不会把危害放大到代码执行层面。这里要特别提醒一个常见的失误有人为了让改造省事写一个通用反序列化工具类对外号称安全内部却用Marshal.load包了一层。这种封装只是把危险藏得更深搜索时反而不容易看到。安全改造最忌讳的是换个写法本质不变。5.2 如果必须保留Marshal签名校验和来源控制有些场景确实绕不开 Marshal比如处理历史遗留的二进制缓存、对接某个只输出 Marshal 的旧系统。这种情况下能做的不是让 Marshal 变安全而是限制到达 Marshal 的数据来源和完整性。最有效的办法是对数据做 HMAC 签名只有携带正确签名的数据才允许反序列化require openssl require active_support/security_utils key Rails.application.secrets.queue_hmac_key def valid_signature?(data, provided_signature) digest OpenSSL::HMAC.hexdigest(SHA256, key, data) ActiveSupport::SecurityUtils.secure_compare(digest, provided_signature) end签名方案能防住外部攻击者往里塞数据但要注意它防不住内部滥用和密钥泄露。如果攻击者已经拿到了应用内存或配置里的密钥签名校验就形同虚设。所以签名只是第一层后面还要配合强隔离。另外不建议用类名白名单来拯救 Marshal。很多人以为只要限制反序列化出来的对象必须是指定类就安全了但 Marshal 重建的是完整对象图嵌套内部仍然可能出现攻击者想要触发的类。白名单在它面前很难真正收严与其费力维护不可靠的白名单不如换个格式或者加强签名。5.3 运行层加固让Worker回归最小权限应用层修复解决的是这个洞怎么补运行层加固解决的是万一没补住损失怎么控制。两件事缺一不可。给 Worker 做最小权限加固我通常会从这几项入手使用独立的低权限系统账号运行 Worker绝不能是 root 或高权限服务账号。数据库账号重新规划Worker 只允许访问它必需的表不做 admin 级授权。Redis 启用认证和 ACL把队列键空间与缓存、其它业务键空间隔离开避免 Worker 被动读取不相关数据。容器场景下禁止挂载 docker.sock去掉特权模式文件系统尽量只读。网络层面Worker 所在主机默认拒绝出站外连只放行必需的目标端口避免攻击者拿到命令执行后直接下载工具或反弹连接。我见过一个非常好的实践某项目的 Worker 容器用 seccomp 配置限制了execve相关系统调用攻击者即使通过反序列化跑出命令执行逻辑也会在真正拉起子进程时被内核拦截。这个方案虽然不能防住所有绕过手段但能把攻击成本抬高一大截。安全治理的本质不是追求绝对防御而是让攻击者在每一步都要付出更多代价。5.4 上线前的自动化排查与长期机制修复不是一次性的Worker 业务代码不断迭代新的反序列化调用随时可能被引入。所以我建议把检查项固化到 CI/CD 流程里。最简单的做法是在 CI 里加一个正则扫描任务遇到危险 API 就让流水线失败if grep -RIn --include*.rb -E Marshal\.(load|restore)|YAML\.unsafe_load|Psych\.unsafe_load app lib config; then echo danger deserialization API found exit 1 fi也可以引入专门的安全扫描工具比如 Brakeman 这类针对 Ruby 应用的静态分析器它会产出报告并标记可疑的反序列化调用点。依赖层面用bundle audit之类的工具定期检查 gem 版本更新Psych 这类底层库的升级要及时跟。长期机制上还有一条容易被忽略队列中的数据模型要定期盘点。很多团队写的时候清楚队列里存什么半年之后就完全没人记得了。建议每隔一段时间把队列消费端的数据格式、来源、序列化方式拉一遍清单对照本期讲的判断标准逐项核查。安全能力不是上线时做一次就够而是要在持续迭代里保持住。我个人做了不少次 Ruby 服务的反序列化排查之后最大的体会是这类问题从来不是单一代码失误造成的而是不可信数据 危险 API 高权限进程这三个条件恰好凑到了一起。反过来只要拆掉其中任何一环攻击链就断了。很多团队头疼说Worker 架构改不动那至少先把高权限这一环解决掉——把 Worker 降权、隔离、监控即使一时半会儿清理不完所有危险调用整体风险也已经下降了一个量级。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询