diff --git a/kube-hunter.py b/kube-hunter.py index d9ea26c..587421f 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -6,42 +6,81 @@ import sys import time parser = argparse.ArgumentParser(description='Kube-Hunter - hunts for security weaknesses in Kubernetes clusters') +parser.add_argument('--internal', action="store_true", help="set hunting of all internal network interfaces") parser.add_argument('--pod', action="store_true", help="set hunter as an insider pod") parser.add_argument('--cidr', type=str, help="set manual cidr to scan, example: 192.168.0.0/16") -parser.add_argument('--quick', action="store_true", help="scanning only known small sections of the subnet") -parser.add_argument('--mapping', action="store_true", help="outputs only mapping of cluster's nodes") +parser.add_argument('--mapping', action="store_true", help="outputs only a mapping of the cluster's nodes") parser.add_argument('--remote', nargs='+', metavar="HOST", default=list(), 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('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set log level, options are: debug, info, warn, none") +parser.add_argument('--token', type=str, metavar="AQUA_TOKEN", help="specify the token retrieved from Aqua, after finished executing, the report will be visible on kube-hunter's site") + config = parser.parse_args() try: loglevel = getattr(logging, config.log.upper()) except: pass -logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s') +if config.log.lower() != "none": + logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s') -import log +from report import reporter from src.core.events import handler from src.modules.discovery import HostDiscovery from src.modules.discovery.hosts import HostScanEvent + +def interactive_set_config(): + """Sets config manually, returns True for success""" + options = { + "Remote scanning": "scans one or more specific IPs or DNS names", + "Internal scanning": "scans all network interfaces", + "CIDR scanning": "scans a spesific cidr" + } # maps between option and its explanation + + print "Choose one of the options below:" + for i, (option, explanation) in enumerate(options.items()): + print "{}. {} ({})".format(i+1, option.ljust(20), explanation) + choice = raw_input("Your choice: ") + if choice == '1': + config.remote = raw_input("Remotes (seperated by a ','): ").replace(' ', '').split(',') + elif choice == '2': + config.internal = True + elif choice == '3': + config.cidr = raw_input("CIDR (example - 192.168.1.0/24): ").replace(' ', '') + else: + return False + return True + def main(): - logging.info("Started") + scan_options = [ + config.pod, + config.cidr, + config.remote, + config.internal + ] + hunt_started = False try: - handler.publish_event(HostScanEvent(predefined_hosts=config.remote)) + if not any(scan_options): + if not interactive_set_config(): return + hunt_started = True + logging.info("Started") + handler.publish_event(HostScanEvent()) + # Blocking to see discovery output handler.join() except KeyboardInterrupt: logging.debug("Kube-Hunter stopped by user") finally: - handler.free() - logging.debug("Cleaned Queue") - log.print_results() + if hunt_started: + handler.free() + logging.debug("Cleaned Queue") + if config.token: + reporter.send_report(token=config.token) + else: + reporter.print_tables() + if config.pod: while True: time.sleep(5) if __name__ == '__main__': - main() - - -# Proof -> Evidence \ No newline at end of file + main() \ No newline at end of file diff --git a/log/reporter.py b/log/reporter.py deleted file mode 100644 index ae9b4cd..0000000 --- a/log/reporter.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging - -from prettytable import ALL, PrettyTable - -from __main__ import config -from src.core.events import handler -from src.core.events.types import Service, Vulnerability - -services = list() -vulnerabilities = list() - -EVIDENCE_PREVIEW = 40 -MAX_WIDTH_VULNS = 70 -MAX_WIDTH_SERVICES = 60 - -@handler.subscribe(Vulnerability) -class VulnerabilityReport(object): - def __init__(self, event): - self.vulnerability = event - - def execute(self): - logging.info("[VULNERABILITY - {name}] {desc}".format( - name=self.vulnerability.get_name(), - desc=self.vulnerability.explain(), - )) - vulnerabilities.append(self.vulnerability) - # TODO: Add ActiveHunter replacement by id, when a vulnerability comes from active hunter, it replaces it's predecessor - -@handler.subscribe(Service) -class OpenServiceReport(object): - def __init__(self, event): - self.service = event - - def execute(self): - logging.info("[OPEN SERVICE - {name}] IP:{host} PORT:{port}".format( - name=self.service.name, - desc=self.service.desc, - host=self.service.host, - port=self.service.port - )) - services.append(self.service) - -def print_nodes(): - nodes_table = PrettyTable(["Type", "Location"], hrules=ALL) - nodes_table.align="l" - nodes_table.max_width=MAX_WIDTH_SERVICES - nodes_table.padding_width=1 - nodes_table.sortby="Type" - nodes_table.reversesort=True - nodes_table.header_style="upper" - - # TODO: replace with sets - id_memory = list() - for service in services: - if service.id not in id_memory: - nodes_table.add_row(["Slave/Master", service.host]) - id_memory.append(service.id) - print "Nodes:" - print nodes_table - print - -def print_services(): - services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) - services_table.align="l" - services_table.max_width=MAX_WIDTH_SERVICES - services_table.padding_width=1 - services_table.sortby="Service" - services_table.reversesort=True - services_table.header_style="upper" - for service in services: - services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) - print "Open Services:" - print services_table - print - -def print_vulnerabilities(): - column_names = ["Location", "Category", "Vulnerability", "Description"] - if config.active: column_names.append("Evidence") - vuln_table = PrettyTable(column_names, hrules=ALL) - vuln_table.align="l" - vuln_table.max_width=MAX_WIDTH_VULNS - vuln_table.sortby="Category" - vuln_table.reversesort=True - vuln_table.padding_width=1 - vuln_table.header_style="upper" - for vuln in vulnerabilities: - row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.component.name, vuln.get_name(), vuln.explain()] - if config.active: - evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence) - row.append(evidence) - vuln_table.add_row(row) - print "Vulnerabilities:" - print vuln_table - print - - -def print_results(): - print_nodes() - if not config.mapping: - print_services() - print_vulnerabilities() \ No newline at end of file diff --git a/log/__init__.py b/report/__init__.py similarity index 100% rename from log/__init__.py rename to report/__init__.py diff --git a/report/reporter.py b/report/reporter.py new file mode 100644 index 0000000..27fb748 --- /dev/null +++ b/report/reporter.py @@ -0,0 +1,209 @@ +import json +import logging +import time +from collections import defaultdict + +import requests +from prettytable import ALL, PrettyTable + +from __main__ import config +from src.core.events import handler +from src.core.events.types import Service, Vulnerability + +# [event, ...] +services = list() + +# [(TypeClass, event), ...] +insights = list() + +vulnerabilities = list() + +EVIDENCE_PREVIEW = 40 +MAX_WIDTH_VULNS = 70 +MAX_WIDTH_SERVICES = 60 + +AQUA_PUSH_URL = "https://qlyscbqwl7.execute-api.us-east-1.amazonaws.com/Prod/submit?token={token}" +AQUA_RESULTS_URL = "https://qlyscbqwl7.execute-api.us-east-1.amazonaws.com/Prod/result?token={token}" + +@handler.subscribe(Service) +@handler.subscribe(Vulnerability) +class Reporter(object): + """Reportes can be initiated by the event handler, and by regular decaration. for usage on end of runtime""" + def __init__(self, event=None): + self.event = event + self.insights_by_id = defaultdict(list) + self.services_by_id = defaultdict(list) + + def execute(self): + """function is called only when collecting data""" + global services, insights + bases = self.event.__class__.__mro__ + if Service in bases: + services.append(self.event) + logging.info("[OPEN SERVICE - {name}] IP:{host} PORT:{port}".format( + host=self.event.host, + port=self.event.port, + name=self.event.get_name(), + desc=self.event.explain() + )) + elif Vulnerability in bases: + insights.append((Vulnerability, self.event)) + vulnerabilities.append(self.event) + logging.info("[VULNERABILITY - {name}] {desc}".format( + name=self.event.get_name(), + desc=self.event.explain(), + )) + + if config.token: + self.send_report(token=config.token) + + def print_tables(self): + """generates report tables and outputs to stdout""" + if len(services): + print_nodes() + if not config.mapping: + print_services() + print_vulnerabilities() + else: + print "\nKube Hunter couldn't find any clusters" + # print "\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "") + + def build_sub_services(self, services_list): + # correlation functions + def get_insights_by_service(service): + """generates list of insights related to a given service""" + insights = list() + for insight_type, insight in self.insights_by_id[service.event_id]: + if service in insight.history: + insights.append((insight_type, insight)) + return insights + + def get_services_by_service(parent_service): + """generates list of insights related to a given service""" + services = list() + for service in self.services_by_id[parent_service.event_id]: + if service != parent_service and parent_service in service.history: + services.append(service) + self.services_by_id[parent_service.event_id].remove(service) + return services + + current_list = list() + for service in services_list: + current_list.append( + { + "type": service.get_name(), + "metadata": { + "port": service.port, + "path": service.get_path() + }, + "description": service.explain() + }) + next_services = get_services_by_service(service) + if next_services: + current_list[-1]["services"] = self.build_sub_services(next_services) + current_list[-1]["insights"] = [{ + "type": insight_type.__name__, + "name": insight.get_name(), + "description": insight.explain(), + "evidence": insight.evidence if insight_type == Vulnerability else "" + } for insight_type, insight in get_insights_by_service(service)] + return current_list + + def send_report(self, token): + def generate_report(): + """function generates a report corresponding to specifications of the frontend of kubehunter""" + for service in services: + self.services_by_id[service.event_id].append(service) + for insight_type, insight in insights: + self.insights_by_id[insight.event_id].append((insight_type, insight)) + + # building first layer of services (nodes) + report = defaultdict(list) + for _, services_list in self.services_by_id.items(): + service_report = { + "type": "Node", # on future, determine if slave or master + "metadata": { + "host": str(services_list[0].host) + }, + # then constructing their sub services tree + "services": self.build_sub_services(services_list) + } + report["services"].append(service_report) + return report + + finished = (not handler.unfinished_tasks) + logging.debug("generating report") + report = { + 'results': generate_report(), + 'metadata': { + 'finished': finished + } + } + logging.debug("uploading report") + r = requests.put(AQUA_PUSH_URL.format(token=token), json=report) + + if r.status_code == 201: # created status + logging.debug("report was uploaded successfully") + if finished: + print "\nYour report: \n{}".format(AQUA_RESULTS_URL.format(token=token)) + else: + logging.debug("Failed sending report with:{}, {}".format(r.status_code, r.text)) + if finished: + print "\nCould not send report.\n{}".format(json.loads(r.text).get("status", "")) + +reporter = Reporter() + + +""" Tables Generation """ +def print_nodes(): + nodes_table = PrettyTable(["Type", "Location"], hrules=ALL) + nodes_table.align="l" + nodes_table.max_width=MAX_WIDTH_SERVICES + nodes_table.padding_width=1 + nodes_table.sortby="Type" + nodes_table.reversesort=True + nodes_table.header_style="upper" + + # TODO: replace with sets + id_memory = list() + for service in services: + if service.event_id not in id_memory: + nodes_table.add_row(["Slave/Master", service.host]) + id_memory.append(service.event_id) + print "Nodes:" + print nodes_table + print + +def print_services(): + services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) + services_table.align="l" + services_table.max_width=MAX_WIDTH_SERVICES + services_table.padding_width=1 + services_table.sortby="Service" + services_table.reversesort=True + services_table.header_style="upper" + for service in services: + services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) + print "Open Services:" + print services_table + print + +def print_vulnerabilities(): + column_names = ["Location", "Category", "Vulnerability", "Description"] + if config.active: column_names.append("Evidence") + vuln_table = PrettyTable(column_names, hrules=ALL) + vuln_table.align="l" + vuln_table.max_width=MAX_WIDTH_VULNS + vuln_table.sortby="Category" + vuln_table.reversesort=True + vuln_table.padding_width=1 + vuln_table.header_style="upper" + for vuln in vulnerabilities: + row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.component.name, vuln.get_name(), vuln.explain()] + if config.active: + evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence) + row.append(evidence) + vuln_table.add_row(row) + print "Vulnerabilities:" + print vuln_table + print diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index a782712..66b4044 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -41,9 +41,6 @@ class Service(object): def explain(self): return self.__doc__ - def proof(self): - return self.name - class Vulnerability(object): def __init__(self, component, name): self.component = component @@ -64,7 +61,7 @@ class NewHostEvent(Event): def __init__(self, host, cloud=None): global event_id_count self.host = host - self.id = event_id_count + self.event_id = event_id_count self.cloud = cloud event_id_count += 1 diff --git a/src/modules/discovery/hosts.py b/src/modules/discovery/hosts.py index d0b922a..9a400cc 100644 --- a/src/modules/discovery/hosts.py +++ b/src/modules/discovery/hosts.py @@ -33,14 +33,14 @@ class HostScanEvent(Event): def get_auth_token(self): if config.pod: - with open("/run/secrets/kubernetes.io/serviceaccount/token") as token_file: - return token_file.read() - return None - + try: + with open("/run/secrets/kubernetes.io/serviceaccount/token") as token_file: + return token_file.read() + except IOError: + pass def get_client_cert(self): if config.pod: return "/run/secrets/kubernetes.io/serviceaccount/ca.crt" - return None @handler.subscribe(HostScanEvent) class HostDiscovery(Hunter): @@ -58,16 +58,16 @@ class HostDiscovery(Hunter): self.publish_event(NewHostEvent(host=ip, cloud=cloud)) except: logging.error("unable to parse cidr") + elif config.internal: + self.scan_interfaces() + elif len(config.remote) > 0: + for host in config.remote: + self.publish_event(NewHostEvent(host=host, cloud=self.get_cloud(host))) elif config.pod: if self.is_azure_pod(): self.azure_metadata_discovery() else: self.traceroute_discovery() - elif len(self.event.predefined_hosts) == 0: - self.scan_interfaces() - else: - for host in self.event.predefined_hosts: - self.publish_event(NewHostEvent(host=host, cloud=self.get_cloud(host))) def get_cloud(self, host): metadata = requests.get("http://www.azurespeed.com/api/region?ipOrUrl={ip}".format(ip=host)).text @@ -76,11 +76,10 @@ class HostDiscovery(Hunter): def is_azure_pod(self): try: - if requests.get("http://169.254.169.254/metadata/instance?api-version=2017-08-01", headers={"Metadata":"true"}).status_code == 200: + if requests.get("http://169.254.169.254/metadata/instance?api-version=2017-08-01", headers={"Metadata":"true"}, timeout=5).status_code == 200: return True - except Exception as ex: - logging.debug("Not azure cluster " + str(ex.message)) - return False + except requests.exceptions.ConnectionError: + return False # for pod scanning def traceroute_discovery(self): diff --git a/src/modules/discovery/kubelet.py b/src/modules/discovery/kubelet.py index 09c8132..aa3a206 100644 --- a/src/modules/discovery/kubelet.py +++ b/src/modules/discovery/kubelet.py @@ -24,22 +24,6 @@ class SecureKubeletEvent(Service, Event): Service.__init__(self, name="Kubelet API") -""" Vulnerabilities """ -class ExposedPodsHandler(Vulnerability, Event): - """Exposes sensitive information about pods that are bound to the node""" - def __init__(self): - Vulnerability.__init__(self, Kubelet, "Exposed /pods") - -class AnonymousAuthEnabled(Vulnerability, Event): - """Anonymous Auth to the kubelet, exposes secure access to all requests on the kubelet""" - def __init__(self): - Vulnerability.__init__(self, Kubelet, "Anonymous Authentication") - - def proof(self): - pass # TODO: decide on an appropriate proof - - - class KubeletPorts(Enum): SECURED = 10250 READ_ONLY = 10255 @@ -53,21 +37,18 @@ class KubeletDiscovery(Hunter): logging.debug(self.event.host) r = requests.get("http://{host}:{port}/pods".format(host=self.event.host, port=self.event.port)) if r.status_code == 200: - self.publish_event(ExposedPodsHandler()) self.publish_event(ReadOnlyKubeletEvent()) def get_secure_access(self): event = SecureKubeletEvent() if self.ping_kubelet(authenticate=False) == 200: - self.publish_event(ExposedPodsHandler()) - self.publish_event(AnonymousAuthEnabled()) - event.anonymous_auth = True + event.secure = False # anonymous authentication is disabled elif self.ping_kubelet(authenticate=True) == 200: - event.anonymous_auth = False + event.secure = True self.publish_event(event) - def ping_kubelet(self, authenticate=False): + def ping_kubelet(self, authenticate): r = requests.Session() if authenticate: if self.event.auth_token: diff --git a/src/modules/hunting/kubelet.py b/src/modules/hunting/kubelet.py index 4b50c4c..5df08d5 100644 --- a/src/modules/hunting/kubelet.py +++ b/src/modules/hunting/kubelet.py @@ -8,12 +8,22 @@ import urllib3 from __main__ import config from ...core.events import handler from ...core.events.types import Vulnerability, Event -from ..discovery.kubelet import ReadOnlyKubeletEvent, SecureKubeletEvent, ExposedPodsHandler +from ..discovery.kubelet import ReadOnlyKubeletEvent, SecureKubeletEvent from ...core.types import Hunter, ActiveHunter, KubernetesCluster, Kubelet urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) """ Vulnerabilities """ +class ExposedPodsHandler(Vulnerability, Event): + """Exposes all complete PodSpecs bound to a node""" + def __init__(self): + Vulnerability.__init__(self, Kubelet, "Exposed /pods") + +class AnonymousAuthEnabled(Vulnerability, Event): + """Anonymous Auth to the kubelet, exposes secure access to all requests on the kubelet""" + def __init__(self): + Vulnerability.__init__(self, Kubelet, "Anonymous Authentication") + class ExposedContainerLogsHandler(Vulnerability, Event): """Outputs logs from a running container""" def __init__(self): @@ -50,6 +60,11 @@ class ExposedAttachHandler(Vulnerability, Event): Vulnerability.__init__(self, Kubelet, "Exposed /attach") self.remediation="--enable-debugging-handlers=False On Kubelet" +class ExposedHealthzHandler(Vulnerability, Event): + """By accessing open /healthz handler, an attacker could get the cluster health state""" + def __init__(self): + Vulnerability.__init__(self, Kubelet, "Cluster Health Disclosure") + class K8sVersionDisclosure(Vulnerability, Event): """Discloses the kubernetes version, exposed from a log on the /metrics endpoint""" def __init__(self, version): @@ -70,6 +85,7 @@ class ReadOnlyKubeletPortHunter(Hunter): def __init__(self, event): self.event = event self.path = "http://{}:{}/".format(self.event.host, self.event.port) + self.pods_endpoint_data = "" def get_k8s_version(self): metrics = requests.get(self.path + "metrics").text @@ -82,23 +98,35 @@ class ReadOnlyKubeletPortHunter(Hunter): # returns list of tuples of Privileged container and their pod. def find_privileged_containers(self): - pods = json.loads(requests.get(self.path + "pods").text) privileged_containers = list() - if "items" in pods: - for pod in pods["items"]: + if self.pods_endpoint_data: + for pod in self.pods_endpoint_data["items"]: for container in pod["spec"]["containers"]: if "securityContext" in container and "privileged" in container["securityContext"] and container["securityContext"]["privileged"]: privileged_containers.append((pod["metadata"]["name"], container["name"])) return privileged_containers if len(privileged_containers) > 0 else None + + def get_pods_endpoint(self): + response = requests.get(self.path + "pods") + if "items" in response.text: + return json.loads(response.text) + + def check_healthz_endpoint(self): + return requests.get(self.path + "healthz", verify=False).status_code == 200 def execute(self): + self.pods_endpoint_data = self.get_pods_endpoint() k8s_version = self.get_k8s_version() privileged_containers = self.find_privileged_containers() if k8s_version: self.publish_event(K8sVersionDisclosure(version=k8s_version)) if privileged_containers: self.publish_event(PrivilegedContainers(containers=privileged_containers)) - + if self.pods_endpoint_data: + self.publish_event(ExposedPodsHandler()) + if self.check_healthz_endpoint(): + self.publish_event(ExposedHealthzHandler()) + @handler.subscribe(SecureKubeletEvent) class SecureKubeletPortHunter(Hunter): class DebugHandlers(object): @@ -186,55 +214,69 @@ class SecureKubeletPortHunter(Hunter): self.session = requests.Session() if self.event.secure: self.session.headers.update({"Authorization": "Bearer {}".format(self.event.auth_token)}) - self.session.cert = self.event.client_cert + # self.session.cert = self.event.client_cert self.path = "https://{}:{}/".format(self.event.host, 10250) + self.kubehunter_pod = {"name": "kube-hunter", "namespace": "default", "container": "kube-hunter"} + self.pods_endpoint_data = "" + + def get_pods_endpoint(self): + response = self.session.get(self.path + "pods", verify=False) + if "items" in response.text: + return json.loads(response.text) + + def check_healthz_endpoint(self): + return requests.get(self.path + "healthz", verify=False).status_code == 200 def execute(self): - self.test_debugging_handlers() + self.pods_endpoint_data = self.get_pods_endpoint() + if not self.event.secure: + self.publish_event(AnonymousAuthEnabled()) + if self.pods_endpoint_data: + self.publish_event(ExposedPodsHandler()) + if self.check_healthz_endpoint(): + self.publish_event(ExposedHealthzHandler()) + self.test_handlers() - def test_debugging_handlers(self): + def test_handlers(self): # if kube-hunter runs in a pod, we test with kube-hunter's pod - pod = self.get_self_pod() if config.pod else self.get_random_pod() - debug_handlers = self.DebugHandlers(self.path, pod=pod, session=self.session) - - try: - if debug_handlers.test_container_logs(): - self.publish_event(ExposedContainerLogsHandler()) - if debug_handlers.test_exec_container(): - self.publish_event(ExposedExecHandler()) - if debug_handlers.test_run_container(): - self.publish_event(ExposedRunHandler()) - if debug_handlers.test_running_pods(): - self.publish_event(ExposedRunningPodsHandler()) - if debug_handlers.test_port_forward(): - self.publish_event(ExposedPortForwardHandler()) # not implemented - if debug_handlers.test_attach_container(): - self.publish_event(ExposedAttachHandler()) - except Exception as ex: - logging.debug(str(ex.message)) - - def get_self_pod(self): - return {"name": "kube-hunter", - "namespace": "default", - "container": "kube-hunter"} + pod = self.kubehunter_pod if config.pod else self.get_random_pod() + if pod: + debug_handlers = self.DebugHandlers(self.path, pod=pod, session=self.session) + try: + if debug_handlers.test_container_logs(): + self.publish_event(ExposedContainerLogsHandler()) + if debug_handlers.test_exec_container(): + self.publish_event(ExposedExecHandler()) + if debug_handlers.test_run_container(): + self.publish_event(ExposedRunHandler()) + if debug_handlers.test_running_pods(): + self.publish_event(ExposedRunningPodsHandler()) + if debug_handlers.test_port_forward(): + self.publish_event(ExposedPortForwardHandler()) # not implemented + if debug_handlers.test_attach_container(): + self.publish_event(ExposedAttachHandler()) + except Exception as ex: + logging.debug(str(ex.message)) + else: + pass # no pod to check on. # trying to get a pod from default namespace, if doesnt exist, gets a kube-system one def get_random_pod(self): - pods_data = json.loads(self.session.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False).text)['items'] - # filter running kubesystem pod - is_default_pod = lambda pod: pod["metadata"]["namespace"] == "default" and pod["status"]["phase"] == "Running" - is_kubesystem_pod = lambda pod: pod["metadata"]["namespace"] == "kube-system" and pod["status"]["phase"] == "Running" - pod_data = next((pod_data for pod_data in pods_data if is_default_pod(pod_data)), None) - if not pod_data: - pod_data = next((pod_data for pod_data in pods_data if is_kubesystem_pod(pod_data)), None) - - container_data = (container_data for container_data in pod_data["spec"]["containers"]).next() - return { - "name": pod_data["metadata"]["name"], - "container": container_data["name"], - "namespace": pod_data["metadata"]["namespace"] - } - + if self.pods_endpoint_data: + pods_data = self.pods_endpoint_data["items"] + # filter running kubesystem pod + is_default_pod = lambda pod: pod["metadata"]["namespace"] == "default" and pod["status"]["phase"] == "Running" + is_kubesystem_pod = lambda pod: pod["metadata"]["namespace"] == "kube-system" and pod["status"]["phase"] == "Running" + pod_data = next((pod_data for pod_data in pods_data if is_default_pod(pod_data)), None) + if not pod_data: + pod_data = next((pod_data for pod_data in pods_data if is_kubesystem_pod(pod_data)), None) + + container_data = (container_data for container_data in pod_data["spec"]["containers"]).next() + return { + "name": pod_data["metadata"]["name"], + "container": container_data["name"], + "namespace": pod_data["metadata"]["namespace"] + } @handler.subscribe(ExposedRunHandler) class ProveRunHandler(ActiveHunter): @@ -267,6 +309,16 @@ class ProveRunHandler(ActiveHunter): self.event.evidence = "uname: " + output break + +@handler.subscribe(ExposedHealthzHandler) +class ProveHealthzHandler(ActiveHunter): + def __init__(self, event): + self.event = event + + def execute(self): + protocol = "https" if self.event.port == 10250 else "http" + self.event.evidence = requests.get("{protocol}://{host}:{port}/healthz".format(protocol=protocol, host=self.event.host, port=self.event.port), verify=False).text + @handler.subscribe(ExposedPodsHandler) class ProvePodsHandler(ActiveHunter): def __init__(self, event):