From 7c62cc21af384de6942f42b6f967f106c959e53e Mon Sep 17 00:00:00 2001 From: danielsagi Date: Fri, 28 Jan 2022 18:54:36 +0200 Subject: [PATCH] Feature: Custom Hunting (#489) * added partial and partial-names flag. mechanism for whitelisting hunter subscrption for custom hunts * changed name from partial to custom * ran black to format * flake8 formatting * added documentation in readme for Custom hunting and made Advanced Usage a higher level topic * added Collector, StartedInfo and SendFullReport to the core_hunters * changed old name class-names to raw-hunter-names * fixed bug in import loop --- README.md | 50 ++++++++++++++++++++--- kube_hunter/__main__.py | 13 +++++- kube_hunter/conf/__init__.py | 9 +++- kube_hunter/conf/parser.py | 16 ++++++++ kube_hunter/core/events/handler.py | 30 +++++++++++++- kube_hunter/modules/report/dispatchers.py | 6 +-- 6 files changed, 108 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7e6c7a9..b8cdc3e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Table of Contents ================= - [Table of Contents](#table-of-contents) - - [Kuberentes ATT&CK Matrix](#kuberentes-attck-matrix) + - [Kubernetes ATT&CK Matrix](#kubernetes-attck-matrix) - [Hunting](#hunting) - [Where should I run kube-hunter?](#where-should-i-run-kube-hunter) - [Scanning options](#scanning-options) @@ -39,8 +39,9 @@ Table of Contents - [Nodes Mapping](#nodes-mapping) - [Output](#output) - [Dispatching](#dispatching) - - [Advanced Usage](#advanced-usage) - - [Azure Quick Scanning](#azure-quick-scanning) + - [Advanced Usage](#advanced-usage) + - [Azure Quick Scanning](#azure-quick-scanning) + - [Custom Hunting](#custom-hunting) - [Deployment](#deployment) - [On Machine](#on-machine) - [Prerequisites](#prerequisites) @@ -53,6 +54,7 @@ Table of Contents --- ## Kubernetes ATT&CK Matrix + kube-hunter now supports the new format of the Kubernetes ATT&CK matrix. While kube-hunter's vulnerabilities are a collection of creative techniques designed to mimic an attacker in the cluster (or outside it) The Mitre's ATT&CK defines a more general standardised categories of techniques to do so. @@ -64,7 +66,6 @@ _Some kube-hunter vulnerabities which we could not map to Mitre technique, are p ![kube-hunter](./MITRE.png) ## Hunting - ### Where should I run kube-hunter? There are three different ways to run kube-hunter, each providing a different approach to detecting weaknesses in your cluster: @@ -157,11 +158,48 @@ Available dispatch methods are: * KUBEHUNTER_HTTP_DISPATCH_METHOD (defaults to: POST) -### Advanced Usage -#### Azure Quick Scanning +## Advanced Usage +### Azure Quick Scanning When running **as a Pod in an Azure or AWS environment**, kube-hunter will fetch subnets from the Instance Metadata Service. Naturally this makes the discovery process take longer. To hardlimit subnet scanning to a `/24` CIDR, use the `--quick` option. +### Custom Hunting +Custom hunting enables advanced users to have control over what hunters gets registered at the start of a hunt. +**If you know what you are doing**, this can help if you want to adjust kube-hunter's hunting and discovery process for your needs. + +Example: +``` +kube-hunter --custom +``` +Enabling Custom hunting removes all hunters from the hunting process, except the given whitelisted hunters. + +The `--custom` flag reads a list of hunters class names, in order to view all of kube-hunter's class names, you can combine the flag `--raw-hunter-names` with the `--list` flag. + +Example: +``` +kube-hunter --active --list --raw-hunter-names +``` + +**Notice**: Due to kube-huner's architectural design, the following "Core Hunters/Classes" will always register (even when using custom hunting): +* HostDiscovery + * _Generates ip addresses for the hunt by given configurations_ + * _Automatically discovers subnets using cloud Metadata APIs_ +* FromPodHostDiscovery + * _Auto discover attack surface ip addresses for the hunt by using Pod based environment techniques_ + * _Automatically discovers subnets using cloud Metadata APIs_ +* PortDiscovery + * _Port scanning given ip addresses for known kubernetes services ports_ +* Collector + * _Collects discovered vulnerabilities and open services for future report_ +* StartedInfo + * _Prints the start message_ +* SendFullReport + * _Dispatching the report based on given configurations_ + + + + + ## Deployment There are three methods for deploying kube-hunter: diff --git a/kube_hunter/__main__.py b/kube_hunter/__main__.py index b48fc39..fb244c8 100755 --- a/kube_hunter/__main__.py +++ b/kube_hunter/__main__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # flake8: noqa: E402 +from functools import partial import logging import threading @@ -29,6 +30,7 @@ config = Config( service_account_token=args.service_account_token, kubeconfig=args.kubeconfig, enable_cve_hunting=args.enable_cve_hunting, + custom=args.custom, ) setup_logger(args.log, args.log_file) set_config(config) @@ -73,16 +75,20 @@ def interactive_set_config(): return True -def list_hunters(): +def list_hunters(class_names=False): print("\nPassive Hunters:\n----------------") for hunter, docs in handler.passive_hunters.items(): name, doc = hunter.parse_docs(docs) + if class_names: + name = hunter.__name__ print(f"* {name}\n {doc}\n") if config.active: print("\n\nActive Hunters:\n---------------") for hunter, docs in handler.active_hunters.items(): name, doc = hunter.parse_docs(docs) + if class_names: + name = hunter.__name__ print(f"* {name}\n {doc}\n") @@ -95,7 +101,10 @@ def main(): scan_options = [config.pod, config.cidr, config.remote, config.interface, config.k8s_auto_discover_nodes] try: if args.list: - list_hunters() + if args.raw_hunter_names: + list_hunters(class_names=True) + else: + list_hunters() return if not any(scan_options): diff --git a/kube_hunter/conf/__init__.py b/kube_hunter/conf/__init__.py index 153654b..a2bf8d8 100644 --- a/kube_hunter/conf/__init__.py +++ b/kube_hunter/conf/__init__.py @@ -1,7 +1,11 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Optional +def get_default_core_hunters(): + return ["FromPodHostDiscovery", "HostDiscovery", "PortDiscovery", "SendFullReport", "Collector", "StartedInfo"] + + @dataclass class Config: """Config is a configuration container. @@ -41,6 +45,9 @@ class Config: service_account_token: Optional[str] = None kubeconfig: Optional[str] = None enable_cve_hunting: bool = False + custom: Optional[list] = None + raw_hunter_names: bool = False + core_hunters: list = field(default_factory=get_default_core_hunters) _config: Optional[Config] = None diff --git a/kube_hunter/conf/parser.py b/kube_hunter/conf/parser.py index 15c8513..3b3bd02 100644 --- a/kube_hunter/conf/parser.py +++ b/kube_hunter/conf/parser.py @@ -46,6 +46,22 @@ def parser_add_arguments(parser): help="One or more remote ip/dns to hunt", ) + parser.add_argument( + "-c", + "--custom", + nargs="+", + metavar="HUNTERS", + default=list(), + help="Custom hunting. Only given hunter names will register in the hunt." + "for a list of options run `--list --raw-hunter-names`", + ) + + parser.add_argument( + "--raw-hunter-names", + action="store_true", + help="Use in combination with `--list` to display hunter class names to pass for custom hunting flag", + ) + parser.add_argument( "--k8s-auto-discover-nodes", action="store_true", diff --git a/kube_hunter/core/events/handler.py b/kube_hunter/core/events/handler.py index 1a49cb0..8224d0b 100644 --- a/kube_hunter/core/events/handler.py +++ b/kube_hunter/core/events/handler.py @@ -257,9 +257,33 @@ class EventQueue(Queue): self.hooks[event].append((hook, predicate)) logging.debug("{} subscribed to {}".format(hook, event)) + def allowed_for_custom_registration(self, target_hunter): + """ + Check if the partial input list contains the hunter we are about to register for events + If hunter is considered a Core hunter as specified in `config.core_hunters` we allow it anyway + + Returns true if: + 1. partial hunt is disabled + 2. partial hunt is enabled and hunter is in core hunter class + 3. partial hunt is enabled and hunter is specified in config.partial + + @param target_hunter: hunter class for registration check + """ + config = get_config() + if not config.custom: + return True + + hunter_class_name = target_hunter.__name__ + if hunter_class_name in config.core_hunters or hunter_class_name in config.custom: + return True + + return False + def subscribe_event(self, event, hook=None, predicate=None, is_register=True): if not is_register: return + if not self.allowed_for_custom_registration(hook): + return if not self._register_hunters(hook): return @@ -272,9 +296,11 @@ class EventQueue(Queue): def subscribe_events(self, events, hook=None, predicates=None, is_register=True): if not is_register: - return False + return + if not self.allowed_for_custom_registration(hook): + return if not self._register_hunters(hook): - return False + return if predicates is None: predicates = [None] * len(events) diff --git a/kube_hunter/modules/report/dispatchers.py b/kube_hunter/modules/report/dispatchers.py index 1f17b7c..1f1a5b6 100644 --- a/kube_hunter/modules/report/dispatchers.py +++ b/kube_hunter/modules/report/dispatchers.py @@ -12,11 +12,7 @@ class HTTPDispatcher: dispatch_url = os.environ.get("KUBEHUNTER_HTTP_DISPATCH_URL", "https://localhost/") try: r = requests.request( - dispatch_method, - dispatch_url, - json=report, - headers={"Content-Type": "application/json"}, - verify=False + dispatch_method, dispatch_url, json=report, headers={"Content-Type": "application/json"}, verify=False ) r.raise_for_status() logger.info(f"Report was dispatched to: {dispatch_url}")