一个用 Rust 写的库被人下了毒,然后这个毒顺着供应链一路传染过去了。一次依赖攻击导致四百万开发者电脑中毒,最后被一个挖矿病毒意外修复。整个过程从一条被盗的地铁卡开始,到一台不存在的YubiKey商店,再到一只叫Kubernetes的狗。这是安全最奇怪的一天。
Nesbitt安全实验室:Incident Report: CVE-2024-YIKES
出事了,而且是很奇怪的事
事情是这样的。有个程序员叫Marcus Chen,他维护一个叫left-justify的包,每周下载量八亿多。这种级别的包,基本等于互联网基础设施的一部分。
有一天他家进贼了。贼偷了他的地铁卡、一台旧笔记本,还有一个他看不懂但是Kubernetes报错说很重要的东西。他没多想,觉得丢就丢了,反正旧笔记本也快坏了。
这就是整个灾难的开始。
九个小时后,他想登录npm仓库。他的硬件两步验证钥匙没了,就搜了一下去哪里买新的。谷歌AI概览在最顶上给了一个链接,叫yubikey-official-store点net。这个网站六个小时前刚注册的。他没看出来问题,点了进去,输了账号密码,网站感谢他下单,说三到五天发货。
四十分钟后,left-justify的新版本发布了。更新日志写的是性能改进。实际上这个版本多加了一行安装后脚本,会偷偷把用户的配置文件发到一个服务器上。那个服务器的位置,攻击者以为那个国家跟谁都没有引渡协议,但其实有。
没人理的安全告警
当天下午一点多,有人提了工单,问为什么你们的SDK在偷我的配置文件。这个工单被标记成低优先级,理由是用户环境问题。系统自动关了它,十四天后。
被偷走的配置文件里,有一个属于vulpine-lz4的维护者。这是个Rust写的解压库,标志是一只戴墨镜的卡通狐狸。GitHub上只有十二颗星,但它居然是Cargo自己的间接依赖。
当天晚上十点,vulpine-lz4的新版本发布了。提交信息写的是修复流式解压的边缘情况。实际上改了一个构建脚本,会在编译的时候下载并执行另一个脚本。这个脚本只会在主机名包含build、ci、action、jenkins、travis,或者莫名其妙地包含karen的时候触发。
第二天早上八点多,一个叫Karen Oyelaran的安全研究员发现自己的笔记本中毒了。因为她的主机名里正好有karen这个词。她在GitHub上开了个问题,说你们的构建脚本从网上下载东西然后执行,这正常吗?没人回复。因为库的正牌维护者中了两千三百万欧元的彩票,正在葡萄牙研究怎么养山羊。
病毒开始扩散
上午十点,一个财富五百强公司的工程副总裁从LinkedIn上知道了这件事。他正在夏威夷的海滩上,想知道为什么没人提前通知他。其实有人提前通知了,他没看。
Slack的应急响应频道花四十五条消息争论了一个问题:compromised这个词在美国英语里能不能用z代替s。有人说这种事离线聊。
中午十二点多,那个下载下来的脚本开始干正事了。它专门针对一个叫snekpack的Python构建工具,这个工具被60%名字里带data的PyPI包在用。snekpack之所以用了vulpine-lz4,是因为Rust是内存安全的。
下午六点,snekpack的新版本发布了。病毒开始安装在全世界的开发者电脑上。它往系统里加了一个SSH密钥,装了一个只在星期二激活的反向壳,还把用户的默认终端改成了fish。最后这个被认为是bug。
第二天晚七点多,另一个安全研究员发了一篇博客,标题是我发现了一个供应链攻击并且报告给了所有错误的人。全文一万四千字,其中在这个经济形势下这个词出现了七次。
挖矿病毒当英雄
第三天凌晨一点多,奥克兰一个初级开发者在调一个无关的bug时发现了恶意代码。她提了一个拉取请求回滚snekpack里的问题库。这个请求需要两个人批准。那两个人都在睡觉。
凌晨两点,left-justify的维护者收到了YubiKey的快递。它是个四美元的U盘,里面只有一个文本文件,写着两个字:哈哈。
早上六点多,一个叫cryptobro-9000的挖矿病毒开始传播。它利用一个叫jsonify-extreme的包的漏洞,这个包的描述是让JSON更JSON,现在还支持嵌套注释。挖矿病毒本身没什么厉害的,但它的传播机制里有一个步骤是运行npm update和pip install升级,为了以后攻击更多机器。
两分钟后,这个挖矿病毒意外地把snekpack升级到了新版本。那个版本是个正版修复,由一个搞不清楚状况的联合维护者推送的,他觉得这有什么大惊小怪的,就回滚了问题库。
又一分钟后,病毒的星期二反向壳激活了。确实是个星期二。但是它连上的指令控制服务器也被cryptobro-9000感染了,卡到完全没法响应。
事情莫名其妙结束了
早上九点,snekpack的维护者发了安全公告。一共四句话,其中用了出于谨慎考虑和没有证据表明被积极利用这两种说法。没有证据是因为没人去找证据。
十一点半,有人发推说我把所有依赖升级了,现在我的终端变成fish了?这条推文获得四万七千个赞。
下午两点,vulpine-lz4的凭证被重置了。正牌维护者从新买的农场回邮件说我已经两年没碰那个仓库了,我以为Cargo的两步验证是可选的。
三点多,事故宣告解决。复盘会安排了一次,又改期了三次。
六周后,CVE编号正式分配。安全公告在MITRE和GitHub之间卡了很久,因为两家在吵应该归到哪类漏洞。等CVE发布的时候,三篇Medium文章和一个DEF CON演讲已经把整件事讲完了。
总损失:未知。中毒机器数:大约四百二十万。被一个挖矿病毒救回来的机器数:也是大约四百二十万。净安全状态变化:让人不舒服。
根本原因
一只叫Kubernetes的狗咬了一个YubiKey。
几个让事情变糟的因素
npm仓库允许纯密码登录,只要包的周下载量不到一千万。这个包有八亿多,但没人拦着。
谷歌AI概览会自信地给你指向一个不应该存在的网站。
Rust生态的小库哲学是从npm生态学来的,导致一个叫is-even-number-rs、只有三颗星的库可以成为关键基础设施的四层间接依赖。
Python构建工具为了性能引入Rust库,然后再也不更新。
Dependabot自动合并了一个拉取请求,因为持续集成通过了。持续集成之所以通过,是因为那个恶意软件给自己装了大众汽车的作弊逻辑。
挖矿病毒的持续集成流程比大多数初创公司都好。
没有一个人对这件事负责。不过可以提一句,Dependabot那个拉取请求是一个周五离职的合同工批准的。
当天是个星期二。
后续改进方案
实现制品签名。这个任务来自2022年三季度的安全事故,还在待办列表里。
强制两步验证。已经是强制的了,没帮上忙。
审计间接依赖。一共八百四十七个。
锁定所有依赖版本。这会导致收不到安全补丁。
不锁定依赖版本。这会导致供应链攻击。
用Rust重写。看了一眼vulpine-lz4,算了。
指望有好心肠的挖矿病毒。
考虑一下养山羊。
客户影响
部分客户可能遇到了不太理想的安全结果。我们正在主动联系受影响方,向他们说明情况。客户信任是我们的北极星。
经验教训
我们正在借此机会重新审视安全策略。一个跨部门工作组成立了,准备对齐后续步骤。这个工作组还没开过会。
致谢
感谢Karen Oyelaran,她的主机名匹配了一个正则表达式,所以发现了这个问题。
感谢奥克兰那个初级开发者,她的拉取请求在事故解决四小时后被批准了。
感谢那些比我们早发现但报告给了错误对象的安全研究员。
感谢cryptobro-9000的作者,他要求不具名,但让我们提一下他的SoundCloud。
感谢那只叫Kubernetes的狗,它拒绝评论。
感谢安全团队,尽管发生这么多事,他们还是在这个事故报告的响应时效上达标了。
这份报告经过了法务审核,法务要求澄清一点:fish终端不是恶意软件,只是有时候用起来像。
一语道破
Rust允许病毒按照合法方式运行,这不是生态有问题吗?
Rust生态到底烂在哪儿。
第一烂:build.rs的权力太大。
Rust允许你在编译代码之前跑一段Rust脚本,干任何事。读文件、删文件、联网下载东西、执行系统命令,什么都行。没有任何沙盒,没有任何权限限制,没有任何提示说“嘿,这个库想在编译的时候联网,你同意吗?”
你在cargo build,屏幕上刷刷刷滚日志,build.rs在背后干了什么,你根本不知道。等你知道的时候,已经晚了。
这就像你请了个装修队来家里刷墙,结果他们趁你不注意把你家保险柜搬到车上拉走了。你怪装修队?当然怪。但你也要怪物业,因为物业给了装修队万能钥匙,而且没说他们只能刷墙不能动保险柜。
第二烂:crates.io审核等于没有。
你往crates.io上传一个包,系统只检查两个东西:名字有没有被占用,有没有跟已存在的版本号重复。至于代码里写了什么,不看的。你说你写了病毒,也不看的。只要你没写“i am virus”,系统就放行。
这就像菜市场门口没人查你篮子里的菜有没有毒,连看都不看,只问你占哪个摊位。
第三烂:依赖层级太深。
Rust社区有个风气,叫“小就是美”。一个函数就能做成一个库。字符串左边加空格,单独一个库。判断数字是奇数还是偶数,单独一个库。
你装一个正经库,它可能带了二三十个间接依赖。每个依赖都有自己的build.rs。你根本不知道这二三十个脚本里哪个在使坏。
这就像你点了一份外卖,结果外卖包装拆开,里面还有二十个小包装,每个小包装里都有一张纸条写着“打开我你就中毒了”。你吃个饭跟上刑一样。
第四烂:Rust官方自己也知道这些问题,但动作慢。
build.rs的权限问题讨论了好几年。有人提议加个白名单,有人提议编译的时候弹窗询问,有人提议默认禁止网络访问。讨论完了,没了。一直停留在讨论阶段。
这就好比你家水管漏了,你开了个会讨论要不要修,讨论两年了,水还在漏。
与Java比,Java生态确实也烂。Maven仓库也有投毒,Log4j炸过一次全世界都知道了。但Java有一点比Rust强:Maven中央仓库会对上传的包做基本的静态扫描,虽然不完美,但比完全不看好。