Por: Rafael Pereira

  1. Introdução

A ascensão da infraestrutura como serviço (IaaS), abriu um novo horizonte de possibilidades para a computação em geral, criando paradigmas novos no que se refere ao desenvolvimento de aplicações. Dentre estes, a arquitetura serverless se destaca, possibilitando a construção de aplicações sem a necessidade de gerenciar uma infraestrutura física.

Porém, por se tratar de uma tecnologia emergente, a inexperiência dos desenvolvedores com o paradigma, associada às comuns inconsistências no hardening de ambientes cloud based, criam um solo fértil para bugs e problemas de segurança.  Dentre eles, uma das mais relevantes, segundo o projeto OWASP serverless top 10 (2017), é chamada de event data injection e será fruto do presente estudo.

  1. Entendendo a arquitetura Serverless

De forma simples, arquiteturas serverless nada mais são do que aplicações construídas sem a dependência de um servidor explícito ao desenvolvedor. É claro que para que a lógica da aplicação seja interpretada, deve existir um servidor físico. O grande diferencial desta estrutura é que o servidor é mantido e gerenciado pelo provedor de nuvem e entregue como serviço para o desenvolvedor da aplicação.

Desta forma, a construção da aplicação é baseada em serviços, abstraindo os componentes de uma infraestrutura web convencional – como servidores, bancos de dados, gateways etc. – em serviços gerenciados e mantidos pelo provedor de nuvem.

2.1. Um breve exemplo

A imagem a seguir ilustra a arquitetura básica de uma aplicação web que informa ao usuário a previsão do tempo:

Fonte: https://aws.amazon.com/lambda

Note que, neste cenário, cada bloco representa um serviço que o provedor disponibiliza. Estes blocos são independentes entre si, mas podem se comunicar para construir uma infraestrutura completa para a aplicação. 

Para realizar essa comunicação, os serviços disparam eventos entre si, os quais contêm uma representação dos dados gerados e que podem ser interpretados por outro componente.

  1. Desvendando as Functions

Como você deve ter notado, o cenário anterior não apresenta nenhum tipo de servidor web responsável por interpretar a entrada do usuário e se comunicar com o banco de dados. Isso ocorre porque a aplicação utiliza uma abstração deste componente, popularmente chamada de function. As functions são um ponto importante para nosso estudo, pois é nelas que ocorre a parte “lógica” da aplicação. 

Apesar de semelhantes a um servidor web, functions se diferenciam destes componentes por só serem executadas quando um evento – com formato previamente definido – ocorra. 

Durante a criação de uma function, dois componentes principais devem ser definidos pelo desenvolvedor:

  1. Qual tipo de evento aciona a function? Esta informação é utilizada para acionar a execução do código sempre que um novo dado for recebido.
  2. Qual código será executado?  Esta é a parte funcional do componente, neste campo o desenvolvedor insere o código que será executado quando a função for acionada.

Após criada, de forma interna – e não transparente ao desenvolvedor — estabelece-se um runtime para a function que aguarda até a chegada de evento trigger. Quando este evento ocorre, um container é criado e o código da função é executado nele de maneira isolada.

Assim sendo, uma function nada mais é que um runtime que cria containers efêmeros que executam um bloco de código toda vez que um determinado tipo de dado é recebido. 

Como toda esta estrutura é gerenciada pelo provedor de nuvem, algumas medidas de segurança são tomadas para proteger sua infraestrutura interna. Dentre elas, as mais interessantes para este estudo são listadas a seguir:

  • O sistema operacional do container normalmente é baseado em Linux e geralmente os utilitários capazes de fazer requisições externas são removidos do ambiente.
  • Todo o conteúdo do container é ReadOnly exceto o diretório /tmp.
  • O container não é exposto para internet e não existe persistência de dados.
  • O código da aplicação fica armazenado no diretório /var/task
  • As variáveis de ambiente podem conter chaves de acesso do provedor e de serviços que a function irá se comunicar.
  1. Uma nova perspectiva para os ataques de injeção

