Segredos do CTF revelados: explicação do desafio TopLang do SnykCon 2021
Michael Aquilina
6 de janeiro de 2022
0 minutos de leituraSe você participou do SnykCon 2021, talvez se lembre do nosso primeiro CTF: Fetch the Flag. Nesse CTF, TopLang era um desafio web de dificuldade média que recebeu muitos comentários positivos. Para quem gostou dele, este passo a passo explica como nossa equipe abordou internamente o desafio e chegou à solução. E tem mais: você pode experimentar o desafio por conta própria acessando https://ctf-2021.snyk.io/ e entrando na seção Challenges.
Este desafio era um exemplo bastante típico do que chamamos de “ataque oracle”, usando injeção de SQL às cegas. Vou explicar como abordamos o problema, partindo do princípio de que você não conhece os desafios de CTF. Os passos estão descritos com bastante detalhe, mas pressuponho que você tenha algum conhecimento de Python e SQL.
A descrição do desafio era:
Qual é sua linguagem de programação favorita?
Às vezes, os desafios dão pistas sobre a possível solução. Mas parece que não há nenhuma informação útil neste caso. Então, vamos direto às nossas primeiras investigações!
Investigação inicial
No início de qualquer desafio web de CTF, é bom conhecer as páginas disponíveis e o que elas contêm antes de começar a escrever código. Essa investigação inicial deve dar uma ideia dos possíveis vetores de ataque e ajudar a entender onde a flag do CTF provavelmente está armazenada. Ela não deve levar mais de 10 minutos: basta clicar nos vários links disponíveis e anotar tudo que pareça potencialmente útil.
Ao abrir o link da web fornecido na descrição do desafio, encontramos a seguinte página:

Vemos uma tabela com as principais linguagens de programação de 2020 e 2021, além de alguns metadados, como as classificações.
Parece possível ordenar as colunas clicando no cabeçalho. Mais importante: ao ordenar por colunas específicas, o parâmetro de string de consulta sort é adicionado à URL. Por exemplo, ao ordenar pela coluna “Jun 2021”, o caminho da URL fica /?sort=jun2021
Qualquer tipo de entrada que possamos manipular é um possível vetor de ataque que podemos explorar, então isso parece uma pista interessante para analisarmos mais tarde.

Outro lugar óbvio para explorar é o link Admin panel no fim da página. Ao clicar nele, somos redirecionados para /admin.php. Não parece haver muito o que fazer aqui sem estar conectado.

Se formos até a página de login, veremos um formulário padrão:

