forked from p34709852/monkey
Merge branch '2216-tcp-relay' into develop
This commit is contained in:
commit
b81f7ad69c
|
@ -59,6 +59,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- `/api/registration` endpoint to `/api/register`. #2105
|
||||
- `/api/file-upload` endpoit to `/api/pba/upload`. #2154
|
||||
- Improved the speed of ransomware encryption by 2-3x. #2123
|
||||
- "-s/--server" to "-s/--servers". #2216
|
||||
- "-s/--servers" accepts list of servers separated by comma. #2216
|
||||
|
||||
### Removed
|
||||
- VSFTPD exploiter. #1533
|
||||
|
@ -103,6 +105,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- "/api/configuration/import" endpoint. #2002
|
||||
- "/api/configuration/export" endpoint. #2002
|
||||
- "/api/island-configuration" endpoint. #2003
|
||||
- "-t/--tunnel" from agent command line arguments. #2216
|
||||
|
||||
### Fixed
|
||||
- A bug in network map page that caused delay of telemetry log loading. #1545
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||
PYTHON_VERSION="3.7.13"
|
||||
PYTHON_VERSION="3.7.14"
|
||||
PYTHON_APPIMAGE_URL="https://github.com/niess/python-appimage/releases/download/python3.7/python${PYTHON_VERSION}-cp37-cp37m-manylinux1_x86_64.AppImage"
|
||||
APPIMAGE_DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
|
||||
APPDIR="$APPIMAGE_DIR/squashfs-root"
|
||||
|
|
|
@ -11,6 +11,7 @@ GCP_TEST_MACHINE_LIST = {
|
|||
"tunneling-10",
|
||||
"tunneling-11",
|
||||
"tunneling-12",
|
||||
"tunneling-13",
|
||||
"zerologon-25",
|
||||
],
|
||||
"europe-west1-b": [
|
||||
|
@ -52,7 +53,6 @@ DEPTH_3_A = {
|
|||
"tunneling-9",
|
||||
"tunneling-10",
|
||||
"tunneling-11",
|
||||
"tunneling-12",
|
||||
"mimikatz-15",
|
||||
],
|
||||
"europe-west1-b": [
|
||||
|
@ -63,6 +63,16 @@ DEPTH_3_A = {
|
|||
],
|
||||
}
|
||||
|
||||
DEPTH_4_A = {
|
||||
"europe-west1-b": [
|
||||
"tunneling-9",
|
||||
"tunneling-10",
|
||||
"tunneling-12",
|
||||
"tunneling-13",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
POWERSHELL_EXPLOITER_REUSE = {
|
||||
"europe-west1-b": [
|
||||
"powershell-3-46",
|
||||
|
@ -88,6 +98,7 @@ GCP_SINGLE_TEST_LIST = {
|
|||
"test_depth_2_a": DEPTH_2_A,
|
||||
"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_wmi_and_mimikatz_exploiters": WMI_AND_MIMIKATZ,
|
||||
|
|
|
@ -18,6 +18,7 @@ from envs.monkey_zoo.blackbox.test_configurations import (
|
|||
wmi_mimikatz_test_configuration,
|
||||
zerologon_test_configuration,
|
||||
)
|
||||
from envs.monkey_zoo.blackbox.test_configurations.depth_4_a import depth_4_a_test_configuration
|
||||
from envs.monkey_zoo.blackbox.test_configurations.test_configuration import TestConfiguration
|
||||
from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest
|
||||
from envs.monkey_zoo.blackbox.utils.gcp_machine_handlers import (
|
||||
|
@ -123,6 +124,11 @@ class TestMonkeyBlackbox:
|
|||
island_client, depth_3_a_test_configuration, "Depth3A test suite"
|
||||
)
|
||||
|
||||
def test_depth_4_a(self, island_client):
|
||||
TestMonkeyBlackbox.run_exploitation_test(
|
||||
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):
|
||||
|
|
|
@ -16,7 +16,7 @@ from .utils import (
|
|||
|
||||
# Tests:
|
||||
# Powershell (10.2.3.45, 10.2.3.46, 10.2.3.47, 10.2.3.48)
|
||||
# Tunneling (SSH brute force) (10.2.2.9, 10.2.1.10, 10.2.0.12, 10.2.0.11)
|
||||
# Tunneling through grandparent agent (SSH brute force) (10.2.2.9, 10.2.1.10, 10.2.0.11)
|
||||
# WMI pass the hash (10.2.2.15)
|
||||
|
||||
|
||||
|
@ -38,7 +38,6 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
|||
"10.2.3.47",
|
||||
"10.2.3.48",
|
||||
"10.2.1.10",
|
||||
"10.2.0.12",
|
||||
"10.2.0.11",
|
||||
"10.2.2.15",
|
||||
]
|
||||
|
@ -62,7 +61,6 @@ CREDENTIALS = (
|
|||
Credentials(None, Password("Passw0rd!")),
|
||||
Credentials(None, Password("3Q=(Ge(+&w]*")),
|
||||
Credentials(None, Password("`))jU7L(w}")),
|
||||
Credentials(None, Password("t67TC5ZDmz")),
|
||||
Credentials(None, NTHash("d0f0132b308a0c4e5d1029cc06f48692")),
|
||||
Credentials(None, NTHash("5da0889ea2081aa79f6852294cba4a5e")),
|
||||
Credentials(None, NTHash("50c9987a6bf1ac59398df9f911122c9b")),
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
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_exploiters,
|
||||
add_subnets,
|
||||
add_tcp_ports,
|
||||
replace_agent_configuration,
|
||||
replace_propagation_credentials,
|
||||
set_keep_tunnel_open_time,
|
||||
set_maximum_depth,
|
||||
)
|
||||
|
||||
# Tests:
|
||||
# Tunneling (SSH brute force) (10.2.2.9, 10.2.1.10, 10.2.0.12, 10.2.0.13)
|
||||
|
||||
|
||||
def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
brute_force = [
|
||||
PluginConfiguration(name="SSHExploiter", options={}),
|
||||
PluginConfiguration(name="WmiExploiter", options={"smb_download_timeout": 30}),
|
||||
]
|
||||
|
||||
return add_exploiters(agent_configuration, brute_force=brute_force, vulnerability=[])
|
||||
|
||||
|
||||
def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
subnets = [
|
||||
"10.2.2.9",
|
||||
"10.2.1.10",
|
||||
"10.2.0.12",
|
||||
"10.2.2.13",
|
||||
]
|
||||
return add_subnets(agent_configuration, subnets)
|
||||
|
||||
|
||||
def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration:
|
||||
ports = [22, 135, 5985, 5986]
|
||||
return add_tcp_ports(agent_configuration, ports)
|
||||
|
||||
|
||||
test_agent_configuration = set_maximum_depth(noop_test_configuration.agent_configuration, 4)
|
||||
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_tcp_ports(test_agent_configuration)
|
||||
|
||||
CREDENTIALS = (
|
||||
Credentials(Username("m0nk3y"), None),
|
||||
Credentials(None, Password("3Q=(Ge(+&w]*")),
|
||||
Credentials(None, Password("`))jU7L(w}")),
|
||||
Credentials(None, Password("prM2qsroTI")),
|
||||
Credentials(None, Password("t67TC5ZDmz")),
|
||||
)
|
||||
|
||||
depth_4_a_test_configuration = dataclasses.replace(noop_test_configuration)
|
||||
replace_agent_configuration(
|
||||
test_configuration=depth_4_a_test_configuration, agent_configuration=test_agent_configuration
|
||||
)
|
||||
replace_propagation_credentials(
|
||||
test_configuration=depth_4_a_test_configuration, propagation_credentials=CREDENTIALS
|
||||
)
|
|
@ -303,7 +303,7 @@ Update all requirements using deployment script:<br>
|
|||
</tr>
|
||||
<tr class="even">
|
||||
<td>Root password:</td>
|
||||
<td>3Q=(Ge(+&w]*</td>
|
||||
<td>3Q=(Ge(+&w]*</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
|
@ -343,7 +343,10 @@ Update all requirements using deployment script:<br>
|
|||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
<td>Default</td>
|
||||
<td>Contains firewall rules to block everything from 10.2.1.10 except ssh.
|
||||
This prevents tunneling communication, but allows ssh exploitation.
|
||||
Contains firewall rules to allow everything from 10.2.1.9 except ssh.
|
||||
This prevents ssh exploitation, but allows tunneling.</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
|
@ -384,6 +387,38 @@ Update all requirements using deployment script:<br>
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th><p><span id="_Toc536021463" class="anchor"></span>Nr. <strong>13</strong> Tunneling M5</p>
|
||||
<p>(10.2.0.13)</p></th>
|
||||
<th>(Exploitable)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>OS:</td>
|
||||
<td><strong>Ubuntu 18 x64</strong></td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default service’s port:</td>
|
||||
<td>22</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Root password:</td>
|
||||
<td>prM2qsroTI</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
<td>Configured to disable traffic from/to 10.2.0.10 and 10.2.0.11(via ufw and iptables)</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>Accessible only through Nr.12</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1 @@
|
|||
<mxfile host="app.diagrams.net" modified="2022-09-09T14:43:18.604Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" etag="hR7zJg_PJGBkx010pojR" version="20.3.0" type="device"><diagram id="UsVsRtHn_Xg_yxI8nRbj" name="Page-1">7VrbbuM2EP0aP9aQRN38uLHsTYEtGiAo0jwFjMSV2KVEg6J86deXtEhbF1eWW9tSsEHyYM6MJHLOOcPRZQLm6fYrg6vkNxohMrGMaDsBwcSyTNuyJvLfiHalxZvZpSFmOFJBR8Mz/hspo6GsBY5QXgvklBKOV3VjSLMMhbxmg4zRTT3sOyX1q65gjFqG5xCStvUFRzwprb5jHO2PCMeJvrJpKE8KdbAy5AmM6KZiAosJmDNKefkr3c4RkcnTeSmPW/6L9zAxhjLe5wAneCPB79/gSxC+PT4t053Jlr94am58pxeMIrF+NaSMJzSmGSSLo/WB0SKLkDyrIUbHmG+UroTRFMa/EOc7BSYsOBWmhKdEedszV4vJacFC1DFdzQDIYsQ74hTp5FoqF1B5+YpoijjbiQCGCOR4XccaKsrEhzh16BfG4K4SsKI443nlzE/SIAIU+w/QK+6bbgOhy+LFj3IGelRZytG0R/0CBqhFryEpVBp+zQnMohYx6rBvEszR8wru4doI7feEeI0YR9tOUJTXbyRDS25zlOFBbElFgvq4UzBWUnl5pvyPpRWrp1bMUWpFbwB9tdKIv41WZkMwQADPdn+q4/eDVzmYWo4eB9uqN9ip0e2pA0ZBHdCsFF43dc7E34Y6VqvM8kJ0LARnsaSVS0SeH96Z+BXzPXZNiykQF38nYvcec9om570rtuWOrWTrLvKj1GzQU3j2KIRnzxp4n6nZZ+Jv1N9YP0nR7ssdZ5Tc0TXgWlwIXsHzDIePThy8sMfiD7i0uG51hqgGjXTfFmJrEESNBqLGmWrQHX+bagA6tmFZjXruw+b0VPDeZUxPbDr33olte3Q78YDa+y9Ssy+R2jWr6f/Kst1Fb/MSep8I1vRuIzk4vb2h2Q2GbTIqfcVr1Xn1JsPpKYtx3BnarlPjCbhLw2n/JA1nXy64o+CC12w3/O725Ez8bbjjdNVvq3f9FkW6fddz7yLtNB6zHJ4ODFelnY/Vg7g9BTaOOzrPrOMNzjwNOBN/G4G5XQIDlwisvePfW2CePTqBea30trKU/0A8TFSS9pxCbLFGklplZg7vSWVABPPkkNJK+nLO6A80p4QyYcloJqX6HROiTRMLBEt/bov8PRD4jsgTzTHHNBO+EMmLCofMNQ6FlhsB75RzmlYCvhAcSweXmn+AanQ4Dy24YJCYjn4HbZTrWMk1p9tYvhqfrhGCqTUVgcX2ShW2+b5w1iYAsNv4a9v18fc/8b8j/qBZAMyh8Z994j+g/r2B4dfT+YT/LvD7DfiB4Q6Mf/tbkjb+14J3sfSXljkueKdp/rbBWbT/6uwaELuNN6rAakvcPtHhgdnUvxhkMTx+jVY23Mdv+sDiHw==</diagram></mxfile>
|
|
@ -23,6 +23,10 @@ data "google_compute_image" "tunneling-12" {
|
|||
name = "tunneling-12"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "tunneling-13" {
|
||||
name = "tunneling-13"
|
||||
project = local.monkeyzoo_project
|
||||
}
|
||||
data "google_compute_image" "sshkeys-11" {
|
||||
name = "sshkeys-11"
|
||||
project = local.monkeyzoo_project
|
||||
|
|
|
@ -127,6 +127,10 @@ resource "google_compute_instance_from_template" "tunneling-11" {
|
|||
subnetwork="${local.resource_prefix}tunneling2-main"
|
||||
network_ip="10.2.0.11"
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="${local.resource_prefix}tunneling-main"
|
||||
network_ip="10.2.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "tunneling-12" {
|
||||
|
@ -144,6 +148,21 @@ resource "google_compute_instance_from_template" "tunneling-12" {
|
|||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "tunneling-13" {
|
||||
name = "${local.resource_prefix}tunneling-13"
|
||||
source_instance_template = local.default_ubuntu
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
image = data.google_compute_image.tunneling-13.self_link
|
||||
}
|
||||
auto_delete = true
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="${local.resource_prefix}tunneling2-main"
|
||||
network_ip="10.2.0.13"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "sshkeys-11" {
|
||||
name = "${local.resource_prefix}sshkeys-11"
|
||||
source_instance_template = local.default_ubuntu
|
||||
|
|
|
@ -8,4 +8,3 @@ class TelemCategoryEnum:
|
|||
SCAN = "scan"
|
||||
STATE = "state"
|
||||
TRACE = "trace"
|
||||
TUNNEL = "tunnel"
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
# TODO: `address_to_port()` should return the port as an integer.
|
||||
def address_to_ip_port(address: str) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Split a string containing an IP address (and optionally a port) into IP and Port components.
|
||||
Currently only works for IPv4 addresses.
|
||||
|
||||
:param address: The address string.
|
||||
:return: Tuple of IP and port strings. The port may be None if no port was in the address.
|
||||
"""
|
||||
if ":" in address:
|
||||
ip, port = address.split(":")
|
||||
return ip, port or None
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import queue
|
||||
from typing import Any, List, MutableMapping, TypeVar
|
||||
from typing import Any, Dict, List, MutableMapping, Type, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
@ -15,7 +15,7 @@ class abstractstatic(staticmethod):
|
|||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
_instances: Dict[Type, type] = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
|
|
|
@ -26,6 +26,7 @@ pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement
|
|||
paramiko = {editable = true, ref = "2.10.3.dev1", git = "https://github.com/VakarisZ/paramiko.git"}
|
||||
pypubsub = "*"
|
||||
pydantic = "*"
|
||||
egg-timer = "*"
|
||||
|
||||
[dev-packages]
|
||||
ldap3 = "*"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "7d1e1d151dbf74cf73466f7a73305efc4e5e187245fe789c0db103f4045eb722"
|
||||
"sha256": "cb55ba9c2d5b1763315473d9e766aade31137f61839644c1bcdf6ce0e7601ddb"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -97,11 +97,11 @@
|
|||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
|
||||
"sha256:43dadad18a7f168740e66944e4fa82c6611848ff9056ad910f8f7a3e46ab89e0",
|
||||
"sha256:cffdcd380919da6137f76633531a5817e3a9f268575c128249fb637e4f9e73fb"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.6.15"
|
||||
"version": "==2022.6.15.1"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
|
@ -251,6 +251,14 @@
|
|||
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"egg-timer": {
|
||||
"hashes": [
|
||||
"sha256:125efb1fdc1582e3354dbbf3218010968a875b97bf7c10e00c8ca6010d9c18ad",
|
||||
"sha256:12b789c8d17d1aab11c5b994bad336089a4c1ac5e0565e3ad2dd0470eae34939"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.0.post1"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
|
||||
|
@ -489,6 +497,7 @@
|
|||
"hashes": [
|
||||
"sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==2022.5.30"
|
||||
},
|
||||
|
@ -799,14 +808,32 @@
|
|||
"version": "==0.6.0"
|
||||
},
|
||||
"pywin32": {
|
||||
"sys_platform": "== 'win32'",
|
||||
"version": "*"
|
||||
"hashes": [
|
||||
"sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc",
|
||||
"sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269",
|
||||
"sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3",
|
||||
"sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48",
|
||||
"sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4",
|
||||
"sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96",
|
||||
"sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df",
|
||||
"sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1",
|
||||
"sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43",
|
||||
"sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9",
|
||||
"sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f",
|
||||
"sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988",
|
||||
"sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e",
|
||||
"sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==304"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
"sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
|
||||
"sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
|
@ -854,11 +881,29 @@
|
|||
"tls"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680",
|
||||
"sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2"
|
||||
"sha256:8d4718d1e48dcc28933f8beb48dc71cfe77a125e37ad1eb7a3d0acc49baf6c99",
|
||||
"sha256:e5b60de39f2d1da153fbe1874d885fe3fcbdb21fcc446fa759a53e8fc3513bed"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.7'",
|
||||
"version": "==22.4.0"
|
||||
"markers": "python_full_version >= '3.7.1'",
|
||||
"version": "==22.8.0"
|
||||
},
|
||||
"twisted-iocpsupport": {
|
||||
"hashes": [
|
||||
"sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41",
|
||||
"sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d",
|
||||
"sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9",
|
||||
"sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf",
|
||||
"sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323",
|
||||
"sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32",
|
||||
"sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4",
|
||||
"sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f",
|
||||
"sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546",
|
||||
"sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878",
|
||||
"sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565",
|
||||
"sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415"
|
||||
],
|
||||
"markers": "platform_system == 'Windows'",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
|
@ -919,6 +964,7 @@
|
|||
"sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942",
|
||||
"sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==1.5.1"
|
||||
},
|
||||
|
|
|
@ -2,21 +2,16 @@ import json
|
|||
import logging
|
||||
import platform
|
||||
from socket import gethostname
|
||||
from typing import Mapping, Optional
|
||||
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
from urllib3 import disable_warnings
|
||||
|
||||
import infection_monkey.tunnel as tunnel
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
|
||||
from infection_monkey.config import GUID
|
||||
from infection_monkey.network.info import get_host_subnets, local_ips
|
||||
from infection_monkey.transport.http import HTTPConnectProxy
|
||||
from infection_monkey.transport.tcp import TcpProxy
|
||||
from infection_monkey.utils import agent_process
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
disable_warnings() # noqa DUO131
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,8 +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: str, proxies: Optional[Mapping[str, str]] = None):
|
||||
self.proxies = {} if not proxies else proxies
|
||||
def __init__(self, server_address: str):
|
||||
self.server_address = server_address
|
||||
|
||||
def wakeup(self, parent=None):
|
||||
|
@ -51,69 +45,14 @@ class ControlClient:
|
|||
"launch_time": agent_process.get_start_time(),
|
||||
}
|
||||
|
||||
if self.proxies:
|
||||
monkey["tunnel"] = self.proxies.get("https")
|
||||
|
||||
requests.post( # noqa: DUO123
|
||||
f"https://{self.server_address}/api/agent",
|
||||
data=json.dumps(monkey),
|
||||
headers={"content-type": "application/json"},
|
||||
verify=False,
|
||||
proxies=self.proxies,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
)
|
||||
|
||||
def find_server(self, default_tunnel=None):
|
||||
logger.debug(f"Trying to wake up with Monkey Island server: {self.server_address}")
|
||||
if default_tunnel:
|
||||
logger.debug("default_tunnel: %s" % (default_tunnel,))
|
||||
|
||||
try:
|
||||
debug_message = "Trying to connect to server: %s" % self.server_address
|
||||
if self.proxies:
|
||||
debug_message += " through proxies: %s" % self.proxies
|
||||
logger.debug(debug_message)
|
||||
requests.get( # noqa: DUO123
|
||||
f"https://{self.server_address}/api?action=is-up",
|
||||
verify=False,
|
||||
proxies=self.proxies,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
)
|
||||
return True
|
||||
except ConnectionError as exc:
|
||||
logger.warning("Error connecting to control server %s: %s", self.server_address, exc)
|
||||
|
||||
if self.proxies:
|
||||
return False
|
||||
else:
|
||||
logger.info("Starting tunnel lookup...")
|
||||
proxy_find = tunnel.find_tunnel(default=default_tunnel)
|
||||
if proxy_find:
|
||||
self.set_proxies(proxy_find)
|
||||
return self.find_server()
|
||||
else:
|
||||
logger.info("No tunnel found")
|
||||
return False
|
||||
|
||||
def set_proxies(self, proxy_find):
|
||||
"""
|
||||
Note: The proxy schema changes between different versions of requests and urllib3,
|
||||
which causes the machine to not open a tunnel back.
|
||||
If we get "ValueError: check_hostname requires server_hostname" or
|
||||
"Proxy URL had not schema, should start with http:// or https://" errors,
|
||||
the proxy schema needs to be changed.
|
||||
Keep this in mind when upgrading to newer python version or when urllib3 and
|
||||
requests are updated there is possibility that the proxy schema is changed.
|
||||
https://github.com/psf/requests/issues/5297
|
||||
https://github.com/psf/requests/issues/5855
|
||||
"""
|
||||
proxy_address, proxy_port = proxy_find
|
||||
logger.info("Found tunnel at %s:%s" % (proxy_address, proxy_port))
|
||||
if is_windows_os():
|
||||
self.proxies["https"] = f"http://{proxy_address}:{proxy_port}"
|
||||
else:
|
||||
self.proxies["https"] = f"{proxy_address}:{proxy_port}"
|
||||
|
||||
def send_telemetry(self, telem_category, json_data: str):
|
||||
if not self.server_address:
|
||||
logger.error(
|
||||
|
@ -128,7 +67,6 @@ class ControlClient:
|
|||
data=json.dumps(telemetry),
|
||||
headers={"content-type": "application/json"},
|
||||
verify=False,
|
||||
proxies=self.proxies,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
@ -144,41 +82,16 @@ class ControlClient:
|
|||
data=json.dumps(telemetry),
|
||||
headers={"content-type": "application/json"},
|
||||
verify=False,
|
||||
proxies=self.proxies,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"Error connecting to control server {self.server_address}: {exc}")
|
||||
|
||||
def create_control_tunnel(self, keep_tunnel_open_time: int):
|
||||
if not self.server_address:
|
||||
return None
|
||||
|
||||
my_proxy = self.proxies.get("https", "").replace("https://", "")
|
||||
if my_proxy:
|
||||
proxy_class = TcpProxy
|
||||
try:
|
||||
target_addr, target_port = my_proxy.split(":", 1)
|
||||
target_port = int(target_port)
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
proxy_class = HTTPConnectProxy
|
||||
target_addr, target_port = None, None
|
||||
|
||||
return tunnel.MonkeyTunnel(
|
||||
proxy_class,
|
||||
keep_tunnel_open_time=keep_tunnel_open_time,
|
||||
target_addr=target_addr,
|
||||
target_port=target_port,
|
||||
)
|
||||
|
||||
def get_pba_file(self, filename):
|
||||
try:
|
||||
return requests.get( # noqa: DUO123
|
||||
PBA_FILE_DOWNLOAD % (self.server_address, filename),
|
||||
verify=False,
|
||||
proxies=self.proxies,
|
||||
timeout=LONG_REQUEST_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
|
|
|
@ -175,7 +175,7 @@ def _publish_credentials_stolen_event(
|
|||
credentials_stolen_event = CredentialsStolenEvent(
|
||||
source=get_agent_id(),
|
||||
tags=SSH_COLLECTOR_EVENT_TAGS,
|
||||
stolen_credentials=[collected_credentials],
|
||||
stolen_credentials=collected_credentials,
|
||||
)
|
||||
|
||||
event_queue.publish(credentials_stolen_event)
|
||||
|
|
|
@ -44,8 +44,7 @@ class MonkeyDrops(object):
|
|||
def __init__(self, args):
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument("-p", "--parent")
|
||||
arg_parser.add_argument("-t", "--tunnel")
|
||||
arg_parser.add_argument("-s", "--server")
|
||||
arg_parser.add_argument("-s", "--servers", type=lambda arg: arg.strip().split(","))
|
||||
arg_parser.add_argument("-d", "--depth", type=positive_int, default=0)
|
||||
arg_parser.add_argument("-l", "--location")
|
||||
arg_parser.add_argument("-vp", "--vulnerable-port")
|
||||
|
@ -132,8 +131,7 @@ class MonkeyDrops(object):
|
|||
|
||||
monkey_options = build_monkey_commandline_explicitly(
|
||||
parent=self.opts.parent,
|
||||
tunnel=self.opts.tunnel,
|
||||
server=self.opts.server,
|
||||
servers=self.opts.servers,
|
||||
depth=self.opts.depth,
|
||||
location=None,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
import threading
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import Dict, Sequence
|
||||
|
||||
from common.event_queue import IAgentEventQueue
|
||||
from common.utils.exceptions import FailedExploitationError
|
||||
|
@ -35,6 +35,7 @@ class HostExploiter:
|
|||
self.event_queue = None
|
||||
self.options = {}
|
||||
self.exploit_result = {}
|
||||
self.servers = []
|
||||
|
||||
def set_start_time(self):
|
||||
self.exploit_info["started"] = datetime.now().isoformat()
|
||||
|
@ -58,6 +59,7 @@ class HostExploiter:
|
|||
def exploit_host(
|
||||
self,
|
||||
host,
|
||||
servers: Sequence[str],
|
||||
current_depth: int,
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
event_queue: IAgentEventQueue,
|
||||
|
@ -66,6 +68,7 @@ class HostExploiter:
|
|||
interrupt: threading.Event,
|
||||
):
|
||||
self.host = host
|
||||
self.servers = servers
|
||||
self.current_depth = current_depth
|
||||
self.telemetry_messenger = telemetry_messenger
|
||||
self.event_queue = event_queue
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import io
|
||||
import threading
|
||||
from functools import lru_cache
|
||||
from typing import Mapping
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -18,9 +17,8 @@ class CachingAgentBinaryRepository(IAgentBinaryRepository):
|
|||
request is actually sent to the island for each requested binary.
|
||||
"""
|
||||
|
||||
def __init__(self, island_url: str, proxies: Mapping[str, str]):
|
||||
def __init__(self, island_url: str):
|
||||
self._island_url = island_url
|
||||
self._proxies = proxies
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def get_agent_binary(
|
||||
|
@ -40,7 +38,6 @@ class CachingAgentBinaryRepository(IAgentBinaryRepository):
|
|||
response = requests.get( # noqa: DUO123
|
||||
f"{self._island_url}/api/agent-binaries/{os_name}",
|
||||
verify=False,
|
||||
proxies=self._proxies,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import threading
|
||||
from typing import Dict, Type
|
||||
from typing import Dict, Sequence, Type
|
||||
|
||||
from common.event_queue import IAgentEventQueue
|
||||
from infection_monkey.model import VictimHost
|
||||
|
@ -31,11 +31,17 @@ class ExploiterWrapper:
|
|||
self._agent_binary_repository = agent_binary_repository
|
||||
|
||||
def exploit_host(
|
||||
self, host: VictimHost, current_depth: int, options: Dict, interrupt: threading.Event
|
||||
self,
|
||||
host: VictimHost,
|
||||
servers: Sequence[str],
|
||||
current_depth: int,
|
||||
options: Dict,
|
||||
interrupt: threading.Event,
|
||||
):
|
||||
exploiter = self._exploit_class()
|
||||
return exploiter.exploit_host(
|
||||
host,
|
||||
servers,
|
||||
current_depth,
|
||||
self._telemetry_messenger,
|
||||
self._event_queue,
|
||||
|
|
|
@ -104,7 +104,7 @@ class HadoopExploiter(WebRCE):
|
|||
|
||||
def _build_command(self, path, http_path):
|
||||
# Build command to execute
|
||||
monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1)
|
||||
monkey_cmd = build_monkey_commandline(self.servers, self.current_depth + 1)
|
||||
if self.host.is_windows():
|
||||
base_command = HADOOP_WINDOWS_COMMAND
|
||||
else:
|
||||
|
|
|
@ -115,7 +115,7 @@ class Log4ShellExploiter(WebRCE):
|
|||
|
||||
def _build_command(self, path: PurePath, http_path) -> str:
|
||||
# Build command to execute
|
||||
monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1, location=path)
|
||||
monkey_cmd = build_monkey_commandline(self.servers, self.current_depth + 1, location=path)
|
||||
if self.host.is_windows():
|
||||
base_command = LOG4SHELL_WINDOWS_COMMAND
|
||||
else:
|
||||
|
|
|
@ -179,7 +179,7 @@ class MSSQLExploiter(HostExploiter):
|
|||
|
||||
def _build_agent_launch_command(self, agent_path_on_victim: PureWindowsPath) -> str:
|
||||
agent_args = build_monkey_commandline(
|
||||
self.host, self.current_depth + 1, agent_path_on_victim
|
||||
self.servers, self.current_depth + 1, agent_path_on_victim
|
||||
)
|
||||
|
||||
return f"{agent_path_on_victim} {DROPPER_ARG} {agent_args}"
|
||||
|
|
|
@ -15,7 +15,7 @@ from infection_monkey.exploit.powershell_utils.powershell_client import (
|
|||
PowerShellClient,
|
||||
)
|
||||
from infection_monkey.exploit.tools.helpers import get_agent_dst_path, get_random_file_suffix
|
||||
from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost
|
||||
from infection_monkey.model import DROPPER_ARG, RUN_MONKEY
|
||||
from infection_monkey.utils.commands import build_monkey_commandline
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
from infection_monkey.utils.threading import interruptible_iter
|
||||
|
@ -169,7 +169,7 @@ class PowerShellExploiter(HostExploiter):
|
|||
|
||||
def _run_monkey_executable_on_victim(self, executable_path):
|
||||
monkey_execution_command = build_monkey_execution_command(
|
||||
self.host, self.current_depth + 1, executable_path
|
||||
self.servers, self.current_depth + 1, executable_path
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
@ -179,9 +179,9 @@ class PowerShellExploiter(HostExploiter):
|
|||
self._client.execute_cmd_as_detached_process(monkey_execution_command)
|
||||
|
||||
|
||||
def build_monkey_execution_command(host: VictimHost, depth: int, executable_path: str) -> str:
|
||||
def build_monkey_execution_command(servers: List[str], depth: int, executable_path: str) -> str:
|
||||
monkey_params = build_monkey_commandline(
|
||||
target_host=host,
|
||||
servers,
|
||||
depth=depth,
|
||||
location=executable_path,
|
||||
)
|
||||
|
|
|
@ -91,14 +91,14 @@ class SMBExploiter(HostExploiter):
|
|||
cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {
|
||||
"dropper_path": remote_full_path
|
||||
} + build_monkey_commandline(
|
||||
self.host,
|
||||
self.servers,
|
||||
self.current_depth + 1,
|
||||
str(dest_path),
|
||||
)
|
||||
else:
|
||||
cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {
|
||||
"monkey_path": remote_full_path
|
||||
} + build_monkey_commandline(self.host, self.current_depth + 1)
|
||||
} + build_monkey_commandline(self.servers, self.current_depth + 1)
|
||||
|
||||
smb_conn = None
|
||||
for str_bind_format, port in SMBExploiter.KNOWN_PROTOCOLS.values():
|
||||
|
|
|
@ -4,6 +4,7 @@ from pathlib import PurePath
|
|||
|
||||
import paramiko
|
||||
|
||||
from common import OperatingSystem
|
||||
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
|
||||
from common.credentials.credentials import get_plaintext
|
||||
from common.utils import Timer
|
||||
|
@ -119,7 +120,7 @@ class SSHExploiter(HostExploiter):
|
|||
ssh.connect(
|
||||
self.host.ip_addr,
|
||||
username=user,
|
||||
password=current_password,
|
||||
password=get_plaintext(current_password),
|
||||
port=port,
|
||||
timeout=SSH_CONNECT_TIMEOUT,
|
||||
auth_timeout=SSH_AUTH_TIMEOUT,
|
||||
|
@ -181,7 +182,8 @@ class SSHExploiter(HostExploiter):
|
|||
_, 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 = "linux"
|
||||
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}"
|
||||
|
||||
|
@ -245,7 +247,7 @@ class SSHExploiter(HostExploiter):
|
|||
|
||||
try:
|
||||
cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}"
|
||||
cmdline += build_monkey_commandline(self.host, self.current_depth + 1)
|
||||
cmdline += build_monkey_commandline(self.servers, self.current_depth + 1)
|
||||
cmdline += " > /dev/null 2>&1 &"
|
||||
ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT)
|
||||
|
||||
|
|
|
@ -370,14 +370,16 @@ class WebRCE(HostExploiter):
|
|||
default_path = self.get_default_dropper_path()
|
||||
if default_path is False:
|
||||
return False
|
||||
monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1, default_path)
|
||||
monkey_cmd = build_monkey_commandline(
|
||||
self.servers, self.current_depth + 1, default_path
|
||||
)
|
||||
command = RUN_MONKEY % {
|
||||
"monkey_path": path,
|
||||
"monkey_type": DROPPER_ARG,
|
||||
"parameters": monkey_cmd,
|
||||
}
|
||||
else:
|
||||
monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1)
|
||||
monkey_cmd = build_monkey_commandline(self.servers, self.current_depth + 1)
|
||||
command = RUN_MONKEY % {
|
||||
"monkey_path": path,
|
||||
"monkey_type": MONKEY_ARG,
|
||||
|
|
|
@ -103,14 +103,14 @@ class WmiExploiter(HostExploiter):
|
|||
cmdline = DROPPER_CMDLINE_WINDOWS % {
|
||||
"dropper_path": remote_full_path
|
||||
} + build_monkey_commandline(
|
||||
self.host,
|
||||
self.servers,
|
||||
self.current_depth + 1,
|
||||
DROPPER_TARGET_PATH_WIN64,
|
||||
)
|
||||
else:
|
||||
cmdline = MONKEY_CMDLINE_WINDOWS % {
|
||||
"monkey_path": remote_full_path
|
||||
} + build_monkey_commandline(self.host, self.current_depth + 1)
|
||||
} + build_monkey_commandline(self.servers, self.current_depth + 1)
|
||||
|
||||
# execute the remote monkey
|
||||
result = WmiTools.get_object(wmi_connection, "Win32_Process").Create(
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
import threading
|
||||
import time
|
||||
from ipaddress import IPv4Interface
|
||||
from typing import Any, Callable, Collection, List, Optional
|
||||
from typing import Any, Callable, Collection, List, Optional, Sequence
|
||||
|
||||
from common.agent_configuration import CustomPBAConfiguration, PluginConfiguration
|
||||
from common.utils import Timer
|
||||
|
@ -35,6 +35,7 @@ class AutomatedMaster(IMaster):
|
|||
def __init__(
|
||||
self,
|
||||
current_depth: Optional[int],
|
||||
servers: Sequence[str],
|
||||
puppet: IPuppet,
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
victim_host_factory: VictimHostFactory,
|
||||
|
@ -43,6 +44,7 @@ class AutomatedMaster(IMaster):
|
|||
credentials_store: IPropagationCredentialsRepository,
|
||||
):
|
||||
self._current_depth = current_depth
|
||||
self._servers = servers
|
||||
self._puppet = puppet
|
||||
self._telemetry_messenger = telemetry_messenger
|
||||
self._control_channel = control_channel
|
||||
|
@ -174,8 +176,8 @@ class AutomatedMaster(IMaster):
|
|||
current_depth = self._current_depth if self._current_depth is not None else 0
|
||||
logger.info(f"Current depth is {current_depth}")
|
||||
|
||||
if maximum_depth_reached(config.propagation.maximum_depth, current_depth):
|
||||
self._propagator.propagate(config.propagation, current_depth, self._stop)
|
||||
if not maximum_depth_reached(config.propagation.maximum_depth, current_depth):
|
||||
self._propagator.propagate(config.propagation, current_depth, self._servers, self._stop)
|
||||
else:
|
||||
logger.info("Skipping propagation: maximum depth reached")
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import json
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from typing import MutableMapping, Optional, Sequence
|
||||
from typing import Optional, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
import requests
|
||||
|
@ -22,10 +22,9 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class ControlChannel(IControlChannel):
|
||||
def __init__(self, server: str, agent_id: str, proxies: MutableMapping[str, str]):
|
||||
def __init__(self, server: str, agent_id: str):
|
||||
self._agent_id = agent_id
|
||||
self._control_channel_server = server
|
||||
self._proxies = proxies
|
||||
|
||||
def register_agent(self, parent: Optional[UUID] = None):
|
||||
agent_registration_data = AgentRegistrationData(
|
||||
|
@ -44,7 +43,6 @@ class ControlChannel(IControlChannel):
|
|||
url,
|
||||
json=agent_registration_data.dict(simplify=True),
|
||||
verify=False,
|
||||
proxies=self._proxies,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
@ -68,7 +66,6 @@ class ControlChannel(IControlChannel):
|
|||
response = requests.get( # noqa: DUO123
|
||||
url,
|
||||
verify=False,
|
||||
proxies=self._proxies,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
@ -89,7 +86,6 @@ class ControlChannel(IControlChannel):
|
|||
response = requests.get( # noqa: DUO123
|
||||
f"https://{self._control_channel_server}/api/agent-configuration",
|
||||
verify=False,
|
||||
proxies=self._proxies,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
@ -116,7 +112,6 @@ class ControlChannel(IControlChannel):
|
|||
response = requests.get( # noqa: DUO123
|
||||
propagation_credentials_url,
|
||||
verify=False,
|
||||
proxies=self._proxies,
|
||||
timeout=SHORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
|
|
@ -53,6 +53,7 @@ class Exploiter:
|
|||
exploitation_config: ExploitationConfiguration,
|
||||
hosts_to_exploit: Queue,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
results_callback: Callback,
|
||||
scan_completed: Event,
|
||||
stop: Event,
|
||||
|
@ -67,6 +68,7 @@ class Exploiter:
|
|||
exploiters_to_run,
|
||||
hosts_to_exploit,
|
||||
current_depth,
|
||||
servers,
|
||||
results_callback,
|
||||
scan_completed,
|
||||
stop,
|
||||
|
@ -103,6 +105,7 @@ class Exploiter:
|
|||
exploiters_to_run: Sequence[PluginConfiguration],
|
||||
hosts_to_exploit: Queue,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
results_callback: Callback,
|
||||
scan_completed: Event,
|
||||
stop: Event,
|
||||
|
@ -113,7 +116,7 @@ class Exploiter:
|
|||
try:
|
||||
victim_host = hosts_to_exploit.get(timeout=QUEUE_TIMEOUT)
|
||||
self._run_all_exploiters(
|
||||
exploiters_to_run, victim_host, current_depth, results_callback, stop
|
||||
exploiters_to_run, victim_host, current_depth, servers, results_callback, stop
|
||||
)
|
||||
except queue.Empty:
|
||||
if _all_hosts_have_been_processed(scan_completed, hosts_to_exploit):
|
||||
|
@ -130,6 +133,7 @@ class Exploiter:
|
|||
exploiters_to_run: Sequence[PluginConfiguration],
|
||||
victim_host: VictimHost,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
results_callback: Callback,
|
||||
stop: Event,
|
||||
):
|
||||
|
@ -147,7 +151,7 @@ class Exploiter:
|
|||
continue
|
||||
|
||||
exploiter_results = self._run_exploiter(
|
||||
exploiter_name, exploiter.options, victim_host, current_depth, stop
|
||||
exploiter_name, exploiter.options, victim_host, current_depth, servers, stop
|
||||
)
|
||||
results_callback(exploiter_name, victim_host, exploiter_results)
|
||||
|
||||
|
@ -160,6 +164,7 @@ class Exploiter:
|
|||
options: Dict,
|
||||
victim_host: VictimHost,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
stop: Event,
|
||||
) -> ExploiterResultData:
|
||||
logger.debug(f"Attempting to use {exploiter_name} on {victim_host.ip_addr}")
|
||||
|
@ -169,7 +174,7 @@ class Exploiter:
|
|||
|
||||
try:
|
||||
return self._puppet.exploit_host(
|
||||
exploiter_name, victim_host, current_depth, options, stop
|
||||
exploiter_name, victim_host, current_depth, servers, options, stop
|
||||
)
|
||||
except Exception as ex:
|
||||
msg = (
|
||||
|
|
|
@ -47,7 +47,11 @@ class Propagator:
|
|||
self._hosts_to_exploit: Queue = Queue()
|
||||
|
||||
def propagate(
|
||||
self, propagation_config: PropagationConfiguration, current_depth: int, stop: Event
|
||||
self,
|
||||
propagation_config: PropagationConfiguration,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
stop: Event,
|
||||
):
|
||||
logger.info("Attempting to propagate")
|
||||
|
||||
|
@ -66,7 +70,13 @@ class Propagator:
|
|||
exploit_thread = create_daemon_thread(
|
||||
target=self._exploit_hosts,
|
||||
name="PropagatorExploitThread",
|
||||
args=(propagation_config.exploitation, current_depth, network_scan_completed, stop),
|
||||
args=(
|
||||
propagation_config.exploitation,
|
||||
current_depth,
|
||||
servers,
|
||||
network_scan_completed,
|
||||
stop,
|
||||
),
|
||||
)
|
||||
|
||||
scan_thread.start()
|
||||
|
@ -167,6 +177,7 @@ class Propagator:
|
|||
self,
|
||||
exploitation_config: ExploitationConfiguration,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
network_scan_completed: Event,
|
||||
stop: Event,
|
||||
):
|
||||
|
@ -176,6 +187,7 @@ class Propagator:
|
|||
exploitation_config,
|
||||
self._hosts_to_exploit,
|
||||
current_depth,
|
||||
servers,
|
||||
self._process_exploit_attempts,
|
||||
network_scan_completed,
|
||||
stop,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from common import OperatingSystem
|
||||
|
||||
|
@ -7,10 +7,9 @@ class VictimHost(object):
|
|||
def __init__(self, ip_addr: str, domain_name: str = ""):
|
||||
self.ip_addr = ip_addr
|
||||
self.domain_name = str(domain_name)
|
||||
self.os = {}
|
||||
self.services = {}
|
||||
self.os: Dict[str, Any] = {}
|
||||
self.services: Dict[str, Any] = {}
|
||||
self.icmp = False
|
||||
self.default_tunnel = None
|
||||
self.default_server = None
|
||||
|
||||
def as_dict(self):
|
||||
|
|
|
@ -4,7 +4,6 @@ from typing import Optional, Tuple
|
|||
from infection_monkey.model import VictimHost
|
||||
from infection_monkey.network import NetworkAddress
|
||||
from infection_monkey.network.tools import get_interface_to_target
|
||||
from infection_monkey.tunnel import MonkeyTunnel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -12,12 +11,10 @@ logger = logging.getLogger(__name__)
|
|||
class VictimHostFactory:
|
||||
def __init__(
|
||||
self,
|
||||
tunnel: Optional[MonkeyTunnel],
|
||||
island_ip: Optional[str],
|
||||
island_port: Optional[str],
|
||||
on_island: bool,
|
||||
):
|
||||
self.tunnel = tunnel
|
||||
self.island_ip = island_ip
|
||||
self.island_port = island_port
|
||||
self.on_island = on_island
|
||||
|
@ -26,19 +23,15 @@ class VictimHostFactory:
|
|||
domain = network_address.domain or ""
|
||||
victim_host = VictimHost(network_address.ip, domain)
|
||||
|
||||
if self.tunnel:
|
||||
victim_host.default_tunnel = self.tunnel.get_tunnel_for_ip(victim_host.ip_addr)
|
||||
|
||||
if self.island_ip:
|
||||
ip, port = self._choose_island_address(victim_host.ip_addr)
|
||||
victim_host.set_island_address(ip, port)
|
||||
|
||||
logger.debug(f"Default tunnel for {victim_host} set to {victim_host.default_tunnel}")
|
||||
logger.debug(f"Default server for {victim_host} set to {victim_host.default_server}")
|
||||
|
||||
return victim_host
|
||||
|
||||
def _choose_island_address(self, victim_ip: str) -> Tuple[str, Optional[str]]:
|
||||
def _choose_island_address(self, victim_ip: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
# Victims need to connect back to the interface they can reach
|
||||
# On island, choose the right interface to pass to children monkeys
|
||||
if self.on_island:
|
||||
|
|
|
@ -3,13 +3,12 @@ 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
|
||||
|
||||
from pubsub.core import Publisher
|
||||
|
||||
import infection_monkey.tunnel as tunnel
|
||||
from common.event_queue import IAgentEventQueue, PyPubSubAgentEventQueue
|
||||
from common.event_serializers import (
|
||||
EventSerializerRegistry,
|
||||
|
@ -45,7 +44,13 @@ from infection_monkey.master import AutomatedMaster
|
|||
from infection_monkey.master.control_channel import ControlChannel
|
||||
from infection_monkey.model import VictimHostFactory
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_network_interfaces
|
||||
from infection_monkey.network.info import get_free_tcp_port, get_network_interfaces, local_ips
|
||||
from infection_monkey.network.relay import TCPRelay
|
||||
from infection_monkey.network.relay.utils import (
|
||||
find_server,
|
||||
notify_disconnect,
|
||||
send_remove_from_waitlist_control_message_to_relays,
|
||||
)
|
||||
from infection_monkey.network_scanning.elasticsearch_fingerprinter import ElasticSearchFingerprinter
|
||||
from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter
|
||||
from infection_monkey.network_scanning.mssql_fingerprinter import MSSQLFingerprinter
|
||||
|
@ -77,7 +82,6 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im
|
|||
LegacyTelemetryMessengerAdapter,
|
||||
)
|
||||
from infection_monkey.telemetry.state_telem import StateTelem
|
||||
from infection_monkey.telemetry.tunnel_telem import TunnelTelem
|
||||
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
|
||||
|
@ -99,29 +103,52 @@ class InfectionMonkey:
|
|||
logger.info("Monkey is initializing...")
|
||||
self._singleton = SystemSingleton()
|
||||
self._opts = self._get_arguments(args)
|
||||
self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(self._opts.server)
|
||||
self._control_client = ControlClient(self._opts.server)
|
||||
|
||||
# TODO: Revisit variable names
|
||||
server = self._get_server()
|
||||
# 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=server)
|
||||
|
||||
# TODO Refactor the telemetry messengers to accept control client
|
||||
# and remove control_client_object
|
||||
ControlClient.control_client_object = self._control_client
|
||||
self._monkey_inbound_tunnel = None
|
||||
self._control_channel = None
|
||||
self._telemetry_messenger = LegacyTelemetryMessengerAdapter()
|
||||
self._current_depth = self._opts.depth
|
||||
self._master = None
|
||||
self._inbound_tunnel_opened = False
|
||||
self._relay: TCPRelay
|
||||
|
||||
@staticmethod
|
||||
def _get_arguments(args):
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument("-p", "--parent")
|
||||
arg_parser.add_argument("-t", "--tunnel")
|
||||
arg_parser.add_argument("-s", "--server")
|
||||
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)
|
||||
|
||||
return opts
|
||||
|
||||
def _get_server(self):
|
||||
logger.debug(f"Trying to wake up with servers: {', '.join(self._opts.servers)}")
|
||||
servers_iterator = (s for s in self._opts.servers)
|
||||
server = find_server(servers_iterator)
|
||||
if server:
|
||||
logger.info(f"Successfully connected to the island via {server}")
|
||||
else:
|
||||
raise Exception(
|
||||
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
|
||||
# machines, is it possible for a machine to unintentionally unregister itself from the
|
||||
# relay if it is able to connect to the relay over multiple interfaces?
|
||||
send_remove_from_waitlist_control_message_to_relays(servers_iterator)
|
||||
|
||||
return server
|
||||
|
||||
@staticmethod
|
||||
def _log_arguments(args):
|
||||
arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()])
|
||||
|
@ -135,7 +162,7 @@ class InfectionMonkey:
|
|||
logger.info("Agent is starting...")
|
||||
logger.info(f"Agent GUID: {GUID}")
|
||||
|
||||
self._connect_to_island()
|
||||
self._control_client.wakeup(parent=self._opts.parent)
|
||||
|
||||
# TODO: Reevaluate who is responsible to send this information
|
||||
if is_windows_os():
|
||||
|
@ -143,9 +170,7 @@ class InfectionMonkey:
|
|||
|
||||
run_aws_environment_check(self._telemetry_messenger)
|
||||
|
||||
should_stop = ControlChannel(
|
||||
self._control_client.server_address, GUID, self._control_client.proxies
|
||||
).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
|
||||
|
@ -153,21 +178,6 @@ class InfectionMonkey:
|
|||
self._setup()
|
||||
self._master.start()
|
||||
|
||||
def _connect_to_island(self):
|
||||
# Sets island's IP and port for monkey to communicate to
|
||||
if self._current_server_is_set():
|
||||
logger.debug(f"Default server set to: {self._control_client.server_address}")
|
||||
else:
|
||||
raise Exception(f"Monkey couldn't find server with {self._opts.tunnel} default tunnel.")
|
||||
|
||||
self._control_client.wakeup(parent=self._opts.parent)
|
||||
|
||||
def _current_server_is_set(self) -> bool:
|
||||
if self._control_client.find_server(default_tunnel=self._opts.tunnel):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _setup(self):
|
||||
logger.debug("Starting the setup phase.")
|
||||
|
||||
|
@ -178,25 +188,26 @@ class InfectionMonkey:
|
|||
|
||||
_ = self._setup_agent_event_serializers()
|
||||
|
||||
control_channel = ControlChannel(
|
||||
self._control_client.server_address, GUID, self._control_client.proxies
|
||||
)
|
||||
control_channel.register_agent(self._opts.parent)
|
||||
self._control_channel = ControlChannel(self._control_client.server_address, GUID)
|
||||
self._control_channel.register_agent(self._opts.parent)
|
||||
|
||||
config = control_channel.get_config()
|
||||
self._monkey_inbound_tunnel = self._control_client.create_control_tunnel(
|
||||
config.keep_tunnel_open_time
|
||||
config = self._control_channel.get_config()
|
||||
|
||||
relay_port = get_free_tcp_port()
|
||||
self._relay = TCPRelay(
|
||||
relay_port,
|
||||
IPv4Address(self._cmd_island_ip),
|
||||
self._cmd_island_port,
|
||||
client_disconnect_timeout=config.keep_tunnel_open_time,
|
||||
)
|
||||
if self._monkey_inbound_tunnel and maximum_depth_reached(
|
||||
config.propagation.maximum_depth, self._current_depth
|
||||
):
|
||||
self._inbound_tunnel_opened = True
|
||||
self._monkey_inbound_tunnel.start()
|
||||
relay_servers = [f"{ip}:{relay_port}" for ip in local_ips()]
|
||||
|
||||
if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth):
|
||||
self._relay.start()
|
||||
|
||||
StateTelem(is_done=False, version=get_version()).send()
|
||||
TunnelTelem(self._control_client.proxies).send()
|
||||
|
||||
self._build_master()
|
||||
self._build_master(relay_servers)
|
||||
|
||||
register_signal_handlers(self._master)
|
||||
|
||||
|
@ -207,15 +218,12 @@ class InfectionMonkey:
|
|||
|
||||
return agent_event_serializer_registry
|
||||
|
||||
def _build_master(self):
|
||||
def _build_master(self, relay_servers: List[str]):
|
||||
local_network_interfaces = InfectionMonkey._get_local_network_interfaces()
|
||||
|
||||
# TODO control_channel and control_client have same responsibilities, merge them
|
||||
control_channel = ControlChannel(
|
||||
self._control_client.server_address, GUID, self._control_client.proxies
|
||||
)
|
||||
propagation_credentials_repository = AggregatingPropagationCredentialsRepository(
|
||||
control_channel
|
||||
self._control_channel
|
||||
)
|
||||
|
||||
event_queue = PyPubSubAgentEventQueue(Publisher())
|
||||
|
@ -226,15 +234,16 @@ class InfectionMonkey:
|
|||
victim_host_factory = self._build_victim_host_factory(local_network_interfaces)
|
||||
|
||||
telemetry_messenger = ExploitInterceptingTelemetryMessenger(
|
||||
self._telemetry_messenger, self._monkey_inbound_tunnel
|
||||
self._telemetry_messenger, self._relay
|
||||
)
|
||||
|
||||
self._master = AutomatedMaster(
|
||||
self._current_depth,
|
||||
self._opts.servers + relay_servers,
|
||||
puppet,
|
||||
telemetry_messenger,
|
||||
victim_host_factory,
|
||||
control_channel,
|
||||
self._control_channel,
|
||||
local_network_interfaces,
|
||||
propagation_credentials_repository,
|
||||
)
|
||||
|
@ -284,7 +293,7 @@ class InfectionMonkey:
|
|||
puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER)
|
||||
|
||||
agent_binary_repository = CachingAgentBinaryRepository(
|
||||
f"https://{self._control_client.server_address}", self._control_client.proxies
|
||||
f"https://{self._control_client.server_address}"
|
||||
)
|
||||
exploit_wrapper = ExploiterWrapper(
|
||||
self._telemetry_messenger, event_queue, agent_binary_repository
|
||||
|
@ -384,9 +393,7 @@ class InfectionMonkey:
|
|||
on_island = self._running_on_island(local_network_interfaces)
|
||||
logger.debug(f"This agent is running on the island: {on_island}")
|
||||
|
||||
return VictimHostFactory(
|
||||
self._monkey_inbound_tunnel, self._cmd_island_ip, self._cmd_island_port, on_island
|
||||
)
|
||||
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, _ = address_to_ip_port(self._control_client.server_address)
|
||||
|
@ -403,10 +410,7 @@ class InfectionMonkey:
|
|||
self._master.cleanup()
|
||||
|
||||
reset_signal_handlers()
|
||||
|
||||
if self._inbound_tunnel_opened:
|
||||
self._monkey_inbound_tunnel.stop()
|
||||
self._monkey_inbound_tunnel.join()
|
||||
self._stop_relay()
|
||||
|
||||
if firewall.is_enabled():
|
||||
firewall.remove_firewall_rule()
|
||||
|
@ -421,21 +425,28 @@ class InfectionMonkey:
|
|||
).send() # Signal the server (before closing the tunnel)
|
||||
|
||||
self._close_tunnel()
|
||||
self._singleton.unlock()
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred while cleaning up the monkey agent: {e}")
|
||||
if deleted is None:
|
||||
InfectionMonkey._self_delete()
|
||||
finally:
|
||||
self._singleton.unlock()
|
||||
|
||||
logger.info("Monkey is shutting down")
|
||||
|
||||
def _stop_relay(self):
|
||||
if self._relay and self._relay.is_alive():
|
||||
self._relay.stop()
|
||||
|
||||
while self._relay.is_alive() and not self._control_channel.should_agent_stop():
|
||||
self._relay.join(timeout=5)
|
||||
|
||||
if self._control_channel.should_agent_stop():
|
||||
self._relay.join(timeout=60)
|
||||
|
||||
def _close_tunnel(self):
|
||||
tunnel_address = (
|
||||
self._control_client.proxies.get("https", "").replace("http://", "").split(":")[0]
|
||||
)
|
||||
if tunnel_address:
|
||||
logger.info("Quitting tunnel %s", tunnel_address)
|
||||
tunnel.quit_tunnel(tunnel_address)
|
||||
logger.info(f"Quitting tunnel {self._cmd_island_ip}")
|
||||
notify_disconnect(self._cmd_island_ip, self._cmd_island_port)
|
||||
|
||||
def _send_log(self):
|
||||
monkey_log_path = get_agent_log_path()
|
||||
|
|
|
@ -3,14 +3,18 @@ import socket
|
|||
import struct
|
||||
from dataclasses import dataclass
|
||||
from ipaddress import IPv4Interface
|
||||
from random import randint # noqa: DUO102
|
||||
from typing import List
|
||||
from random import shuffle # noqa: DUO102
|
||||
from threading import Lock
|
||||
from typing import Dict, List, Set
|
||||
|
||||
import netifaces
|
||||
import psutil
|
||||
from egg_timer import EggTimer
|
||||
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
|
||||
from .ports import COMMON_PORTS
|
||||
|
||||
# Timeout for monkey connections
|
||||
LOOPBACK_NAME = b"lo"
|
||||
SIOCGIFADDR = 0x8915 # get PA address
|
||||
|
@ -118,16 +122,93 @@ else:
|
|||
return routes
|
||||
|
||||
|
||||
def get_free_tcp_port(min_range=1024, max_range=65535):
|
||||
min_range = max(1, min_range)
|
||||
max_range = min(65535, max_range)
|
||||
class TCPPortSelector:
|
||||
"""
|
||||
Select an available TCP port that a new server can listen on
|
||||
|
||||
in_use = [conn.laddr[1] for conn in psutil.net_connections()]
|
||||
Examines the system to find which ports are not in use and makes an intelligent decision
|
||||
regarding what port can be used to host a server. In multithreaded applications, a race occurs
|
||||
between the time when the OS reports that a port is free and when the port is actually used. In
|
||||
other words, two threads which request a free port simultaneously may be handed the same port,
|
||||
as the OS will report that the port is not in use. To combat this, the TCPPortSelector will
|
||||
reserve a port for a period of time to give the requester ample time to start their server. Once
|
||||
the requester's server is listening on the port, the OS will report the port as "LISTEN".
|
||||
"""
|
||||
|
||||
for i in range(min_range, max_range):
|
||||
port = randint(min_range, max_range)
|
||||
def __init__(self):
|
||||
self._leases: Dict[int, EggTimer] = {}
|
||||
self._lock = Lock()
|
||||
|
||||
if port not in in_use:
|
||||
def get_free_tcp_port(
|
||||
self, min_range: int = 1024, max_range: int = 65535, lease_time_sec: float = 30
|
||||
):
|
||||
"""
|
||||
Get a free TCP port that a new server can listen on
|
||||
|
||||
This function will attempt to provide a well-known port that the caller can listen on. If no
|
||||
well-known ports are available, a random port will be selected.
|
||||
|
||||
:param min_range: The smallest port number a random port can be chosen from, defaults to
|
||||
1024
|
||||
:param max_range: The largest port number a random port can be chosen from, defaults to
|
||||
65535
|
||||
:param lease_time_sec: The amount of time a port should be reserved for if the OS does not report
|
||||
it as in use, defaults to 30 seconds
|
||||
:return: A TCP port number
|
||||
"""
|
||||
with self._lock:
|
||||
ports_in_use = {conn.laddr[1] for conn in psutil.net_connections()}
|
||||
|
||||
common_port = self._get_free_common_port(ports_in_use, lease_time_sec)
|
||||
if common_port is not None:
|
||||
return common_port
|
||||
|
||||
return self._get_free_random_port(ports_in_use, min_range, max_range, lease_time_sec)
|
||||
|
||||
def _get_free_common_port(self, ports_in_use: Set[int], lease_time_sec):
|
||||
for port in COMMON_PORTS:
|
||||
if self._port_is_available(port, ports_in_use):
|
||||
self._reserve_port(port, lease_time_sec)
|
||||
return port
|
||||
|
||||
return None
|
||||
|
||||
def _get_free_random_port(
|
||||
self, ports_in_use: Set[int], min_range: int, max_range: int, lease_time_sec: float
|
||||
):
|
||||
min_range = max(1, min_range)
|
||||
max_range = min(65535, max_range)
|
||||
ports = list(range(min_range, max_range))
|
||||
shuffle(ports)
|
||||
for port in ports:
|
||||
if self._port_is_available(port, ports_in_use):
|
||||
self._reserve_port(port, lease_time_sec)
|
||||
return port
|
||||
|
||||
return None
|
||||
|
||||
def _port_is_available(self, port: int, ports_in_use: Set[int]):
|
||||
if port in ports_in_use:
|
||||
return False
|
||||
|
||||
if port not in self._leases:
|
||||
return True
|
||||
|
||||
if self._leases[port].is_expired():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _reserve_port(self, port: int, lease_time_sec: float):
|
||||
timer = EggTimer()
|
||||
timer.set(lease_time_sec)
|
||||
self._leases[port] = timer
|
||||
|
||||
|
||||
# TODO: This function is here because existing components rely on it. Refactor these components to
|
||||
# accept a TCPPortSelector instance and use that instead.
|
||||
def get_free_tcp_port(min_range=1024, max_range=65535, lease_time_sec=30):
|
||||
return get_free_tcp_port.port_selector.get_free_tcp_port(min_range, max_range, lease_time_sec)
|
||||
|
||||
|
||||
get_free_tcp_port.port_selector = TCPPortSelector() # type: ignore[attr-defined]
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from typing import List
|
||||
|
||||
COMMON_PORTS: List[int] = [
|
||||
1025, # NFS, IIS
|
||||
1433, # Microsoft SQL Server
|
||||
1434, # Microsoft SQL Monitor
|
||||
1720, # h323q931
|
||||
1723, # Microsoft PPTP VPN
|
||||
3306, # mysql
|
||||
3389, # Windows Terminal Server (RDP)
|
||||
5900, # vnc
|
||||
6001, # X11:1
|
||||
8080, # http-proxy
|
||||
8888, # sun-answerbook
|
||||
]
|
|
@ -0,0 +1,10 @@
|
|||
from .relay_connection_handler import (
|
||||
RelayConnectionHandler,
|
||||
RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST,
|
||||
)
|
||||
from .relay_user_handler import RelayUser, RelayUserHandler
|
||||
from .sockets_pipe import SocketsPipe
|
||||
from .tcp_connection_handler import TCPConnectionHandler
|
||||
from .tcp_pipe_spawner import TCPPipeSpawner
|
||||
from .tcp_relay import TCPRelay
|
||||
from .utils import notify_disconnect
|
|
@ -0,0 +1 @@
|
|||
SOCKET_TIMEOUT = 10
|
|
@ -0,0 +1,40 @@
|
|||
import socket
|
||||
from ipaddress import IPv4Address
|
||||
from logging import getLogger
|
||||
|
||||
from .relay_user_handler import RelayUserHandler
|
||||
from .tcp_pipe_spawner import TCPPipeSpawner
|
||||
|
||||
RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST = b"infection-monkey-relay-control-message: -"
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class RelayConnectionHandler:
|
||||
"""Handles new relay connections."""
|
||||
|
||||
def __init__(self, pipe_spawner: TCPPipeSpawner, relay_user_handler: RelayUserHandler):
|
||||
self._pipe_spawner = pipe_spawner
|
||||
self._relay_user_handler = relay_user_handler
|
||||
|
||||
def handle_new_connection(self, sock: socket.socket):
|
||||
"""
|
||||
Spawn a new pipe, or remove the user if the user requested to disconnect.
|
||||
|
||||
:param sock: The socket for the new connection.
|
||||
"""
|
||||
addr, _ = sock.getpeername()
|
||||
addr = IPv4Address(addr)
|
||||
|
||||
control_message = sock.recv(
|
||||
len(RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST), socket.MSG_PEEK
|
||||
)
|
||||
|
||||
if control_message.startswith(RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST):
|
||||
self._relay_user_handler.disconnect_user(addr)
|
||||
else:
|
||||
try:
|
||||
self._pipe_spawner.spawn_pipe(sock)
|
||||
self._relay_user_handler.add_relay_user(addr)
|
||||
except OSError as err:
|
||||
logger.debug(f"Failed to spawn pipe: {err}")
|
|
@ -0,0 +1,106 @@
|
|||
from dataclasses import dataclass
|
||||
from ipaddress import IPv4Address
|
||||
from logging import getLogger
|
||||
from threading import Lock
|
||||
from typing import Dict
|
||||
|
||||
from egg_timer import EggTimer
|
||||
|
||||
from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT
|
||||
from common.utils.code_utils import del_key
|
||||
|
||||
# Wait for potential new clients to connect
|
||||
DEFAULT_NEW_CLIENT_TIMEOUT = 2.5 * MEDIUM_REQUEST_TIMEOUT
|
||||
DEFAULT_DISCONNECT_TIMEOUT = 60 * 2 # Wait up to 2 minutes for clients to disconnect
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RelayUser:
|
||||
address: IPv4Address
|
||||
timer: EggTimer
|
||||
|
||||
|
||||
class RelayUserHandler:
|
||||
"""Manages membership to a network relay."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
new_client_timeout: float = DEFAULT_NEW_CLIENT_TIMEOUT,
|
||||
client_disconnect_timeout: float = DEFAULT_DISCONNECT_TIMEOUT,
|
||||
):
|
||||
self._new_client_timeout = new_client_timeout
|
||||
self._client_disconnect_timeout = client_disconnect_timeout
|
||||
self._relay_users: Dict[IPv4Address, RelayUser] = {}
|
||||
self._potential_users: Dict[IPv4Address, RelayUser] = {}
|
||||
|
||||
self._lock = Lock()
|
||||
|
||||
def add_relay_user(self, user_address: IPv4Address):
|
||||
"""
|
||||
Handle new user connection.
|
||||
|
||||
:param source_socket: A source socket
|
||||
:param user_address: An address defining RelayUser which will be added to the relay
|
||||
"""
|
||||
|
||||
with self._lock:
|
||||
if user_address in self._potential_users:
|
||||
del_key(self._potential_users, user_address)
|
||||
|
||||
timer = EggTimer()
|
||||
timer.set(self._client_disconnect_timeout)
|
||||
user = RelayUser(user_address, timer)
|
||||
self._relay_users[user_address] = user
|
||||
logger.debug(f"Added relay user {user}")
|
||||
|
||||
def add_potential_user(self, user_address: IPv4Address):
|
||||
"""
|
||||
Notify RelayUserHandler that a new user may try and connect.
|
||||
|
||||
:param user_address: An address defining potential RelayUser
|
||||
that tries to connect to the relay
|
||||
"""
|
||||
with self._lock:
|
||||
timer = EggTimer()
|
||||
timer.set(self._new_client_timeout)
|
||||
user = RelayUser(user_address, timer)
|
||||
self._potential_users[user_address] = user
|
||||
logger.debug(f"Added potential relay user {user}")
|
||||
|
||||
def disconnect_user(self, user_address: IPv4Address):
|
||||
"""
|
||||
Handle when a user disconnects.
|
||||
|
||||
:param user_address: The address of the disconnecting user.
|
||||
"""
|
||||
with self._lock:
|
||||
if user_address in self._relay_users:
|
||||
logger.debug(f"Disconnected user {user_address}")
|
||||
del_key(self._relay_users, user_address)
|
||||
|
||||
def has_potential_users(self) -> bool:
|
||||
"""
|
||||
Return whether or not we have any potential users.
|
||||
"""
|
||||
with self._lock:
|
||||
self._potential_users = RelayUserHandler._remove_expired_users(self._potential_users)
|
||||
|
||||
return len(self._potential_users) > 0
|
||||
|
||||
def has_connected_users(self) -> bool:
|
||||
"""
|
||||
Return whether or not we have any relay users.
|
||||
"""
|
||||
with self._lock:
|
||||
self._relay_users = RelayUserHandler._remove_expired_users(self._relay_users)
|
||||
|
||||
return len(self._relay_users) > 0
|
||||
|
||||
@staticmethod
|
||||
def _remove_expired_users(
|
||||
user_list: Dict[IPv4Address, RelayUser]
|
||||
) -> Dict[IPv4Address, RelayUser]:
|
||||
return dict(filter(lambda ru: not ru[1].timer.is_expired(), user_list.items()))
|
|
@ -0,0 +1,75 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import select
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
from typing import Callable
|
||||
|
||||
from .consts import SOCKET_TIMEOUT
|
||||
|
||||
READ_BUFFER_SIZE = 8192
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class SocketsPipe(Thread):
|
||||
"""Manages a pipe between two sockets."""
|
||||
|
||||
_thread_count: int = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source,
|
||||
dest,
|
||||
pipe_closed: Callable[[SocketsPipe], None],
|
||||
timeout=SOCKET_TIMEOUT,
|
||||
):
|
||||
self.source = source
|
||||
self.dest = dest
|
||||
self.timeout = timeout
|
||||
super().__init__(name=f"SocketsPipeThread-{self._next_thread_num()}", daemon=True)
|
||||
self._pipe_closed = pipe_closed
|
||||
|
||||
@classmethod
|
||||
def _next_thread_num(cls):
|
||||
cls._thread_count += 1
|
||||
return cls._thread_count
|
||||
|
||||
def _pipe(self):
|
||||
sockets = [self.source, self.dest]
|
||||
socket_closed = False
|
||||
|
||||
while not socket_closed:
|
||||
read_list, _, except_list = select.select(sockets, [], sockets, self.timeout)
|
||||
if except_list:
|
||||
raise OSError("select() failed on sockets {except_list}")
|
||||
|
||||
if not read_list:
|
||||
raise TimeoutError("pipe did not receive data for {self.timeout} seconds")
|
||||
|
||||
for r in read_list:
|
||||
other = self.dest if r is self.source else self.source
|
||||
data = r.recv(READ_BUFFER_SIZE)
|
||||
if data:
|
||||
other.sendall(data)
|
||||
else:
|
||||
socket_closed = True
|
||||
break
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self._pipe()
|
||||
except OSError as err:
|
||||
logger.debug(err)
|
||||
|
||||
try:
|
||||
self.source.close()
|
||||
except OSError as err:
|
||||
logger.debug(f"Error while closing source socket: {err}")
|
||||
|
||||
try:
|
||||
self.dest.close()
|
||||
except OSError as err:
|
||||
logger.debug(f"Error while closing destination socket: {err}")
|
||||
|
||||
self._pipe_closed(self)
|
|
@ -0,0 +1,50 @@
|
|||
import logging
|
||||
import socket
|
||||
from threading import Thread
|
||||
from typing import Callable, List
|
||||
|
||||
from infection_monkey.utils.threading import InterruptableThreadMixin
|
||||
|
||||
PROXY_TIMEOUT = 2.5
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TCPConnectionHandler(Thread, InterruptableThreadMixin):
|
||||
"""Accepts connections on a TCP socket."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bind_host: str,
|
||||
bind_port: int,
|
||||
client_connected: List[Callable[[socket.socket], None]] = [],
|
||||
):
|
||||
self.bind_host = bind_host
|
||||
self.bind_port = bind_port
|
||||
self._client_connected = client_connected
|
||||
|
||||
Thread.__init__(self, name="TCPConnectionHandler", daemon=True)
|
||||
InterruptableThreadMixin.__init__(self)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
l_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
l_socket.bind((self.bind_host, self.bind_port))
|
||||
l_socket.settimeout(PROXY_TIMEOUT)
|
||||
l_socket.listen(5)
|
||||
|
||||
while not self._interrupted.is_set():
|
||||
try:
|
||||
source, _ = l_socket.accept()
|
||||
except socket.timeout:
|
||||
continue
|
||||
|
||||
logging.debug(f"New connection received from: {source.getpeername()}")
|
||||
for notify_client_connected in self._client_connected:
|
||||
notify_client_connected(source)
|
||||
except OSError:
|
||||
logging.exception("Uncaught error in TCPConnectionHandler thread")
|
||||
finally:
|
||||
l_socket.close()
|
||||
|
||||
logging.info("Exiting connection handler.")
|
|
@ -0,0 +1,58 @@
|
|||
import socket
|
||||
from ipaddress import IPv4Address
|
||||
from logging import getLogger
|
||||
from threading import Lock
|
||||
from typing import Set
|
||||
|
||||
from .consts import SOCKET_TIMEOUT
|
||||
from .sockets_pipe import SocketsPipe
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class TCPPipeSpawner:
|
||||
"""
|
||||
Creates bi-directional pipes between the configured client and other clients.
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
def spawn_pipe(self, source: socket.socket):
|
||||
"""
|
||||
Attempt to create a pipe on between the configured client and the provided socket
|
||||
|
||||
:param source: A socket to the connecting client.
|
||||
:raises OSError: If a socket to the configured client could not be created.
|
||||
"""
|
||||
dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
dest.settimeout(SOCKET_TIMEOUT)
|
||||
try:
|
||||
dest.connect((str(self._target_addr), self._target_port))
|
||||
except OSError as err:
|
||||
source.close()
|
||||
dest.close()
|
||||
raise err
|
||||
|
||||
pipe = SocketsPipe(source, dest, self._handle_pipe_closed)
|
||||
with self._lock:
|
||||
self._pipes.add(pipe)
|
||||
|
||||
pipe.start()
|
||||
|
||||
def has_open_pipes(self) -> bool:
|
||||
"""Return whether or not the TCPPipeSpawner has any open pipes."""
|
||||
with self._lock:
|
||||
for p in self._pipes:
|
||||
if p.is_alive():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _handle_pipe_closed(self, pipe: SocketsPipe):
|
||||
with self._lock:
|
||||
logger.debug(f"Closing pipe {pipe}")
|
||||
self._pipes.discard(pipe)
|
|
@ -0,0 +1,77 @@
|
|||
from ipaddress import IPv4Address
|
||||
from logging import getLogger
|
||||
from threading import Lock, Thread
|
||||
from time import sleep
|
||||
|
||||
from infection_monkey.network.relay import (
|
||||
RelayConnectionHandler,
|
||||
RelayUserHandler,
|
||||
TCPConnectionHandler,
|
||||
TCPPipeSpawner,
|
||||
)
|
||||
from infection_monkey.utils.threading import InterruptableThreadMixin
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class TCPRelay(Thread, InterruptableThreadMixin):
|
||||
"""
|
||||
Provides and manages a TCP proxy connection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
relay_port: int,
|
||||
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_addr, dest_port)
|
||||
relay_filter = RelayConnectionHandler(self._pipe_spawner, self._user_handler)
|
||||
self._connection_handler = TCPConnectionHandler(
|
||||
bind_host="",
|
||||
bind_port=relay_port,
|
||||
client_connected=[
|
||||
relay_filter.handle_new_connection,
|
||||
],
|
||||
)
|
||||
super().__init__(name="MonkeyTcpRelayThread", daemon=True)
|
||||
InterruptableThreadMixin.__init__(self)
|
||||
self._lock = Lock()
|
||||
|
||||
def run(self):
|
||||
self._connection_handler.start()
|
||||
|
||||
self._interrupted.wait()
|
||||
self._wait_for_users_to_disconnect()
|
||||
|
||||
self._connection_handler.stop()
|
||||
self._connection_handler.join()
|
||||
self._wait_for_pipes_to_close()
|
||||
logger.info("TCP Relay closed.")
|
||||
|
||||
def add_potential_user(self, user_address: IPv4Address):
|
||||
"""
|
||||
Notify TCPRelay of a user that may try to connect.
|
||||
|
||||
:param user_address: The address of the potential new user.
|
||||
"""
|
||||
self._user_handler.add_potential_user(user_address)
|
||||
|
||||
def _wait_for_users_to_disconnect(self):
|
||||
"""
|
||||
Blocks until the users disconnect or the timeout has elapsed.
|
||||
"""
|
||||
while self._user_handler.has_potential_users() or self._user_handler.has_connected_users():
|
||||
sleep(0.5)
|
||||
|
||||
def _wait_for_pipes_to_close(self):
|
||||
"""
|
||||
Blocks until the pipes have closed.
|
||||
"""
|
||||
while self._pipe_spawner.has_open_pipes():
|
||||
sleep(0.5)
|
|
@ -0,0 +1,101 @@
|
|||
import logging
|
||||
import socket
|
||||
from contextlib import suppress
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict, Iterable, Iterator, MutableMapping, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT
|
||||
from common.network.network_utils import address_to_ip_port
|
||||
from infection_monkey.network.relay import RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST
|
||||
from infection_monkey.utils.threading import (
|
||||
ThreadSafeIterator,
|
||||
create_daemon_thread,
|
||||
run_worker_threads,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The number of Island servers to test simultaneously. 32 threads seems large enough for all
|
||||
# practical purposes. Revisit this if it's not.
|
||||
NUM_FIND_SERVER_WORKERS = 32
|
||||
|
||||
|
||||
def find_server(servers: Iterable[str]) -> Optional[str]:
|
||||
server_list = list(servers)
|
||||
server_iterator = ThreadSafeIterator(server_list.__iter__())
|
||||
server_results: Dict[str, bool] = {}
|
||||
|
||||
run_worker_threads(
|
||||
_find_island_server,
|
||||
"FindIslandServer",
|
||||
args=(server_iterator, server_results),
|
||||
num_workers=NUM_FIND_SERVER_WORKERS,
|
||||
)
|
||||
|
||||
for server in server_list:
|
||||
if server_results[server]:
|
||||
return server
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_island_server(servers: Iterator[str], server_status: MutableMapping[str, bool]):
|
||||
with suppress(StopIteration):
|
||||
server = next(servers)
|
||||
server_status[server] = _check_if_island_server(server)
|
||||
|
||||
|
||||
def _check_if_island_server(server: str) -> bool:
|
||||
logger.debug(f"Trying to connect to server: {server}")
|
||||
|
||||
try:
|
||||
requests.get( # noqa: DUO123
|
||||
f"https://{server}/api?action=is-up",
|
||||
verify=False,
|
||||
timeout=MEDIUM_REQUEST_TIMEOUT,
|
||||
)
|
||||
|
||||
return True
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
logger.error(f"Unable to connect to server/relay {server}: {err}")
|
||||
except TimeoutError as err:
|
||||
logger.error(f"Timed out while connecting to server/relay {server}: {err}")
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Exception encountered when trying to connect to server/relay {server}: {err}"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def send_remove_from_waitlist_control_message_to_relays(servers: Iterable[str]):
|
||||
for server in servers:
|
||||
t = create_daemon_thread(
|
||||
target=_send_remove_from_waitlist_control_message_to_relay,
|
||||
name="SendRemoveFromWaitlistControlMessageToRelaysThread",
|
||||
args=(server,),
|
||||
)
|
||||
t.start()
|
||||
|
||||
|
||||
def _send_remove_from_waitlist_control_message_to_relay(server: str):
|
||||
ip, port = address_to_ip_port(server)
|
||||
notify_disconnect(IPv4Address(ip), int(port))
|
||||
|
||||
|
||||
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:
|
||||
try:
|
||||
d_socket.connect((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_ip}:{server_port}")
|
||||
except OSError as err:
|
||||
logger.error(f"Error connecting to socket {server_ip}:{server_port}: {err}")
|
|
@ -70,11 +70,12 @@ class Puppet(IPuppet):
|
|||
name: str,
|
||||
host: VictimHost,
|
||||
current_depth: int,
|
||||
servers: Sequence[str],
|
||||
options: Dict,
|
||||
interrupt: threading.Event,
|
||||
) -> ExploiterResultData:
|
||||
exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER)
|
||||
return exploiter.exploit_host(host, current_depth, options, interrupt)
|
||||
return exploiter.exploit_host(host, servers, current_depth, options, interrupt)
|
||||
|
||||
def run_payload(self, name: str, options: Dict, interrupt: threading.Event):
|
||||
payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD)
|
||||
|
|
|
@ -1,32 +1,41 @@
|
|||
from functools import singledispatch
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
from infection_monkey.network.relay import TCPRelay
|
||||
from infection_monkey.telemetry.exploit_telem import ExploitTelem
|
||||
from infection_monkey.telemetry.i_telem import ITelem
|
||||
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
|
||||
from infection_monkey.tunnel import MonkeyTunnel
|
||||
|
||||
|
||||
class ExploitInterceptingTelemetryMessenger(ITelemetryMessenger):
|
||||
def __init__(self, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel):
|
||||
def __init__(self, telemetry_messenger: ITelemetryMessenger, relay: TCPRelay):
|
||||
self._telemetry_messenger = telemetry_messenger
|
||||
self._tunnel = tunnel
|
||||
self._relay = relay
|
||||
|
||||
def send_telemetry(self, telemetry: ITelem):
|
||||
_send_telemetry(telemetry, self._telemetry_messenger, self._tunnel)
|
||||
_send_telemetry(telemetry, self._telemetry_messenger, self._relay)
|
||||
|
||||
|
||||
# Note: We can use @singledispatchmethod instead of @singledispatch if we migrate to Python 3.8 or
|
||||
# later.
|
||||
@singledispatch
|
||||
def _send_telemetry(
|
||||
telemetry: ITelem, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel
|
||||
telemetry: ITelem,
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
relay: TCPRelay,
|
||||
):
|
||||
telemetry_messenger.send_telemetry(telemetry)
|
||||
|
||||
|
||||
@_send_telemetry.register
|
||||
def _(telemetry: ExploitTelem, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel):
|
||||
def _(
|
||||
telemetry: ExploitTelem,
|
||||
telemetry_messenger: ITelemetryMessenger,
|
||||
relay: TCPRelay,
|
||||
):
|
||||
if telemetry.propagation_result is True:
|
||||
tunnel.set_wait_for_exploited_machines()
|
||||
if relay:
|
||||
address = IPv4Address(str(telemetry.host["ip_addr"]))
|
||||
relay.add_potential_user(address)
|
||||
|
||||
telemetry_messenger.send_telemetry(telemetry)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
from typing import Mapping
|
||||
|
||||
from common.common_consts.telem_categories import TelemCategoryEnum
|
||||
from infection_monkey.telemetry.base_telem import BaseTelem
|
||||
|
||||
|
||||
class TunnelTelem(BaseTelem):
|
||||
def __init__(self, proxy: Mapping[str, str]):
|
||||
"""
|
||||
Default tunnel telemetry constructor
|
||||
"""
|
||||
super(TunnelTelem, self).__init__()
|
||||
self.proxy = proxy.get("https")
|
||||
|
||||
telem_category = TelemCategoryEnum.TUNNEL
|
||||
|
||||
def get_data(self):
|
||||
return {"proxy": self.proxy}
|
|
@ -1,31 +0,0 @@
|
|||
import time
|
||||
from threading import Thread
|
||||
|
||||
g_last_served = None
|
||||
PROXY_TIMEOUT = 2.5
|
||||
|
||||
|
||||
class TransportProxyBase(Thread):
|
||||
def __init__(self, local_port, dest_host=None, dest_port=None, local_host=""):
|
||||
global g_last_served
|
||||
|
||||
self.local_host = local_host
|
||||
self.local_port = local_port
|
||||
self.dest_host = dest_host
|
||||
self.dest_port = dest_port
|
||||
self._stopped = False
|
||||
super(TransportProxyBase, self).__init__()
|
||||
self.daemon = True
|
||||
|
||||
def stop(self):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
def update_last_serve_time():
|
||||
global g_last_served
|
||||
g_last_served = time.time()
|
||||
|
||||
|
||||
def get_last_serve_time():
|
||||
global g_last_served
|
||||
return g_last_served
|
|
@ -1,17 +1,9 @@
|
|||
import http.server
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
import urllib
|
||||
from logging import getLogger
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from infection_monkey.network.tools import get_interface_to_target
|
||||
from infection_monkey.transport.base import (
|
||||
PROXY_TIMEOUT,
|
||||
TransportProxyBase,
|
||||
update_last_serve_time,
|
||||
)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
@ -110,56 +102,6 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
|||
)
|
||||
|
||||
|
||||
class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler):
|
||||
timeout = 30 # timeout with clients, set to None not to make persistent connection
|
||||
|
||||
def version_string(self):
|
||||
return ""
|
||||
|
||||
def do_CONNECT(self):
|
||||
logger.info("Received a connect request!")
|
||||
# just provide a tunnel, transfer the data with no modification
|
||||
req = self
|
||||
req.path = "https://%s/" % req.path.replace(":443", "")
|
||||
|
||||
u = urlsplit(req.path)
|
||||
address = (u.hostname, u.port or 443)
|
||||
try:
|
||||
conn = socket.create_connection(address)
|
||||
except socket.error as e:
|
||||
logger.debug(
|
||||
"HTTPConnectProxyHandler: Got exception while trying to connect to %s: %s"
|
||||
% (repr(address), e)
|
||||
)
|
||||
self.send_error(504) # 504 Gateway Timeout
|
||||
return
|
||||
self.send_response(200, "Connection Established")
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
conns = [self.connection, conn]
|
||||
keep_connection = True
|
||||
while keep_connection:
|
||||
keep_connection = False
|
||||
rlist, wlist, xlist = select.select(conns, [], conns, self.timeout)
|
||||
if xlist:
|
||||
break
|
||||
for r in rlist:
|
||||
other = conns[1] if r is conns[0] else conns[0]
|
||||
data = r.recv(8192)
|
||||
if data:
|
||||
other.sendall(data)
|
||||
keep_connection = True
|
||||
update_last_serve_time()
|
||||
conn.close()
|
||||
|
||||
def log_message(self, format_string, *args):
|
||||
logger.debug(
|
||||
"HTTPConnectProxyHandler: %s - [%s] %s"
|
||||
% (self.address_string(), self.log_date_time_string(), format_string % args)
|
||||
)
|
||||
|
||||
|
||||
class LockedHTTPServer(threading.Thread):
|
||||
"""
|
||||
Same as HTTPServer used for file downloads just with locks to avoid racing conditions.
|
||||
|
@ -226,11 +168,3 @@ class LockedHTTPServer(threading.Thread):
|
|||
def stop(self, timeout=STOP_TIMEOUT):
|
||||
self._stopped = True
|
||||
self.join(timeout)
|
||||
|
||||
|
||||
class HTTPConnectProxy(TransportProxyBase):
|
||||
def run(self):
|
||||
httpd = http.server.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)
|
||||
httpd.timeout = PROXY_TIMEOUT
|
||||
while not self._stopped:
|
||||
httpd.handle_request()
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
import select
|
||||
import socket
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
|
||||
from infection_monkey.transport.base import (
|
||||
PROXY_TIMEOUT,
|
||||
TransportProxyBase,
|
||||
update_last_serve_time,
|
||||
)
|
||||
|
||||
READ_BUFFER_SIZE = 8192
|
||||
SOCKET_READ_TIMEOUT = 10
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class SocketsPipe(Thread):
|
||||
def __init__(self, source, dest, timeout=SOCKET_READ_TIMEOUT):
|
||||
Thread.__init__(self)
|
||||
self.source = source
|
||||
self.dest = dest
|
||||
self.timeout = timeout
|
||||
self._keep_connection = True
|
||||
super(SocketsPipe, self).__init__()
|
||||
self.daemon = True
|
||||
|
||||
def run(self):
|
||||
sockets = [self.source, self.dest]
|
||||
while self._keep_connection:
|
||||
self._keep_connection = False
|
||||
rlist, wlist, xlist = select.select(sockets, [], sockets, self.timeout)
|
||||
if xlist:
|
||||
break
|
||||
for r in rlist:
|
||||
other = self.dest if r is self.source else self.source
|
||||
try:
|
||||
data = r.recv(READ_BUFFER_SIZE)
|
||||
except Exception:
|
||||
break
|
||||
if data:
|
||||
try:
|
||||
other.sendall(data)
|
||||
update_last_serve_time()
|
||||
except Exception:
|
||||
break
|
||||
self._keep_connection = True
|
||||
|
||||
self.source.close()
|
||||
self.dest.close()
|
||||
|
||||
|
||||
class TcpProxy(TransportProxyBase):
|
||||
def run(self):
|
||||
pipes = []
|
||||
l_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
l_socket.bind((self.local_host, self.local_port))
|
||||
l_socket.settimeout(PROXY_TIMEOUT)
|
||||
l_socket.listen(5)
|
||||
|
||||
while not self._stopped:
|
||||
try:
|
||||
source, address = l_socket.accept()
|
||||
except socket.timeout:
|
||||
continue
|
||||
|
||||
dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
dest.connect((self.dest_host, self.dest_port))
|
||||
except socket.error:
|
||||
source.close()
|
||||
dest.close()
|
||||
continue
|
||||
|
||||
pipe = SocketsPipe(source, dest)
|
||||
pipes.append(pipe)
|
||||
logger.debug(
|
||||
"piping sockets %s:%s->%s:%s",
|
||||
address[0],
|
||||
address[1],
|
||||
self.dest_host,
|
||||
self.dest_port,
|
||||
)
|
||||
pipe.start()
|
||||
|
||||
l_socket.close()
|
||||
for pipe in pipes:
|
||||
pipe.join()
|
|
@ -1,230 +0,0 @@
|
|||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
from threading import Event, Thread
|
||||
|
||||
from common.utils import Timer
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_free_tcp_port, local_ips
|
||||
from infection_monkey.network.tools import check_tcp_port, get_interface_to_target
|
||||
from infection_monkey.transport.base import get_last_serve_time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MCAST_GROUP = "224.1.1.1"
|
||||
MCAST_PORT = 5007
|
||||
BUFFER_READ = 1024
|
||||
DEFAULT_TIMEOUT = 10
|
||||
QUIT_TIMEOUT = 60 * 10 # 10 minutes
|
||||
|
||||
|
||||
def _set_multicast_socket(timeout=DEFAULT_TIMEOUT, adapter=""):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.settimeout(timeout)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((adapter, MCAST_PORT))
|
||||
sock.setsockopt(
|
||||
socket.IPPROTO_IP,
|
||||
socket.IP_ADD_MEMBERSHIP,
|
||||
struct.pack("4sl", socket.inet_aton(MCAST_GROUP), socket.INADDR_ANY),
|
||||
)
|
||||
return sock
|
||||
|
||||
|
||||
def _check_tunnel(address, port, existing_sock=None):
|
||||
if not existing_sock:
|
||||
sock = _set_multicast_socket()
|
||||
else:
|
||||
sock = existing_sock
|
||||
|
||||
logger.debug("Checking tunnel %s:%s", address, port)
|
||||
is_open, _ = check_tcp_port(address, int(port))
|
||||
if not is_open:
|
||||
logger.debug("Could not connect to %s:%s", address, port)
|
||||
if not existing_sock:
|
||||
sock.close()
|
||||
return False
|
||||
|
||||
try:
|
||||
sock.sendto(b"+", (address, MCAST_PORT))
|
||||
except Exception as exc:
|
||||
logger.debug("Caught exception in tunnel registration: %s", exc)
|
||||
|
||||
if not existing_sock:
|
||||
sock.close()
|
||||
return True
|
||||
|
||||
|
||||
def find_tunnel(default=None, attempts=3, timeout=DEFAULT_TIMEOUT):
|
||||
l_ips = local_ips()
|
||||
|
||||
if default:
|
||||
if default.find(":") != -1:
|
||||
address, port = default.split(":", 1)
|
||||
if _check_tunnel(address, port):
|
||||
return address, port
|
||||
|
||||
for adapter in l_ips:
|
||||
for attempt in range(0, attempts):
|
||||
try:
|
||||
logger.info("Trying to find using adapter %s", adapter)
|
||||
sock = _set_multicast_socket(timeout, adapter)
|
||||
sock.sendto(b"?", (MCAST_GROUP, MCAST_PORT))
|
||||
tunnels = []
|
||||
|
||||
while True:
|
||||
try:
|
||||
answer, address = sock.recvfrom(BUFFER_READ)
|
||||
if answer not in [b"?", b"+", b"-"]:
|
||||
tunnels.append(answer)
|
||||
except socket.timeout:
|
||||
break
|
||||
|
||||
for tunnel in tunnels:
|
||||
if tunnel.find(":") != -1:
|
||||
address, port = tunnel.split(":", 1)
|
||||
if address in l_ips:
|
||||
continue
|
||||
|
||||
if _check_tunnel(address, port, sock):
|
||||
sock.close()
|
||||
return address, port
|
||||
|
||||
except Exception as exc:
|
||||
logger.debug("Caught exception in tunnel lookup: %s", exc)
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def quit_tunnel(address, timeout=DEFAULT_TIMEOUT):
|
||||
try:
|
||||
sock = _set_multicast_socket(timeout)
|
||||
sock.sendto(b"-", (address, MCAST_PORT))
|
||||
sock.close()
|
||||
logger.debug("Success quitting tunnel")
|
||||
except Exception as exc:
|
||||
logger.debug("Exception quitting tunnel: %s", exc)
|
||||
return
|
||||
|
||||
|
||||
class MonkeyTunnel(Thread):
|
||||
def __init__(
|
||||
self,
|
||||
proxy_class,
|
||||
keep_tunnel_open_time,
|
||||
target_addr=None,
|
||||
target_port=None,
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
):
|
||||
self._target_addr = target_addr
|
||||
self._target_port = target_port
|
||||
self._proxy_class = proxy_class
|
||||
self._keep_tunnel_open_time = keep_tunnel_open_time
|
||||
self._broad_sock = None
|
||||
self._timeout = timeout
|
||||
self._stopped = Event()
|
||||
self._clients = []
|
||||
self.local_port = None
|
||||
super(MonkeyTunnel, self).__init__(name="MonkeyTunnelThread")
|
||||
self.daemon = True
|
||||
self.l_ips = None
|
||||
self._wait_for_exploited_machines = Event()
|
||||
|
||||
def run(self):
|
||||
self._broad_sock = _set_multicast_socket(self._timeout)
|
||||
self.l_ips = local_ips()
|
||||
self.local_port = get_free_tcp_port()
|
||||
|
||||
if not self.local_port:
|
||||
return
|
||||
|
||||
if not firewall.listen_allowed(localport=self.local_port):
|
||||
logger.info("Machine firewalled, listen not allowed, not running tunnel.")
|
||||
return
|
||||
|
||||
proxy = self._proxy_class(
|
||||
local_port=self.local_port, dest_host=self._target_addr, dest_port=self._target_port
|
||||
)
|
||||
logger.info(
|
||||
"Running tunnel using proxy class: %s, listening on port %s, routing to: %s:%s",
|
||||
proxy.__class__.__name__,
|
||||
self.local_port,
|
||||
self._target_addr,
|
||||
self._target_port,
|
||||
)
|
||||
proxy.start()
|
||||
|
||||
while not self._stopped.is_set():
|
||||
try:
|
||||
search, address = self._broad_sock.recvfrom(BUFFER_READ)
|
||||
if b"?" == search:
|
||||
ip_match = get_interface_to_target(address[0])
|
||||
if ip_match:
|
||||
answer = "%s:%d" % (ip_match, self.local_port)
|
||||
logger.debug(
|
||||
"Got tunnel request from %s, answering with %s", address[0], answer
|
||||
)
|
||||
self._broad_sock.sendto(answer.encode(), (address[0], MCAST_PORT))
|
||||
elif b"+" == search:
|
||||
if not address[0] in self._clients:
|
||||
logger.debug("Tunnel control: Added %s to watchlist", address[0])
|
||||
self._clients.append(address[0])
|
||||
elif b"-" == search:
|
||||
logger.debug("Tunnel control: Removed %s from watchlist", address[0])
|
||||
self._clients = [client for client in self._clients if client != address[0]]
|
||||
|
||||
except socket.timeout:
|
||||
continue
|
||||
|
||||
logger.info("Stopping tunnel, waiting for clients: %s" % repr(self._clients))
|
||||
|
||||
# wait till all of the tunnel clients has been disconnected, or no one used the tunnel in
|
||||
# QUIT_TIMEOUT seconds
|
||||
timer = Timer()
|
||||
timer.set(self._calculate_timeout())
|
||||
while self._clients and not timer.is_expired():
|
||||
try:
|
||||
search, address = self._broad_sock.recvfrom(BUFFER_READ)
|
||||
if b"-" == search:
|
||||
logger.debug("Tunnel control: Removed %s from watchlist", address[0])
|
||||
self._clients = [client for client in self._clients if client != address[0]]
|
||||
except socket.timeout:
|
||||
continue
|
||||
|
||||
timer.set(self._calculate_timeout())
|
||||
|
||||
logger.info("Closing tunnel")
|
||||
self._broad_sock.close()
|
||||
proxy.stop()
|
||||
proxy.join()
|
||||
|
||||
def _calculate_timeout(self) -> float:
|
||||
try:
|
||||
return QUIT_TIMEOUT - (time.time() - get_last_serve_time())
|
||||
except TypeError: # get_last_serve_time() may return None
|
||||
return 0.0
|
||||
|
||||
def get_tunnel_for_ip(self, ip: str):
|
||||
|
||||
if not self.local_port:
|
||||
return
|
||||
|
||||
ip_match = get_interface_to_target(ip)
|
||||
return "%s:%d" % (ip_match, self.local_port)
|
||||
|
||||
def set_wait_for_exploited_machines(self):
|
||||
self._wait_for_exploited_machines.set()
|
||||
|
||||
def stop(self):
|
||||
self._wait_for_exploited_machine_connection()
|
||||
self._stopped.set()
|
||||
|
||||
def _wait_for_exploited_machine_connection(self):
|
||||
if self._wait_for_exploited_machines.is_set():
|
||||
logger.info(
|
||||
f"Waiting {self._keep_tunnel_open_time} seconds for exploited machines to connect "
|
||||
"to the tunnel."
|
||||
)
|
||||
time.sleep(self._keep_tunnel_open_time)
|
|
@ -1,20 +1,20 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from infection_monkey.config import GUID
|
||||
from infection_monkey.exploit.tools.helpers import AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64
|
||||
from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG
|
||||
from infection_monkey.model.host import VictimHost
|
||||
|
||||
# Dropper target paths
|
||||
DROPPER_TARGET_PATH_LINUX = AGENT_BINARY_PATH_LINUX
|
||||
DROPPER_TARGET_PATH_WIN64 = AGENT_BINARY_PATH_WIN64
|
||||
|
||||
|
||||
def build_monkey_commandline(target_host: VictimHost, depth: int, location: str = None) -> str:
|
||||
def build_monkey_commandline(servers: List[str], depth: int, location: Optional[str] = None) -> str:
|
||||
|
||||
return " " + " ".join(
|
||||
build_monkey_commandline_explicitly(
|
||||
GUID,
|
||||
target_host.default_tunnel,
|
||||
target_host.default_server,
|
||||
servers,
|
||||
depth,
|
||||
location,
|
||||
)
|
||||
|
@ -22,23 +22,19 @@ def build_monkey_commandline(target_host: VictimHost, depth: int, location: str
|
|||
|
||||
|
||||
def build_monkey_commandline_explicitly(
|
||||
parent: str = None,
|
||||
tunnel: str = None,
|
||||
server: str = None,
|
||||
depth: int = None,
|
||||
location: str = None,
|
||||
) -> list:
|
||||
parent: Optional[str] = None,
|
||||
servers: Optional[List[str]] = None,
|
||||
depth: Optional[int] = None,
|
||||
location: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
cmdline = []
|
||||
|
||||
if parent is not None:
|
||||
cmdline.append("-p")
|
||||
cmdline.append(str(parent))
|
||||
if tunnel is not None:
|
||||
cmdline.append("-t")
|
||||
cmdline.append(str(tunnel))
|
||||
if server is not None:
|
||||
if servers:
|
||||
cmdline.append("-s")
|
||||
cmdline.append(str(server))
|
||||
cmdline.append(",".join(servers))
|
||||
if depth is not None:
|
||||
cmdline.append("-d")
|
||||
cmdline.append(str(depth))
|
||||
|
@ -49,13 +45,13 @@ def build_monkey_commandline_explicitly(
|
|||
return cmdline
|
||||
|
||||
|
||||
def get_monkey_commandline_windows(destination_path: str, monkey_cmd_args: list) -> list:
|
||||
def get_monkey_commandline_windows(destination_path: str, monkey_cmd_args: List[str]) -> List[str]:
|
||||
monkey_cmdline = [CMD_EXE, CMD_CARRY_OUT, destination_path, MONKEY_ARG]
|
||||
|
||||
return monkey_cmdline + monkey_cmd_args
|
||||
|
||||
|
||||
def get_monkey_commandline_linux(destination_path: str, monkey_cmd_args: list) -> list:
|
||||
def get_monkey_commandline_linux(destination_path: str, monkey_cmd_args: List[str]) -> List[str]:
|
||||
monkey_cmdline = [destination_path, MONKEY_ARG]
|
||||
|
||||
return monkey_cmdline + monkey_cmd_args
|
||||
|
|
|
@ -1,2 +1,10 @@
|
|||
def maximum_depth_reached(maximum_depth: int, current_depth: int) -> bool:
|
||||
return maximum_depth > current_depth
|
||||
"""
|
||||
Return whether or not the current depth has eclipsed the maximum depth.
|
||||
Values are nonnegative. Depth should increase from zero.
|
||||
|
||||
:param maximum_depth: The maximum depth.
|
||||
:param current_depth: The current depth.
|
||||
:return: True if the current depth has reached the maximum depth, otherwise False.
|
||||
"""
|
||||
return current_depth >= maximum_depth
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import logging
|
||||
from functools import wraps
|
||||
from itertools import count
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Callable, Iterable, Optional, Tuple
|
||||
from threading import Event, Lock, Thread
|
||||
from typing import Any, Callable, Iterable, Iterator, Optional, Tuple, TypeVar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -107,3 +107,28 @@ def interruptible_function(*, msg: Optional[str] = None, default_return_value: A
|
|||
return _wrapper
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
class InterruptableThreadMixin:
|
||||
def __init__(self):
|
||||
self._interrupted = Event()
|
||||
|
||||
def stop(self):
|
||||
"""Stop a running thread."""
|
||||
self._interrupted.set()
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ThreadSafeIterator(Iterator[T]):
|
||||
"""Provides a thread-safe iterator that wraps another iterator"""
|
||||
|
||||
def __init__(self, iterator: Iterator[T]):
|
||||
self._lock = Lock()
|
||||
self._iterator = iterator
|
||||
|
||||
def __next__(self) -> T:
|
||||
while True:
|
||||
with self._lock:
|
||||
return next(self._iterator)
|
||||
|
|
|
@ -72,11 +72,11 @@
|
|||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
|
||||
"sha256:43dadad18a7f168740e66944e4fa82c6611848ff9056ad910f8f7a3e46ab89e0",
|
||||
"sha256:cffdcd380919da6137f76633531a5817e3a9f268575c128249fb637e4f9e73fb"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.6.15"
|
||||
"version": "==2022.6.15.1"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
|
@ -164,6 +164,14 @@
|
|||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.3"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da",
|
||||
"sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"
|
||||
],
|
||||
"markers": "platform_system == 'Windows'",
|
||||
"version": "==0.4.5"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a",
|
||||
|
@ -236,6 +244,13 @@
|
|||
"index": "pypi",
|
||||
"version": "==0.3.9"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"gevent": {
|
||||
"hashes": [
|
||||
"sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397",
|
||||
|
@ -485,6 +500,7 @@
|
|||
"hashes": [
|
||||
"sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==2022.5.30"
|
||||
},
|
||||
|
@ -641,6 +657,7 @@
|
|||
"sha256:81a3ebc33b1367f301d1c8eda57eec4868e951504986d5d3fe437479dcdac5b2",
|
||||
"sha256:8455176fd1b86de97d859fed4ae0ef867bf998581f584c7a1a591246dfec330f",
|
||||
"sha256:845b178bd127bb074835d2eac635b980c58ec5e700ebadc8355062df708d5a71",
|
||||
"sha256:858af7c2ab98f21ed06b642578b769ecfcabe4754648b033168a91536f7beef9",
|
||||
"sha256:87e18f29bac4a6be76a30e74de9c9005475e27100acf0830679420ce1fd9a6fd",
|
||||
"sha256:89d7baa847383b9814de640c6f1a8553d125ec65e2761ad146ea2e75a7ad197c",
|
||||
"sha256:8c7ad5cab282f53b9d78d51504330d1c88c83fbe187e472c07e6908a0293142e",
|
||||
|
@ -741,14 +758,32 @@
|
|||
"version": "==2022.2.1"
|
||||
},
|
||||
"pywin32": {
|
||||
"sys_platform": "== 'win32'",
|
||||
"version": "*"
|
||||
"hashes": [
|
||||
"sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc",
|
||||
"sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269",
|
||||
"sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3",
|
||||
"sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48",
|
||||
"sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4",
|
||||
"sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96",
|
||||
"sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df",
|
||||
"sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1",
|
||||
"sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43",
|
||||
"sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9",
|
||||
"sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f",
|
||||
"sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988",
|
||||
"sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e",
|
||||
"sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==304"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
"sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
|
||||
"sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
|
@ -965,11 +1000,11 @@
|
|||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
|
||||
"sha256:43dadad18a7f168740e66944e4fa82c6611848ff9056ad910f8f7a3e46ab89e0",
|
||||
"sha256:cffdcd380919da6137f76633531a5817e3a9f268575c128249fb637e4f9e73fb"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.6.15"
|
||||
"version": "==2022.6.15.1"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
|
@ -987,6 +1022,14 @@
|
|||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.3"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da",
|
||||
"sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"
|
||||
],
|
||||
"markers": "platform_system == 'Windows'",
|
||||
"version": "==0.4.5"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2",
|
||||
|
|
|
@ -64,16 +64,6 @@ class TelemetryFeed(AbstractResource):
|
|||
def get_telem_brief_parser_by_category(telem_category):
|
||||
return TELEM_PROCESS_DICT[telem_category]
|
||||
|
||||
@staticmethod
|
||||
def get_tunnel_telem_brief(telem):
|
||||
tunnel = telem["data"]["proxy"]
|
||||
if tunnel is None:
|
||||
return "No tunnel is used."
|
||||
else:
|
||||
tunnel_host_ip = tunnel.split(":")[-2].replace("//", "")
|
||||
tunnel_host = NodeService.get_monkey_by_ip(tunnel_host_ip)["hostname"]
|
||||
return "Tunnel set up to machine: %s." % tunnel_host
|
||||
|
||||
@staticmethod
|
||||
def get_state_telem_brief(telem):
|
||||
if telem["data"]["done"]:
|
||||
|
@ -132,7 +122,6 @@ TELEM_PROCESS_DICT = {
|
|||
TelemCategoryEnum.SCAN: TelemetryFeed.get_scan_telem_brief,
|
||||
TelemCategoryEnum.STATE: TelemetryFeed.get_state_telem_brief,
|
||||
TelemCategoryEnum.TRACE: TelemetryFeed.get_trace_telem_brief,
|
||||
TelemCategoryEnum.TUNNEL: TelemetryFeed.get_tunnel_telem_brief,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ from monkey_island.cc.services.telemetry.processing.exploit import process_explo
|
|||
from monkey_island.cc.services.telemetry.processing.post_breach import process_post_breach_telemetry
|
||||
from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry
|
||||
from monkey_island.cc.services.telemetry.processing.state import process_state_telemetry
|
||||
from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel_telemetry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,7 +21,6 @@ TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = {
|
|||
TelemCategoryEnum.SCAN: process_scan_telemetry,
|
||||
TelemCategoryEnum.STATE: process_state_telemetry,
|
||||
TelemCategoryEnum.TRACE: lambda *args, **kwargs: None,
|
||||
TelemCategoryEnum.TUNNEL: process_tunnel_telemetry,
|
||||
}
|
||||
|
||||
# Don't save credential telemetries in telemetries collection.
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
from monkey_island.cc.services.node import NodeService
|
||||
from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field
|
||||
from monkey_island.cc.services.telemetry.zero_trust_checks.tunneling import (
|
||||
check_tunneling_violation,
|
||||
)
|
||||
|
||||
|
||||
def process_tunnel_telemetry(telemetry_json, _):
|
||||
check_tunneling_violation(telemetry_json)
|
||||
monkey_id = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"])["_id"]
|
||||
if telemetry_json["data"]["proxy"] is not None:
|
||||
tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(telemetry_json)
|
||||
NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip)
|
||||
else:
|
||||
NodeService.unset_all_monkey_tunnels(monkey_id)
|
|
@ -14,8 +14,3 @@ def get_edge_by_scan_or_exploit_telemetry(telemetry_json):
|
|||
dst_label = NodeService.get_label_for_endpoint(dst_node["_id"])
|
||||
|
||||
return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"], src_label, dst_label)
|
||||
|
||||
|
||||
def get_tunnel_host_ip_from_proxy_field(telemetry_json):
|
||||
tunnel_host_ip = telemetry_json["data"]["proxy"].split(":")[-2].replace("//", "")
|
||||
return tunnel_host_ip
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import common.common_consts.zero_trust_consts as zero_trust_consts
|
||||
from monkey_island.cc.models import Monkey
|
||||
from monkey_island.cc.models.zero_trust.event import Event
|
||||
from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field
|
||||
from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import (
|
||||
MonkeyZTFindingService,
|
||||
)
|
||||
|
||||
|
||||
def check_tunneling_violation(tunnel_telemetry_json):
|
||||
if tunnel_telemetry_json["data"]["proxy"] is not None:
|
||||
# Monkey is tunneling, create findings
|
||||
tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(tunnel_telemetry_json)
|
||||
current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json["monkey_guid"])
|
||||
tunneling_events = [
|
||||
Event.create_event(
|
||||
title="Tunneling event",
|
||||
message="Monkey on {hostname} tunneled traffic through {proxy}.".format(
|
||||
hostname=current_monkey.hostname, proxy=tunnel_host_ip
|
||||
),
|
||||
event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK,
|
||||
timestamp=tunnel_telemetry_json["timestamp"],
|
||||
)
|
||||
]
|
||||
|
||||
MonkeyZTFindingService.create_or_add_to_existing(
|
||||
test=zero_trust_consts.TEST_TUNNELING,
|
||||
status=zero_trust_consts.STATUS_FAILED,
|
||||
events=tunneling_events,
|
||||
)
|
||||
|
||||
MonkeyZTFindingService.add_malicious_activity_to_timeline(tunneling_events)
|
|
@ -6,7 +6,6 @@ import pytest
|
|||
|
||||
from infection_monkey.exploit import powershell
|
||||
from infection_monkey.exploit.tools.helpers import AGENT_BINARY_PATH_WIN64
|
||||
from infection_monkey.model.host import VictimHost
|
||||
|
||||
# Use the path_win32api_get_user_name fixture for all tests in this module
|
||||
pytestmark = pytest.mark.usefixtures("patch_win32api_get_user_name")
|
||||
|
@ -16,6 +15,8 @@ PASSWORD_LIST = ["pass1", "pass2"]
|
|||
LM_HASH_LIST = ["bogo_lm_1"]
|
||||
NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"]
|
||||
|
||||
bogus_servers = ["1.1.1.1:5000", "2.2.2.2:5007"]
|
||||
|
||||
|
||||
mock_agent_binary_repository = MagicMock()
|
||||
mock_agent_binary_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EXECUTABLE")
|
||||
|
@ -33,6 +34,7 @@ def powershell_arguments(http_and_https_both_enabled_host):
|
|||
}
|
||||
arguments = {
|
||||
"host": http_and_https_both_enabled_host,
|
||||
"servers": bogus_servers,
|
||||
"options": options,
|
||||
"current_depth": 2,
|
||||
"telemetry_messenger": MagicMock(),
|
||||
|
@ -179,11 +181,10 @@ def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, po
|
|||
|
||||
|
||||
def test_build_monkey_execution_command():
|
||||
host = VictimHost("127.0.0.1")
|
||||
depth = 2
|
||||
executable_path = "/tmp/test-monkey"
|
||||
|
||||
cmd = powershell.build_monkey_execution_command(host, depth, executable_path)
|
||||
cmd = powershell.build_monkey_execution_command(bogus_servers, depth, executable_path)
|
||||
|
||||
assert f"-d {depth}" in cmd
|
||||
assert executable_path in cmd
|
||||
|
|
|
@ -14,7 +14,7 @@ INTERVAL = 0.001
|
|||
|
||||
|
||||
def test_terminate_without_start():
|
||||
m = AutomatedMaster(None, None, None, None, MagicMock(), [], MagicMock())
|
||||
m = AutomatedMaster(None, [], None, None, None, MagicMock(), [], MagicMock())
|
||||
|
||||
# Test that call to terminate does not raise exception
|
||||
m.terminate()
|
||||
|
@ -34,7 +34,7 @@ def test_stop_if_cant_get_config_from_island(monkeypatch):
|
|||
monkeypatch.setattr(
|
||||
"infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL
|
||||
)
|
||||
m = AutomatedMaster(None, None, None, None, cc, [], MagicMock())
|
||||
m = AutomatedMaster(None, [], None, None, None, cc, [], MagicMock())
|
||||
m.start()
|
||||
|
||||
assert cc.get_config.call_count == CHECK_FOR_CONFIG_COUNT
|
||||
|
@ -73,7 +73,7 @@ def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_
|
|||
"infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL
|
||||
)
|
||||
|
||||
m = AutomatedMaster(None, None, None, None, cc, [], MagicMock())
|
||||
m = AutomatedMaster(None, [], None, None, None, cc, [], MagicMock())
|
||||
m.start()
|
||||
|
||||
assert cc.should_agent_stop.call_count == CHECK_FOR_STOP_AGENT_COUNT
|
||||
|
|
|
@ -66,7 +66,7 @@ def hosts_to_exploit(hosts):
|
|||
|
||||
|
||||
def enqueue_hosts(hosts: Iterable[VictimHost]):
|
||||
q = Queue()
|
||||
q: Queue = Queue()
|
||||
for h in hosts:
|
||||
q.put(h)
|
||||
|
||||
|
@ -85,6 +85,7 @@ def get_host_exploit_combos_from_call_args_list(call_args_list):
|
|||
|
||||
|
||||
CREDENTIALS_FOR_PROPAGATION = {"usernames": ["m0nk3y", "user"], "passwords": ["1234", "pword"]}
|
||||
SERVERS = ["127.0.0.1:5000", "10.10.10.10:5007"]
|
||||
|
||||
|
||||
def get_credentials_for_propagation():
|
||||
|
@ -98,7 +99,7 @@ def run_exploiters(exploiter_config, hosts_to_exploit, callback, scan_completed,
|
|||
scan_completed.set()
|
||||
|
||||
e = Exploiter(puppet, num_workers, get_credentials_for_propagation)
|
||||
e.exploit_hosts(exploiter_config, hosts, 1, callback, scan_completed, stop)
|
||||
e.exploit_hosts(exploiter_config, hosts, 1, SERVERS, callback, scan_completed, stop)
|
||||
|
||||
return inner
|
||||
|
||||
|
@ -124,7 +125,7 @@ def test_credentials_passed_to_exploiter(run_exploiters):
|
|||
run_exploiters(mock_puppet, 1)
|
||||
|
||||
for call_args in mock_puppet.exploit_host.call_args_list:
|
||||
assert call_args[0][3].get("credentials") == CREDENTIALS_FOR_PROPAGATION
|
||||
assert call_args[0][4].get("credentials") == CREDENTIALS_FOR_PROPAGATION
|
||||
|
||||
|
||||
def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, hosts_to_exploit):
|
||||
|
@ -143,7 +144,9 @@ def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, h
|
|||
# Intentionally NOT setting scan_completed.set(); _callback() will set stop
|
||||
|
||||
e = Exploiter(MockPuppet(), callback_barrier_count + 2, get_credentials_for_propagation)
|
||||
e.exploit_hosts(exploiter_config, hosts_to_exploit, 1, stoppable_callback, scan_completed, stop)
|
||||
e.exploit_hosts(
|
||||
exploiter_config, hosts_to_exploit, 1, SERVERS, stoppable_callback, scan_completed, stop
|
||||
)
|
||||
|
||||
assert stoppable_callback.call_count == 2
|
||||
|
||||
|
|
|
@ -110,6 +110,8 @@ os_windows = "windows"
|
|||
|
||||
os_linux = "linux"
|
||||
|
||||
SERVERS = ["127.0.0.1:5000", "10.10.10.10:5007"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ip_scanner():
|
||||
|
@ -134,6 +136,7 @@ class StubExploiter:
|
|||
exploiters_to_run,
|
||||
hosts_to_exploit,
|
||||
current_depth,
|
||||
servers,
|
||||
results_callback,
|
||||
scan_completed,
|
||||
stop,
|
||||
|
@ -171,7 +174,7 @@ def test_scan_result_processing(
|
|||
subnets=["10.0.0.1", "10.0.0.2", "10.0.0.3"],
|
||||
)
|
||||
propagation_config = get_propagation_config(default_agent_configuration, targets)
|
||||
p.propagate(propagation_config, 1, Event())
|
||||
p.propagate(propagation_config, 1, SERVERS, Event())
|
||||
|
||||
assert len(telemetry_messenger_spy.telemetries) == 3
|
||||
|
||||
|
@ -204,6 +207,7 @@ class MockExploiter:
|
|||
exploiters_to_run,
|
||||
hosts_to_exploit,
|
||||
current_depth,
|
||||
servers,
|
||||
results_callback,
|
||||
scan_completed,
|
||||
stop,
|
||||
|
@ -269,7 +273,7 @@ def test_exploiter_result_processing(
|
|||
subnets=["10.0.0.1", "10.0.0.2", "10.0.0.3"],
|
||||
)
|
||||
propagation_config = get_propagation_config(default_agent_configuration, targets)
|
||||
p.propagate(propagation_config, 1, Event())
|
||||
p.propagate(propagation_config, 1, SERVERS, Event())
|
||||
|
||||
exploit_telems = [t for t in telemetry_messenger_spy.telemetries if isinstance(t, ExploitTelem)]
|
||||
assert len(exploit_telems) == 4
|
||||
|
@ -310,7 +314,7 @@ def test_scan_target_generation(
|
|||
subnets=["10.0.0.0/29", "172.10.20.30"],
|
||||
)
|
||||
propagation_config = get_propagation_config(default_agent_configuration, targets)
|
||||
p.propagate(propagation_config, 1, Event())
|
||||
p.propagate(propagation_config, 1, SERVERS, Event())
|
||||
|
||||
expected_ip_scan_list = [
|
||||
"10.0.0.0",
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from infection_monkey.model import VictimHostFactory
|
||||
from infection_monkey.network import NetworkAddress
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tunnel():
|
||||
tunnel = MagicMock()
|
||||
tunnel.get_tunnel_for_ip = lambda _: "1.2.3.4:1234"
|
||||
return tunnel
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_get_interface_to_target(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
|
@ -21,37 +12,18 @@ def mock_get_interface_to_target(monkeypatch):
|
|||
|
||||
|
||||
def test_factory_no_tunnel():
|
||||
factory = VictimHostFactory(
|
||||
tunnel=None, island_ip="192.168.56.1", island_port="5000", on_island=False
|
||||
)
|
||||
factory = VictimHostFactory(island_ip="192.168.56.1", island_port="5000", on_island=False)
|
||||
network_address = NetworkAddress("192.168.56.2", None)
|
||||
|
||||
victim = factory.build_victim_host(network_address)
|
||||
|
||||
assert victim.default_server == "192.168.56.1:5000"
|
||||
assert victim.ip_addr == "192.168.56.2"
|
||||
assert victim.default_tunnel is None
|
||||
assert victim.domain_name == ""
|
||||
|
||||
|
||||
def test_factory_with_tunnel(mock_tunnel):
|
||||
factory = VictimHostFactory(
|
||||
tunnel=mock_tunnel, island_ip="192.168.56.1", island_port="5000", on_island=False
|
||||
)
|
||||
network_address = NetworkAddress("192.168.56.2", None)
|
||||
|
||||
victim = factory.build_victim_host(network_address)
|
||||
|
||||
assert victim.default_server == "192.168.56.1:5000"
|
||||
assert victim.ip_addr == "192.168.56.2"
|
||||
assert victim.default_tunnel == "1.2.3.4:1234"
|
||||
assert victim.domain_name == ""
|
||||
|
||||
|
||||
def test_factory_on_island(mock_tunnel):
|
||||
factory = VictimHostFactory(
|
||||
tunnel=mock_tunnel, island_ip="192.168.56.1", island_port="99", on_island=True
|
||||
)
|
||||
def test_factory_on_island():
|
||||
factory = VictimHostFactory(island_ip="192.168.56.1", island_port="99", on_island=True)
|
||||
network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey")
|
||||
|
||||
victim = factory.build_victim_host(network_address)
|
||||
|
@ -59,14 +31,11 @@ def test_factory_on_island(mock_tunnel):
|
|||
assert victim.default_server == "1.1.1.1:99"
|
||||
assert victim.domain_name == "www.bogus.monkey"
|
||||
assert victim.ip_addr == "192.168.56.2"
|
||||
assert victim.default_tunnel == "1.2.3.4:1234"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("default_port", ["", None])
|
||||
def test_factory_no_port(mock_tunnel, default_port):
|
||||
factory = VictimHostFactory(
|
||||
tunnel=mock_tunnel, island_ip="192.168.56.1", island_port=default_port, on_island=True
|
||||
)
|
||||
def test_factory_no_port(default_port):
|
||||
factory = VictimHostFactory(island_ip="192.168.56.1", island_port=default_port, on_island=True)
|
||||
network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey")
|
||||
|
||||
victim = factory.build_victim_host(network_address)
|
||||
|
@ -74,8 +43,8 @@ def test_factory_no_port(mock_tunnel, default_port):
|
|||
assert victim.default_server == "1.1.1.1"
|
||||
|
||||
|
||||
def test_factory_no_default_server(mock_tunnel):
|
||||
factory = VictimHostFactory(tunnel=mock_tunnel, island_ip=None, island_port="", on_island=True)
|
||||
def test_factory_no_default_server():
|
||||
factory = VictimHostFactory(island_ip=None, island_port="", on_island=True)
|
||||
network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey")
|
||||
|
||||
victim = factory.build_victim_host(network_address)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import socket
|
||||
from ipaddress import IPv4Address
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from monkey.infection_monkey.network.relay import (
|
||||
RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST,
|
||||
RelayConnectionHandler,
|
||||
RelayUserHandler,
|
||||
TCPPipeSpawner,
|
||||
)
|
||||
|
||||
USER_ADDRESS = "0.0.0.1"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pipe_spawner():
|
||||
return MagicMock(spec=TCPPipeSpawner)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def relay_user_handler():
|
||||
return MagicMock(spec=RelayUserHandler)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def close_socket():
|
||||
sock = MagicMock(spec=socket.socket)
|
||||
sock.recv.return_value = RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST
|
||||
sock.getpeername.return_value = (USER_ADDRESS, 12345)
|
||||
return sock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data_socket():
|
||||
sock = MagicMock(spec=socket.socket)
|
||||
sock.recv.return_value = b"some data"
|
||||
sock.getpeername.return_value = (USER_ADDRESS, 12345)
|
||||
return sock
|
||||
|
||||
|
||||
def test_control_message_disconnects_user(pipe_spawner, relay_user_handler, close_socket):
|
||||
connection_handler = RelayConnectionHandler(pipe_spawner, relay_user_handler)
|
||||
|
||||
connection_handler.handle_new_connection(close_socket)
|
||||
|
||||
relay_user_handler.disconnect_user.assert_called_once_with(IPv4Address(USER_ADDRESS))
|
||||
|
||||
|
||||
def test_connection_spawns_pipe(pipe_spawner, relay_user_handler, data_socket):
|
||||
connection_handler = RelayConnectionHandler(pipe_spawner, relay_user_handler)
|
||||
|
||||
connection_handler.handle_new_connection(data_socket)
|
||||
|
||||
pipe_spawner.spawn_pipe.assert_called_once_with(data_socket)
|
||||
|
||||
|
||||
def test_connection_adds_user(pipe_spawner, relay_user_handler, data_socket):
|
||||
connection_handler = RelayConnectionHandler(pipe_spawner, relay_user_handler)
|
||||
|
||||
connection_handler.handle_new_connection(data_socket)
|
||||
|
||||
relay_user_handler.add_relay_user.assert_called_once_with(IPv4Address(USER_ADDRESS))
|
|
@ -0,0 +1,50 @@
|
|||
from ipaddress import IPv4Address
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
|
||||
from monkey.infection_monkey.network.relay import RelayUserHandler
|
||||
|
||||
USER_ADDRESS = IPv4Address("0.0.0.0")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def handler():
|
||||
return RelayUserHandler()
|
||||
|
||||
|
||||
def test_potential_users_added(handler):
|
||||
assert not handler.has_potential_users()
|
||||
handler.add_potential_user(USER_ADDRESS)
|
||||
assert handler.has_potential_users()
|
||||
|
||||
|
||||
def test_potential_user_removed_on_matching_user_added(handler):
|
||||
handler.add_potential_user(USER_ADDRESS)
|
||||
handler.add_relay_user(USER_ADDRESS)
|
||||
|
||||
assert not handler.has_potential_users()
|
||||
|
||||
|
||||
def test_potential_users_time_out():
|
||||
handler = RelayUserHandler(new_client_timeout=0.001)
|
||||
|
||||
handler.add_potential_user(USER_ADDRESS)
|
||||
sleep(0.003)
|
||||
|
||||
assert not handler.has_potential_users()
|
||||
|
||||
|
||||
def test_relay_users_added(handler):
|
||||
assert not handler.has_connected_users()
|
||||
handler.add_relay_user(USER_ADDRESS)
|
||||
assert handler.has_connected_users()
|
||||
|
||||
|
||||
def test_relay_users_time_out():
|
||||
handler = RelayUserHandler(client_disconnect_timeout=0.001)
|
||||
|
||||
handler.add_relay_user(USER_ADDRESS)
|
||||
sleep(0.003)
|
||||
|
||||
assert not handler.has_connected_users()
|
|
@ -0,0 +1,14 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
from monkey.infection_monkey.network.relay import SocketsPipe
|
||||
|
||||
|
||||
def test_sockets_pipe__name_increments():
|
||||
sock_in = MagicMock()
|
||||
sock_out = MagicMock()
|
||||
|
||||
pipe1 = SocketsPipe(sock_in, sock_out, None)
|
||||
assert pipe1.name.endswith("1")
|
||||
|
||||
pipe2 = SocketsPipe(sock_in, sock_out, None)
|
||||
assert pipe2.name.endswith("2")
|
|
@ -0,0 +1,42 @@
|
|||
import pytest
|
||||
import requests
|
||||
import requests_mock
|
||||
|
||||
from infection_monkey.network.relay.utils import find_server
|
||||
|
||||
SERVER_1 = "1.1.1.1:12312"
|
||||
SERVER_2 = "2.2.2.2:4321"
|
||||
SERVER_3 = "3.3.3.3:3142"
|
||||
SERVER_4 = "4.4.4.4:5000"
|
||||
|
||||
|
||||
servers = [SERVER_1, SERVER_2, SERVER_3, SERVER_4]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expected_server,server_response_pairs",
|
||||
[
|
||||
(None, [(server, {"exc": requests.exceptions.ConnectionError}) for server in servers]),
|
||||
(
|
||||
SERVER_2,
|
||||
[(SERVER_1, {"exc": requests.exceptions.ConnectionError})]
|
||||
+ [(server, {"text": ""}) for server in servers[1:]], # type: ignore[dict-item]
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_find_server(expected_server, server_response_pairs):
|
||||
with requests_mock.Mocker() as mock:
|
||||
for server, response in server_response_pairs:
|
||||
mock.get(f"https://{server}/api?action=is-up", **response)
|
||||
|
||||
assert find_server(servers) is expected_server
|
||||
|
||||
|
||||
def test_find_server__multiple_successes():
|
||||
with requests_mock.Mocker() as mock:
|
||||
mock.get(f"https://{SERVER_1}/api?action=is-up", exc=requests.exceptions.ConnectionError)
|
||||
mock.get(f"https://{SERVER_2}/api?action=is-up", text="")
|
||||
mock.get(f"https://{SERVER_3}/api?action=is-up", text="")
|
||||
mock.get(f"https://{SERVER_4}/api?action=is-up", text="")
|
||||
|
||||
assert find_server(servers) == SERVER_2
|
|
@ -0,0 +1,60 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from infection_monkey.network.info import TCPPortSelector
|
||||
from infection_monkey.network.ports import COMMON_PORTS
|
||||
|
||||
|
||||
@dataclass
|
||||
class Connection:
|
||||
laddr: Tuple[str, int]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("port", COMMON_PORTS)
|
||||
def test_tcp_port_selector__checks_common_ports(port: int, monkeypatch):
|
||||
tcp_port_selector = TCPPortSelector()
|
||||
unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS if p is not port]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports
|
||||
)
|
||||
assert tcp_port_selector.get_free_tcp_port() is port
|
||||
|
||||
|
||||
def test_tcp_port_selector__checks_other_ports_if_common_ports_unavailable(monkeypatch):
|
||||
tcp_port_selector = TCPPortSelector()
|
||||
unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS]
|
||||
monkeypatch.setattr(
|
||||
"infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports
|
||||
)
|
||||
|
||||
assert tcp_port_selector.get_free_tcp_port() is not None
|
||||
|
||||
|
||||
def test_tcp_port_selector__none_if_no_available_ports(monkeypatch):
|
||||
tcp_port_selector = TCPPortSelector()
|
||||
unavailable_ports = [Connection(("", p)) for p in range(65535)]
|
||||
monkeypatch.setattr(
|
||||
"infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports
|
||||
)
|
||||
|
||||
assert tcp_port_selector.get_free_tcp_port() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("common_port", COMMON_PORTS)
|
||||
def test_tcp_port_selector__checks_common_ports_leases(common_port: int, monkeypatch):
|
||||
tcp_port_selector = TCPPortSelector()
|
||||
unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS if p is not common_port]
|
||||
monkeypatch.setattr(
|
||||
"infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports
|
||||
)
|
||||
|
||||
free_port_1 = tcp_port_selector.get_free_tcp_port()
|
||||
free_port_2 = tcp_port_selector.get_free_tcp_port()
|
||||
|
||||
assert free_port_1 == common_port
|
||||
assert free_port_2 != common_port
|
||||
assert free_port_2 is not None
|
||||
assert free_port_2 not in COMMON_PORTS
|
|
@ -8,7 +8,7 @@ from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messen
|
|||
)
|
||||
|
||||
|
||||
class MockExpliotTelem(ExploitTelem):
|
||||
class MockExploitTelem(ExploitTelem):
|
||||
def __init__(self, propagation_success):
|
||||
erd = ExploiterResultData()
|
||||
erd.propagation_success = propagation_success
|
||||
|
@ -20,43 +20,43 @@ class MockExpliotTelem(ExploitTelem):
|
|||
|
||||
def test_generic_telemetry(TestTelem):
|
||||
mock_telemetry_messenger = MagicMock()
|
||||
mock_tunnel = MagicMock()
|
||||
mock_relay = MagicMock()
|
||||
|
||||
telemetry_messenger = ExploitInterceptingTelemetryMessenger(
|
||||
mock_telemetry_messenger, mock_tunnel
|
||||
mock_telemetry_messenger, mock_relay
|
||||
)
|
||||
|
||||
telemetry_messenger.send_telemetry(TestTelem())
|
||||
|
||||
assert mock_telemetry_messenger.send_telemetry.called
|
||||
assert not mock_tunnel.set_wait_for_exploited_machines.called
|
||||
assert not mock_relay.add_potential_user.called
|
||||
|
||||
|
||||
def test_propagation_successful_expliot_telemetry():
|
||||
def test_propagation_successful_exploit_telemetry():
|
||||
mock_telemetry_messenger = MagicMock()
|
||||
mock_tunnel = MagicMock()
|
||||
mock_expliot_telem = MockExpliotTelem(True)
|
||||
mock_relay = MagicMock()
|
||||
mock_exploit_telem = MockExploitTelem(True)
|
||||
|
||||
telemetry_messenger = ExploitInterceptingTelemetryMessenger(
|
||||
mock_telemetry_messenger, mock_tunnel
|
||||
mock_telemetry_messenger, mock_relay
|
||||
)
|
||||
|
||||
telemetry_messenger.send_telemetry(mock_expliot_telem)
|
||||
telemetry_messenger.send_telemetry(mock_exploit_telem)
|
||||
|
||||
assert mock_telemetry_messenger.send_telemetry.called
|
||||
assert mock_tunnel.set_wait_for_exploited_machines.called
|
||||
assert mock_relay.add_potential_user.called
|
||||
|
||||
|
||||
def test_propagation_failed_expliot_telemetry():
|
||||
def test_propagation_failed_exploit_telemetry():
|
||||
mock_telemetry_messenger = MagicMock()
|
||||
mock_tunnel = MagicMock()
|
||||
mock_expliot_telem = MockExpliotTelem(False)
|
||||
mock_relay = MagicMock()
|
||||
mock_exploit_telem = MockExploitTelem(False)
|
||||
|
||||
telemetry_messenger = ExploitInterceptingTelemetryMessenger(
|
||||
mock_telemetry_messenger, mock_tunnel
|
||||
mock_telemetry_messenger, mock_relay
|
||||
)
|
||||
|
||||
telemetry_messenger.send_telemetry(mock_expliot_telem)
|
||||
telemetry_messenger.send_telemetry(mock_exploit_telem)
|
||||
|
||||
assert mock_telemetry_messenger.send_telemetry.called
|
||||
assert not mock_tunnel.set_wait_for_exploited_machines.called
|
||||
assert not mock_relay.add_potential_user.called
|
||||
|
|
|
@ -16,7 +16,6 @@ HOST_AS_DICT = {
|
|||
"os": {},
|
||||
"services": {},
|
||||
"icmp": False,
|
||||
"default_tunnel": None,
|
||||
"default_server": None,
|
||||
}
|
||||
EXPLOITER_NAME = "SSHExploiter"
|
||||
|
|
|
@ -14,7 +14,6 @@ HOST_AS_DICT = {
|
|||
"os": {},
|
||||
"services": {},
|
||||
"icmp": False,
|
||||
"default_tunnel": None,
|
||||
"default_server": None,
|
||||
}
|
||||
HOST_SERVICES = {}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from infection_monkey.telemetry.tunnel_telem import TunnelTelem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tunnel_telem_test_instance():
|
||||
return TunnelTelem({})
|
||||
|
||||
|
||||
def test_tunnel_telem_send(tunnel_telem_test_instance, spy_send_telemetry):
|
||||
tunnel_telem_test_instance.send()
|
||||
expected_data = {"proxy": None}
|
||||
expected_data = json.dumps(expected_data, cls=tunnel_telem_test_instance.json_encoder)
|
||||
|
||||
assert spy_send_telemetry.data == expected_data
|
||||
assert spy_send_telemetry.telem_category == "tunnel"
|
|
@ -1,16 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from monkey.infection_monkey.control import ControlClient
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_windows_os,expected_proxy_string",
|
||||
[(True, "http://8.8.8.8:45455"), (False, "8.8.8.8:45455")],
|
||||
)
|
||||
def test_control_set_proxies(monkeypatch, is_windows_os, expected_proxy_string):
|
||||
monkeypatch.setattr("monkey.infection_monkey.control.is_windows_os", lambda: is_windows_os)
|
||||
control_client = ControlClient("8.8.8.8:5000")
|
||||
|
||||
control_client.set_proxies(("8.8.8.8", "45455"))
|
||||
|
||||
assert control_client.proxies["https"] == expected_proxy_string
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from infection_monkey.config import GUID
|
||||
from infection_monkey.model.host import VictimHost
|
||||
from infection_monkey.utils.commands import (
|
||||
build_monkey_commandline,
|
||||
build_monkey_commandline_explicitly,
|
||||
|
@ -12,17 +13,15 @@ def test_build_monkey_commandline_explicitly_arguments():
|
|||
expected = [
|
||||
"-p",
|
||||
"101010",
|
||||
"-t",
|
||||
"10.10.101.10",
|
||||
"-s",
|
||||
"127.127.127.127:5000",
|
||||
"127.127.127.127:5000,138.138.138.138:5007",
|
||||
"-d",
|
||||
"0",
|
||||
"-l",
|
||||
"C:\\windows\\abc",
|
||||
]
|
||||
actual = build_monkey_commandline_explicitly(
|
||||
"101010", "10.10.101.10", "127.127.127.127:5000", 0, "C:\\windows\\abc"
|
||||
"101010", ["127.127.127.127:5000", "138.138.138.138:5007"], 0, "C:\\windows\\abc"
|
||||
)
|
||||
|
||||
assert expected == actual
|
||||
|
@ -46,17 +45,12 @@ def test_get_monkey_commandline_windows():
|
|||
"m0nk3y",
|
||||
"-p",
|
||||
"101010",
|
||||
"-t",
|
||||
"10.10.101.10",
|
||||
"-s",
|
||||
"127.127.127.127:5000,138.138.138.138:5007",
|
||||
]
|
||||
actual = get_monkey_commandline_windows(
|
||||
"C:\\windows\\abc",
|
||||
[
|
||||
"-p",
|
||||
"101010",
|
||||
"-t",
|
||||
"10.10.101.10",
|
||||
],
|
||||
["-p", "101010", "-s", "127.127.127.127:5000,138.138.138.138:5007"],
|
||||
)
|
||||
|
||||
assert expected == actual
|
||||
|
@ -68,27 +62,30 @@ def test_get_monkey_commandline_linux():
|
|||
"m0nk3y",
|
||||
"-p",
|
||||
"101010",
|
||||
"-t",
|
||||
"10.10.101.10",
|
||||
"-s",
|
||||
"127.127.127.127:5000,138.138.138.138:5007",
|
||||
]
|
||||
actual = get_monkey_commandline_linux(
|
||||
"/home/user/monkey-linux-64",
|
||||
[
|
||||
"-p",
|
||||
"101010",
|
||||
"-t",
|
||||
"10.10.101.10",
|
||||
],
|
||||
["-p", "101010", "-s", "127.127.127.127:5000,138.138.138.138:5007"],
|
||||
)
|
||||
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_build_monkey_commandline():
|
||||
example_host = VictimHost(ip_addr="bla")
|
||||
example_host.set_island_address("101010", "5000")
|
||||
servers = ["10.10.10.10:5000", "11.11.11.11:5007"]
|
||||
|
||||
expected = f" -p {GUID} -s 101010:5000 -d 0 -l /home/bla"
|
||||
actual = build_monkey_commandline(target_host=example_host, depth=0, location="/home/bla")
|
||||
expected = f" -p {GUID} -s 10.10.10.10:5000,11.11.11.11:5007 -d 0 -l /home/bla"
|
||||
actual = build_monkey_commandline(servers=servers, depth=0, location="/home/bla")
|
||||
|
||||
assert expected == actual
|
||||
|
||||
|
||||
@pytest.mark.parametrize("servers", [None, []])
|
||||
def test_build_monkey_commandline_empty_servers(servers):
|
||||
|
||||
expected = f" -p {GUID} -d 0 -l /home/bla"
|
||||
actual = build_monkey_commandline(servers, depth=0, location="/home/bla")
|
||||
|
||||
assert expected == actual
|
||||
|
|
|
@ -5,18 +5,18 @@ def test_maximum_depth_reached__current_less_than_max():
|
|||
maximum_depth = 2
|
||||
current_depth = 1
|
||||
|
||||
assert maximum_depth_reached(maximum_depth, current_depth) is True
|
||||
assert maximum_depth_reached(maximum_depth, current_depth) is False
|
||||
|
||||
|
||||
def test_maximum_depth_reached__current_greater_than_max():
|
||||
maximum_depth = 2
|
||||
current_depth = 3
|
||||
|
||||
assert maximum_depth_reached(maximum_depth, current_depth) is False
|
||||
assert maximum_depth_reached(maximum_depth, current_depth) is True
|
||||
|
||||
|
||||
def test_maximum_depth_reached__current_equal_to_max():
|
||||
maximum_depth = 2
|
||||
current_depth = maximum_depth
|
||||
|
||||
assert maximum_depth_reached(maximum_depth, current_depth) is False
|
||||
assert maximum_depth_reached(maximum_depth, current_depth) is True
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
from itertools import zip_longest
|
||||
from threading import Event, current_thread
|
||||
from typing import Any
|
||||
|
||||
from infection_monkey.utils.threading import (
|
||||
ThreadSafeIterator,
|
||||
create_daemon_thread,
|
||||
interruptible_function,
|
||||
interruptible_iter,
|
||||
|
@ -127,3 +129,11 @@ def test_interruptible_decorator_returns_default_value_on_interrupt():
|
|||
|
||||
assert return_value == 777
|
||||
assert fn.call_count == 0
|
||||
|
||||
|
||||
def test_thread_safe_iterator():
|
||||
test_list = [1, 2, 3, 4, 5]
|
||||
tsi = ThreadSafeIterator(test_list.__iter__())
|
||||
|
||||
for actual, expected in zip_longest(tsi, test_list):
|
||||
assert actual == expected
|
||||
|
|
|
@ -18,7 +18,6 @@ SCAN_DATA_MOCK = [
|
|||
},
|
||||
},
|
||||
"monkey_exe": None,
|
||||
"default_tunnel": None,
|
||||
"default_server": None,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -308,6 +308,7 @@ IEventRepository.get_events_by_type
|
|||
IEventRepository.get_events_by_tag
|
||||
IEventRepository.get_events_by_source
|
||||
|
||||
|
||||
# pydantic base models
|
||||
underscore_attrs_are_private
|
||||
extra
|
||||
|
|
Loading…
Reference in New Issue