Estudo de caso: vulnerabilidade de RCE em Python no Celery
Calum Hutton
15 de fevereiro de 2022
0 minutos de leituraVisão geral
Pesquisei vulnerabilidades existentes em Python e identifiquei um padrão de software comum entre elas. Com o poder do nosso mecanismo interno de análise estática, que também impulsiona o Snyk Code, nosso produto de teste estático de segurança de aplicações (SAST), consegui criar regras personalizadas e pesquisar um grande conjunto de dados de código aberto para identificar outros projetos que usavam o mesmo padrão. Isso levou à descoberta de uma vulnerabilidade de injeção de comandos armazenada no Celery. Ferramentas SAST, como o Snyk Code, permitem que desenvolvedores identifiquem bugs no software apenas analisando o código-fonte estático e detectando padrões de código ineficiente ou perigoso.
Motivação
Minha motivação para realizar este projeto de pesquisa veio da experiência pessoal e da investigação de vulnerabilidades em projetos Python de código aberto (CVE-2017-11610 e CVE-2021-32807). Minha hipótese era que a travessia de objetos (a capacidade de obter uma referência a um atributo arbitrário de um objeto a partir de outro objeto) é um recurso comum em Python. Queria investigar e comprovar essa hipótese para identificar a prevalência desse padrão no ecossistema Python como um todo e, em particular, encontrar casos de travessia de objetos arbitrária (ou quase arbitrária) que pudessem levar a vulnerabilidades de segurança.
Contexto
Em Python, quase todos os elementos da linguagem são objetos, com atributos e métodos próprios, explícitos e herdados (incluindo instâncias de classes e módulos). Por isso, aplicações Python podem oferecer um método para percorrer o namespace de um objeto e obter uma referência a seus atributos ou subatributos. Para isso, pode ser feita uma busca recursiva por atributos.
O exemplo simplificado de código a seguir demonstra como percorrer namespaces de objetos (em Python 3). Ao importar um módulo inofensivo (random), é possível obter uma referência a uma função perigosa (os.system) usando getattr para obter uma referência ao módulo os importado (apelidado de _os):
Se uma aplicação Python expuser ao usuário uma funcionalidade de travessia de objetos que permita manipular o caminho do namespace ou atributo solicitado, isso poderá levar à travessia de objetos arbitrária (ou quase arbitrária) e a vários problemas potenciais:
O impacto mais provável da exposição da funcionalidade de travessia de objetos é a quebra dos controles de acesso ou a divulgação de informações, caso o usuário consiga obter uma referência a um método ou atributo privado, como
obj._private_method() or obj._secret.Também é possível executar código remotamente (RCE) se um método arbitrário, como
os.system(), for obtido e chamado com argumentos fornecidos pelo usuário. Isso é menos provável, pois o usuário precisa obter uma referência ao método e conseguir passar argumentos para ele.
Em resumo, um invasor que consiga controlar ou manipular a travessia de objetos em uma aplicação Python poderá acessar atributos e subatributos de determinado módulo ou instância de classe e usar suas funcionalidades de maneira irrestrita ou inesperada.
O padrão vulnerável
O padrão identificado como relevante para os CVEs acima é uma busca recursiva por atributos, geralmente baseada em um caminho delimitado por pontos em Python. O caminho é dividido e percorrido em um loop, usando o elemento atual do caminho para obter uma referência no contexto com getattr(). A referência da chamada anterior de getattr() passa a ser o contexto da próxima chamada, e o processo se repete até não restarem elementos no caminho. O script a seguir demonstra esse processo:
O caminho separado por pontos é dividido em duas strings (_os e system). Na primeira iteração do loop, a variável cls é uma referência ao módulo random. Assim, getattr() busca o atributo _os do módulo random, que é o módulo os. O contexto de getattr() passa então a ser o módulo os e, na iteração seguinte, o atributo system do módulo os é recuperado, ou seja, os.system().
Estudo de caso (Celery – CVE-2021-23727)
Com o mecanismo do Snyk Code, desenvolvi regras para identificar o mesmo padrão em outros códigos Python de código aberto. Uma das correspondências encontradas pela regra estava no Celery, uma fila de tarefas assíncrona de código aberto baseada em troca distribuída de mensagens. A regra encontrou a função exception_to_python na classe celery.backends.base.Backend:

