不论昨天如何,都希望新的一天里,我们大家都能成为更好的人,也希望我们都是走向幸福的那些人
2021 年,我还在进攻性安全领域的早期阶段。虽然我已经成功入侵了几家公司,并通过漏洞赏金狩猎(Bug Bounty Hunting)获得了稳定的收入,但我还没有达到能够快速识别目标关键漏洞的水平。这种技能似乎触手可及,但又遥不可及。直到我遇到了一个在我的漏洞赏金职业生涯中至关重要的人:Snorlhax。
最初,我把他视为竞争对手。他在 HackerOne 法国排行榜上遥遥领先,这激励我提升自己的水平。我们在 Discord 上开始聊天,几周后,我与他分享了一个有潜力的漏洞赏金项目范围。不久之后,他在该目标上发现了一个价值 10,000 美元的关键漏洞——这是我当时在该目标上获得的最高赏金的两倍。受此激励,我重新审视了同一个目标,并在同一周内发现了另一个价值 10,000 美元的关键漏洞,属于不同的漏洞类别。
我们没有继续竞争,而是决定合作。我们的目标是识别该目标上的所有可能的漏洞类别:IDOR、SQL 注入、XSS、OAuth 漏洞、依赖混淆、SSRF、RCE……你能想到的,我们都发现并报告了。这次合作持续了数年,直到现在我们偶尔还会一起回到这个目标。
然而,有一个目标始终未能实现:发现“那个特别的漏洞”。这是一个如此关键的漏洞,以至于它会为我们带来远超标准赏金的额外奖励。这成为了我们的终极挑战。
这就是我和 Snorlhax 最终实现这一目标的故事。通过一次软件供应链攻击,我们成功实现了对开发者、流水线和生产服务器的远程代码执行(RCE),并获得了 50,500 美元的赏金。以下是整个过程。
根据我们的经验,在进入侦察阶段之前,了解任何大型组织的业务背景至关重要。当我们接手这个大目标时,Snorlhax 和我已经了解到,这家公司在收购其他企业时经常会引入意想不到的弱点。新收购的子公司并不总是遵循与母公司相同的安全标准,尤其是在整合的早期阶段。我们之前见过这种情况,但从未真正以寻找那个令人震惊的漏洞为目标去探索收购公司。这一次,我们确信“特别的漏洞”可能隐藏在那些研究人员很少关注的地方。
我们的计划很简单:在这个漏洞赏金计划中,任何公司正式拥有的资产都是合法的目标。我们认为,收购目标很容易被忽视,因为许多猎人专注于主域名。与此同时,被收购企业的旧基础设施、半更新的框架和松散的政策可能会为严重漏洞创造完美的环境。我们觉得,如果要找到一个真正巨大的漏洞,收购目标是我们最好的机会。
当我们开始规划这一方法时,我们不断提醒自己,我们需要一些特别的东西。仅仅满足于轻微是不够的,我们需要钓到大鱼。我们的计划包括深入新收购子公司的环境,超越基本的 Web 漏洞,调查他们如何处理软件开发和部署。我们不知道这会带我们到哪里,但我们知道它有巨大的潜力。
我们一直痴迷于这样一个想法:你不需要攻击那些显而易见的东西。通常,攻击公司引入其敏感环境的资产和服务(如流水线、依赖项、注册表和镜像)会更有效。如果你成功了,你可以在代码进入生产环境之前就对其进行篡改,这比标准的 SSRF 或 XSS 漏洞造成的破坏要大得多。
为了理解软件供应链的攻击面,我们需要看看 SLSA 框架(Supply-chain Levels for Software Artifacts)。它将软件供应链分为三个部分:Source(源代码)、Build(构建)和 Distribution(分发)。攻击其中任何一个环节都可能造成混乱。我们立即锁定了 Source(如 GitHub、DockerHub 和注册表)和 Build(CI/CD 流水线),因为它们通常充斥着令牌、密钥和不良配置。
Snorlhax 和我已经在其他项目中尝试过测试供应链攻击,并发现了一些疯狂的漏洞:Artifactory 访问、员工邮箱接管(让我们访问他们的 GitHub)、依赖混淆等。但这一次,我们有一种预感,认为收购目标可能会依赖过时或未管理的供应链流程,这可能会引导我们找到更大的漏洞。
因此,我们决定将两个想法结合起来:针对收购目标并扰乱其供应链。我们正在追逐一个可能真正改变游戏规则的漏洞。攻击新收购公司的供应链是一个非常小众的领域,甚至可以说是小众中的小众,我们几乎可以肯定没有其他人关注这一点。这给了我们巨大的优势。
我们的第一步是确定一个合适的子公司。我们浏览了公司新闻稿、官方公告,并搜索了 LinkedIn,看看哪些公司被收购以及它们在整合过程中的进展。我们特别选择了一个子公司,注意到它在我们的目标漏洞赏金范围中被明确提及。
我们需要弄清楚这个子公司是否有在线代码库或使用任何流行的包注册表。我们从他们的前端应用程序中抓取了 JavaScript 文件,以查看他们调用了哪些代码依赖项。我们没有进行简单的字符串搜索,而是采用了更强大的方法,将 JS 文件转换为抽象语法树(AST)。
AST 是源代码的树状表示,将所有内容(变量、函数、导入等)分解为层次节点。通过使用 SWC(Speedy Web Compiler)库,我们编写了一段 Rust 代码,将 JS 文件解析为这些 AST,并系统地遍历它们以定位所有导入或 require 语句。
这让我们精确地识别了对一个独特作用域的引用,该作用域显示为 @acquisition-utils/package。我们首先检查是否可以声明 npm 组织的命名空间。然而,公司已经拥有该命名空间,但没有公开的包。
这告诉我们至少有一个私有的 npm 组织在使用。在 npmjs 上,可以设置一个组织,但如果购买了许可证,则可以私下发布包。因此,要么他们在 npm 上有私有包,要么他们正在占位命名空间以避免通过命名空间接管进行依赖混淆。
下一步是检查是否能找到该组织的一些私有包。为此,我们运行了以下 GitHub 搜索路径:**/packages.json @acquisition-utils。
这个查询的目的是查看是否有组织的开发人员通过使用此私有包命名空间发布存储库而泄露了内部源代码。这是查找内部源代码泄漏的好方法。
小提示:你可以使用任何指示内部源代码使用的字符串来执行此操作。例如,如果你找到一个私有的 Artifactory,你可以运行 path:**/package-lock.json artifactory-url.tld 来检查是否有任何锁定文件从该企业 Artifactory 获取。当然,你需要根据你的包管理器(yarn、pip、pnpm 等)进行调整。
不幸的是,我们没有在 GitHub 上直接找到任何东西,于是我们转向 Google,输入 acquisition-utils,天真地希望能揭示更多的工件。就在这时,我们发现了一个与子公司品牌相关的 DockerHub 组织。这个新线索正是我们想要的,一个可能包含私有或安全性较差的 Docker 镜像的环境。
我们的下一步是下载这些镜像并仔细检查,希望能找到引导我们走向真正关键漏洞的线索。
当我们拉取其中一个 Docker 镜像时,特别是以其主要产品命名的镜像时,我们发现了宝藏。在镜像内部,我们找到了完整的专有后端源代码。
通常情况下,发现源代码可能足以获得关键赏金,但我们想要的是一个“特别的漏洞”。源代码本身很有价值,但我们需要展示额外的影响,以向安全团队展示这种暴露的危险性。
进一步挖掘后,我们注意到容器中仍然存在 .git/ 文件夹。这是我们的下一个线索。通过检查 .git/config 文件,我们希望找到一个私有仓库的 URL 或直接的环境变量。我们发现了在 .git/config 中从未见过的东西:一个 base64 编码的授权令牌。
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/Acquisition/backend
fetch = +refs/heads/*:refs/remotes/origin/*
[gc]
auto = 0
[http "https://github.com/"]
extraheader = AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46TG9sWW91V2FudGVkVG9TZWVUaGVUb2tlblJpZ2h0Pw
我们一开始并没有认出它,但经过仔细研究后,我们意识到这是一个 GitHub Actions 令牌(GHS)。我们实际上是在他们的供应链构建阶段中无意间发现了一把钥匙。如果我们能够利用这个令牌,我们可能会操纵流水线本身,获得直接代码注入、工件篡改或进入其他私有仓库的途径。
事实证明,这些 GitHub Actions 令牌通常会自动生成,以允许工作流与其自己的仓库进行交互——推送代码、创建拉取请求或访问私有依赖项。在正常情况下,这些令牌在工作流完成后会过期,限制了它们的利用窗口。
然而,当包含令牌的工件(例如 .git/config 文件、环境变量日志或整个仓库检出)在工作流结束之前上传时,可能会发生竞争条件。该工件可能在令牌过期之前对任何具有读取权限的人变得可访问。如果攻击者能够足够快地下载工件(在我们的案例中是 Docker 镜像),他们可以提取仍然有效的令牌,并使用它来修改或推送代码到仓库,篡改发布,甚至进入同一组织内的其他 GitHub 仓库。
如果工作流授予令牌“写”或“管理员”权限(例如,通过 GitHub Actions YAML 文件中的 permissions: contents: write),这种风险尤其严重。在这种情况下,攻击者获得了注入恶意代码、创建新分支甚至直接提交更改到生产环境的能力。根据 CI/CD 流水线的配置方式,恶意更改可能会迅速传播到下游,可能会危及数百万用户依赖的应用程序。
在许多现代 DevOps 流水线中,Dockerfile 和 CI/CD 工作流紧密相连。一个常见的模式是:
1.工作流中的检出:GitHub Actions 工作流(或任何其他 CI/CD 提供商)使用诸如 actions/checkout 的操作来获取源代码。默认情况下,此步骤可以包括 .git/config 文件中的 git 凭据或令牌。
2.Dockerfile COPY:在 Docker 构建期间,开发人员通常会将整个源目录(包括隐藏文件夹如 .git/)复制到 Docker 镜像中。
3.发布镜像:构建的镜像被推送到公共或私有容器注册表。如果敏感文件(如 .git/ 或环境变量转储)没有被清理干净,它们可能会留在早期层中,或者最终出现在镜像本身中。
只需要一个疏忽——比如忘记删除或忽略 .git/ 文件夹,或者没有正确限制令牌的范围——整个构建流水线就会变得脆弱。发现这些工件的攻击者可以利用 GitHub 令牌、Docker 镜像或两者来提升他们的权限。
因此,在我们的上下文中,我们能否赢得竞争条件?我们访问的 GitHub 工作流(因为我们拥有整个后端源代码)如下所示:
name: Build and push Docker image
on:
push:
tags:
- '*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v3
- name: Define image name
run: |
echo"IMAGE_NAME=acquisition/backend" >> $GITHUB_ENV
- name: Define image tag
run: |
if [[ "${{ github.ref }}" == 'refs/tags/'* ]]; then
echo"IMAGE_TAG=$(git tag --points-at $(git log -1 --oneline | awk '{print $1}'))" >> $GITHUB_ENV
else
exit0
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
run: |
source $GITHUB_ENV
echo IMAGE_NAME=$IMAGE_NAME
echo IMAGE_TAG=$IMAGE_TAG
docker build --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} --tag$IMAGE_NAME:$IMAGE_TAG .
- name: Push image to DockerHub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
run: |
docker login --username$DOCKERHUB_USERNAME--password$DOCKERHUB_PASSWORD
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Deploy to staging
env:
DEPLOYMENT_API_SECRET: ${{ secrets.DEPLOYMENT_API_SECRET }}
run: |
curl-XPOST'https://deploy.acquistion.tld/v1/deploy'
-H"Content-type: application/json"
--data-raw"{
"appName": "backend",
"envName": "backend",
"contName": "backend",
"imageTag": "`echo $IMAGE_NAME`:`echo $IMAGE_TAG`",
"secret": "`echo $DEPLOYMENT_API_SECRET`"
}"
- name: Post status on Slack
id: slack
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": "GitHub Action build result: ${{ job.status }}n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "GitHub Action build result: ${{ job.status }}n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: "https://hooks.slack.com/services/THE_ACTUAL_SLACK_HOOK_HAHA"
SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK"
正如你所注意到的,在 docker push 命令之后有两个步骤:Deploy to staging 和 Post status on Slack。在此期间,令牌仍然可用,因为工作器仍在运行以执行这些步骤。可以合理地说,攻击者可以简单地监控任何新镜像的发布,并下载 Docker 镜像中仅包含 GHS 令牌的特定层,然后对 GitHub 仓库进行后渗透利用。
快速说明:Palo Alto 的 Unit42 发表了一篇题为“ArtiPACKED: Hacking Giants Through a Race Condition in GitHub Actions Artifacts”的文章,大约在我们完成研究一个月后发布。它深入探讨了这种攻击类型。我们可能不是第一个“发布”的人,但这很好地说明了多个安全研究人员可以独立发现并验证同一类漏洞——真是“英雄所见略同”。;)
https://unit42.paloaltonetworks.com/github-repo-artifacts-leak-tokens/
当我们查看构建此镜像的 Dockerfile 时,我们注意到一个 package.json,内容如下:
{
"name": "content",
"version": "1.7.0",
"private": true,
"scripts": {
....
},
"dependencies": {
"@aquistion-utils/internal-react": "^4.12.0",
...
},
"devDependencies": {
...
},
}
在 package.json 中,有一个对我们之前发现的 @aquistion-utils 组织的引用。然而,要拉取它,你需要一个 npm 令牌。不幸的是,我们没有在文件夹的根目录中看到 .npmrc 文件。
背后的原因是 Dockerfile 复制了一个 .npmrc 文件,然后在最后的构建步骤中删除了它。有一段时间,我们认为因为 .npmrc 被删除了,它就永远消失了。我们决定深入研究 Docker 的工作原理。
Docker 镜像使用分层文件系统构建。Dockerfile 中的每条指令(如 FROM、COPY、RUN 等)都会创建一个新层。在构建镜像时,Docker 会查看每条指令,为该指令创建(或重用)一个层,并将其堆叠在现有层之上。层是只读的,当你在一个层中修改文件时,Docker 实际上会添加一个带有“更改”的新层,而不是就地编辑现有层。这个过程使 Docker 构建更高效,并允许多个镜像共享公共层。
要直接检查这些层,你可以使用 docker history <image> 命令,该命令列出了用于构建镜像的指令序列(及相应的层)。
$ docker history hello-world
IMAGE CREATED CREATED BY SIZE COMMENT
ee301c921b8a 20 months ago CMD ["/hello"] 0B buildkit.dockerfile.v0
<missing> 20 months ago COPY hello /
然而,这仅显示了高级历史记录,而无法让你浏览每一层的实际内容。
dive 是一个方便的 CLI 工具,不仅可以显示 Docker 镜像的层,还可以让你深入并检查每一层中添加、删除或更改的文件。要开始使用 dive 探索你的镜像,请安装该工具,然后运行 dive <image_to_dive>:
这提供了一个交互式 UI,左侧显示每一层,右侧显示内容。你可以使用箭头键在每一层的文件系统中导航,查看层之间的更改。
dlayer 是另一个方便的 Docker 层分析器。它可以用于非交互式和交互式模式,以显示 Docker 镜像中每一层的内容和文件结构。典型的工作流程包括将 Docker 镜像保存为 tar 文件,然后使用 dlayer 检查它:
docker save image:tag | dlayer -i
# Save the image to a tar file and then analyze it
docker save -o image.tar image:tag
dlayer -f image.tar -n 1000 -d 10 | less
如果你只想通过管道传输 image.tar 并打印所有层,然后将结果传输到另一个工具中,这会特别有用。此外,你可以使用 Go 代码将 dlayer 实现为一个库,这对于大规模扫描和侦察非常有用。
当我们发现可以检索 Docker 镜像的构建层时,这意味着 .npmrc(以及 NPM_TOKEN)很可能在早期层中仍然暴露。我们提取了每一层并仔细检查。
我们发现了私有的 npm 令牌,授予了对 @acquisition-utils 包的读写权限。这时我们的心跳开始加速。我们意识到我们可以将恶意代码推送到他们的一个私有包中,而他们的开发者、流水线甚至生产环境都会自动获取这些包。因为这是一个私有包,典型的公共扫描器不会检测到任何篡改。而且由于他们的 package.json 版本设置为允许小版本升级(^4.12.0),我们可以悄无声息地植入后门,并危及依赖此包的每个环境。
到了这个阶段,Snorlhax 和我都在颤抖。我们知道我们找到了我们的“特别的漏洞”。这不仅仅是读取私有源代码或劫持单个流水线令牌的问题。我们可以影响整个软件供应链,从本地开发者机器到 CI/CD 流程再到生产服务器。简而言之,我们找到了一个足以证明额外赏金的巨大漏洞。
我们仔细记录了每一步。公司的安全团队一直鼓励我们超越理论漏洞,展示实际影响。我们解释了攻击者如何后门私有 npm 包,然后等待开发者或流水线运行 npm install 命令。如果开发者在不知情的情况下使用受感染的包构建或测试代码,我们可能会窃取秘密或进入其他内部系统。在 CI/CD 流水线中,这可能会打开读取敏感环境变量、窃取凭据甚至提升权限到自托管运行器的大门。最后,在生产环境中,如果这些容器获取相同的包更新或具有自动部署流程,可能会导致广泛的危害。
为了强调威胁,我们证明了内部日志记录或监控不太可能在 npm 级别捕获这种渗透,因为它发生在目标基础设施之外。事实上,让我们想一想。我们:
从 DockerHub 拉取 Docker 镜像。
将包发布到 registry.npmjs.org 的私有组织。
我们从未与目标的 Web 应用程序交互,除了询问 DockerHub 和 npm 的日志外,没有办法知道是谁下载了镜像。最嘈杂的事情可能是发布 npm 包,但我们将在未来的文章中讨论这个问题。
当我们提交我们的发现时,公司立即认识到这是一种连锁反应漏洞,可能会危及整个开发到生产生命周期。他们将其归类为罕见的、最坏情况的漏洞,并奖励了我们 50,500 美元的赏金,远远超出了他们的普通支付结构。Snorlhax 和我终于捕获了我们的“特别的漏洞”,这一发现验证了我们多年来积累的每一个直觉和知识。
对我们来说,最大的教训是,攻击的成功往往取决于结合两个或多个被忽视的角度。收购目标提供了比母公司更柔软的目标,而软件供应链漏洞提供了我们正在寻找的灾难性影响。通过合并这些见解,我们找到了那个完美的风暴。我们还对保护你发布的代码、构建过程的每一层以及开发者从外部源拉取的每一个工件的重要性有了新的认识。
我们继续一起黑客攻击,始终寻找下一个改变游戏规则的发现。我们希望通过分享这一经历,激励其他研究人员超越常规。有时,真正的宝藏隐藏在那些不起眼的角落:一个隐藏的 Docker 镜像、一个处理不当的 npm 令牌,或一个遗留的 .git 文件夹。这些角落可能是改变生活的赏金的关键,就像它们对我们一样。
参考及来源:
https://www.landh.tech/blog/20250211-hack-supply-chain-for-50k/
安全小白团是帮助用户了解信息安全技术、安全漏洞相关信息的微信公众号。安全小白团提供的程序(方法)可能带有攻击性,仅供安全研究与教学之用,用户将其信息做其他用途,由用户承担全部法律及连带责任,安全小白团不承担任何法律及连带责任。安全小白团所分享的工具均来自互联网公开资源,我们不对工具的来源进行任何形式的担保或保证。用户在下载或使用本公众号分享的工具前,应自行评估工具的安全性。我们无法对工具可能存在的病毒、木马或其他恶意软件负责,因此造成的任何损失,安全小白团不承担任何责任。欢迎大家转载,转载请注明出处。如有侵权烦请告知,我们会立即删除并致歉。谢谢!
原文始发于微信公众号(安全小白团):软件供应链攻击:一次价值50,500美元的重大漏洞发现
评论