Tecnologias de segurança

Vizinho perigoso: como uma cadeia de vulnerabilidades permitiu contornar o isolamento na infraestrutura em nuvem

Este artigo foi publicado exclusivamente para fins educacionais, a fim de aumentar a conscientização dos profissionais de segurança da informação. A pesquisa foi conduzida no âmbito de um programa oficial de bug bounty em uma das principais plataformas. O provedor do recurso em nuvem foi notificado sobre as vulnerabilidades de acordo com a política de divulgação responsável, e todas as falhas de segurança identificadas foram completamente corrigidas. A infraestrutura analisada no material foi anonimizada, o que torna impossível identificar a organização em questão. Estamos convictos de que pentests (testes de invasão) regulares, programas de bug bounty e o diálogo entre desenvolvedores e pesquisadores independentes são a base da segurança de qualquer produto de TI.

Introdução

Muitos profissionais acreditam que o comprometimento de dados na nuvem ocorre, na maioria dos casos, por culpa dos próprios clientes, que deixam armazenamentos mal configurados ou portas abertas, por exemplo. No entanto, o nosso teste de invasão de um grande provedor de nuvem mostrou que ameaças críticas podem partir da própria infraestrutura do provedor.

Esta análise baseia-se em um caso real, que conquistou o segundo lugar na categoria “Invasão de sistema” (Web & Logic) no Russian Pentest Award 2026. O material examina a exploração sucessiva de uma série de vulnerabilidades e falhas de lógica. Em conjunto, elas levaram a consequências críticas para o negócio e ao contorno completo do isolamento entre os clientes da mesma nuvem. Em 22 de outubro de 2026, também apresentei uma análise técnica deste caso na conferência Security Analyst Summit (SAS 2026), na palestra “Smash the Tenant Boundary”.

1. Obtenção do acesso inicial por meio do Kubernetes gerenciado

O pentest da infraestrutura em nuvem foi conduzido no âmbito de um estudo do modelo de ameaças de um invasor externo com privilégios de cliente comum. O objetivo final do ataque era assumir o controle total da nuvem.

1.1 Escape do worker node

No primeiro estágio, criamos um recurso no cluster Kubernetes gerenciado da nuvem, na versão v1.25.16+k0s.0. Em geral, o provedor opta por não conceder aos clientes acesso SSH aos worker nodes, pois eles não pagam pela administração dos servidores. No entanto, neste caso, as políticas de segurança do cluster não impediam a criação de pods privilegiados (pod) com acesso aos namespaces do host. Isso nos permitiu implantar um pod desse tipo e transferir o contexto de execução para a máquina host, onde o container estava em execução.

Após a criação do pod privilegiado, dentro do container executamos o comando nsenter, que garantiu a transferência do contexto de execução para a máquina host:

Logo após a execução desse comando, o shell bash passou a ser iniciado já na máquina host real, e não dentro do container. Como o pod era privilegiado, obtivemos acesso root completo à máquina host.

Configuração do kubelet e endereço do servidor API

Configuração do kubelet e endereço do servidor API

Em seguida, analisamos o seu sistema de arquivos. Durante o estudo do data plane, descobrimos um token do serviço konnectivity. Esse componente garante a conexão de túnel entre o control plane e o data plane do cluster Kubernetes. Foi justamente ele que abriu possibilidades interessantes para o avanço do ataque.

Token da conta de serviço do Konnectivity

Token da conta de serviço do Konnectivity

1.2. Análise dos serviços no control plane

Depois de obter acesso à máquina host (o mesmo worker node já mencionado), analisamos as suas interfaces de rede (a VPC do usuário e as interfaces do Calico). Do arquivo de configuração do kubelet, extraímos o endereço do kube-apiserver (192.168.0.4). Em seguida, escaneamos esse endereço e encontramos as seguintes portas abertas:

O maior interesse recai sobre a porta 8132, na qual funciona o serviço Konnectivity. Esse serviço cria um túnel reverso entre o control plane e o data plane (para que haja visibilidade do control plane para o data plane) e permite que os componentes de gerenciamento interajam com os worker nodes mesmo atrás de NAT. O token konnectivity descoberto anteriormente concedia o direito de interagir com esse serviço.

Diagrama de interação entre o plano de controle (control plane) e o plano de dados (data plane) através do Konnectivity