Podemos tentar algumas combinações comuns de login, como “admin” / “admin”, mas nenhuma parece funcionar. Como este é um desafio web, podemos ter bastante certeza de que não precisamos forçar o login com uma ferramenta de quebra de senhas como o THC Hydra.
Parece provável que precisemos extrair de alguma forma os valores de login e password do servidor e usá-los para entrar neste painel de administração.
Certo, agora que entendemos bem as diferentes páginas, vamos investigar um pouco mais para ver se há algo que possa ser explorado.
Injeção de SQL
Os dados das principais linguagens de programação são exibidos em uma tabela na página da web, e há diferentes formas de ordenar os dados com base no parâmetro de string de consulta sort. Com essas informações, é bastante provável que os dados sejam recuperados e consultados em um banco de dados SQL no back-end.
Se o back-end realmente usa SQL, podemos verificar se a página está vulnerável a ataques de injeção de SQL.
Observação: Poderíamos usar uma ferramenta de injeção de SQL, como o sqlmap, para facilitar um pouco este desafio de CTF. Na verdade, em muitos casos, isso funcionaria perfeitamente. No entanto, durante a competição, não tive muita sorte para fazer a ferramenta funcionar neste desafio. Em vez de perder muito tempo tentando descobrir quais opções eu precisava ativar para usá-la corretamente, decidi escrever meu próprio código de exploração. Além disso, é muito mais divertido escrever o próprio código para resolver o desafio!
Já vimos que o parâmetro de string de consulta sort é um bom candidato a possível vetor de ataque. O valor passado para o parâmetro sort parece ser o nome de uma coluna de uma tabela do banco de dados. Estes são os valores possíveis, que você obtém ao clicar nos diferentes cabeçalhos de coluna:
sort=jun2020sort=jun2021sort=ratingssort=change
Se esse nome de coluna for passado a uma instrução SQL ORDER BY usando formatação insegura de strings, poderemos extrair dados por meio de um ataque de injeção de SQL às cegas.
Uma boa forma de testar a vulnerabilidade é verificar se conseguimos mudar a ordem dos resultados usando uma instrução CASE com uma condição booleana. Em particular, a ordenação muda quando a instrução CASE é avaliada como True ou False? Vamos testar os dois valores de sort a seguir e ver o que acontece:
?sort=”(CASE WHEN 1=1 THEN jun2021 ELSE jun2020)”?sort=”(CASE WHEN 1=0 THEN jun2021 ELSE jun2020)”
Nesta etapa do processo, é bom começar a escrever scripts para interagir com a página web alvo. Isso porque provavelmente precisaremos automatizar algumas tarefas muito em breve e porque navegadores como Firefox e Chrome tendem a alterar e transformar entradas complexas que contêm espaços em branco e outros caracteres especiais.
Tenho bastante familiaridade com Python, que costuma ser uma ótima linguagem para CTFs, pois é fácil colocar uma solução funcional em execução com a ajuda de alguns pacotes externos.
Se instalarmos as bibliotecas requests e BeautifulSoup do PyPI, podemos solicitar a página e analisar a saída HTML para verificar diferenças nos resultados. Em particular, podemos detectar alterações verificando se a ordem da coluna de linguagens na página mudou.
Vemos que “C” é sempre a principal linguagem em 2020 e 2021. No entanto, “Go” foi a linguagem menos favorita em 2021, e “Fortran” foi a menos favorita em 2020. Com isso em mente, podemos escrever código para verificar qual foi a linguagem menos favorita e detectar diferenças.
Vamos criar uma função que retorne a última linguagem exibida na página:
Então, se associarmos uma condição True a Jun2021 e uma condição False a Jun2020, devemos esperar “Go” como saída se passarmos uma instrução True para get_data e “Fortran” se, por outro lado, passarmos uma instrução False para get_data.
Uma maneira simples de passar uma instrução True para o SQL é “1=1”; “1=0” pode ser nossa instrução False.
Vamos testar isso com um pouco de código!
O resultado é:
Sucesso! Conseguimos mudar a ordenação usando uma condição booleana! Esse teste comprova que o parâmetro order é vulnerável a um ataque de injeção de SQL às cegas. Agora só precisamos explorar essa vulnerabilidade.
Ataque oracle
Nosso ataque de injeção de SQL às cegas é uma forma de “ataque oracle”. Esse tipo de ataque funciona fazendo perguntas de “sim ou não” ao servidor, que nos dão uma indicação de quão perto estamos do valor desejado.
No nosso caso, parece provável que o objetivo sejam os valores de login e password do formulário do painel de administração que vimos antes.
Com um ataque oracle, não podemos pedir diretamente ao servidor que nos informe o login e a senha. Mas podemos continuar enviando palpites sobre esses dados e refiná-los aos poucos até chegar às respostas corretas.
O truque habitual para explorar esse ataque oracle é fazer perguntas sobre substrings que ficam cada vez mais específicas à medida que recebemos respostas positivas.
Veja como seria uma sequência de perguntas ao oracle, sem usar código:
“O login começa com a?” Servidor: Não
“O login começa com b?” Servidor: Não
“O login começa com c?” Servidor: Sim
“O login começa com ca?” Servidor: Não
“O login começa com cb?” Servidor: Não
“O login começa com cc?” Servidor: Não
“O login começa com ce?” Servidor: Sim
Repetimos essa operação até extrair o login inteiro, caractere por caractere. Se a senha também estiver em texto simples, podemos fazer o mesmo com ela.
Vamos traduzir isso para código. Podemos começar escrevendo uma função oracle que retorna True se a resposta à nossa pergunta for “sim” e False se for “não”. Se ajustarmos nossa função get_data original, podemos verificar qual é o resultado para a última linguagem na tabela HTML e determinar a resposta às nossas perguntas de sim ou não:
Extraindo informações de login
Sabemos que precisamos recuperar o login e a password do banco de dados. O problema é que não sabemos nada sobre o esquema SQL para escrever nossas consultas. Poderíamos tentar adivinhar os nomes das tabelas e das colunas (sabemos de algumas equipes que fizeram isso), mas nossa equipe decidiu primeiro extrair os metadados do banco para descobrir quais tabelas e colunas consultar.
Ao tentar várias consultas específicas para cada tipo popular de back-end SQL (SQLite, MySQL, PostgreSQL e SQL Server) e observar quais delas não faziam a página da web falhar, conseguimos descobrir que o back-end usava um banco de dados SQLite.
Consultar as tabelas e colunas disponíveis em um banco de dados SQLite é uma operação simples. Vamos começar identificando quais tabelas estão disponíveis para extrair dados.
A consulta a seguir retornaria uma lista com os nomes de todas as tabelas em um banco de dados SQLite:
No entanto, não podemos simplesmente passar isso para nossa função oracle, porque não é uma pergunta de “sim” ou “não”. Para resolver isso, podemos concatenar todos os nomes das tabelas usando a função GROUP_CONCAT e enviar palpites sobre os valores do resultado dessa concatenação usando a função substr.
Combinando tudo, temos:
Observe que temos end e guess como parâmetros da consulta. O parâmetro guess corresponde ao nosso palpite atual, e end é o número de caracteres do palpite mais um.
Agora só precisamos atualizar nosso código para enviar palpites ao servidor, caractere por caractere. O código deve continuar enviando essas solicitações até que o oracle retorne uma resposta True. Quando recebermos uma resposta True, passamos para o próximo caractere e repetimos o processo para refinar o palpite.
Veja como fica esse código:
Ao executar esse código, você verá que o conteúdo de todos os nomes das tabelas será revelado caractere por caractere:

