1 pontos por GN⁺ 2 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • CVE-2026-31431 Copy Fail permite que um usuário local sem privilégios obtenha um shell root e, mesmo dentro de contêineres rootless do Podman, possibilita a elevação para root dentro do contêiner
  • Os contêineres rootless do Podman combinam namespaces de usuário, separação de UID e Linux capabilities para mapear o root interno do contêiner para um usuário sem privilégios no host e limitar os privilégios no host
  • Nos testes, o usuário foo de um contêiner rootless non-root conseguiu se tornar root dentro do contêiner após executar o Copy Fail, mas os privilégios permaneceram limitados ao que o usuário sem privilégios bar podia fazer no host, e arquivos do host pertencentes ao root não puderam ser lidos
  • Ao aplicar --security-opt=no-new-privileges ou --cap-drop=all, mesmo após a execução do Copy Fail o shell permaneceu como foo e com capabilities none, impedindo a obtenção imediata de um shell root e a elevação de capabilities
  • Os efeitos do Copy Fail podem persistir além do ciclo de vida do contêiner, exigindo patch do kernel e reinicialização, e também devem ser aplicadas medidas de defesa em profundidade como sistema de arquivos raiz somente leitura, limites de recursos com cgroups, imagens de runtime enxutas e firewall

Copy Fail e o escopo de exposição dos contêineres rootless do Podman

  • CVE-2026-31431 foi divulgado em 29 de abril no copy.fail e, ao executar o script em Python publicado, um usuário local sem privilégios pode obter um shell root
  • O Copy Fail também pode ser explorado dentro de contêineres Linux, e mesmo em contêineres rootless do Podman é possível obter um shell root dentro do contêiner
  • Nos testes, o root do contêiner ficou limitado, no nível do host, ao escopo de privilégios do usuário sem privilégios bar, que executou o contêiner
  • A implementação rootless do Podman combina namespaces de usuário, separação de UID e Linux capabilities para limitar os privilégios no host dos processos do contêiner
  • O Copy Fail mostra que contêineres rootless não são imunes à vulnerabilidade, mas que a configuração do Podman pode reduzir o alcance do ataque após a invasão