1.3. Exploração do Konnectivity

Para analisar o serviço na porta 8132, implantamos um ambiente de teste local. Descobrimos que, na versão v1.25.16+k0s.0, é utilizado um fork konnectivity-server, herdado do apiserver-network-proxy do Kubernetes original.

Cabe aqui uma breve introdução ao k0s. Trata-se de uma distribuição minimalista do Kubernetes, destinada a provedores de nuvem, sobre a qual é conveniente implantar clusters gerenciados (Managed k8s) com consumo mínimo de recursos. Uma característica importante do k0s é que, por padrão, nos nós de gerenciamento (master nodes) é proibida a execução de pods de usuário, o que atende aos requisitos de segurança do mk8s quanto ao isolamento do control plane.

Na porta 8132 mencionada acima encontra-se o serviço AgentService, que disponibiliza um único método, o Connect. Esse mesmo método recebe um fluxo de pacotes Packet.

No código, descobrimos um detalhe curioso. Ao receber do worker node um pacote do tipo DIAL_REQ, o serviço AgentService executa a função net.Dial() no lado do control plane. A estrutura dos pacotes para o método Connect é a seguinte:

O servidor lê quaisquer pacotes Packet do stream que o agente (worker node) iniciou:

Em seguida, os pacotes Packet chegam ao dispatcher do backend:

Então acontece a parte mais interessante:

Ou seja, o DIAL_REQ, vindo do agente (worker node), é considerado válido e é repassado ao handler de pacotes do tipo correspondente (DIAL_REQ).

Isso criou uma primitiva de SSRF no contexto do control plane, o que permitiu acessar não apenas recursos de rede, mas também sockets UNIX do host. Escolhemos o D-Bus como candidato promissor para a interação (/run/dbus/system_bus_socket). Com o auxílio do utilitário busctl no control plane, descobrimos os seguintes serviços disponíveis:

Por meio do método org.freedesktop.DBus.GetConnectionCredentials (tendo escrito o nosso próprio cliente D-Bus baseado em github.com/godbus/dbus/v5), conseguimos obter o UID do processo atual. Neste caso, era o 997. Infelizmente, não era root, o que significa que não seria possível iniciar imediatamente um serviço de sistema no systemd. Ainda assim, é possível criar um serviço de usuário (system –user). Para isso, primeiro é necessário ativar o linger para o nosso UID:

Depois disso, abriram-se os seguintes sockets:

Por meio do novo socket, acessamos o método StartTransientUnit do serviço org.freedesktop.systemd1.Manager:

No parâmetro properties passamos justamente as diretivas padrão de descrição de serviços do systemd. Inserimos nele a seguinte linha:

Com isso, conseguimos um callback do control plane.

Contexto do konnectivity-server e interfaces de rede do nó

Contexto do konnectivity-server e interfaces de rede do nó

Os desenvolvedores do projeto k0s, a empresa Mirantis, confirmaram a existência da vulnerabilidade.

1.4. Escalação de privilégios no host

Como, por padrão, o componente konnectivity-server é executado com privilégios restritos, escalamos esses privilégios depois de obter o RCE no control plane. A análise dos processos no master node revelou um salt-minion em execução. Ter privilégios de root permite autenticar-se no salt-minion em nome desse minion e avançar o vetor de ataque sobre ele (mas isso virá mais adiante).

No meu caso, o host alvo era baseado na distribuição Ubuntu 22.04, com o kernel na versão 5.15.x. Como os namespaces de usuário (user namespaces) estavam ativados, a maneira mais indicada de escalar privilégios passou a ser o conhecido bug do OverlayFS, que permite preservar o privilégio cap_setuid+eip em um arquivo após manipulações com os namespaces.

Para explorar a vulnerabilidade, criamos uma cópia do interpretador python3 com a capability de setuid definida, o que permitiu obter privilégios de root.

 Confirmação da execução de comandos com privilégios de root

Confirmação da execução de comandos com privilégios de root

Após a obtenção de privilégios de root, abriram-se vários vetores interessantes:

  1. No host, descobrimos a interface eth2 na sub-rede 100.64.0.0/10. A rede overlay da nuvem, como vimos, é plana, o que permite enxergar absolutamente todos os serviços gerenciados (managed).
  2. O sistema armazenava um par de chaves do salt-minion. Isso amplia significativamente a superfície de ataque, abrindo acesso ao control plane da própria nuvem gerenciada por meio do SaltStack.

