From faa7571127c4df8bee988ebf323861c451454046 Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Sun, 10 Jun 2018 19:34:07 +0300 Subject: [PATCH] 1. Added an --active flag, to allow optional "Proof" result, which will do an active hunting of a found vulnerability 2. Added a --remote flag to specify remote clusters/machines for hunting. 3. Improved a bit of the architecture, (Services) Note: The reporter module, will gather vulnerabilities before their active hunting will start. This is not an issue, as we can access all of the attributes of the event directly from the active hunter (event.previous), which we will proccess on the end in the report --- kube-hunter.py | 8 +++++--- log/reporter.py | 30 ++++++++++-------------------- modules/discovery/dashboard.py | 6 ++---- modules/discovery/hosts.py | 10 +++++++--- modules/discovery/kubelet.py | 14 ++++++++------ modules/discovery/proxy.py | 1 + modules/events/types/common.py | 7 ++++++- modules/hunting/kubelet.py | 25 +++++++++++++++++++------ modules/hunting/proxy.py | 19 +++++++++++++++---- 9 files changed, 73 insertions(+), 47 deletions(-) diff --git a/kube-hunter.py b/kube-hunter.py index 95b250d..680ce75 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -6,8 +6,10 @@ import sys import time parser = argparse.ArgumentParser(description='Kube-Hunter, Hunter for weak Kubernetes cluster') +parser.add_argument('--pod', action="store_true", help="set hunter as an insider pod") +parser.add_argument('--remote', nargs='+', metavar="LOGLEVEL", help="one or more remote ip/dns to hunt") +parser.add_argument('--active', action="store_true", help="enables active hunting") parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set log level, options are:\nDEBUG INFO WARNING") -parser.add_argument('--pod', action="store_true", help="When set, will scan the cluster as a pod, when unset, will scan all network interfaces") args = parser.parse_args() try: loglevel = getattr(logging, args.log.upper()) @@ -25,7 +27,7 @@ from modules.discovery.hosts import HostScanEvent def main(): logging.info("Started") try: - handler.publish_event(HostScanEvent(pod=args.pod, active=True)) + handler.publish_event(HostScanEvent(pod=args.pod, active=args.active, predefined_hosts=args.remote)) # Blocking to see discovery output while(True): time.sleep(100) @@ -34,7 +36,7 @@ def main(): finally: handler.free() logging.debug("Cleaned Queue") - log.print_results() + log.print_results(args.active) if __name__ == '__main__': main() diff --git a/log/reporter.py b/log/reporter.py index 7040df4..e530437 100644 --- a/log/reporter.py +++ b/log/reporter.py @@ -6,7 +6,6 @@ from modules.discovery.kubelet import KubeletExposedHandler services = list() vulnerabilities = list() -informations = list() @handler.subscribe(Vulnerability) class VulnerabilityReport(object): @@ -15,23 +14,11 @@ class VulnerabilityReport(object): def execute(self): logging.info("[VULNERABILITY - {name}] {desc}".format( - name=self.vulnerability.name, + name=self.vulnerability.get_name(), desc=self.vulnerability.explain(), )) vulnerabilities.append(self.vulnerability) -@handler.subscribe(Information) -class ClusterInformation(object): - def __init__(self, event): - self.information = event - - def execute(self): - logging.info("[INFORMATION - {name}] {desc}".format( - name=self.information.get_name(), - desc=self.information.explain(), - )) - informations.append(self.information) - @handler.subscribe(Service) class OpenServiceReport(object): def __init__(self, event): @@ -46,16 +33,19 @@ class OpenServiceReport(object): )) services.append(self.service) - - -def print_results(): +def print_results(active): services_table = PrettyTable(["Service", "Location", "Description"]) for service in services: - services_table.add_row([service.get_name(), "{}:{}".format(service.host, service.port), service.explain()]) + services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) - vuln_table = PrettyTable(["Location", "From Component", "Vulnerability", "Description"]) + column_names = ["Location", "From Component", "Vulnerability", "Description"] + if active: column_names.append("Proof") + + vuln_table = PrettyTable(column_names) for vuln in vulnerabilities: - vuln_table.add_row(["{}:{}".format(vuln.host, vuln.port), vuln.component.name, vuln.get_name(), vuln.explain()]) + row = ["{}:{}".format(vuln.host, vuln.port), vuln.component.name, vuln.get_name(), vuln.explain()] + if active: row.append(vuln.attrs) + vuln_table.add_row(row) print "\nOpen Services:" print services_table diff --git a/modules/discovery/dashboard.py b/modules/discovery/dashboard.py index f2c3321..28fef2b 100644 --- a/modules/discovery/dashboard.py +++ b/modules/discovery/dashboard.py @@ -8,10 +8,8 @@ from ..types import Hunter class KubeDashboardEvent(Service, Event): """Allows multiple arbitrary operations on the cluster from all connections""" - def __init__(self, path="/", secure=False): - self.path = path - self.secure - Service.__init__(self, name="Kubernetes Dashboard") + def __init__(self, **kargs): + Service.__init__(self, name="Kubernetes Dashboard", **kargs) @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 30000) class KubeDashboard(Hunter): diff --git a/modules/discovery/hosts.py b/modules/discovery/hosts.py index bc81a49..b4f5526 100644 --- a/modules/discovery/hosts.py +++ b/modules/discovery/hosts.py @@ -13,9 +13,10 @@ from ..events.types import Event, NewHostEvent from ..types import Hunter class HostScanEvent(Event): - def __init__(self, pod=False, active=False): + def __init__(self, pod=False, active=False, predefined_hosts=list()): self.pod = pod self.active = active # flag to specify whether to get actual data from vulnerabilities + self.predefined_hosts = predefined_hosts self.auth_token = self.get_auth_token() self.client_cert = self.get_client_cert() @@ -37,14 +38,17 @@ class HostDiscovery(Hunter): def execute(self): logging.info("Discovering Open Kubernetes Services...") + if self.event.pod: if self.is_azure_cluster(): self.azure_metadata_discovery() else: self.traceroute_discovery() - else: - # self.publish_event(NewHostEvent(host="acs954agent1.westus2.cloudapp.azure.com")) # test cluster + elif len(self.event.predefined_hosts) == 0: self.scan_interfaces() + else: + for host in self.event.predefined_hosts: + self.publish_event(NewHostEvent(host=host)) def is_azure_cluster(self): try: diff --git a/modules/discovery/kubelet.py b/modules/discovery/kubelet.py index 10aec1e..2ed8127 100644 --- a/modules/discovery/kubelet.py +++ b/modules/discovery/kubelet.py @@ -23,6 +23,7 @@ class SecureKubeletEvent(Service, Event): self.token = token Service.__init__(self, name="Kubelet API") + """ Vulnerabilities """ class PodsHandler: """Exposes sensitive information about pods that are bound to the node""" @@ -47,6 +48,12 @@ class AnonymousAuthEnabled(Vulnerability, Event): def proof(self): pass # TODO: decide on an appropriate proof + + +class KubeletPorts(Enum): + SECURED = 10250 + READ_ONLY = 10255 + @handler.subscribe(OpenPortEvent, predicate= lambda x: x.port == 10255 or x.port == 10250) class KubeletDiscovery(Hunter): def __init__(self, event): @@ -89,9 +96,4 @@ class KubeletDiscovery(Hunter): if self.event.port == KubeletPorts.SECURED.value: self.get_secure_access() elif self.event.port == KubeletPorts.READ_ONLY.value: - self.get_read_only_access() - - -class KubeletPorts(Enum): - SECURED = 10250 - READ_ONLY = 10255 + self.get_read_only_access() \ No newline at end of file diff --git a/modules/discovery/proxy.py b/modules/discovery/proxy.py index d5d3f48..eed83fa 100644 --- a/modules/discovery/proxy.py +++ b/modules/discovery/proxy.py @@ -8,6 +8,7 @@ from ..events import handler from ..events.types import Service, Event, OpenPortEvent class KubeProxyEvent(Event, Service): + """proxies from a localhost address to the Kubernetes apiserver""" def __init__(self): Service.__init__(self, name="Kubernetes Proxy") diff --git a/modules/events/types/common.py b/modules/events/types/common.py index 6b67d93..c68e61f 100644 --- a/modules/events/types/common.py +++ b/modules/events/types/common.py @@ -35,12 +35,17 @@ class Kubelet(KubernetesCluster): """ Event Types """ # TODO: make proof an abstract method. class Service(object): - def __init__(self, name): + def __init__(self, name, path="", secure=False): self.name = name + self.secure = secure + self.path = path def get_name(self): return self.name + def get_path(self): + return "/" + self.path if self.path else "" + def explain(self): return self.__doc__ diff --git a/modules/hunting/kubelet.py b/modules/hunting/kubelet.py index 4a45dcc..8bcb802 100644 --- a/modules/hunting/kubelet.py +++ b/modules/hunting/kubelet.py @@ -14,7 +14,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class ContainerLogsHandler: """Outputs logs from a running container""" - name="/containerlogs" + name="/containerLogs" remediation="--enable-debugging-handlers=False On Kubelet" class RunningPodsHandler: @@ -248,24 +248,37 @@ class SecureKubeletPortHunter(Hunter): """ Active Hunting Of Handlers""" -@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler=="exec" and x.active) +@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==ExecHandler.name and x.active) class ActiveExecHandler(Hunter): def __init__(self, event): - self.event = Event + self.event = event def execute(self): pass - -@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler=="run" and x.active) + +@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==RunHandler.name and x.active) class ActiveRunHandler(Hunter): def __init__(self, event): - self.event = Event + self.event = event def execute(self): pass +@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==ContainerLogsHandler.name and x.active) +class ActiveContainerLogs(Hunter): + def __init__(self, event): + self.event = event + def execute(self): + pass +@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==AttachHandler.name and x.active) +class ActiveContainerLogs(Hunter): + def __init__(self, event): + self.event = event + + def execute(self): + pass # def get_kubesystem_pod_container(self): # pods_data = json.loads(requests.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False).text)['items'] # # filter running kubesystem pod diff --git a/modules/hunting/proxy.py b/modules/hunting/proxy.py index 36565b5..16a95bd 100644 --- a/modules/hunting/proxy.py +++ b/modules/hunting/proxy.py @@ -1,15 +1,23 @@ +import logging from enum import Enum -from ..types import Hunter from requests import get -from ..events import handler from ..discovery.dashboard import KubeDashboardEvent from ..discovery.proxy import KubeProxyEvent +from ..events import handler +from ..events.types import Vulnerability, Event, KubernetesCluster +from ..types import Hunter + class Service(Enum): DASHBOARD = "kubernetes-dashboard" +class KubeProxyExposed(Vulnerability, Event): + """Exposes all oprations on the cluster""" + def __init__(self): + Vulnerability.__init__(self, KubernetesCluster, "Proxy Exposed") + @handler.subscribe(KubeProxyEvent) class KubeProxy(Hunter): def __init__(self, event): @@ -17,10 +25,12 @@ class KubeProxy(Hunter): self.api_url = "http://{host}:{port}/api/v1".format(host=self.event.host, port=self.event.port) def execute(self): + self.publish_event(KubeProxyExposed()) for namespace, services in self.services.items(): for service in services: - curr_path = "api/v1/namespaces/{ns}/services/{sv}/proxy".format(ns=namespace,sv=service) # TODO: check if /proxy is a convention on other services - if service == Service.DASHBOARD: + if service == Service.DASHBOARD.value: + logging.debug(service) + curr_path = "api/v1/namespaces/{ns}/services/{sv}/proxy".format(ns=namespace,sv=service) # TODO: check if /proxy is a convention on other services self.publish_event(KubeDashboardEvent(path=curr_path, secure=False)) @property @@ -36,6 +46,7 @@ class KubeProxy(Hunter): resource_path = "/namespaces/{ns}/services".format(ns=namespace) resource_json = get(self.api_url + resource_path).json() services[namespace] = self.extract_names(resource_json) + logging.debug(services) return services @staticmethod