供应链污染:为CTF挑战挖掘每周1600万下载量的npm包漏洞

本文详细记录了作者如何通过模式匹配和功能分组技术,在npm生态中发现原型污染漏洞,重点分析了ini解析器的高危漏洞及其影响范围,并分享了负责任的漏洞披露流程。

供应链污染:为CTF挑战挖掘每周1600万下载量的npm包漏洞

2020年12月23日 · 1527字 · 8分钟阅读

背景 🔗

GovTech网络安全小组最近在2020年12月4日至6日举办了STACK the Flags网络安全夺旗赛(CTF)。对于Web领域,我的团队希望构建能够解决我们在政府Web应用程序和商业现成产品渗透测试中遇到的实际问题的挑战。

根据我的经验,大量漏洞源于开发人员对其代码中使用的第三方库缺乏熟悉度。如果这些库被恶意行为者破坏或以不安全的方式应用,开发人员可能会在不知情的情况下在其应用程序中引入毁灭性弱点。SolarWinds供应链攻击就是这方面的典型例子。

作为Web开发人员最流行的编程语言之一,Node.js生态系统在第三方库方面也存在不少问题。Node包管理器(更广为人知的是npm)每月提供超过1000亿个包,并托管近150万个包。包管理器如此庞大的部分原因在于树状依赖结构。每次在项目中安装一个包时,还会安装该包的依赖项,以及它们的依赖项,依此类推——有时最终会安装数十个包!

如果此链中的单个依赖项受到破坏或存在漏洞,可能会对整个生态系统产生连锁效应。2018年,一个广泛使用的npm包event-stream被恶意作者接管,他添加了针对Copay比特币钱包的比特币窃取代码。尽管攻击者心中只有一个目标,但在恶意代码被发现之前的2.5个月内,流行的event-stream包被下载了近800万次。2019年,我在Black Hat Asia上展示了一个名为npm-scan的工具,旨在识别恶意包,但很明显npm需要系统地解决这个问题。幸运的是,自那时以来,npm生态系统已显著改进,包括发布了npm审计功能和更积极的监控。

寻找NPM包漏洞 🔗

考虑到这一背景,我开始设计一个使用易受攻击的npm包的挑战。此外,我想利用原型污染漏洞。简而言之,原型污染涉及通过污染对象的原型来覆盖应用程序中Javascript对象的属性。例如,如果我覆盖了对象的toString属性并使用console.log打印该对象,它将输出我覆盖的值,而不是该对象的实际字符串表示。这可能导致关键问题——想象一下,如果我将用户对象的isAdmin属性覆盖为始终为true,会发生什么!然而,由于原型污染的影响仍然取决于应用程序上下文,很少有人知道如何正确利用它。

接下来,我应用了两种策略来查找易受原型污染攻击的npm包:模式匹配和功能分组。

模式匹配 🔗

当编写易受攻击的代码时,它通常落入可识别的模式,这些模式可以被静态扫描器捕获。这构成了许多工具(如GitHub的CodeQL)的基础,这些工具扫描开源代码库以查找不安全的代码模式。虽然扫描器被防御性地用于提前发现漏洞,但攻击者也可以执行自己的模式匹配以发现开源代码中未报告的漏洞。

我选择的工具是grep.app,这是一个快速的正则表达式搜索引擎,可搜索GitHub上超过50万个公共存储库。由于大多数npm包在GitHub上托管其代码,我有信心它会发现至少几个易受攻击的包。下一步是确定一个有用的正则表达式模式。我查找了之前披露的npm包中的原型污染漏洞,并找到了2020年1月Snyk对dot-prop包的咨询。接下来,我检查了修补该漏洞的GitHub提交。

dot-prop通过黑名单以下键来修补原型污染漏洞:

1
2
3
4
5
const disallowedKeys = [
    '__proto__',
    'prototype',
    'constructor'
];

这里,没有明显的 inherently vulnerable 代码模式;正是缺乏黑名单使其易受攻击。我决定稍微放大范围,专注于dot-prop最初需要黑名单的功能。根据包描述,dot-prop是一个使用点路径从嵌套对象中获取、设置或删除属性的包。

例如,我可以这样设置一个属性:

1
2
3
4
// Setter
const object = {foo: {bar: 'a'}};
dotProp.set(object, 'foo.bar', 'b');
console.log(object); // {foo: {bar: 'b'}}

