Skip to main content

Fetch the Flag CTF 2022: solução de Juggalo Central

Escrito por

Alexander Kraev

feature ctf juggalo central

10 de novembro de 2022

0 minutos de leitura

Obrigado por participar do Fetch com a gente! Parabéns aos milhares de participantes que se juntaram a nós noFetch the Flag CTF. E um agradecimento especial aos Snykers que criaram, testaram e documentaram os desafios!

Se você participou do Fetch the Flag de 2022 da Snyk e não conseguiu concluir o desafio Juggalo Central, veio ao lugar certo. Vamos analisar a solução juntos!

Passo a passo

Para começar, há um link para um site com o formulário de login de sempre, com dois campos: username e password. Também há um arquivo com extensão .php. Vamos dar uma olhada primeiro no site.

O site

Como mencionei, a página tem dois campos de entrada (e uma imagem de um palhaço). Primeiro, verifiquei o código-fonte da página, o arquivo .css e até a imagem. Não encontrei nada que pudesse aproveitar. Notei uma coisa: havia duas mensagens de erro diferentes — uma quando eu usava um nome de usuário comum, como “admin”, e outra quando usava uma sequência aleatória, como “qweasdzxc”.

O código

Dentro de index.php, encontrei três funções: getFlag, searchUserByName e validateCredentails.

A primeira função, getFlag, consulta uma tabela do banco de dados chamada flag e retorna a primeira linha encontrada. Depois de analisar o código, concluí que a chave que procuro está armazenada nessa tabela.

A segunda função, searchUserByName, tenta encontrar um usuário com base na variável de entrada chamada $username. Se houver resultados, a função os retorna. Notei uma coisa nesta linha:

     return $stmt->fetchAll(PDO::FETCH_ASSOC);

Isso significa que a função não retornaria apenas uma linha (já que espero que os usuários tenham nomes de usuário únicos): se mais de um usuário tiver o mesmo nome, retornará todos eles. Mais tarde, descobri que isso não nos ajuda, por causa do que acontece na próxima função. Às vezes, é assim que funciona um CTF.

A terceira função, validateCredentails, lê o nome de usuário e a senha enviados no corpo da requisição POST e chama searchUserByName para verificar se existe um usuário com esse nome. Também entendi por que a mensagem de erro era diferente para um nome de usuário com caracteres aleatórios. Se o usuário informado não existir, a função retorna: "Juggalo not found!". Já para qualquer nome de usuário existente com uma senha incorreta, a mensagem de erro era: "Invalid credalo".

Encontrando um nome de usuário válido

Agora vamos encontrar credenciais válidas. Quando tentei algo comum, como “admin”, apareceu a mensagem de erro "Invalid credalo". Com a análise do código da etapa anterior, posso concluir que existe um usuário com o valor “admin” no campo de nome de usuário.

A primeira tentativa

Agora que temos um nome de usuário válido, vamos ver como descobrir a senha. A mensagem de erro indicando que não temos acesso a esse usuário é exibida por causa da seguinte linha de código:

    if ($results[0]['password'] != substr(md5($username . $password), 0, 20)) {

No lado esquerdo da condição, a senha do usuário é recuperada do banco de dados. No lado direito, estão os primeiros 20 caracteres do hash md5 que contém duas variáveis fornecidas pelo usuário: username e password.

A princípio, pensei que, por causa dessa condição, qualquer senha válida teria 20 caracteres. Resumindo: eu estava errado.

A segunda tentativa

Depois de pensar um pouco no desafio, tive outra ideia. Esta linha usa o operador de comparação !=. Então consultei a documentação oficial do PHP para entender como funcionam os operadores de comparação no PHP. Uma frase da documentação que não entendi estava logo ao lado do operador !=:

true if $a is not equal to $b after type juggling.

Espera. O que é “type juggling”? Depois de ler isso, abri de novo a página do desafio. O palhaço continuava lá, me observando.

O ataque

Acontece que o PHP tem algo chamado “type juggling”. Depois de ler sobre o assunto e executar alguns testes de PHP online, cheguei à seguinte condição

    if ("0" == "0e12345")

Surpreendentemente, isso retornou “true”. Isso acontece porque o PHP converte os dois operandos em valores numéricos. Agora, para encontrar a senha certa do usuário “admin”, basta gerar um hash md5 que atenda a duas condições: começar com “0e” e conter apenas números depois disso (ou pelo menos 18 caracteres depois, por causa do uso de substr).

O script

Este é um script em Python que encontra o hash certo:

import hashlib

target = '0e'
candidate = 0;
while True:
   plaintext = 'admin'+target+str(candidate)
   hash = hashlib.md5(plaintext.encode('ascii')).hexdigest()
   # Hash starts with “0e”
   if hash[:2] == target:
       # Hash contains only one letter (“e”) in first twenty characters
       # So it can be considered as a number by PHP
       if sum(c.isalpha() for c in hash[:20]) == 1:
           print('username and password:' + plaintext);
           break
   candidate = candidate + 1

Depois de um minuto, consegui entrar na página Juggalo Central usando as credenciais geradas por este script.

Mais soluções do Fetch the Flag

Quer saber como encontramos as outras flags? Confira a página de soluções do Fetch the Flag para ver como fizemos isso.