Code Fighters

Dicas e truques para Pentest em API

Com o objetivo de possibilitar a comunicação entre diferentes plataformas, a utilização de APIs (Application Programming Interface) torna-se cada vez mais comum em todo tipo de ambiente. Isso ocorre sobretudo em contextos nos quais a expansão da infraestrutura é um fator relevante. Com isso, há a necessidade de garantir a segurança dos dados envolvidos nessas interações, e consequentemente, a demanda por serviços de segurança focados nestes componentes acompanham essa curva de crescimento.

Pensando nisso, o objetivo deste artigo é abordar algumas questões específicas que são observadas com frequência em análises que possuem esse foco, buscando passar uma visão geral de alguns erros e/ou técnicas que geram resultados constantes em serviços de Pentest em API.

Em um primeiro momento, demonstrando algumas ações que podem ser adicionadas a uma metodologia já existente para otimização dos resultados e reproduzindo, em uma API de teste, casos reais já constatados anteriormente.

Fuzzing

O fuzzing faz parte do processo de reconhecimento ativo na grande maioria dos pentests web – e essa realidade não é diferente quando o alvo é especificamente uma API. O objetivo deste tópico é introduzir algumas técnicas que podem ser adicionadas a uma metodologia base caso ainda não estejam presentes, podendo fazer grande diferença na descoberta de endpoints válidos durante o teste.

Contexto

Primeiramente, a wordlist utilizada deve ser cuidadosamente escolhida ou elaborada, levando em consideração a maior quantidade possível de fatores contextuais. Por exemplo: o idioma no qual os endpoints já conhecidos se encontram, formatos, padrões. Para ilustrar um fuzzing contextual, ao observar uma requisição como esta:

É possível observar que sua denominação possui o prefixo “server_”. Com isso, podemos partir do pressuposto que outros endpoints da API utilizem essa mesma nomenclatura (prefixo “server_” seguido de uma palavra em inglês) e executar um fuzzing direcionado utilizando uma wordlist com palavras relevantes em inglês, com o prefixo “server_”, em busca de outros endpoints válidos invisíveis ao usuário, descobrindo dessa forma o endpoint “/api/v1/server_access” nesse caso, que retorna um token de acesso:

Profundidade

A profundidade do fuzzing executado pode ser o diferencial entre encontrar ou não funcionalidades ocultas no ambiente alvo. Nesse contexto, a profundidade se refere aos diretórios presentes no servidor. Alguns pontos importantes precisam ser observados para que os resultados sejam sempre otimizados em tudo que estiver ao alcance do analista. No quesito profundidade, o primeiro passo é executar o fuzzing em todos os diretórios observados, portanto, ao observar uma requisição como esta, por exemplo:

Percebe-se ao menos dois diretórios adicionais além da raiz, “/api/” e “/api/v1/”, e executar o fuzzing também destes diretórios pode trazer bons resultados. Tendo isso em mente, é recomendado que o processo seja feito em todos os diretórios observados ao longo do teste, não se limitando apenas à raiz do servidor ou diretórios padrão. Dessa forma, podemos garantir que o “content discovery” seja aplicado de forma sistêmica nos diretórios já conhecidos do ambiente.