Como funcionam os contêineres rootless

  • Exemplo básico: usuário sem privilégios bar executa um servidor HTTP

    • O ambiente de exemplo consiste em um usuário sem privilégios bar, com UID 1001, que compila uma imagem baseada em ubuntu:latest com o Podman e executa python3 -m http.server
    • No host, ao verificar com ps, o processo python3 aparece sendo executado sob o usuário bar
    • Como o Podman usa um modelo de fork/exec, o processo do contêiner se torna descendente do processo podman run, e a separação de UID normalmente isola o processo do contêiner do root do host e de outros usuários
    • Em uma configuração típica do Docker, mesmo que um usuário sem privilégios execute docker run, o cliente Docker se comunica com um daemon com privilégios de root, e como o daemon é quem finalmente cria o processo do contêiner, no host esse processo pode aparecer como root
  • Rootless rootful

    • Se a imagem do contêiner não tiver uma diretiva USER explícita nem a flag --user, o comando do contêiner normalmente será executado como root dentro do contêiner
    • Na saída de podman top, o processo do servidor HTTP é mapeado para o usuário 1001 no host, mas dentro do contêiner é executado como root
    • Essa configuração caracteriza um estado rootless rootful: sem privilégios no host, mas como root dentro do contêiner
  • Namespaces de usuário

    • Os contêineres rootless do Podman usam namespaces de usuário para mapear UID/GID de forma diferente dentro e fora do contêiner
    • No exemplo, o root com UID 0 dentro do contêiner é mapeado para bar, com UID 1001, no host
    • A configuração bar:165536:65536 em /etc/subuid define o intervalo de UIDs que pode ser atribuído aos processos no namespace de bar
    • No exemplo, além do UID 1001 de bar, os UIDs de 165536 até 231072 podem ser atribuídos aos processos de bar
    • Se sleep for executado como o usuário www-data dentro do contêiner, ele aparece como www-data internamente, mas como 165568 no host
    • Ao entrar no namespace de usuário com podman unshare, o diretório home que no host pertence a bar:bar aparece como root:root dentro do namespace
    • O Docker também oferece suporte a namespaces de usuário, mas requer configuração separada e permite apenas um único namespace de usuário, enquanto o Podman executa os contêineres rootless de cada usuário UNIX dentro do namespace desse próprio usuário
  • Operações privilegiadas e Linux capabilities

    • O Podman usa Linux capabilities para conceder privilégios de root granulares aos processos do contêiner
    • Durante a compilação da imagem, operações como apt install se tornam possíveis por meio de uma combinação de capabilities como chown, dac_override, fowner, setgid, setuid, net_bind_service e sys_chroot
    • Se todas as capabilities forem removidas com podman build --cap-drop=all, o apt falha em operações como setgroups, setegid, seteuid e chown, e a compilação da imagem falha
    • Também é possível adicionar apenas as capabilities necessárias e, no exemplo, a instalação de pacotes é feita adicionando CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER
    • No estado padrão de execução, o servidor HTTP roda como root dentro do contêiner e possui várias capabilities efetivas, como CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT
    • Como o servidor HTTP não precisa desses privilégios, é possível remover todas as capabilities com podman run --cap-drop=all; nesse caso, podman top mostra as capabilities efetivas como none
  • Rootless non-root

    • Para executar o servidor HTTP como um usuário sem privilégios também dentro do contêiner, pode-se usar um usuário já existente em /etc/passwd, como www-data, ou criar um usuário dedicado durante a compilação da imagem
    • No exemplo, são criados o usuário e grupo foo, com UID 1002, são concedidas permissões de leitura em /var/www/html, e então é definido USER foo:foo
    • Ao executar essa imagem com --cap-drop=all, o processo passa a ficar como foo dentro do contêiner, UID 166537 no host e capabilities efetivas none
    • Os processos do contêiner devem ser executados com o menor privilégio necessário; por exemplo, se foo precisar fazer bind na porta privilegiada 80, deve-se adicionar --cap-add=CAP_NET_BIND_SERVICE
    • As formas de execução de contêiner podem ser divididas em quatro categorias
      • usuário root no host + root no contêiner: root rootful
      • usuário root no host + usuário sem privilégios no contêiner: root non-root
      • usuário sem privilégios no host + root no contêiner: rootless rootful
      • usuário sem privilégios no host + usuário sem privilégios no contêiner: rootless non-root
    • O Podman facilita a execução de contêineres rootless rootful e, se for possível executar os processos do contêiner como usuários sem privilégios, também é relativamente fácil montar uma configuração rootless non-root

Montagens bind e isolamento de UID

  • Ao montar um diretório do host no contêiner, a possibilidade de acessar arquivos pertencentes ao root do host, ao bar do host e ao foo do namespace varia de acordo com o mapeamento de UID
  • No exemplo, são criados no diretório /var/lib/bar/test os arquivos root.txt, pertencente ao root do host, e bar.txt, pertencente ao bar do host, e o diretório é montado no contêiner como /test com leitura e escrita
  • Ao executar o contêiner como foo, o arquivo pertencente ao bar do host aparece como root:root dentro do contêiner, enquanto o arquivo pertencente ao root do host aparece como nobody:nogroup por não estar mapeado para o namespace
  • O foo dentro do contêiner não consegue ler bar.txt nem root.txt, e um rootless non-root oferece isolamento adicional em relação ao rootless rootful
  • O foo.txt criado por foo no diretório montado aparece no host como pertencente ao UID 166537, e o usuário bar do host não consegue ler o conteúdo desse arquivo
  • Ao executar o contêiner como root interno, o root do namespace consegue ler o arquivo pertencente ao bar do host e o arquivo pertencente ao foo, mas não consegue ler o arquivo root.txt pertencente ao root do host
  • Ao executar como root interno com --cap-drop=all, ele também não consegue ler o arquivo de foo e só consegue ler o arquivo pertencente ao bar do host