Agora que entendemos a estrutura deste tipo de arquitetura, vamos analisar a sua superfície de ataque que ela propicia para um possível atacante e onde os ataques de injeção se enquadram nesse ambiente.

Quando analisamos aplicações web convencionais, o caminho de entrada dos dados é linear e bem definido, isto quer dizer que sabemos exatamente, a origem dos dados, qual caminho eles percorrem e qual seu destino. 

Em contrapartida, em aplicações serverless, os dados de entrada estão encapsulados em eventos que serão consumidos pelas functions. Estes eventos podem se originar de inúmeras fontes e com vários formatos diferentes, dificultando o uso de mecanismos de proteção tradicional como Web Application Firewalls (WAFs) e estruturas semelhantes.

Como consequência, o atacante não tem mais um ponto de entrada e, sim, uma infinidade deles. Na verdade, qualquer fonte geradora de eventos que interaja diretamente com a function é potencialmente vulnerável. 

Desta forma, as entradas do usuário estão muito mais próximas do código da aplicação e expandem, consideravelmente, a superfície de ataque quando comparada a uma aplicação convencional. Esta diferença pode ser vista através do comparativo ilustrado pela imagem a seguir:

Fonte: https://owasp.org/www-pdf-archive//OWASP_DC_SLS_Top10.pdf

Outro fator importante, e que pode gerar grande impacto a segurança, é que nem sempre os eventos fazem um caminho linear entre usuário e a function. Na grande maioria das vezes, existirão serviços intermediários entre a entrada do usuário e o destino dos dados.

Mesmo que a entrada do usuário, em primeira instância, não pareça perigosa, ao processar os dados informados, um serviço aparentemente seguro pode gerar um dado contaminado e acarretar possíveis novas vulnerabilidades.

Desta forma, qualquer dado que tenha contato com a entrada do usuário, é potencialmente um dado contaminado. Isso faz com que, caso não sejam bem construídas e implementadas, as aplicações serverless se tornem um ambiente fértil para atacantes.

Com o aumento do perímetro de ataque e a confiança excessiva do desenvolvedor nas entradas do usuário, ao manipular a geração dos eventos, um atacante pode ser capaz de provocar reações adversas no sistema e, por consequência, manipular seu comportamento. 

Isso permite a um agente malicioso performar ataques bem conhecidos em aplicações web convencionais, porém a partir da perspectiva de uma arquitetura serverless, geralmente, consistindo na exploração de um ou mais das seguintes vulnerabilidades:

  • Injeção de comandos SQL e NoSQL; 
  • Inclusão de arquivos locais e remotos (LFI e RFI);
  • Execução código no sistema operacional;
  • Performar solicitações em nome do servidor (SSRF);
  • Execução de código HTML/Javascript (XSS);
  • Entre outros.
  1. Cenários de ataque

Para entender, de forma empírica, o funcionamento desta classe de vulnerabilidades, foram construídas duas aplicações serverless propositalmente vulneráveis. 

A primeira trata-se de um serviço de armazenamento de arquivos PDF com descompressão de tamanho. A aplicação converte os arquivos PDF de entrada em arquivos de texto e os armazena em um bucket na nuvem. Neste caso a injeção ocorre através do upload dos arquivos de forma transparente para o atacante.

Já o segundo cenário trata-se de uma aplicação web convencional onde arquivos também são armazenados em um bucket na nuvem. Porém, neste caso a injeção acontece de forma cega ao atacante através de um backup dos arquivos programado através de outro serviço do provedor de nuvem. Veremos mais detalhes ao final deste artigo.

5.1. Caso 1 – Injeção através do upload de arquivos

Para este caso, a entrada do atacante trata-se de um arquivo PDF malicioso que será enviado para bucket da aplicação e, por consequência, processado por uma function, a fim de ser convertido em arquivo de texto.

Fonte: Próprio autor

Neste cenário, o desenvolvedor não se preocupou em sanitizar corretamente os nomes dos arquivos enviados pelos usuários da aplicação e, por consequência, deixou uma brecha para que dados maliciosos possam ser enviados neste campo.