然而,以下概念验证将使用dot-prop的set函数触发原型污染:

1
2
3
4
const object = {};
console.log("Before " + object.b); // Undefined
dotProp.set(object, '__proto__.b', true);
console.log("After " + {}.b); // true

这是因为dot-prop的功能是将点路径字符串解析为对象中的键并设置这些键的值。根据我们对原型污染的了解,这本质上是危险的,除非某些键被列入黑名单。

考虑这一点后,我决定搜索匹配其他点路径解析器的模式。dot-prop使用path.split(’.’)来分割点路径,尽管我后来发现key.split(’.’)也被其他包常用。通过这种方法,我发现了几个易受攻击的包,但这需要我手动检查每个包的代码以验证是否使用了黑名单。此外,并非所有点路径解析器都使用key或path来表示点路径字符串,因此我可能错过了更多。

功能分组 🔗

我意识到更好的方法是将npm包基于其功能进行分组——在前一种情况下,是点路径解析器。这是因为这种功能默认是不安全的,除非设置了适当的黑名单或保障措施。在浏览了点路径解析器之后,我偶然发现了一组更丰富的包——配置文件解析器。

配置文件有各种格式,如YAML、JSON等。其中,TOML和INI非常相似,并匹配以下格式:

1
2
[foo]
bar = "baz"

典型的INI解析器会将此文件解析为以下对象:

1
iniParser.parse(fs.readFileSync('./config.ini', 'utf-8')) // { foo: { bar: 'baz' } }

然而,除非解析器设置了黑名单,否则以下配置文件将导致原型污染:

1
2
[__proto__]
polluted = "polluted"

然而,除非解析器使用黑名单,否则以下配置文件将导致原型污染:

1
2
3
4
iniParser.parse(fs.readFileSync('./payload.ini', 'utf-8')) // { }
console.log(parsed.__proto__) // { polluted: 'polluted' }
console.log({}.polluted) // polluted
console.log(polluted) // polluted

确实,之前在此类解析器中报告过原型污染漏洞,但只是临时性的。我构建了概念验证代码以快速大规模测试包,然后使用npm的搜索功能发现其他解析器。搜索功能支持按标签搜索,如keywords:toml或keywords:toml-parser,使我能够快速发现多个易受攻击的包。

其中之一是ini,一个简单的INI解析器,每周下载量惊人地达到1600万:

这是因为近2000个依赖包使用ini,包括npm CLI本身!由于npm随每个默认Node.js安装包一起提供,这意味着每个Node.js用户也在下载易受攻击的ini包。其他著名的依赖包括Angular CLI和sodium-native,一个围绕libsodium加密库的包装器。虽然这些包将ini作为依赖项包含,但它们的风险取决于ini的使用方式;如果它们不调用易受攻击的函数,漏洞就不会被触发。

尽管我没有在挑战中使用ini,但我确保负责任地向npm披露了易受攻击的包列表。

负责任披露 🔗

npm支持一个强大的负责任披露流程,包括一个目前暂停的漏洞披露计划。开源安全公司Snyk也提供了一个简单的漏洞披露表单,我用来协调披露。幸运的是,ini的披露过程顺利进行,开发人员在两天内修补了漏洞。

  • 2020年12月6日:向Snyk初步披露
  • 2020年12月7日:Snyk首次回应
  • 2020年12月8日:向开发者披露
  • 2020年12月10日:发布补丁
  • 2020年12月10日:披露发布
  • 2020年12月11日:分配CVE-2020–7788

其他包正在进行负责任披露或已被披露,例如multi-ini。

漏洞狩猎过程突出了开源包的优势和弱点。尽管第三方编写的开源包可以被分析漏洞或被恶意行为者破坏,但开发人员也可以快速发现、报告和修补漏洞。组织和开发人员在使用包之前有责任审查它们。虽然并非每个人都能负担直接检查代码所需的资源,但有免费工具(如Snyk Advisor)使用更新频率和贡献历史等指标来估计包的健康状况。开发人员还应审查包的新版本,特别是如果它们由不同的作者编写或在非正常时间发布。

从长远来看,开源包安全没有简单的答案。然而,组织可以应用合理的措施来有效保护其项目。

附言:我们的一位参与者Yeo Quan Yang发布了一篇优秀的挑战报告,说明了将包中的原型污染与模板引擎中的远程代码执行小工具链接的预期解决方案。在此查看!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计