Teste do Copy Fail

  • Condições de teste

    • No teste de Copy Fail, foi usada a versão de exploit do commit originalmente publicado 8e918b5
    • A imagem de contêiner de exemplo adiciona curl à imagem existente do servidor HTTP para permitir baixar o script de exploit de dentro do contêiner
    • A imagem é construída com o nome copyfail
    • O kernel de teste é o 6.12.74+deb13+1-amd64 do Debian, e, no contexto do Debian, versões recentes abaixo de 6.12.85 ainda podem ser consideradas kernels sem patch
    • Em geral, quando o usuário sem privilégios foo chama su, é exigida a senha de root
    • Em cada teste, o usuário do contêiner baixa o script do Copy Fail em /tmp, executa-o e, ao obter um shell de root, chama sleep
    • Como o Copy Fail persiste além do ciclo de vida do contêiner, a VM é reiniciada antes de cada teste
  • Resultado em rootless rootful

    • Ao executar o contêiner com --user=root, os processos dentro do contêiner já estão como root
    • Nesse estado, ao executar o script do Copy Fail e chamar su, obtém-se um shell uid=0(root), mas como o usuário root já pode abrir outro shell root com su sem senha, o Copy Fail não acrescenta nada de prático
    • No podman top, /bin/bash, python3 copy_fail_exp.py, su e sleep aparecem todos como root dentro do contêiner e usuário 1001 no host
    • O mesmo conjunto de capabilities é mantido, e aparecem CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT
    • O root interno consegue ler bar.txt e foo.txt em /test, mas não consegue ler root.txt, pertencente ao root do host
  • Resultado em rootless non-root

    • Após executar o contêiner como foo, ao rodar o script do Copy Fail e chamar su, ocorre escalonamento de privilégios para root dentro do contêiner
    • O id do shell resultante aparece como uid=0(root) gid=1002(foo) groups=1002(foo)
    • No podman top, o /bin/bash inicial, o processo que executa o exploit e a chamada de su aparecem com UID 166537 no host, usuário foo no contêiner e capabilities none
    • Após a elevação de privilégios, [sh] e sleep aparecem como usuário 1001 no host e usuário root no contêiner, obtendo o mesmo conjunto de capabilities do rootless rootful
    • O root elevado dentro do contêiner ainda não consegue ler root.txt, pertencente ao root do host
    • Nessa situação, o contêiner foi comprometido, mas o alcance do ataque fica limitado ao contêiner e ao que o usuário sem privilégios bar no host conseguiria fazer
  • Resultado com no-new-privileges

    • O Podman permite usar --security-opt=no-new-privileges para impedir que os processos do contêiner obtenham mais privilégios do que tinham no momento da inicialização
    • Ao aplicar essa opção a um contêiner rootless non-root e executar o Copy Fail, o shell é aberto, mas continua no estado uid=1002(foo)
    • No podman top, todos os processos também permanecem com UID 166537 no host, usuário foo no contêiner e capabilities none
    • Mesmo em /test montado, foo só consegue ler o próprio arquivo e não consegue ler bar.txt nem root.txt
    • O contêiner foi comprometido, mas permanece limitado ao usuário interno sem privilégios foo e a um estado sem capabilities
  • Resultado com --cap-drop=all

    • Mesmo ao executar um contêiner rootless non-root com --cap-drop=all, o foo originalmente não possui capabilities
    • Nesse estado, ao executar o Copy Fail e chamar su, o shell aberto permanece como uid=1002(foo)
    • No podman top, /bin/bash, a execução do exploit, su, o shell e sleep permanecem todos como foo e com capabilities none
    • O exploit falha em obter um shell de root, e foo só consegue ler o próprio arquivo em /test
    • Esse resultado é semelhante ao teste com no-new-privileges, e as duas medidas podem ser usadas em conjunto para reduzir de forma eficaz a exposição a capabilities
  • Persistência do exploit

    • Embora a obtenção imediata de um shell de root e de capabilities tenha podido ser bloqueada com no-new-privileges ou --cap-drop=all, o efeito do próprio exploit permanece
    • Se depois disso um novo contêiner for executado sem restrições de capabilities, o usuário sem privilégios foo no contêiner poderá se tornar root no contêiner apenas chamando su
    • Portanto, o patch do kernel e a reinicialização ainda são necessários