Se esperarmos o script terminar, o resultado final será “users:languages”. Isso significa que temos uma tabela users e uma tabela languages que podemos investigar melhor. Como nosso objetivo é entrar no painel de administração, podemos apostar que a tabela “users” é a que queremos analisar.
O próximo passo é descobrir quais colunas estão disponíveis na tabela users. O SQLite tem uma função PRAGMA_TABLE_INFO(table_name) que nos permite consultar os nomes das colunas de uma tabela específica. Neste caso, a consulta seria algo como:
Mais uma vez, como queremos enviar ao servidor apenas perguntas no estilo oracle, podemos transformar isso usando as funções GROUP_CONCAT e substr para obter este payload de consulta SQL:
Só precisamos alterar o script anterior para atualizar a variável command com esta nova consulta.
Ao executar o script novamente, eventualmente obteremos a resposta:
Parece que encontramos as colunas desejadas: login e password. Agora só falta enviar payloads SQL para extrair todas as informações delas:
Veja o payload SQL no formato oracle que extrairia os dados de login:
E aqui está o payload SQL no formato Oracle para extrair os dados de senha:
Você pode executar os dois casos substituindo o valor de command no mesmo script, como fizemos antes. Esses ataques vão revelar com sucesso dois conjuntos de combinações de login e senha.
Não vou estragar a diversão: vou deixar você descobrir as respostas por conta própria.
Login no painel de administração
Com as informações de login que acabamos de extrair, conseguimos acessar o painel de administração em /admin.php.
No entanto, parece que ainda precisamos superar um último desafio: somos recebidos por esta página da web:

Felizmente, resolver essa última etapa é bem fácil. É sempre uma boa ideia verificar quais cookies estão armazenados no navegador e ver se podemos manipulá-los a nosso favor. Na aba Storage do Firefox, vemos o seguinte:

Em particular, o valor analisado mostra que o cookie armazena um valor isAdmin, que no momento está definido como 0. Vamos ver o que acontece se mudarmos esse valor para 1.
Se copiarmos o valor completo do cookie, teremos:
Sem precisar entender o tipo de codificação, nossa equipe decidiu que o mais fácil seria simplesmente trocar o “0” perto do fim da string por um “1” e substituir o valor do cookie no navegador:
Depois que o CTF terminou, investigamos melhor e ficou claro que se tratava apenas de um objeto serializado em PHP. Ainda assim, acho importante mostrar que atalhos como esse são perfeitamente aceitáveis em um desafio de CTF, no qual economizar tempo pode fazer diferença na sua posição geral no placar.
Se você colar isso na aba Value (dê um clique duplo e cole o cookie recém-editado) e atualizar a página, verá a cobiçada flag do CTF SNYK{...}! Mais uma vez, removi o resultado real da tela para que você possa tentar por conta própria:

Concluindo... por enquanto!
Resumindo, veja uma recapitulação rápida de todas as etapas:
Investigamos as páginas disponíveis e descobrimos uma query string order e uma página de login de administrador
Testamos com sucesso e confirmamos que a query string order era vulnerável a ataques de injeção SQL às cegas
Extraímos os nomes das tabelas e das colunas, além dos dados de login da página de administração.
Alteramos os dados nos cookies do navegador para enganar o servidor e fazê-lo acreditar que estávamos conectados como administradores e, por fim, recuperamos a flag do Snyk CTF
Espero que você tenha gostado deste relato do CTF e talvez até aprendido algo novo! Os desafios de CTF são uma ótima maneira de aprender sobre exploits do mundo real e, consequentemente, de se preparar melhor para se defender deles nos seus próprios sistemas. Publicaremos mais relatos no futuro, então fique de olho para conferir outros guias detalhados.
Por fim, John Hammond (pesquisador sênior de segurança na Huntress) criou explicações detalhadas sobre alguns dos outros CTFs Fetch the Flag da SnykCon 2021. Recomendo que você assista:
Se você tem interesse em fazer parte da equipe de pesquisa de segurança da Snyk, que cria esses CTFs (e muito mais), confira nossas vagas abertas!
