详述 PyPI 中的远程代码执行漏洞,可引发供应链攻击

 聚焦源代码安全,网罗国内外最新资讯!

编译:奇安信代码卫士

日本安全研究员RyotaK 曾在上周发现Cloudflare 公司CDNJS 服务中的一个漏洞,可导致第三方在当前12%的网站上运行恶意代码从而引发供应链攻击。这几天他又在 Python 库的官方仓库 PyPI 中找到了三个漏洞,其中一个远程代码漏洞可导致 PyPI 遭完全接管。RyotaK 因此获得3000美元的奖励。如下是他发布的博客文章内容。

前言

虽然 PyPI 设置了安全页面,但并未清晰说明漏洞评估策略。本文说明的是已报告的潜在漏洞,它们都是通过公开信息发现的,在没有实际利用/展示这些漏洞的情况下发现。本文并非鼓励读者执行未授权的漏洞评估。

要点概述

PyPI仓库的 GitHub Actions 中存在一个漏洞,可导致恶意 pull 请求执行任意命令,从而使攻击者能够获得仓库的写权限,最终在 pypi.org 上执行任意代码。

PyPI 简介

PyPI 是 Python 数据包管理器 (pip) 使用的包注册表,在用户运行 pip install [package name] 等命令时被引用。很多项目如 Flask 和 TensorFlow 都间接使用 PyPI。

调查原因

在阅读 Max Justicz 的博客文章时我注意到他们报告了 PyPI 中的一个漏洞。查看了该文中提到的安全公告后,我发现 PyPI 的源代码在 GitHub 上公开了,于是决定阅读一下。

源代码调查

当前 PyPI 的源代码位于 pypa/warehouse 仓库中。

快速阅读代码后,发现它使用的是 Python 的一个 web 框架 Pyramid。阅读了 Pyramid 文档后,我继续阅读 PyPI 的代码,发现了两个漏洞。

任意遗留文档删除