Estratégias de defesa em profundidade

  • Imagens somente leitura

    • Adicionar --read-only ao podman run faz com que o sistema de arquivos raiz do contêiner seja montado como somente leitura
    • O Podman monta por padrão alguns diretórios como graváveis, como /tmp, /run e /var/tmp, portanto, para torná-lo completamente somente leitura, também é preciso adicionar --read-only-tmpfs=false
    • Se um contêiner somente leitura for comprometido, gravações no sistema não serão permitidas, o que pode limitar alguns ataques após o exploit
    • Ainda assim, como é possível enviar a saída de curl por pipe para python3, a configuração somente leitura por si só não impede a execução do exploit
    • O servidor HTTP python3 do exemplo não precisa gravar no sistema de arquivos, então essa opção pode ser usada com segurança
    • Muitas imagens pré-compiladas presumem acesso de gravação a determinados diretórios e podem não funcionar corretamente com um sistema de arquivos raiz somente leitura
    • Um sistema de arquivos raiz somente leitura é independente dos volumes graváveis anexados ao contêiner, e em caso de comprometimento ainda será possível gravar nesses diretórios montados
  • Limites de recursos

    • Docker e Podman podem usar cgroups para limitar os recursos fornecidos ao contêiner
    • O contêiner não precisa de memória, CPU ou PIDs ilimitados
    • Após verificar o uso de recursos do contêiner com podman stats, é possível aplicar limites adequados
  • Restrição de binários disponíveis

    • O exemplo usa a imagem ubuntu por simplicidade, mas a imagem ubuntu inclui muitos binários que um invasor pode usar em caso de comprometimento
    • A execução do servidor HTTP não precisa da maioria desses binários
    • É melhor manter a imagem de runtime o mais enxuta possível
    • Você pode usar builds multiestágio para separar o ambiente de build do ambiente de runtime
    • Também é possível usar imagens voltadas a um propósito específico, como python3, variantes -slim do Debian ou distribuições menores como alpine
    • Se for compatível com o processo do contêiner, é possível usar distroless images ou scratch para criar um runtime sem shell, gerenciador de pacotes ou utilitários de sistema
  • Firewall

    • É possível usar iptables ou nftables para restringir processos de contêiner com firewall
    • Devem ser permitidas apenas as conexões de entrada e saída estritamente necessárias para o processo do contêiner
    • No exemplo do servidor HTTP, não há necessidade de DNS nem de conexões com servidores locais ou remotos, então é possível restringir para permitir apenas pacotes tcp vindos de conexões de entrada já estabelecidas