Note que, no código da function, a entrada do usuário – nome do arquivo – é passada diretamente para a linha de comando que chama um binário responsável por converter o documento em texto. 

Desta forma, um atacante – ao deduzir o uso de uma arquitetura serverless — seria capaz de criar um payload e injetar comandos no sistema operacional.

Para validar essa possibilidade, vamos analisar cada parte do possível payload a seguir:

  1. Primeiramente, escapamos do binário pdftotext através do caractere “;” , possibilitando a execução de uma cadeia de comandos;
  2. Em sequência, resgatamos as variáveis de ambiente do container hospedeiro da function através do comando env. Note que a saída deste comando é transmitida para o comando seguinte através do condutor “|”.
  3. Por fim, realizamos uma chamada externa através do runtime da function, transmitindo os dados para o servidor do atacante através do parâmetro X.

Desta forma, se enviado um arquivo onde o nome é exatamente igual ao payload acima, ao executar a function, a aplicação deverá, necessariamente, realizar uma chamada externa e enviar as variáveis de ambiente para o servidor do atacante. 

Note, na imagem a seguir, o momento em que as informações são recebidas no servidor externo após o envio do arquivo:

Fonte: Próprio autor

Ao decodificar as informações obtidas, é possível notar a presença das chaves de acesso utilizadas pela function. A partir destas informações, caso o ambiente de nuvem não tenha sido corretamente configurado, seria possível ao atacante buscar por novas vulnerabilidades para escalar privilégios e tomar controle completo deste ambiente.

Fonte: Próprio autor

5.1.1. Injeção através de arquivos encadeados

O cenário acima pode ser útil para executar uma cadeia pequena de comandos, contudo, quando uma sequência considerável de comandos se faz necessária, podem surgir problemas relacionados ao limite de caracteres no nome do arquivo. 

Para contornar este problema, podemos encadear dois arquivos para realizar ataques mais complexos. Neste caso, um dos arquivos é responsável por armazenar o código python malicioso, enquanto o outro arquivo é responsável por agir como um gatilho, buscando o código do primeiro e transmitindo-o para o runtime da function.

Com o objetivo de extrair o código fonte, tomemos como exemplo a aplicação anterior. Inicialmente, podemos criar um arquivo chamado “payload.pdf” com o conteúdo a seguir e submetê-lo à aplicação:

Fonte: Próprio autor

Note que o conteúdo do arquivo acima trata de um código responsável por listar o conteúdo de todos os arquivos python no diretório /var/task e enviá-los para o servidor do atacante. Contudo, enviar apenas este arquivo não causará comportamento algum na aplicação, precisaremos enviar um segundo arquivo com um payload em seu nome, o qual será responsável por obter o conteúdo do arquivo “payload.pdf” e enviá-lo para o runtime da function.

Desta forma, o conteúdo definido no nome deverá seguir o padrão descrito abaixo:

Fonte: Próprio autor

Ao enviar este conteúdo, é esperado que a aplicação execute o teor do arquivo “payload.pdf” e envie para o servidor do atacante a matéria de todos os arquivos .py armazenados no diretório padrão da function. Como evidenciado a seguir, foi possível obter este material cifrado em base64 através do parâmetro “q”:

Fonte: Próprio autor

Após exfiltrar o conteúdo obtido e decodificá-lo, é possível ler todo o código fonte da aplicação com sucesso:

Fonte: Próprio autor

5.2 – Caso 2 – Blind event injection

Como o ambiente serverless é muito vasto e inúmeras fontes podem ser associados a uma única function, a injeção de eventos pode ocorrer desde o envio de SMS, até requisições HTTP tradicionais. Desde que as entradas não sejam sanitizadas corretamente, o atacante tem um mundo de possibilidades. O problema é que nem sempre é tão simples identificar estes cenários, cada aplicação tem suas especificidades e cenários únicos. 

Por exemplo, imagine uma aplicação que armazena arquivos em um bucket S3, semelhante a aplicativos como dropbox e google drive. O processo de armazenamento é feito de forma segura, com controle de acesso, logs etc. Em conjunto com esse processo, existe uma rotina interna que é acionada semanalmente para fazer backup dos arquivos armazenados e salvá-los em um bucket glacial.

