From 5695808adbf7f4d6e9c698fe42d389fdb42c629e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 08:33:08 -0500 Subject: [PATCH 01/15] Agent: Add options parameter to IPuppet.fingerprint() --- monkey/infection_monkey/i_puppet/i_puppet.py | 3 +++ monkey/infection_monkey/master/ip_scanner.py | 4 +++- monkey/infection_monkey/puppet/mock_puppet.py | 1 + monkey/infection_monkey/puppet/puppet.py | 3 ++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index c0a42d95c..1908e5337 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -83,6 +83,7 @@ class IPuppet(metaclass=abc.ABCMeta): host: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: """ Runs a fingerprinter against a remote host @@ -91,6 +92,8 @@ class IPuppet(metaclass=abc.ABCMeta): :param PingScanData ping_scan_data: Data retrieved from the target host via ICMP :param Dict[int, PortScanData] port_scan_data: Data retrieved from the target host via a TCP port scan + :param Dict options: A dictionary containing options that modify the behavior of the + fingerprinter :return: The data collected by running the fingerprinter on the specified host :rtype: FingerprintData """ diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 135f79c94..c78b2e2f9 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -95,6 +95,8 @@ class IPScanner: fingerprint_data = {} for f in interruptable_iter(fingerprinters, stop): - fingerprint_data[f] = self._puppet.fingerprint(f, ip, ping_scan_data, port_scan_data) + fingerprint_data[f] = self._puppet.fingerprint( + f, ip, ping_scan_data, port_scan_data, {} + ) return fingerprint_data diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index d35ec2cbb..ec3984685 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -206,6 +206,7 @@ class MockPuppet(IPuppet): host: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: logger.debug(f"fingerprint({name}, {host})") empty_fingerprint_data = FingerprintData(None, None, {}) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 175a3f0eb..b7be64002 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -47,8 +47,9 @@ class Puppet(IPuppet): host: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: - return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data) + return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data, options) def exploit_host( self, name: str, host: str, options: Dict, interrupt: threading.Event From 4361aa2325be7001fbbd5b394b07ee101ad35278 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 10:23:28 -0500 Subject: [PATCH 02/15] Agent: Add IFingerprinter --- monkey/infection_monkey/i_puppet/__init__.py | 1 + .../i_puppet/i_fingerprinter.py | 27 +++++++++++++++++++ monkey/infection_monkey/i_puppet/i_puppet.py | 5 ++-- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/i_puppet/i_fingerprinter.py diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index 0ba1096d1..c4e6b5b1c 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -9,3 +9,4 @@ from .i_puppet import ( PostBreachData, UnknownPluginError, ) +from .i_fingerprinter import IFingerprinter diff --git a/monkey/infection_monkey/i_puppet/i_fingerprinter.py b/monkey/infection_monkey/i_puppet/i_fingerprinter.py new file mode 100644 index 000000000..e6f177021 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/i_fingerprinter.py @@ -0,0 +1,27 @@ +from abc import abstractmethod +from typing import Dict + +from . import FingerprintData, PingScanData, PortScanData + + +class IFingerprinter: + @abstractmethod + def get_host_fingerprint( + self, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, + ) -> FingerprintData: + """ + Attempts to gather detailed information about a host and its services + :param str host: The domain name or IP address of a host + :param PingScanData ping_scan_data: Data retrieved from the target host via ICMP + :param Dict[int, PortScanData] port_scan_data: Data retrieved from the target host via a TCP + port scan + :param Dict options: A dictionary containing options that modify the behavior of the + fingerprinter + :return: Detailed information about the target host + :rtype: FingerprintData + """ + pass diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 1908e5337..69c128e68 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -86,7 +86,8 @@ class IPuppet(metaclass=abc.ABCMeta): options: Dict, ) -> FingerprintData: """ - Runs a fingerprinter against a remote host + Runs a specific fingerprinter to attempt to gather detailed information about a host and its + services :param str name: The name of the fingerprinter to run :param str host: The domain name or IP address of a host :param PingScanData ping_scan_data: Data retrieved from the target host via ICMP @@ -94,7 +95,7 @@ class IPuppet(metaclass=abc.ABCMeta): port scan :param Dict options: A dictionary containing options that modify the behavior of the fingerprinter - :return: The data collected by running the fingerprinter on the specified host + :return: Detailed information about the target host :rtype: FingerprintData """ From f5ef660bd26e271a490e0270f0c09a52c51a88f6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 10:26:20 -0500 Subject: [PATCH 03/15] Agent: Refactor HTTPFinger to conform to IFingerprinter interface * Remove dependency on Plugin, HostFinger, and WormConfiguration * Improve readability * Reduce unnecessary HTTP requests by using the PortScanData to only query ports we know are open. --- monkey/infection_monkey/network/httpfinger.py | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 99e9deaab..5c242a204 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -1,47 +1,80 @@ import logging +from contextlib import closing +from typing import Dict, Iterable, Optional, Set, Tuple -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger +from requests import head +from requests.exceptions import ConnectionError, Timeout + +from infection_monkey.i_puppet import ( + FingerprintData, + IFingerprinter, + PingScanData, + PortScanData, + PortStatus, +) logger = logging.getLogger(__name__) -class HTTPFinger(HostFinger): +class HTTPFinger(IFingerprinter): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ - _SCANNED_SERVICE = "HTTP" + def get_host_fingerprint( + self, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, + ): + services = {} + http_ports = set(options.get("http_ports", [])) + ports_to_fingerprint = _get_open_http_ports(http_ports, port_scan_data) - def __init__(self): - self._config = infection_monkey.config.WormConfiguration - self.HTTP = [(port, str(port)) for port in self._config.HTTP_PORTS] + for port in ports_to_fingerprint: + server_header_contents, ssl = _query_potential_http_server(host, port) - def get_host_fingerprint(self, host): - from contextlib import closing + if server_header_contents is not None: + services[f"tcp-{port}"] = { + "display_name": "HTTP", + "port": port, + "name": "http", + "data": (server_header_contents, ssl), + } - from requests import head - from requests.exceptions import ConnectionError, Timeout + return FingerprintData(None, None, services) - for port in self.HTTP: - # check both http and https - http = "http://" + host.ip_addr + ":" + port[1] - https = "https://" + host.ip_addr + ":" + port[1] - # try http, we don't optimise for 443 - for url in (https, http): # start with https and downgrade - try: - with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - server = req.headers.get("Server") - ssl = True if "https://" in url else False - self.init_service(host.services, ("tcp-" + port[1]), port[0]) - host.services["tcp-" + port[1]]["name"] = "http" - host.services["tcp-" + port[1]]["data"] = (server, ssl) - logger.info("Port %d is open on host %s " % (port[0], host)) - break # https will be the same on the same port - except Timeout: - logger.debug(f"Timeout while requesting headers from {url}") - except ConnectionError: # Someone doesn't like us - logger.debug(f"Connection error while requesting headers from {url}") +def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], Optional[bool]]: + # check both http and https + http = f"http://{host}:{port}" + https = f"https://{host}:{port}" - return True + # try http, we don't optimise for 443 + for url, ssl in ((https, True), (http, False)): # start with https and downgrade + server_header_contents = _get_server_from_headers(url) + + if server_header_contents is not None: + return (server_header_contents, ssl) + + return (None, None) + + +def _get_server_from_headers(url: str) -> Optional[str]: + try: + with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 + return req.headers.get("Server") + except Timeout: + logger.debug(f"Timeout while requesting headers from {url}") + except ConnectionError: # Someone doesn't like us + logger.debug(f"Connection error while requesting headers from {url}") + + return None + + +def _get_open_http_ports( + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] +) -> Iterable[int]: + open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.Open) + return (port for port in open_ports if port in allowed_http_ports) From 4b2fb260c393ba83b556af363badf2c83326ab1d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 10:29:23 -0500 Subject: [PATCH 04/15] Agent: Rename HTTPFinger -> HTTPFingerprinter --- .../network/{httpfinger.py => http_fingerprinter.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename monkey/infection_monkey/network/{httpfinger.py => http_fingerprinter.py} (98%) diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/http_fingerprinter.py similarity index 98% rename from monkey/infection_monkey/network/httpfinger.py rename to monkey/infection_monkey/network/http_fingerprinter.py index 5c242a204..5b58db22c 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -16,7 +16,7 @@ from infection_monkey.i_puppet import ( logger = logging.getLogger(__name__) -class HTTPFinger(IFingerprinter): +class HTTPFingerprinter(IFingerprinter): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ From a989e5543a18c3cb09c2d0b4f470c1580916f80d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 12:07:45 -0500 Subject: [PATCH 05/15] Island: Format fingerprinter config with options --- monkey/monkey_island/cc/services/config.py | 13 +++++++++++-- .../monkey_island/cc/services/test_config.py | 13 ++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index a0af1632c..163c9d1da 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -538,10 +538,19 @@ class ConfigService: @staticmethod def _format_fingerprinters_from_flat_config(config: Dict): flat_fingerprinter_classes_field = "finger_classes" + flat_http_ports_field = "HTTP_PORTS" + + formatted_fingerprinters = [ + {"name": f, "options": {}} for f in sorted(config[flat_fingerprinter_classes_field]) + ] + + if "HTTPFinger" in config[flat_fingerprinter_classes_field]: + for fp in formatted_fingerprinters: + if fp["name"] == "HTTPFinger": + fp["options"] = {"http_ports": sorted(config[flat_http_ports_field])} + break - formatted_fingerprinters = config[flat_fingerprinter_classes_field] config.pop(flat_fingerprinter_classes_field) - return formatted_fingerprinters @staticmethod diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 3ad02a7a6..fe8ec639e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -147,11 +147,14 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): "timeout_ms": 1000, }, "fingerprinters": [ - "SMBFinger", - "SSHFinger", - "HTTPFinger", - "MSSQLFinger", - "ElasticFinger", + {"name": "ElasticFinger", "options": {}}, + { + "name": "HTTPFinger", + "options": {"http_ports": [80, 443, 7001, 8008, 8080, 9200]}, + }, + {"name": "MSSQLFinger", "options": {}}, + {"name": "SMBFinger", "options": {}}, + {"name": "SSHFinger", "options": {}}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From 46487be05d5d6cc185f4af095025dbfab0cea950 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 12:55:57 -0500 Subject: [PATCH 06/15] Agent: Handle new fingerprinters config format in IPScanner --- monkey/infection_monkey/master/ip_scanner.py | 4 ++-- .../unit_tests/infection_monkey/master/test_ip_scanner.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index c78b2e2f9..67520054e 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -95,8 +95,8 @@ class IPScanner: fingerprint_data = {} for f in interruptable_iter(fingerprinters, stop): - fingerprint_data[f] = self._puppet.fingerprint( - f, ip, ping_scan_data, port_scan_data, {} + fingerprint_data[f["name"]] = self._puppet.fingerprint( + f["name"], ip, ping_scan_data, port_scan_data, f["options"] ) return fingerprint_data diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py index 59bb6bf77..c6aa0d532 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py @@ -30,7 +30,12 @@ def scan_config(): "icmp": { "timeout_ms": 1000, }, - "fingerprinters": {"HTTPFinger", "SMBFinger", "SSHFinger"}, + "fingerprinters": [ + {"name": "HTTPFinger", "options": {}}, + {"name": "SMBFinger", "options": {}}, + {"name": "SSHFinger", "options": {}}, + ] + } From 6d5b55be10278e63bc696538ae10a528f19b6c81 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 12:56:54 -0500 Subject: [PATCH 07/15] Agent: Implement fingerprinting in Puppet --- monkey/infection_monkey/monkey.py | 2 ++ monkey/infection_monkey/network/http_fingerprinter.py | 8 ++++++-- monkey/infection_monkey/puppet/puppet.py | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 20ed730a8..3b31e3a00 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -18,6 +18,7 @@ from infection_monkey.master.control_channel import ControlChannel from infection_monkey.model import DELAY_DELETE_CMD, VictimHostFactory from infection_monkey.network import NetworkInterface from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.puppet.puppet import Puppet @@ -183,6 +184,7 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() + puppet.load_plugin("HTTPFinger", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 5b58db22c..dabef920b 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -63,8 +63,12 @@ def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], O def _get_server_from_headers(url: str) -> Optional[str]: try: + logger.debug(f"Sending request for headers to {url}") with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - return req.headers.get("Server") + server = req.headers.get("Server") + + logger.debug(f'Got server string "{server}" from {url}') + return server except Timeout: logger.debug(f"Timeout while requesting headers from {url}") except ConnectionError: # Someone doesn't like us @@ -76,5 +80,5 @@ def _get_server_from_headers(url: str) -> Optional[str]: def _get_open_http_ports( allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] ) -> Iterable[int]: - open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.Open) + open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) return (port for port in open_ports if port in allowed_http_ports) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index b7be64002..ad9354d66 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -49,7 +49,8 @@ class Puppet(IPuppet): port_scan_data: Dict[int, PortScanData], options: Dict, ) -> FingerprintData: - return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data, options) + fingerprinter = self._plugin_registry.get_plugin(name, PluginType.FINGERPRINTER) + return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options) def exploit_host( self, name: str, host: str, options: Dict, interrupt: threading.Event From 207a65e2a9c3daa1cb4021e00a6d170219feee2b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 13:02:01 -0500 Subject: [PATCH 08/15] Island: Simplify the names of fingerprinters in the config --- monkey/monkey_island/cc/services/config.py | 8 +++++++- .../monkey_island/cc/services/test_config.py | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 163c9d1da..1396a130b 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -2,6 +2,7 @@ import collections import copy import functools import logging +import re from typing import Dict, List from jsonschema import Draft4Validator, validators @@ -548,11 +549,16 @@ class ConfigService: for fp in formatted_fingerprinters: if fp["name"] == "HTTPFinger": fp["options"] = {"http_ports": sorted(config[flat_http_ports_field])} - break + + fp["name"] = ConfigService._translate_fingerprinter_name(fp["name"]) config.pop(flat_fingerprinter_classes_field) return formatted_fingerprinters + @staticmethod + def _translate_fingerprinter_name(name: str): + return re.sub(r"Finger", "", name).lower() + @staticmethod def _format_targets_from_flat_config(config: Dict): flat_blocked_ips_field = "blocked_ips" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index fe8ec639e..daecec1b6 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -147,14 +147,14 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): "timeout_ms": 1000, }, "fingerprinters": [ - {"name": "ElasticFinger", "options": {}}, + {"name": "elastic", "options": {}}, { - "name": "HTTPFinger", + "name": "http", "options": {"http_ports": [80, 443, 7001, 8008, 8080, 9200]}, }, - {"name": "MSSQLFinger", "options": {}}, - {"name": "SMBFinger", "options": {}}, - {"name": "SSHFinger", "options": {}}, + {"name": "mssql", "options": {}}, + {"name": "smb", "options": {}}, + {"name": "ssh", "options": {}}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From 479627c71ed75f5664d4eb56cb03fb5acd164628 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 13:05:10 -0500 Subject: [PATCH 09/15] Agent: Load the HTTPFingerprinter using the new name, "http" --- monkey/infection_monkey/monkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 3b31e3a00..5d029428a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -184,7 +184,7 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() - puppet.load_plugin("HTTPFinger", HTTPFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet From 916222c2d993ae6c4d43d6ee55d1b7872c761705 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 14:08:52 -0500 Subject: [PATCH 10/15] UT: Add unit tests for HTTPFingerprinter --- .../network/http_fingerprinter.py | 2 +- .../network/test_http_fingerprinter.py | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index dabef920b..5ebc2c514 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -24,7 +24,7 @@ class HTTPFingerprinter(IFingerprinter): def get_host_fingerprint( self, host: str, - ping_scan_data: PingScanData, + _: PingScanData, port_scan_data: Dict[int, PortScanData], options: Dict, ): diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py new file mode 100644 index 000000000..5b2a89445 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py @@ -0,0 +1,113 @@ +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.i_puppet import PortScanData, PortStatus +from infection_monkey.network.http_fingerprinter import HTTPFingerprinter + +OPTIONS = {"http_ports": [80, 443, 8080, 9200]} + +PYTHON_SERVER_HEADER = "SimpleHTTP/0.6 Python/3.6.9" +APACHE_SERVER_HEADER = "Apache/Server/Header" + +SERVER_HEADERS = { + "https://127.0.0.1:443": PYTHON_SERVER_HEADER, + "http://127.0.0.1:8080": APACHE_SERVER_HEADER, +} + + +@pytest.fixture +def mock_get_server_from_headers(): + return MagicMock(side_effect=lambda port: SERVER_HEADERS.get(port, None)) + + +@pytest.fixture(autouse=True) +def patch_get_server_from_headers(monkeypatch, mock_get_server_from_headers): + monkeypatch.setattr( + "infection_monkey.network.http_fingerprinter._get_server_from_headers", + mock_get_server_from_headers, + ) + + +@pytest.fixture +def http_fingerprinter(): + return HTTPFingerprinter() + + +def test_no_http_ports_open(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + http_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, OPTIONS) + + assert not mock_get_server_from_headers.called + + +def test_fingerprint_only_port_443(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.OPEN, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_server_from_headers.call_count == 1 + mock_get_server_from_headers.assert_called_with("https://127.0.0.1:443") + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 1 + + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][1] is True + + +def test_open_port_no_http_server(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 9200: PortScanData(9200, PortStatus.OPEN, "", "tcp-9200"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_server_from_headers.call_count == 2 + mock_get_server_from_headers.assert_any_call("https://127.0.0.1:9200") + mock_get_server_from_headers.assert_any_call("http://127.0.0.1:9200") + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 0 + + +def test_multiple_open_ports(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 443: PortScanData(443, PortStatus.OPEN, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.OPEN, "", "tcp-8080"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_server_from_headers.call_count == 3 + mock_get_server_from_headers.assert_any_call("https://127.0.0.1:443") + mock_get_server_from_headers.assert_any_call("https://127.0.0.1:8080") + mock_get_server_from_headers.assert_any_call("http://127.0.0.1:8080") + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 2 + + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][1] is True + assert fingerprint_data.services["tcp-8080"]["data"][0] == APACHE_SERVER_HEADER + assert fingerprint_data.services["tcp-8080"]["data"][1] is False From 0b33aacb8252199520d0cf36c441b44caf4ffecc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 07:38:24 -0500 Subject: [PATCH 11/15] Island: Add missing return types to some functions in ConfigService --- monkey/monkey_island/cc/services/config.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 1396a130b..ba37d357c 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -3,7 +3,7 @@ import copy import functools import logging import re -from typing import Dict, List +from typing import Any, Dict, List from jsonschema import Draft4Validator, validators @@ -406,7 +406,7 @@ class ConfigService: return ConfigService.get_config_value(EXPORT_MONKEY_TELEMS_PATH) @staticmethod - def get_config_propagation_credentials_from_flat_config(config): + def get_config_propagation_credentials_from_flat_config(config) -> Dict[str, List[str]]: return { "exploit_user_list": config.get("exploit_user_list", []), "exploit_password_list": config.get("exploit_password_list", []), @@ -483,8 +483,8 @@ class ConfigService: config["propagation"] = formatted_propagation_config @staticmethod - def _format_network_scan_from_flat_config(config: Dict): - formatted_network_scan_config = {"tcp": {}, "icmp": {}} + def _format_network_scan_from_flat_config(config: Dict) -> Dict[str, Any]: + formatted_network_scan_config = {"tcp": {}, "icmp": {}, "fingerprinters": []} formatted_network_scan_config["tcp"] = ConfigService._format_tcp_scan_from_flat_config( config @@ -499,7 +499,7 @@ class ConfigService: return formatted_network_scan_config @staticmethod - def _format_tcp_scan_from_flat_config(config: Dict): + def _format_tcp_scan_from_flat_config(config: Dict) -> Dict[str, Any]: flat_http_ports_field = "HTTP_PORTS" flat_tcp_timeout_field = "tcp_scan_timeout" flat_tcp_ports_field = "tcp_target_ports" @@ -526,7 +526,7 @@ class ConfigService: return sorted(combined_ports) @staticmethod - def _format_icmp_scan_from_flat_config(config: Dict): + def _format_icmp_scan_from_flat_config(config: Dict) -> Dict[str, Any]: flat_ping_timeout_field = "ping_scan_timeout" formatted_icmp_scan_config = {} @@ -537,7 +537,7 @@ class ConfigService: return formatted_icmp_scan_config @staticmethod - def _format_fingerprinters_from_flat_config(config: Dict): + def _format_fingerprinters_from_flat_config(config: Dict) -> List[Dict[str, Any]]: flat_fingerprinter_classes_field = "finger_classes" flat_http_ports_field = "HTTP_PORTS" @@ -556,11 +556,11 @@ class ConfigService: return formatted_fingerprinters @staticmethod - def _translate_fingerprinter_name(name: str): + def _translate_fingerprinter_name(name: str) -> str: return re.sub(r"Finger", "", name).lower() @staticmethod - def _format_targets_from_flat_config(config: Dict): + def _format_targets_from_flat_config(config: Dict) -> Dict[str, Any]: flat_blocked_ips_field = "blocked_ips" flat_inaccessible_subnets_field = "inaccessible_subnets" flat_local_network_scan_field = "local_network_scan" @@ -587,7 +587,7 @@ class ConfigService: return formatted_scan_targets_config @staticmethod - def _format_exploiters_from_flat_config(config: Dict): + def _format_exploiters_from_flat_config(config: Dict) -> Dict[str, List[Dict[str, Any]]]: flat_config_exploiter_classes_field = "exploiter_classes" brute_force_category = "brute_force" vulnerability_category = "vulnerability" From 8e4eeb2f5eb49bab2a824a89f3e7ee5622a9d231 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 07:45:24 -0500 Subject: [PATCH 12/15] Agent: Fix inaccurate type-hint in IPScanner._run_fingerprinters() --- monkey/infection_monkey/master/ip_scanner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 67520054e..5c768506b 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -3,7 +3,7 @@ import queue import threading from queue import Queue from threading import Event -from typing import Callable, Dict, List +from typing import Any, Callable, Dict, List from infection_monkey.i_puppet import ( FingerprintData, @@ -87,7 +87,7 @@ class IPScanner: def _run_fingerprinters( self, ip: str, - fingerprinters: List[str], + fingerprinters: List[Dict[str, Any]], ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], stop: Event, From 373a25d5f62405ac079bb7ded47511ee68094ff6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 08:41:00 -0500 Subject: [PATCH 13/15] Agent: Improve comments in HTTPFingerprinter --- monkey/infection_monkey/network/http_fingerprinter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 5ebc2c514..190e056ef 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) class HTTPFingerprinter(IFingerprinter): """ - Goal is to recognise HTTP servers, where what we currently care about is apache. + Queries potential HTTP(S) ports and attempt to determine the server software that handles the + HTTP requests. """ def get_host_fingerprint( @@ -51,7 +52,6 @@ def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], O http = f"http://{host}:{port}" https = f"https://{host}:{port}" - # try http, we don't optimise for 443 for url, ssl in ((https, True), (http, False)): # start with https and downgrade server_header_contents = _get_server_from_headers(url) From 0a04e846ba83cb5ae37b30072e1aadc66290ddaa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 08:48:17 -0500 Subject: [PATCH 14/15] Agent: Add missing return type to HTTPFingerprinter --- monkey/infection_monkey/network/http_fingerprinter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 190e056ef..6333dad6a 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -28,7 +28,7 @@ class HTTPFingerprinter(IFingerprinter): _: PingScanData, port_scan_data: Dict[int, PortScanData], options: Dict, - ): + ) -> FingerprintData: services = {} http_ports = set(options.get("http_ports", [])) ports_to_fingerprint = _get_open_http_ports(http_ports, port_scan_data) From 69fa4adf1fa7eaf4c1dfaa24bc18719e023e53c8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 09:04:59 -0500 Subject: [PATCH 15/15] Island: Add comment describing _translate_fingerprinter_name() --- monkey/monkey_island/cc/services/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index ba37d357c..f113c437e 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -557,6 +557,10 @@ class ConfigService: @staticmethod def _translate_fingerprinter_name(name: str) -> str: + # This translates names like "HTTPFinger" to "http". "HTTPFinger" is an old classname on the + # agent-side and is therefore unnecessarily couples the island to the fingerprinter's + # implementation within the agent. For the time being, fingerprinters will have names like + # "http", "ssh", "elastic", etc. This will be revisited when fingerprinters become plugins. return re.sub(r"Finger", "", name).lower() @staticmethod