Implicações operacionais

  • Contêineres rootless padrão do Podman oferecem, por padrão, um isolamento melhor do que configurações padrão de contêineres Docker
  • O Docker também permite execução rootless e uso de namespaces de usuário não privilegiados, mas isso exige mais esforço de configuração do que no Podman, e diferenças de arquitetura também influenciam
  • O Docker ainda é amplamente usado, e ferramentas de self-hosting como Dokku, Kamal, Coolify e Dokploy também usam Docker por padrão
  • Se imagens do Docker Hub forem executadas sem revisão adequada ou sem aplicação de medidas de contenção, os serviços podem operar com uma superfície de ataque maior do que o necessário
  • É preciso entender os detalhes de implementação das imagens de contêiner
    • Você deve saber qual usuário ou quais usuários executam o processo do contêiner
    • Você deve saber de quais diretórios do sistema de arquivos raiz o processo do contêiner depende
    • É preciso distinguir entre as capacidades Linux necessárias e as que não são necessárias
  • Ao combinar os vários mecanismos oferecidos pelo Podman e pelos contêineres, é possível reforçar o contêiner e reduzir o raio de impacto em caso de comprometimento
  • Dependendo da carga de trabalho, não se deve confiar no contêiner como única fronteira de segurança
  • Usar contêineres em conjunto com máquinas físicas ou virtuais separadas pode fornecer isolamento de forma eficaz
  • O Podman oferece uma forma de isolar cargas de trabalho executando cada uma, mesmo no mesmo host, com um usuário não privilegiado separado e seu próprio namespace de usuário

Materiais adicionais