Fonte: Próprio autor

Note que, nesse cenário, o processo de backup (área pontilhada) não requer uma interação direta do usuário e acontece de forma completamente autônoma através de um gerador de eventos programado (semelhante a uma crontab).

Durante este processo, caso a function realize qualquer tipo de processamento dos dados do usuário, seja para ler o conteúdo dos arquivos ou utilizar os nomes como índices para alguma rotina, a função estará potencialmente vulnerável, mesmo que essa estrutura não seja acessível para o usuário final.

A principal diferença será que a execução do código não será imediata, pois ocorrerá apenas quando a rotina for executada, fazendo com que o payload fique “adormecido” por um prazo arbitrário.

  1. Pós exploração e conclusão

Como demonstrado anteriormente, é possível que um atacante obtenha dados sensíveis quanto a aplicação, como o código fonte ou informações a ele vinculadas. Porém, por se tratar de uma classe de vulnerabilidades inerente a arquiteturas cloud based, uma gama de possibilidades é aberta. Por exemplo:

  • Em casos de configuração incorreta de permissionamento, é possível obter acesso ao console da nuvem através das credenciais obtidas;
  • Como é necessário que exista um Runtime persistente para criar os containers que executam o código da function, é possível criar uma conexão persistente (backdoor) neste contexto – visto que o código injetado é executado nele – e interceptar todos os dados de eventos que são recebidos;
  • Ao obter acesso a ambientes mal configurados, pode-se criar serviços e, por consequência, hospedar uma aplicação “fantasma” para fins diversos;
  • A fim de obter persistência no ambiente, também pode ser possível criar usuários sem que o administrador da nuvem perceba e assumir controle do ambiente;
  • A partir de políticas permissivas, também pode ser possível escalar privilégios, explorar serviços vulneráveis, exfiltrar dados dos usuários com senhas fracas e movimentar-se lateralmente dentro da infraestrutura interna.

Contudo, é importante ressaltar que estas são apenas algumas possibilidades, cada caso dependerá de como o ambiente foi construído e quais as permissões são atribuídas a ele.

  1. Mitigação

Como demonstrado neste artigo, a origem de ataques de injeção vem, principalmente, da confiança excessiva nas entradas do usuário ou de um determinado serviço, portanto, uma forma de proteção é adotar uma política de confiança zero (zero trust architecture). 

Isso implica em nunca confiar nas entradas recebidas e sempre verificá-las antes de fazer qualquer processamento delas, bem como sanitizar os dados sem qualquer tipo de suposição de sua origem ou veracidade.

Recomenda-se, também, adotar uma política de desenvolvimento seguro de forma a mitigar o máximo de possíveis vulnerabilidades e bugs durante o processo de desenvolvimento, bem como adotar o uso de ferramentas de análise estática de código, sejam elas integradas ao ambiente de cloud ou softwares a parte do sistema. Essas ferramentas são de suma importância para verificar pontos que podem passar despercebidos, porém, é importante ressaltar que elas não são uma bala de prata e não eximem a necessidade do fator humano na análise do código.

Por fim, apesar da ineficiência dos WAFs (Web Application Firewalls) em determinados tipos de comunicações, em casos em que estes mecanismos sejam eficientes — como em requisições HTTP —, é altamente recomendável considerar seu uso, podendo ser colocado entre o client e o gateway de forma a proteger pontos específicos na aplicação.

Referências

AWS Lambda Function. Disponível em:  <https://aws.amazon.com/lambda/>

AWS Lambda command injection.Disponível em: <https://www.safe.security/resources/blog/aws-lambda-command-injection>

OWASP Serverless Top 10.Disponível em:  <https://owasp.org/www-project-serverless-top-10/>

Hacking AWS Lambda for security fun and profit. Disponível em: <https://blog.appsecco.com/hacking-aws-lambda-for-security-fun-and-profit-c140426b6167>

Serverless as a service top 10. Disponível em:  <https://github.com/puresec/sas-top-10>