diff --git a/app/metasploit.py b/app/metasploit.py index c3af800..72203cb 100644 --- a/app/metasploit.py +++ b/app/metasploit.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 """ Module to control Metasploit and related tools (MSFVenom) on the attack server """ +import time +import socket +import os +import random +import requests + from pymetasploit3.msfrpc import MsfRpcClient # from app.machinecontrol import Machine from app.attack_log import AttackLog from app.interface_sfx import CommandlineColors -import time -import socket from app.exceptions import MetasploitError, ServerError -import requests -import random - -import os # https://github.com/DanMcInerney/pymetasploit3 class Metasploit(): + """ Metasploit class for basic Metasploit wrapping """ + def __init__(self, password, attack_logger, **kwargs): """ @@ -42,7 +44,7 @@ class Metasploit(): time.sleep(3) # Waiting for server to start. Or we would get https connection errors when getting the client. def start_exploit_stub_for_external_payload(self, payload='linux/x64/meterpreter_reverse_tcp', exploit='exploit/multi/handler'): - """ + """ Start a metasploit handler and wait for external payload to connect @:returns: res, which contains "job_id" and "uuid" """ @@ -128,18 +130,18 @@ class Metasploit(): # Get_ip can also return a network name. Matching a session needs a real ip name_resolution_worked = True try: - ip = socket.gethostbyname(target.get_ip()) + target_ip = socket.gethostbyname(target.get_ip()) except socket.gaierror: - ip = target.get_ip() # Limp on feature if we can not get a name resolution + target_ip = target.get_ip() # Limp on feature if we can not get a name resolution name_resolution_worked = False print(f"Name resolution for {target.get_ip()} failed. Sessions are: {self.get_client().sessions.list}") retries = 100 while retries > 0: - for k, v in self.get_client().sessions.list.items(): - if v["session_host"] == ip: + for key, value in self.get_client().sessions.list.items(): + if value["session_host"] == target_ip: # print(f"session list: {self.get_client().sessions.list}") - return k + return key time.sleep(1) retries -= 1 @@ -180,12 +182,12 @@ class Metasploit(): shell.write(cmd) time.sleep(delay) retries = 20 - r = "" + shell_result = "" while retries > 0: - r += shell.read() + shell_result += shell.read() time.sleep(0.5) # Command needs time to execute retries -= 1 - res.append(r) + res.append(shell_result) return res @@ -223,6 +225,8 @@ class Metasploit(): class MSFVenom(): + """ Class to remote controll payload generator MSFVenom on the attacker machine """ + def __init__(self, attacker, target, attack_logger: AttackLog): """ @@ -348,23 +352,18 @@ class MetasploitInstant(Metasploit): """ - def __init__(self, password, attack_logger, **kwargs): - """ - - :param password: password for the msfrpcd - :param attack_logger: The attack logging - :param kwargs: Relevant ones: uri, port, server, username - """ - super().__init__(password, attack_logger, **kwargs) - def parse_ps(self, ps_output): - d = [] + """ Parses the data from ps + :param ps_output: Metasploit ps output + :return: A list of dicts + """ + ps_data = [] for line in ps_output.split("\n")[6:]: pieces = line.split(" ") cleaned_pieces = [] - for p in pieces: - if len(p): - cleaned_pieces.append(p) + for piece in pieces: + if len(piece): + cleaned_pieces.append(piece) if len(cleaned_pieces) > 2: rep = {"PID": int(cleaned_pieces[0].strip()), @@ -382,9 +381,9 @@ class MetasploitInstant(Metasploit): rep["User"] = cleaned_pieces[5].strip() if len(cleaned_pieces) >= 7: rep["Path"] = cleaned_pieces[6].strip() - d.append(rep) + ps_data.append(rep) - return d + return ps_data def filter_ps_results(self, data, user=None, name=None, arch=None): """ Filter the process lists for certain diff --git a/app/pluginmanager.py b/app/pluginmanager.py index d81d92a..52b0099 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ Manage plugins """ -import straight.plugin from glob import glob import os @@ -10,6 +9,7 @@ from plugins.base.attack import AttackPlugin from plugins.base.machinery import MachineryPlugin from plugins.base.sensor import SensorPlugin from plugins.base.vulnerability_plugin import VulnerabilityPlugin +import straight.plugin from app.interface_sfx import CommandlineColors # from app.interface_sfx import CommandlineColors @@ -180,15 +180,16 @@ class PluginManager(): # TODO: Add verify command to verify all plugins (or a specific one) def print_default_config(self, subclass_name, name): + """ Pretty prints the default config for this plugin """ subclass = None - for a in sections: - if a["name"] == subclass_name: - subclass = a["subclass"] + for section in sections: + if section["name"] == subclass_name: + subclass = section["subclass"] if subclass is None: print("Use proper subclass. Available subclasses are: ") - "\n- ".join([a for a in sections["name"]]) + "\n- ".join(list(sections["name"])) plugins = self.get_plugins(subclass, [name]) for plugin in plugins: diff --git a/plugins/base/machinery.py b/plugins/base/machinery.py index 5252e84..5849bd3 100644 --- a/plugins/base/machinery.py +++ b/plugins/base/machinery.py @@ -3,11 +3,10 @@ """ Base class for classes to control any kind of machine: vm, bare metal, cloudified """ from enum import Enum - +import os from app.config import MachineConfig from app.interface_sfx import CommandlineColors from plugins.base.plugin_base import BasePlugin -import os class MachineStates(Enum): diff --git a/plugins/base/sensor.py b/plugins/base/sensor.py index faeae20..1f53e74 100644 --- a/plugins/base/sensor.py +++ b/plugins/base/sensor.py @@ -20,28 +20,20 @@ class SensorPlugin(BasePlugin): super().__init__() # pylint:disable=useless-super-delegation self.debugit = False - def set_sysconf(self, config): - """ Set system config - - @param config: A dict with system configuration relevant for all plugins - """ - - super().set_sysconf(config) - - def prime(self): + def prime(self): # pylint: disable=no-self-use """ prime sets hard core configs in the target. You can use it to call everything that permanently alters the OS by settings. If your prime function returns True the machine will be rebooted after prime-ing it. This is very likely what you want. Only use prime if install is not sufficient. """ return False - def install(self): + def install(self): # pylint: disable=no-self-use """ Install the sensor. Executed on the target. Take the sensor from the share and (maybe) copy it to its destination. Do some setup """ return True - def start(self, disown=None): + def start(self, disown=None): # pylint: disable=unused-argument, no-self-use """ Start the sensor. The connection to the client is disowned here. = Sent to background. This keeps the process running. @param disown: Send async into background @@ -49,7 +41,7 @@ class SensorPlugin(BasePlugin): return True - def stop(self): + def stop(self): # pylint: disable=no-self-use """ Stop the sensor """ return True diff --git a/plugins/base/ssh_features.py b/plugins/base/ssh_features.py index 27758e4..fec3aed 100644 --- a/plugins/base/ssh_features.py +++ b/plugins/base/ssh_features.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 """ A class you can use to add SSH features to you plugin. Useful for vm_controller/machinery classes """ import os.path +import socket +import time +import paramiko from fabric import Connection -from app.exceptions import NetworkError from invoke.exceptions import UnexpectedExit -import paramiko -import time -import socket +from app.exceptions import NetworkError from plugins.base.plugin_base import BasePlugin class SSHFeatures(BasePlugin): + """ A Mixin class to add SSH features to all kind of VM machinery """ def __init__(self): + self.config = None super().__init__() self.connection = None + def get_ip(self): + """ Get the IP of a machine, must be overwritten in the machinery class """ + raise NotImplementedError + def connect(self): """ Connect to a machine """ @@ -79,14 +85,13 @@ class SSHFeatures(BasePlugin): result = self.connection.run(cmd, disown=disown) print(result) # paramiko.ssh_exception.SSHException in the next line is needed for windows openssh - except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit, paramiko.ssh_exception.SSHException): + except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit, paramiko.ssh_exception.SSHException) as error: if retry <= 0: - raise NetworkError - else: - self.disconnect() - self.connect() - retry -= 1 - self.vprint("Got some SSH errors. Retrying", 2) + raise NetworkError from error + self.disconnect() + self.connect() + retry -= 1 + self.vprint("Got some SSH errors. Retrying", 2) else: break @@ -122,8 +127,8 @@ class SSHFeatures(BasePlugin): time.sleep(retry_sleep) self.disconnect() self.connect() - except FileNotFoundError as e: - self.vprint(f"File not found: {e}", 0) + except FileNotFoundError as error: + self.vprint(f"File not found: {error}", 0) break else: return res @@ -146,16 +151,15 @@ class SSHFeatures(BasePlugin): while retry > 0: try: res = self.connection.get(src, dst) - except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit): + except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit) as error: if retry <= 0: - raise NetworkError - else: - self.disconnect() - self.connect() - retry -= 1 - self.vprint("Got some SSH errors. Retrying", 2) - except FileNotFoundError as e: - self.vprint(e, 0) + raise NetworkError from error + self.disconnect() + self.connect() + retry -= 1 + self.vprint("Got some SSH errors. Retrying", 2) + except FileNotFoundError as error: + self.vprint(error, 0) break else: break