1 comentários

 
GN⁺ 2 시간 전
Opiniões do Lobste.rs
  • É preciso focar no comportamento primitivo que a vulnerabilidade torna possível, mais do que no exploit divulgado
    Essa vulnerabilidade permite gravar no cache de páginas independentemente de ser somente leitura ou não, então um contêiner malicioso pode alterar páginas pertencentes a arquivos da imagem base do overlayfs, e, dependendo da forma como os contêineres são implantados, o impacto pode se propagar para outros contêineres também
    Numa configuração rootless como esta, o alvo seriam outros contêineres executados com o mesmo usuário no sistema host
    Outra forma de explorar seria iniciar ou localizar um contêiner baseado numa imagem base que já se saiba estar em uso, alterar o cache de páginas dentro desse contêiner e então fazer com que outro contêiner que compartilhe o mesmo runtime e os mesmos dados do overlayfs execute esse código
    Rootless e namespaces de usuário são importantes, mas aqui não ajudam muito e, como diz o site copy.fail, vale considerar bloquear em contêineres a chamada de sistema seccomp socket(AF_ALG, ...)

    • Eu não tinha pensado tão a fundo no comportamento primitivo em si e foquei mais em organizar os namespaces e capabilities fornecidos por contêineres rootless para avaliar a exposição de um contêiner comprometido
      Seria bom explicar com mais detalhe o que exatamente significa “dependendo da forma como os contêineres são implantados”
      Uma vantagem do Podman rootless é que, dependendo da carga de trabalho, não é necessário executar contêineres no host com o mesmo usuário
      Se você está falando do caso de rodar vários contêineres rootless com o usuário principal da workstation, concordo, mas em servidores é possível separar cada um em usuários distintos, e até a mesma imagem de contêiner pode ser executada por usuários diferentes sem privilégios
      Isso é bem diferente do padrão do Docker, que executa a maior parte como root, mas eu também escrevi no fim do texto que isso não é a fronteira de segurança definitiva, e se faz sentido ou não distribuir contêineres rootless entre vários usuários sem privilégios depende do caso de uso
      Algumas cargas de trabalho eu isolo em VMs
      Fiquei na dúvida se dizer que rootless e namespaces de usuário não ajudam aqui significa impedir o exploit
      Ainda não usei políticas explícitas de seccomp em contêineres, então não tratei disso, mas é um bom incentivo para olhar melhor
  • Gosto de Podman e de contêineres rootless, mas ao ver o CopyFail cheguei à mesma conclusão do comentário irmão
    Mesmo com a vantagem adicional de controle de acesso de podman+rootless, isso acaba reafirmando o conselho clássico de que contêiner não é fronteira de segurança, e um único exploit de kernel pode furar tudo
    Sou apenas um administrador de sistemas por hobby, mas como novidade nessa área vi o backend libkrun para crun com podman
    A promessa é continuar lidando com a maior parte das cargas de trabalho conteinerizadas como estão, mas por baixo executando-as em uma MicroVM com um kernel convidado separado; só não sei o nível de maturidade, validação em produção e auditoria de segurança, e parte disso parece bem de ponta
    Como MicroVM está sendo adotada ativamente em ferramentas de codificação com LLM, esse estado talvez não dure muito
    podman machine também parecia promissor, mas infelizmente foi pensado só para workstations de desenvolvedor e com um modelo de apenas uma VM de execução de contêineres por sistema host
    Ainda assim, acho que dizer “contêiner não é fronteira de segurança” simplifica demais. Contêiner claramente é uma fronteira de segurança, só que não é tão forte quanto gostaríamos de acreditar

    • Esse também é o motivo de a maioria das implantações de contêineres em nuvem usar VM. VM é uma fronteira defensável
      Em implantações locais, essa linha fica um pouco mais borrada
      Do ponto de vista de hardware, VM não é inerentemente mais segura que processos, mas a fronteira é mais defensável por três motivos
      Escape de VM é menos comum que chamada de sistema, então há mais espaço para aplicar mitigação de canal lateral sem perda de desempenho
      A interface de host de uma VM é muito mais simples. Um dispositivo de bloco tem uma interface de leitura e escrita em blocos, e um dispositivo de rede envia e recebe quadros
      A chamada setsockopt que Linux ou *BSD oferecem em sockets representa uma superfície de ataque muito maior do que a maioria dos drivers emulados ou paravirtualizados, e mesmo isso é apenas uma parte muito pequena da superfície total de ataque do kernel
      A interface de VM também tende a ter bem menos estado. Há transações em andamento em um anel no modelo de requisição-resposta, mas fora isso quase nada
      Credenciais, UID, GID, tabelas de descritores de arquivo e afins adicionam complexidade baseada em estado ao kernel, e um processo pode abusar disso se houver bugs
      A dificuldade da variação para workstation é que ela reintroduz essa complexidade
      Por exemplo, a camada base do contêiner pode ser exposta como um dispositivo de bloco contendo um sistema de arquivos imutável, mas volumes e pastas compartilhadas provavelmente seriam montados com 9pfs ou VirtIO-FS, isto é, 9p ou FUSE sobre VirtIO
      Aí a superfície de ataque volta a crescer
      Com sorte, seria necessária uma cadeia de exploits
      Tenho mais familiaridade com o lado do FreeBSD, onde normalmente o componente que fornece dispositivos paravirtualizados ou emulados é colocado em sandbox com Capsicum, então primeiro é preciso comprometer o processo do host e depois ainda furar o kernel para acessar algo ao qual a VM não tinha permissão
      Mas, sem esse sandboxing adicional, o escape de contêiner volta ao mundo em que pode fazer tudo o que o usuário conseguiria fazer, o que num desktop não é muito melhor do que comprometer o root
    • Kata Containers e Firecracker já são tecnologias relativamente antigas, e como pesquisadores vêm avaliando isso, considero razoável dizer que amadureceram
      Pessoalmente prefiro mais o gVisor. Não é um runtime baseado em VMM, mas já existe há anos, é usado por empresas como a Tencent, e se encaixa bem no meu ambiente, onde todos os contêineres já rodam dentro de uma VM do Proxmox
      Outra coisa que estou testando é o syd-oci, que parece ter recebido um pouco menos de atenção do que as recomendações padrão de MicroVM ou gVisor
    • Essa formulação também bate com a minha experiência, e escrever esse texto foi quase um exercício de aceitar esse fato
      Obrigado pela referência ao libkrun; parece uma possibilidade promissora
    • A adoção ativa no lado das ferramentas de codificação com LLM provavelmente tornará MicroVM mais madura, com mais validação em produção e reforço
      Também parece haver uma boa chance de isso levar a auditorias de segurança