Porém, diretórios válidos que não aparecem na documentação do ativo, ou durante a utilização de um componente que consome seus recursos (aplicação web, aplicativo etc.), não serão necessariamente encontrados dessa forma. Por isso, a profundidade de alcance do fuzzing também deve ser levada em consideração. Isto é, executar um fuzzing não apenas de endpoints dos diretórios conhecidos mas também de diretórios em si. Para isso, deve-se utilizar no mínimo dupla profundidade ao executar um fuzzing com este objetivo (ao menos 2 segmentos da URL.

A diferença entre o fuzzing recursivo e o de múltipla profundidade é que o recursivo depende da enumeração de diretórios válidos para descobrir novos endpoints, enquanto o de múltipla profundidade sacrifica recursos em prol de mais assertividade e eficiência. Para ilustrar esse caso, a API de teste possui o endpoint “ /api/v1/configuration/certificate”,  que retorna um certificado SSL formatado em JSON. Porém, ao enviar uma requisição ao diretório “/api/v1/configuration”, o servidor responde com “404 File not found”:

Ou seja, o processo padrão e/ou recursivo de fuzzing não irá descobrir endpoints válidos deste diretório, visto que seu comportamento irá gerar um falso negativo. Já o fuzzing de múltipla profundidade é capaz de fazer a descoberta destes endpoints ocultos por requisitá-los diretamente, eliminando a dependência da enumeração de diretórios válidos.

Métodos HTTP

Os métodos ou verbos HTTP também podem ser a diferença entre descobrir ou não endpoints válidos presentes no servidor. O motivo disso é que, em muitos casos, algumas funcionalidades respondem apenas a requisições que utilizam métodos específicos de acordo com sua implementação, retornando por exemplo “404” ao receber uma requisição fora do padrão esperado. Para ilustrar um caso como esse, a API de teste possui o endpoint “/api/v1/active”, que responde apenas a requisições “POST” e retorna “404” ao receber qualquer outro método:

Portanto é recomendado que também sejam utilizados métodos distintos no processo de fuzzing, como “PUT”, “POST”, “PATCH”, “DELETE”, etc., expandindo ainda mais o alcance dos resultados.

Autorização

Falhas de controle de acesso estão entre as mais relevantes em testes de API e vulnerabilidades denominadas “Insecure direct object references” (IDOR) são bastante comuns, podendo levar a grandes impactos em certos contextos. Esse tipo de vulnerabilidade surge quando um serviço utiliza dados fornecidos pelo usuário para acessar objetos diretamente. Existem diversas formas de identificar e explorar vulnerabilidades desse tipo e muitas variáveis devem ser consideradas para que a análise produza o melhor resultado em cada caso. Neste texto, o foco é demonstrar algumas formas específicas de interagir diretamente com objetos, comuns em testes de API.Na API de teste, o endpoint “/api/v1/users/me” retorna informações do usuário logado:

Nesse exemplo, a API retorna um objeto que representa o usuário autenticado que enviou a requisição. uma das primeiras validações de controle de acesso que podem ser feitas é enviar uma requisição diretamente ao endpoint “/api/v1/users”, com o intuito de acessar todos os objetos referentes a usuários:

Nesse caso, a API retorna todos os seus objetos e cada um deles possui um ID único, diferente do ID numérico visto anteriormente. O que pode ser feito em seguida é testar o acesso direto a um objeto, referenciando-o pelo seu ID no caminho da requisição, no lugar da palavra “me”:

Em seguida, podemos tentar alterar suas informações enviando uma requisição “PUT” contendo o objeto em seu corpo, com as alterações desejadas:

Em alguns casos, também é possível inserir novos dados arbitrários ao objeto, alterando seu formato padrão, o que pode causar uma diversidade de impactos imprevisíveis:

Muitas vezes, o servidor implementa um certo nível de controle, impedindo o acesso direto a objetos referenciados no caminho da requisição, porém o acesso direto referenciado de outras formas não é corretamente avaliado. Por exemplo, ao referenciar o objeto no “path” da requisição, a API retorna “401 Unauthorized”:

Porém, ao enviar uma requisição “PUT” ao endpoint “/api/v1/users”, contendo o objeto no corpo da requisição, incluindo o ID que o referencia, o controle de acesso implementado não impede sua modificação:

Consumo de Recursos

Um tipo de vulnerabilidade bastante recorrente em testes de API é o consumo exagerado de recursos. Essas falhas normalmente surgem quando o usuário consegue fazer com que a quantidade de dados processados pela API seja muito maior do que o esperado em situações comuns, introduzindo a possibilidade de exaustão dos recursos do servidor em certas situações. Já houve casos nos quais uma única requisição “maliciosa” era o suficiente para indisponibilizar os serviços aos usuários legítimos, o que pode representar um grande impacto em diversos contextos.

Vulnerabilidades como essa podem se apresentar em diversos contextos e formas, e muitas vezes é preciso atenção e pensamento crítico para identificá-las. Neste tópico, serão abordados alguns exemplos dessa falha, explorada de forma constante. Frequentemente, implementações de paginação na API podem introduzir essa possibilidade de exploração.

O intuito da paginação é justamente separar o conteúdo da resposta em diversas páginas, fazendo com que cada requisição obtenha resultados referentes a uma página com um número pré-definido de “registros”. Por exemplo, supondo que existam 100 registros no endpoint, pode ser definido um limite de 20 registros por página. Dessa forma, o conteúdo será dividido em 5 páginas, cada uma contendo 20 itens. Com isso, a quantidade de dados retornados por requisição é bastante reduzida e o restante dos dados será gradualmente requisitado conforme o usuário acessar as próximas páginas.

Nesse cenário, o problema surge quando o usuário consegue controlar esses valores arbitrariamente, mesmo que essa possibilidade não esteja diretamente disponível no front end. Normalmente, isso pode ser feito através do envio de um parâmetro na requisição contendo o número de itens retornados por página, o que muitas vezes está presente por padrão em bibliotecas ou frameworks utilizado para implementar essa funcionalidade. O parâmetro pode possuir uma diversidade de nomes como “limit”, “max”, “per_page”, “page_size” entre outros, muitas vezes sendo encontrado por meio de fuzzing, quando não se tem conhecimento da tecnologia utilizada no backend. Para ilustrar esse cenário, foi criado o endpoint “/api/v1/get_itens” na API de teste, contendo um total de 10.000 itens e utilizando um mecanismo de paginação que aplica um limite de 20 itens retornados por requisição:

Porém, quando o parâmetro “limit” é enviado na requisição, seu valor é interpretado e usado pelo backend na definição do número de itens retornados por página:

Por conta disso, ao descobrir a existência deste parâmetro o usuário passa a ser capaz de aumentar arbitrariamente a quantidade de dados processados e retornados pela API, podendo “fazer o dump” de todos os registros em uma única requisição caso não sejam implementados controles. Dependendo da quantidade de dados acessados por este endpoint, o resultado de uma exploração maliciosa desse comportamento pode causar a indisponibilidade total do serviço, mesmo em ataques com uso extremamente limitado de recursos.

Muitas vezes, essa possibilidade é desconhecida pelos desenvolvedores da API, normalmente por ser uma funcionalidade padrão da solução/ferramenta terceira utilizada, cuja documentação não foi suficientemente avaliada durante o desenvolvimento.

A vulnerabilidade também pode surgir de falhas lógicas na implementação do código server side. Para ilustrar alguns exemplos, foram reproduzidos dois cenários reais encontrados em alguns pentests que executei. No primeiro cenário, a aplicação enviava um filtro de busca na requisição, através do parâmetro “filter”. O filtro possuía o seguinte formato: *busca*=*operador*:*valor*. O campo representado por “*busca*” se refere ao que está sendo buscado, o campo “*operador*” se refere a um operador de comparação e o campo “*valor*” representa o conteúdo da busca, por exemplo: ?id=eq:1

No exemplo acima, o backend irá fazer uma busca pelo item com ID igual a 1, retornando-o na resposta. Esse tipo de abordagem é utilizada para flexibilizar a funcionalidade de busca da API, possibilitando filtragens mais específicas do conteúdo. Porém, quando essa flexibilidade está sob livre influência do usuário final, sem implementação de controles, diversas possibilidades podem ser introduzidas, entre elas o processamento de uma quantidade elevada de dados. Neste mesmo exemplo, o usuário poderia alterar o filtro enviado pela aplicação com o objetivo de maximizar o número de resultados retornados, uma das formas de fazê-lo seria “?id=ne:0”:

Com o filtro acima, a API irá fazer uma busca pelos itens cujo ID seja diferente de 0 (ne = not equal), resultando no processamento e envio de todos os itens existentes no banco de dados. Os detalhes de explorações como essa costumam ser contextuais, necessitando de uma análise caso a caso, porém, acabam seguindo a mesma lógica.

No segundo cenário, a API inclui uma funcionalidade de correlação de palavras-chave com objetos, isto é, a partir do recebimento de uma palavra-chave em um parâmetro da requisição de busca, a API retorna a totalidade do objeto referenciado por ela. Em casos como esse, quando não há implementação de controles, esse tipo de funcionalidade pode introduzir diversas vulnerabilidades. No exemplo reproduzido, a palavra-chave “template1” poderia ser incluída no parâmetro “s” na requisição de busca, fazendo com que a API retornasse o objeto relacionado:

Um dos problemas que podem surgir nesse caso é a ausência de um controle de repetições, possibilitando que o usuário envie várias ocorrências da mesma palavra-chave separadas por uma vírgula, por exemplo. Novamente, os detalhes da exploração costumam ser contextuais de acordo com a implementação, porém compartilhando a mesma ideia central. Nesse caso, o backend irá enviar um objeto para cada uma das palavras-chave que o referenciam, resultando no processamento e envio de uma grande quantidade de dados:

Por mais que casos como esse sejam, de certa forma, “limitados” pela implementação do protocolo HTTP dos webservers, devido ao tamanho da requisição, ainda é muito provável que sua exploração cause um grande impacto, principalmente em situações nas quais o atacante consegue identificar palavras-chave que referenciam objetos que possuem naturalmente uma grande quantidade de dados, ou valores que podem ser modificados/inseridos pelo atacante.

Pentest em API – O que podemos concluir?

Estes são apenas alguns exemplos de abordagens que produzem resultados constantes em testes de API. Assim como todas as particularidades da segurança da informação, existe um universo a ser explorado nos testes de API, cujos limites conhecidos se mantêm em constante expansão. O mais importante é desenvolver constantemente a capacidade e o hábito de visualizar por inteiro o caminho percorrido pelas informações, idealizar e desenvolver subversões de qualquer ativo envolvido neste fluxo.

Leia também: A fantástica palavra de GraphQL

Nova call to action
About author

Articles

Sempre tive interesse na área de segurança da informação e minha primeira atuação foi através do bug bounty, que me fascinou por ser uma maneira de praticar o que estava aprendendo e ser pago por isso. Pouco tempo depois, comecei a trabalhar como pentester e venho atuando como tal desde então. Gosto de fazer trabalhos de pesquisa, entender como certas coisas funcionam até o mais baixo nível.
Related posts
Code FightersSegurança de Aplicação

Segurança em GraphQL

Para iniciar, GraphQL é uma linguagem de consulta de API poderosa e flexível que tem ganhado…
Read more
Code FightersSegurança de Aplicação

Como integrar o Semgrep no CI/CD e enviar os resultados para a Conviso Platform

Atualmente, é uma prática muito comum integrar verificações de segurança durante a fase de…
Read more
Code FightersSegurança de Aplicação

Escrevendo código seguro - Um guia de boas práticas

Escrever código seguro envolve a adoção de um conjunto de boas práticas de desenvolvimento de…
Read more

Deixe um comentário

Descubra mais sobre Conviso AppSec

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continue reading