2. Aventuras na rede overlay

2.1. Victoria stack

O estudo da atividade de rede dentro do recurso gerenciado trouxe resultados interessantes de imediato. A análise do tráfego mostrou que, de um determinado endereço IP, chegavam requisições de coleta de métricas do nó (acessos à nossa instância de kube-state-metrics no mk8s). Por trás desse endereço IP, constatamos a presença de um coletor de métricas, uma das instâncias do VictoriaMetrics, que atende diversos recursos gerenciados. De forma análoga ao Prometheus, a API do VictoriaMetrics expõe a lista de targets dos quais as métricas são coletadas.

Ao acessar o endpoint http://100.64.13.37:8429/api/v1/targets, descobrimos que os labels de cada target contêm metadados valiosos:

Vale registrar que, por meio do VictoriaMetrics, é possível obter em uma única requisição a lista completa de hosts do control plane pertencentes a outros usuários da nuvem. A porta 6897 indica o funcionamento do exporter kube-state-metrics, e o campo profile: kubernetes confirma ainda o tipo de target neste endereço.

Nessa mesma rede, descobrimos mais um host do ecossistema Victoria, o VictoriaLogs. A interface web desse serviço na porta 9428 também possui endpoints de API que permitem extrair dados sensíveis. Por exemplo, o endpoint /select/logsql/query permitia ler, sem autorização, absolutamente todos os logs de quaisquer recursos gerenciados na nuvem.

Ao analisar os logs do control plane para o Managed k8s, descobrimos um detalhe crítico. Nos scripts de cloud-init, por padrão, eram geradas chaves SSH privadas para a conta root. Essas chaves eram exibidas no stdout e salvas automaticamente nos logs gerais do sistema.

A correlação de alguns valores de labels do VictoriaMetrics ajudou a reconstruir os nomes originais do host alvo. Isso facilita a filtragem de dados no VictoriaLogs e permite associar de forma inequívoca a chave privada comprometida ao nó alvo no qual essa chave pode ser utilizada.

Chave SSH privada em um registro do VictoriaLogs

Chave SSH privada em um registro do VictoriaLogs

Utilizando a chave privada comprometida de uma conta privilegiada, podíamos acessar os nós alvo por SSH e assumir o controle total dos clusters de outros clientes.

Privilégios do processo no host e ações disponíveis no Kubernetes

Privilégios do processo no host e ações disponíveis no Kubernetes

É provável que, nos logs de serviços de outro tipo, também seja possível encontrar outros dados sensíveis para aumentar ainda mais o impacto.

2.2 Comprometimento das instâncias gerenciadas do Kafka

Investigamos a possibilidade de implantar recursos gerenciados do Apache Kafka. Como esperávamos, também encontramos essas instâncias na rede overlay plana. Assim, em um dos hosts com Kafka, a porta 9999 estava aberta e se revelou nada menos que um JMX Registry.

Serviço Java RMI descoberto na porta 9999

Serviço Java RMI descoberto na porta 9999

O mecanismo do JMX Registry é vulnerável por sua própria natureza. Ele retorna um RMI stub-host ao cliente que o invoca, o que permite carregar remotamente uma classe MBean arbitrária própria e executar código arbitrário. Ao executar uma varredura com o utilitário nmap -sC na porta do registry, determinamos a porta exata do serviço RMI com a qual interagiríamos em seguida. Para realizar o ataque, preparamos um conjunto de scripts Java com a seguinte estrutura:

  • JmxCreateMBean.java: responsável pela conexão ao JMX Registry e pela criação do componente MBean javax.management.loading.MLet (uma classe que permite carregar classes a partir de uma URL indicada)
  • JmxInvoke.java: invoca o método selecionado do MBean por meio do JMX Registry
  • ExecMBean.java e Exec.java: um componente MBean de teste, personalizado e leve, com o método run(String cmd), que implementa a interface de execução remota de comandos.

O trecho de código responsável pela conexão e pela inicialização do componente MBean é o seguinte:

Abaixo está a implementação da classe maliciosa Exec.java, que fornece métodos para executar comandos shell, bem como para ler e gravar arquivos via Base64:

Depois de compilar o nosso próprio MBean, hospedamos o arquivo .jar gerado em um servidor web remoto e preparamos o arquivo de configuração mlet.html para que o servidor alvo o carregasse em seguida.

Em seguida, reproduzimos a seguinte cadeia de ações:

  1. Criação do carregador MLet padrão dentro da JVM do Kafka
  2. Carregamento do MBean malicioso no servidor alvo por meio da chamada do método getMBeansFromURL no componente MLet padrão (DefaultDomain:type=MLet). Como argumento, é passada a URL do arquivo preparado: http://attacker_ip:8000/mlet.html
  3. Chamada do método run(String cmd) no MBean carregado para obter RCE no host.
Contexto do processo do Kafka e interfaces de rede do nó

Contexto do processo do Kafka e interfaces de rede do nó

Esse vetor tornou-se possível porque o RMI stub-host, que, segundo os padrões de segurança, deveria estar vinculado apenas à interface loopback (127.0.0.1), foi erroneamente exposto na rede overlay compartilhada da nuvem.

2.3. Vazamento de credenciais por meio do redis-exporter

O estágio seguinte foi a análise dos hosts com instâncias gerenciadas do Redis. Em cada um deles, ao lado da porta principal 6379, o serviço redis exporter (porta 9121) estava implantado. A análise do seu código-fonte revelou uma vulnerabilidade de lógica no tratamento do parâmetro target:

Em essência, o handler /scrape recebe um parâmetro target arbitrário e tenta coletar métricas dele, usando as mesmas credenciais com as quais o exporter se conecta à sua instância principal do Redis. Se direcionarmos a requisição do redis exporter para o nosso próprio host (por exemplo, com um nc -nvlp 6379 em execução), surge a possibilidade de interceptar as seguintes credenciais:

Comando AUTH em uma requisição interceptada ao Redis

Comando AUTH em uma requisição interceptada ao Redis

As credenciais obtidas permitiram conectar-se às instâncias do Redis de outros clientes e realizar operações de leitura, modificação ou exclusão dos dados armazenados.

Informações de diagnóstico da instância do Valkey

Informações de diagnóstico da instância do Valkey

(Observação: durante a pesquisa, foi divulgada uma vulnerabilidade que leva a RCE no Redis, o que, combinado com o acesso obtido, permitia comprometer completamente os serviços de outros usuários).

2.4. Comprometimento dos clusters gerenciados do PostgreSQL (Patroni)

Juntamente com os clusters gerenciados (managed) do Kubernetes, o SGBD PostgreSQL é um dos recursos gerenciados mais requisitados. Por isso, o estágio seguinte na rede overlay foi a análise dos hosts PostgreSQL. Ao escanear os nós com a porta do PostgreSQL aberta (5432), descobrimos serviços adjacentes nas portas 2379 (o armazenamento distribuído etcd) e 8008 (Patroni REST API). Enquanto a REST API do Patroni exigia autenticação, o acesso ao etcd revelou-se totalmente aberto. Durante a análise do conteúdo do etcd, identificamos uma chave crítica, /v2/keys/service/pg-cluster/config, que continha a configuração do Patroni.

Configuração do Patroni no etcd

Configuração do Patroni no etcd



A possibilidade de gravar neste arquivo de configuração permite alterar o parâmetro archive_command para um comando conveniente ao invasor, a fim de obter RCE no host alvo ao iniciar o processo de arquivamento. Para não esperar o preenchimento natural do WAL, encontramos uma forma de forçar o início do arquivamento mesmo com o nível mínimo de acesso:

  1. Atualizamos a configuração do Patroni com o archive_command modificado:
  2. Atualizamos novamente a configuração, adicionando o parâmetro standby_cluster:

Após a aplicação desse parâmetro, o Patroni detecta um conflito de papéis para o master atual. Nos logs aparece uma sequência semelhante a esta:

Em seguida, é iniciada a reinicialização do Postgres, bem como o procedimento de arquivamento e a execução do comando injetado:

Contexto do processo do PostgreSQL e interfaces de rede do nó

Contexto do processo do PostgreSQL e interfaces de rede do nó