O padrão recursivo de getattr aparece na função, na linha 354. Ao analisar melhor a função e como ela era usada no Celery, ficou claro que todo o objeto dict exc vinha de dados JSON armazenados em um backend do Celery. Portanto, um usuário com acesso ao servidor do backend poderia potencialmente controlá-lo ou manipulá-lo; por isso, todos os campos do dict deveriam ser considerados potencialmente contaminados.
Uma análise mais aprofundada do código mostrou que uma referência a um módulo arbitrário é acessada com base em uma propriedade do objeto dict exc (linha 352). Em seguida, o padrão recursivo de atributos é usado para buscar um atributo arbitrário do módulo. A referência ao atributo obtida é então chamada com um único argumento de string, também extraído do objeto dict exc (linha 364).
Como todo o objeto dict exc poderia estar contaminado e não havia validação para impedir o acesso a módulos e atributos arbitrários, esse código foi identificado como provavelmente vulnerável à injeção de comandos armazenada.
Comprovei essa hipótese usando o console Python: importei as classes relevantes e criei um dict malicioso para simular um invasor criando um bloco JSON malicioso no banco de dados de um backend do Celery:
Primeiro, crio um dict Python para passar à função vulnerável. Esse dict contém propriedades que controlam qual módulo será acessado (exc_module), a qual atributo do módulo será obtida uma referência (exc_type) e, por fim, qual argumento será passado ao método obtido (exc_message).
As linhas seguintes importam o código vulnerável do Celery para o console Python e inicializam a classe Backend com um objeto Celery.
Por fim, a vulnerabilidade é acionada quando o dict é passado ao método vulnerável exception_to_python.
A última linha do trecho de código acima mostra a saída do comando id, comprovando que o dict criado foi desserializado e acionou com sucesso a injeção de comandos arbitrários na classe Backend.
Em um sistema que usa Celery, um invasor que explore essa vulnerabilidade poderá injetar comandos no produtor, o que pode permitir a tomada total do sistema. Se o backend do Celery for remoto, a vulnerabilidade também poderá permitir que invasores com acesso ao backend se movimentem lateralmente pela rede da organização e obtenham acesso a outros componentes da infraestrutura de rede.
Correção
O problema foi comunicado de forma responsável ao Celery e corrigido na versão 5.2.2 do software. A correção adicionou a validação do módulo de destino e a verificação dos tipos dos atributos resolvidos antes de chamar funções arbitrárias com entradas potencialmente contaminadas. Em geral, ao desserializar objetos, é preciso ter cuidado com quaisquer dados potencialmente contaminados, mesmo que venham de uma fonte de dados remota ou de um banco de dados. Se possível, os tipos dos objetos devem ser verificados e validados antes da desserialização ou, pelo menos, antes de chamar um método ou acessar uma propriedade do objeto desserializado.
Encontrar essa vulnerabilidade de injeção de comandos no Celery e realizar a análise necessária dos dados contaminados foi mais simples com o Snyk Code. Nossa ferramenta SAST revolucionária é uma alternativa às ferramentas de segurança tradicionais: fácil de usar para desenvolvedores, rápida e precisa. Os plugins para IDE integram testes em tempo real aos fluxos de trabalho existentes, ajudando desenvolvedores a encontrar e corrigir vulnerabilidades em apenas 5 minutos. Comece hoje mesmo um teste grátis e veja como o Snyk Code simplifica o desenvolvimento seguro.
Comece a jogar Capture the Flag
Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.
Referências
Celery (CVE-2021-23727): https://security.snyk.io/vuln/SNYK-PYTHON-CELERY-2314953
