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