Dessa forma, é possível acessar os SGBDs Postgres alheios e obter controle total sobre os dados. Como fator de risco adicional, nesses hosts descobrimos arquivos com credenciais de acesso ao armazenamento S3.

Parâmetros de acesso ao banco de dados e S3 em um arquivo de backup

Parâmetros de acesso ao banco de dados e S3 em um arquivo de backup

Esse armazenamento contém backups não apenas da instância atual, mas também de todos os demais bancos de dados na nuvem.

Lista de locais de armazenamento de backup no S3

Lista de locais de armazenamento de backup no S3

3. Ataque à infraestrutura de gerenciamento SaltStack

Depois de obter acesso root ao nó do control plane, voltamos a atenção para o serviço de gerenciamento de configuração SaltStack.

3.1. Análise do salt fs

O primeiro passo foi o dump completo do sistema de arquivos do Salt Master para posterior análise estática:

Fragmento da lista de arquivos do Salt Master

Fragmento da lista de arquivos do Salt Master

Os arquivos revelaram-se muito numerosos.

Como a versão do Salt utilizada era atual, os CVEs conhecidos não se aplicavam. No entanto, entre os arquivos do salt-master, descobrimos um script personalizado (_pillar/somebase.py) com uma vulnerabilidade crítica, escrito pelo fornecedor para dar suporte aos minions com o serviço somebase. A vulnerabilidade consiste na construção insegura de uma consulta SQL por meio do método query.format():

Como o salt-minion pode iniciar a atualização do pillar e enviar ao salt-master metadados arbitrários (grains), o invasor tem a possibilidade de injetar código SQL por meio do parâmetro some_id dentro dos grains. Quando a extensão processa a requisição no lado do salt-master, a consulta SQL gerada é executada no banco de dados prod-cloud. Com isso, a vulnerabilidade permite realizar uma SQL injection do tipo Error-Based.

Erro de banco de dados durante o processamento do pillar

Erro de banco de dados durante o processamento do pillar

3.2 Exploração da SQLi

Por meio dessa vulnerabilidade, obtivemos acesso a diferentes schemas do banco, o mais interessante dos quais era o schema cloudsmth, que continha cerca de 50 tabelas com metadados sobre todos os recursos gerenciados e VDS.

Fragmento da lista de tabelas do esquema cloudsmth

Fragmento da lista de tabelas do esquema cloudsmth

Mais notável ainda foi a tabela salt-masters, com os tokens de autenticação de todos os salt-masters.

Registros com tokens de autenticação do Salt Master

Registros com tokens de autenticação do Salt Master

A existência desses tokens permitiu acessar a REST API do Salt Master e executar comandos arbitrários em todos os nós (minions) sob controle da nuvem:

Em acordo com o fornecedor, o avanço posterior do ataque foi interrompido, mas, de modo geral, a obtenção de acesso aos salt-masters e ao banco de dados prod-cloud permitia, em um único passo, realizar a tomada completa de todos os recursos (incluindo VDS, S3 e outros serviços).

4. Outras formas de atacar a nuvem

Gostaríamos de mencionar outras formas de entrar na rede overlay, pois entre elas também havia casos interessantes.

4.1. Exploração de uma vulnerabilidade no Managed Valkey

Durante a investigação das instâncias de Valkey implantadas na infraestrutura em nuvem, constatamos que a versão de software utilizada é vulnerável à CVE-2026-25243. No momento da análise, não existiam provas de conceito (PoC) nem exploits públicos para essa vulnerabilidade. Para estudar os mecanismos de manifestação da vulnerabilidade, analisamos os dados de diagnóstico obtidos com o comando INFO:

O identificador redis_build_id permitiu determinar com precisão o arquivo binário utilizado, que coincidiu com a build oficial para Ubuntu Noble x86_64.

A essência da vulnerabilidade CVE-2026-25243 está na forma incorreta como o operador RESTORE trata os objetos RDB serializados. Ao usar a codificação obsoleta hash-zipmap, o validador aceita uma representação de 5 bytes do comprimento para uma chave curta, porém o parser subsequente interpreta os bytes internos da chave como a codificação do comprimento do valor. Essa inconsistência leva à possibilidade de estouro do buffer e de sobrescrita de chunks adjacentes no heap (Heap Overflow), o que potencialmente permite obter uma escrita arbitrária na memória (Arbitrary Write). Após a escrita bem-sucedida em regiões de memória adjacentes (Arbitrary Write), surgiu um obstáculo na forma das regras estritas do systemd sandbox:

Esses parâmetros proíbem a execução de shells padrão (por exemplo, /bin/sh). Mesmo assim, a configuração de montagem do diretório /tmp permitia a escrita. A modificação da lógica do processo permitiu iniciar o carregamento e a execução de um componente externo a partir de /tmp, que não estava sujeito à regra noexec. Após estabelecer um canal de comunicação estável, adicionamos uma chave SSH ao arquivo /var/lib/valkey/.ssh/authorized_keys. Isso garantiu acesso independente ao host e a possibilidade de escalação posterior de privilégios por métodos padrão (LPE).

Contexto do processo do Valkey e parâmetros do Salt grains

Contexto do processo do Valkey e parâmetros do Salt grains

Neste caso, a vulnerabilidade GRO Frag funcionou, porém novos LPE surgem a cada semana, de modo que pode haver muitas opções para escalar privilégios e continuar o ataque ao Salt Master.

4.2. MySQL

Ao criar o recurso, o usuário do banco de dados MySQL não tinha privilégios de root.

No entanto, a lógica da aplicação permitia que esse usuário excluísse e recriasse a tabela de privilégios dinâmicos:

Após reiniciar o banco e criar um novo usuário, ele recebia automaticamente privilégios completos de administrador. Adicionamos a ele mais alguns privilégios:

No banco, elevamos os privilégios. Restava obter o RCE. Para isso, compilamos uma biblioteca UDF personalizada para a execução de comandos de sistema.

Para obter RCE, realizamos uma CRLF injection no arquivo de configuração por meio da API do provedor de nuvem:

Esse vetor permitiu injetar quebras de linha e redefinir parâmetros críticos de configuração:

Usando os privilégios de escrita obtidos, carregamos a biblioteca UDF maliciosa (Shared Object) no diretório alvo por meio do operador SQL:

Reiniciamos o banco MySQL para aplicar a configuração alterada, de modo que ela tenha efeito e o plugin registrado seja inicializado:

Resultado da execução de id via UDF do MySQL

Resultado da execução de id via UDF do MySQL

Para a escalação de privilégios até o nível de root (LPE), analisamos a lista de serviços ativos. Constatamos que o serviço mysql-watcher.service é executado com privilégios de administrador e inicia periodicamente um script Python para acessar o MySQL por meio do cliente de sistema. Para explorar esse processo, compilamos mais um plugin:

O caminho para o diretório com a biblioteca maliciosa (arquivo .so, parâmetro plugin_dir) e o nome do plugin de autenticação default-auth=plugname são injetados na seção [client] do arquivo de configuração /etc/mysql/my.cnf. Na próxima execução do cliente MySQL, iniciada pelo serviço em segundo plano mysql-watcher.service em nome do root, o plugin descrito acima é executado no host com o servidor. Isso leva à execução de um comando de sistema e à criação de uma cópia do interpretador Bash com o bit SUID definido (/tmp/rbash), o que completa a cadeia de comprometimento total do host.

Conclusões

A infraestrutura do provedor de nuvem baseava-se em desenvolvimentos próprios, e não em soluções open source típicas, o que formou uma superfície de ataque incomum. No âmbito da pesquisa, identificamos também outras vulnerabilidades, que não entraram no relatório devido ao seu caráter padrão. Com base nos resultados do trabalho, apresentamos ao fornecedor recomendações voltadas para elevar o nível de segurança. Entre elas, descrevemos medidas para isolar os serviços gerenciados a fim de eliminar a visibilidade de rede entre eles, aplicar políticas estritas de AppArmor para o isolamento dos processos dos serviços, bem como incorporar às práticas de monitoramento o acompanhamento do uso de serviços potencialmente vulneráveis.

Vizinho perigoso: como uma cadeia de vulnerabilidades permitiu contornar o isolamento na infraestrutura em nuvem

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Relatórios

O passageiro invisível do seu carro

Um especialista da Kaspersky descobriu um novo malware para Android para exibição de anúncios e criação de uma botnet de proxy. A infecção ocorre por meio de um software legítimo desatualizado para centrais multimídia da DoFun.