Python 具有一个可用于 Python 项目中的文档特性。由于对该特性的使用并未增长因此他们将其删除,但并未删除现有文档。于是,删除这些文档的一个特性被requested 且在一个 pull request 中实现(pull request: https://github.com/pypa/warehouse/pull/3413)。

该特性通过内部使用如下代码删除了该文档:

    def remove_by_prefix(self, prefix):
        if self.prefix:
            prefix = os.path.join(self.prefix, prefix)
        keys_to_delete = []
        keys = self.s3_client.list_objects_v2(Bucket=self.bucket_name, Prefix=prefix)
        for key in keys.get("Contents", []):
            keys_to_delete.append({"Key": key["Key"]})
            if len(keys_to_delete) > 99:
                self.s3_client.delete_objects(
                    Bucket=self.bucket_name, Delete={"Objects": keys_to_delete}
                )
                keys_to_delete = []
        if len(keys_to_delete) > 0:
            self.s3_client.delete_objects(
                Bucket=self.bucket_name, Delete={"Objects": keys_to_delete}

从如上代码片段可使,它使用的是参数为 prefix 的函数 list_objects_v2 来提取要删除的对象。由用户拥有的项目名称被传递给参数 prefix。这意味着如果有人删除了名为 “examp” 项目中的遗留文档,则将删除名称以 “examp”开头的项目(如 example、exampleasdf)。

任意角色删除

PyPI 具有对程序包的权限管理特性。在这个特性中,项目所有人能够授予/删除权限,在这个删除进程中存在一个漏洞。PyPI 使用如下代码从数据库中提取权限信息:

role = (
    request.db.query(Role)
    .join(User)
    .filter(Role.id == request.POST["role_id"])
    .one()
)

从如上代码片段中可知,它在提取要删除的权限信息时并未指定项目ID。因此,攻击者能够通过假定 role_id 的方法删除其它项目的权限。

远程代码执行漏洞

如上所述,我在源代码调查过程中发现并提交了两个漏洞。然而,这些漏洞并未产生很大的影响,最多只是起到扰乱的作用。于是我着手寻找具有更大影响力的漏洞如可导致任意代码执行的漏洞。

在休息期间,我找到了一篇名为《PyPI 服务器的非预期部署》的文章,仔细研读后我发现被push 到 pypa/warehouse 仓库 main 分支的代码将被自动部署到 pypi.org 中。换言之,如果我能获得对仓库的写权限,则有可能在 pypi.org 上执行任意代码。因此,我检查了 GitHub Actionis 的工作流文件,它默认拥有该仓库的写权限,并找到了如下漏洞。

调查工作流文件

在 pypa/warehouse 中,存在一个名为 “combine-prs.yml” 的工作流。该工作流用于收集分支名称以 “dependabot” 开头的请求,并将它们合并到单个 pull 请求中。由于 Dependabot 并不具备能够合并所有pull 请求的特性,因此他们使用该工作流进行模拟。在这个工作流中,并不存在 pull 请求作者验证流程。这就意味着,如果有人创建了分支名称以 “dependabot” 开头的pull 请求,则可能强制该工作流处理该构造的 pull 请求。

然而,该工作流仅将 pull 请求组合到单个 pull 请求中。因此由该工作流生成的 pull 请求将由人工审计,而如果其中包含恶意变化,则会被摒弃。因此,无法借此直接执行任意代码。但在通读完代码时,我发现还存在另外一个漏洞。

在第98行代码(echo "${{steps.fetch-branch-names.outputs.result}}")中,combine-prs.yml 通过如下代码打印 pull 请求的分支列表:

run: |
  echo "${{steps.fetch-branch-names.outputs.result}}"

它是一个简单的 echo 命令,乍一看上去没什么问题,但由于 GitHub Actions 行为的原因,它是不安全的。如同文章《保护 GitHub Actions 和工作流的安全:不受信任的输入》中提到的那样,表达式 ${{}} 在被传递给 Bash 前会被升级。这意味着该表达式并不关心 Bash 中的上下文,因此如果 steps.fetch-branch-names.outputs.result 包含字符串如 “";curl https://example.com;#,则会执行curl https://example.com。

由于该工作流使用了 actions/checkout,因此.git/config 包含secrets.GITHUB_TOKEN,而后者拥有写权限。因此,通过执行 cat.git/config 等命令,有可能通过 pypa/warehouse 仓库的写权限泄露 GitHub Access Token。

如上所述,如果有人在main 分支推出变更,则会触发自动部署 pypi.org。因此,通过如下步骤,就可能在 pypi.org上执行任意代码:

1、Fork pypa/warehouse

2、在被 fork 的仓库中,创建一个分支,名为:dependabot;cat$IFS$(echo$IFS'LmdpdA=='|base64$IFS'-d')/config|base64;sleep$IFS'10000';#

3、向所创建的分支增加一个非恶意修改

4、创建一个 pull 请求且名称非恶意(如 :WIP)。

5、 等待执行 combine-prs.yml

6、具有对 pypa/warehouse 写权限的 GitHub Access Token 将被泄露,因此向 main 分支增加任意修改

7、 被修改后的代码将被部署到 pypi.org。

我将这个漏洞告知 Python 安全团队,目前已修复。

有人提到如上的攻击步骤不起作用,查看后我认为可疑使用不同的攻击程序。在combine-prs.yml 第119行的代码如下:

script: |
  const prString = `${{ steps.fetch-branch-names.outputs.prs-string }}`;

如之前所述,steps.fetch-branch-names.outputs.prs-string 包含 pull 请求的题目,可通过如下步骤在 pypi.org 上执行任意代码:

1、Fork pypa/warehouse

2、在pypa/warehouse 中找到以 dependabot 开头的分支

3、在所fork 的仓库中,向在步骤2中找到的分支添加非恶意修改

4、创建一个pull 请求,名为 `;github.auth().then(auth=>console.log(auth.token.split("")))//

5、等待 combine-prs.yml 执行

6、具有对 papa/warehouse 写权限的 GitHub Access Token 将被泄露,因此向 main 分支增加任意修改

7、 被修改的代码将被部署到 pypi.org。

结论

本文提到的这些漏洞对 Python 生态系统造成重大影响。如之前多次提到的那样,某些供应链中存在多个严重漏洞。然而,研究供应链攻击的人数有限,且多数供应链并未得到正确保护。

因此,我认为依赖供应链的用户有必要积极主动地改进供应链安全。

时间线

  • 2021年7月25日:找到并报告文档删除漏洞

  • 2021年7月26日:Python 修复文档删除漏洞

  • 2021年7月26日:找到并报告角色删除漏洞

  • 2021年7月27日:找到并报告 combine-prs 漏洞

  • 2021年7月27日:Python 团队修复角色删除和 combine-prs 漏洞

  • 2021年7月29日:安全公告发布

  • 2021年7月30日:本文发布

推荐阅读

Python 官方软件库 PyPI 遭垃圾软件包洪水攻击

为增强软件供应链安全,NIST 发布《开发者软件验证最低标准指南》

CloudFlare CDNJS 漏洞差点造成大规模的供应链攻击

详细分析PHP源代码后门事件及其供应链安全启示

原文链接

https://blog.ryotak.me/post/pypi-potential-remote-code-execution-en/

题图:Pixabay License

本文由奇安信编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的产品线。

    觉得不错,就点个 “在看” 或 "赞” 吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值