简介
2021年时,我还是个offensive security(攻击性安全)领域的新手。虽然已经成功入侵过几家公司,通过漏洞赏金计划(Bug Bounty Hunting)赚取稳定收入 - 这是一种道德黑客行为,安全研究人员寻找并报告漏洞以获得金钱奖励。但那时的我还无法快速识别目标系统中的严重漏洞,这种高级技能似乎触手可及却又难以掌握。直到我遇到了Snorlhax,他成为了我Bug Bounty生涯中的关键人物,一切都发生了转变。
最初我把他当竞争对手。他在HackerOne法国排行榜上遥遥领先于我,这促使我不断提升自己。我们在Discord上聊天,几周后我跟他分享了一个很有前景的漏洞赏金项目范围。不久后,他在这个目标上发现了一个价值10,000级别的严重漏洞。
与其继续竞争,我们决定合作。我们的重点转向识别这个目标上所有可能的漏洞类型:IDOR、SQL注入、XSS、OAuth漏洞、依赖混淆、SSRF、RCE...只要你能想到的,我们都找到并报告了。这种合作持续了数年,直到现在我们偶尔还会一起重访这个目标。
然而,有一个目标始终难以企及:发现"那个特殊的漏洞"。这将是一个严重到足以让我们获得超出标准赏金表的巨额奖励的漏洞。这成为了我们的终极挑战。
这就是Snorlhax和我最终实现这个目标的疯狂故事。
通过一次软件供应链攻击,我们实现了对开发者环境、流水线和生产服务器的远程代码执行(RCE),获得了$50,500的赏金。以下是事情的经过。
理解我们目标的业务攻击面
基于经验,在开始侦察阶段之前,了解任何大型组织的业务背景都至关重要。当我们着手研究这个大目标时,Snorlhax和我已经意识到这家公司在收购其他企业时往往会引入意想不到的安全漏洞。新收购的子公司,特别是在整合初期,并不总是遵循母公司的相同安全标准。我们之前就见过这种情况,但从未专门针对收购企业去寻找那个惊天动地的漏洞。这一次,我们确信那个"特殊的漏洞"可能就潜伏在很少研究人员关注的地方。
我们的计划很简单:在这个漏洞赏金项目中,任何由该公司正式拥有的资产都是合法目标。我们认为收购的资产容易被忽视,因为许多黑客更关注主要根域名。与此同时,被收购企业的旧基础设施、半更新的框架和宽松的政策可能会创造出严重漏洞的完美温床。我们觉得如果要找到真正惊人的发现,一个收购企业是我们最好的机会。
当我们开始制定这个方案时,我们不断提醒自己需要找到特别的东西。仅仅发现低垂的果实是不够的。我们需要大鱼。我们不断发展的计划包括深入研究新收购子公司的环境,超越基本的web漏洞,去调查他们如何处理软件开发和部署。我们不知道这会带领我们去向何方,但我们知道这很可能会有重大发现。
为什么我们转向软件供应链
我们一直坚信一个理念:你不需要攻击那些显而易见的目标。攻击公司拉入敏感环境的资产和服务往往更有效 - 他们的流水线、依赖项、注册表和镜像。如果你成功了,你可以在代码进入生产环境之前就篡改它,这比标准的SSRF或XSS漏洞可能造成更大的破坏。
要理解软件供应链的攻击面,我们需要看看SLSA框架(软件工件供应链级别)。它将软件供应链分为三个部分:源代码、构建和分发。攻击其中任何一个都会造成混乱。我们立即锁定了源代码(如GitHub、DockerHub和注册表)和构建(CI/CD流水线),因为它们通常充斥着令牌、密钥和错误配置。
Snorlhax和我此前已经在其他项目中尝试过供应链攻击,发现了一些令人震惊的漏洞:Artifactory访问权限、可导致访问GitHub的员工邮箱接管、依赖混淆等等。不过这一次,我们有一种预感,我们认为一个被收购的公司很可能依赖过时或管理不善的供应链流程,这可能带领我们发现更大的问题。
因此,我们决定结合两个思路:针对一个收购企业及其供应链。我们在追寻一个可能改变游戏规则的漏洞。攻击新收购公司的供应链是一个非常小众的领域,可以说是小众中的小众,我们很确定没有其他人在关注这一点。这给了我们巨大的优势。
我们如何开始侦察
第一步是确定一个合适的子公司。我们查阅了企业新闻稿、官方公告,在LinkedIn上搜索哪些公司被收购以及它们在整合过程中的进展如何。我们选中了一家,特别是因为它被明确列在我们目标的漏洞赏金项目范围内。
我们需要弄清楚这个子公司是否有在线代码库或使用任何流行的包注册表。我们从他们的前端应用中抓取JavaScript文件,看看他们调用了哪些代码依赖。我们没有进行简单的字符串搜索,而是采用了更稳健的方法,将JS文件转换成抽象语法树(ASTs)。
AST是源代码的树状表示,它将所有内容 - 变量、函数、导入等 - 分解成层级节点。通过使用SWC(Speedy Web Compiler)库,我们编写了一段Rust代码来将JS文件解析成这些AST,并系统地遍历它们以定位所有import或require语句。
这让我们精确地识别出了一个独特作用域的引用:@acquisition-utils/package
。我们首先检查的是这个npm组织命名空间是否可以被认领。然而,该公司已经拥有这个命名空间,但没有公开的包。
这告诉我们至少存在一个私有的npm组织。在npmjs上,如果购买了许可证,可以设置一个组织但将包私有发布。所以要么他们在npm上确实有私有包,要么他们只是预留了这个命名空间以避免通过命名空间接管导致的依赖混淆。
下一步是检查我们是否能找到这个组织的一些私有包。为此我们运行了以下GitHub搜索 path:**/packages.json @acquisition-utils
:
这个查询的目的是查看该组织的任何开发人员是否通过发布使用这个私有包命名空间的代码库而泄露了内部源代码。这是发现内部源代码泄露的好方法。
小贴士: 你可以用任何表明内部源代码使用的字符串来做这个。比如,如果你发现了一个私有的Artifactory,你可以运行
path:**/package-lock.json artifactory-url.tld
来检查是否有任何锁文件从这个企业Artifactory获取包。当然,你需要根据使用的包管理器(yarn、pip、pnpm等)来调整查询。
不幸的是,我们在GitHub上没有直接找到任何东西,所以我们转向Google,很天真地输入了 acquisition-utils
,希望能发现更多信息。就在这时,我们发现了一个也与该子公司品牌相关的DockerHub组织。这个新线索正是我们想要的,一个可能存放着私有或安全性较差的Docker镜像的环境。
我们的下一步是下载这些镜像并仔细检查它们,希望能找到通向真正重要发现的线索。
深入源代码
当我们拉取了他们的一个Docker镜像时,特别是一个以他们主要产品命名的镜像,我们找到了金矿。在里面,我们发现了完整的专有后端源代码。
通常情况下,发现源代码可能足以获得一个Critical级别的赏金,但我们想要的是"特殊的漏洞"。仅仅是源代码本身虽然有价值,但我们需要展示额外的影响来向安全团队证明这个漏洞有多危险。
深入研究后,我们注意到容器中还保留着.git/
文件夹。这是我们的下一个线索。通过检查.git/config
文件,我们希望能找到私有仓库URL或直接的环境变量。我们在.git/config
中发现了一些我们从未见过的东西:一个base64编码的授权bearer令牌。
[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令牌通常是自动生成的,允许工作流与其自己的仓库交互 - 推送代码、创建pull requests或访问私有依赖。在正常情况下,这些令牌会在工作流完成后过期,限制了其被利用的时间窗口。
然而,当包含令牌的工件(例如.git/config
文件、环境变量日志或整个仓库检出)在工作流结束前被上传时,可能会出现竞争条件。在令牌过期之前,任何具有读取权限的人都可能访问到该工件。如果攻击者能够足够快地下载工件(在我们的案例中是Docker镜像),他们可以提取出仍然有效的令牌,并使用它来修改或推送代码到仓库、篡改发布,甚至进入同一组织内的其他GitHub仓库。
如果工作流通过permissions: contents: write
这样的GitHub Actions YAML配置为令牌授予了"write"或"admin"权限,这个风险就特别严重。在这种情况下,攻击者获得了注入恶意代码、创建新分支,甚至直接将更改提交到生产环境的能力。根据CI/CD流水线的配置方式,恶意更改可能会快速向下游传播,潜在地危及数百万用户依赖的应用程序。
在许多现代DevOps流水线中,Dockerfile和CI/CD工作流是紧密关联的。一个常见的模式是:
-
工作流中的检出: GitHub Actions工作流(或任何其他CI/CD提供商)使用类似 actions/checkout
的action来获取源代码。默认情况下,这一步可能在.git/config
文件中包含git凭证或令牌。 -
Dockerfile COPY: 在Docker构建过程中,开发人员经常复制整个源代码目录,包括 .git/
这样的隐藏文件夹到Docker镜像中。 -
发布镜像: 构建好的镜像被推送到公共或私有容器注册表。如果敏感文件(如 .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
exit 0
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/[email protected]
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命令之后有两步。部署到测试环境和在Slack上发布状态。在这段时间内,令牌仍然可用,因为工作器仍在运行以执行这些步骤。可以现实地说,攻击者可以简单地监控任何新镜像的发布,并下载包含GHS令牌的确切特定层,以便随后利用GitHub仓库。 快速提示:Palo Alto的Unit42发表了一篇题为“ArtiPACKED: 通过GitHub Actions工件中的竞争条件入侵巨人”的文章,这大约在我们完成研究一个月后发布。这是对这种攻击的彻底探索。我们可能不是第一个按下“发布”的人,但这很好地说明了多个安全研究人员如何独立发现并验证同一类漏洞——真正是“英雄所见略同”。
我们想要更多
当我们查看构建此镜像的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 命令,该命令列出了用于构建镜像的指令序列(和相应的层)。
$ 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 / # buildkit 9.14kB buildkit.dockerfile.v0
然而,这仅显示了高层次的历史记录,而不允许你浏览每个层的实际内容。 dive是一个方便的CLI工具,它不仅显示Docker镜像的层,还允许你深入并检查每个层中添加、删除或更改的文件。要开始使用dive探索镜像,请安装该工具,然后运行dive <image_to_dive>: 这提供了一个交互式UI,左侧显示每个层,右侧显示内容。你可以使用箭头键在每个层的文件系统中导航,查看层之间的更改。 dlayer是另一个用于Docker层的便捷分析器。它可以以交互式和非交互式模式使用,显示Docker镜像中每个层的内容和文件结构。典型的使用流程包括将Docker镜像保存为tar文件,然后使用dlayer进行检查:
# 将保存的Docker镜像tar直接传递给dlayer
docker save image:tag | dlayer -i
# 将镜像保存到tar文件,然后进行分析
docker save -o image.tar image:tag
dlayer -f image.tar -n 1000 -d 10 | less
如果你只是想将镜像.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镜像 -
本地找到npm令牌 -
将包发布到registry.npmjs.org私有组织
我们从未与目标的Web应用程序交互,除了询问DockerHub和npm的日志,没有办法知道谁下载了镜像。最吵的事情可能是发布npm包,但我们将在未来的一篇文章中讨论这一点。
结论
当我们提交我们的发现时,公司立即认识到这是一种连锁反应漏洞,可能会危及不仅仅是单个产品,而是整个开发到生产生命周期。他们将其归类为一种罕见的最坏情况漏洞,并授予我们50,500美元的赏金,远远超出了他们通常的奖励结构。Snorlhax和我终于抓住了我们的“卓越漏洞”,这一发现验证了我们多年来积累的每一个直觉和每一条知识。 对我们来说,最大的教训是,攻击的成功往往取决于结合两个或更多被忽视的角度。收购比母公司更易攻破,而软件供应链漏洞提供了我们所寻找的灾难性影响。通过融合这些见解,我们找到了完美的风暴。我们也对确保不仅你发布的代码,而且你的构建过程的每一层以及你的开发人员从外部来源获取的每个工件的安全性有了新的认识。 我们继续一起黑客攻击,始终寻找下一个改变游戏规则的发现。我们希望分享这次经历能激励其他研究人员超越极限。有时,真正的宝藏隐藏在晦涩的角落:一个隐藏的Docker镜像、一个处理不当的npm令牌或一个遗留的.git文件夹。这些角落可能是改变生活的赏金的钥匙,就像对我们一样。
原文地址:https://www.landh.tech/blog/20250211-hack-supply-chain-for-50k/
原文始发于微信公众号(独眼情报):获得 50000美金的供应链突破测试
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论