def run(self, command, container): config = get_config() run_url = f"{self.base_url}/run/{container['namespace']}/{container['pod']}/{container['name']}" return self.event.session.post(run_url, verify=False, params={"cmd": command}, timeout=config.network_timeout)
def aws_metadata_v1_discovery(self): config = get_config() logger.debug("From pod attempting to access aws's metadata v1") mac_address = requests.get( "http://169.254.169.254/latest/meta-data/mac", timeout=config.network_timeout, ).text logger.debug(f"Extracted mac from aws's metadata v1: {mac_address}") cidr = requests.get( f"http://169.254.169.254/latest/meta-data/network/interfaces/macs/{mac_address}/subnet-ipv4-cidr-block", timeout=config.network_timeout, ).text logger.debug(f"Trying to extract cidr from aws's metadata v1: {cidr}") try: cidr = cidr.split("/") address, subnet = (cidr[0], cidr[1]) subnet = subnet if not config.quick else "24" cidr = f"{address}/{subnet}" logger.debug(f"From pod discovered subnet {cidr}") self.publish_event(AWSMetadataApi(cidr=cidr)) return [(address, subnet)], "AWS" except Exception as x: logger.debug(f"ERROR: could not parse cidr from aws metadata api: {cidr} - {x}") return [], "AWS"
def get_pods(self, namespace=None): config = get_config() pods = [] try: if not namespace: r = requests.get( f"{self.path}/api/v1/pods", headers=self.headers, verify=False, timeout=config.network_timeout, ) else: r = requests.get( f"{self.path}/api/v1/namespaces/{namespace}/pods", headers=self.headers, verify=False, timeout=config.network_timeout, ) if r.status_code == 200: resp = json.loads(r.content) for item in resp["items"]: name = item["metadata"]["name"].encode("ascii", "ignore") namespace = item["metadata"]["namespace"].encode( "ascii", "ignore") pods.append({"name": name, "namespace": namespace}) return pods except (requests.exceptions.ConnectionError, KeyError): pass return None
def aws_metadata_v2_discovery(self): config = get_config() logger.debug("From pod attempting to access aws's metadata v2") token = requests.get( "http://169.254.169.254/latest/api/token", headers={"X-aws-ec2-metatadata-token-ttl-seconds": "21600"}, timeout=config.network_timeout, ).text mac_address = requests.get( "http://169.254.169.254/latest/meta-data/mac", headers={"X-aws-ec2-metatadata-token": token}, timeout=config.network_timeout, ).text cidr = requests.get( f"http://169.254.169.254/latest/meta-data/network/interfaces/macs/{mac_address}/subnet-ipv4-cidr-block", headers={"X-aws-ec2-metatadata-token": token}, timeout=config.network_timeout, ).text.split("/") try: address, subnet = (cidr[0], cidr[1]) subnet = subnet if not config.quick else "24" cidr = f"{address}/{subnet}" logger.debug(f"From pod discovered subnet {cidr}") self.publish_event(AWSMetadataApi(cidr=cidr)) return [(address, subnet)], "AWS" except Exception as x: logger.debug(f"ERROR: could not parse cidr from aws metadata api: {cidr} - {x}") return [], "AWS"
def execute(self): config = get_config() # Scan any hosts that the user specified if config.remote or config.cidr: self.publish_event(HostScanEvent()) else: # Discover cluster subnets, we'll scan all these hosts cloud = None if self.is_azure_pod(): subnets, cloud = self.azure_metadata_discovery() else: subnets = self.traceroute_discovery() should_scan_apiserver = False if self.event.kubeservicehost: should_scan_apiserver = True for ip, mask in subnets: if self.event.kubeservicehost and self.event.kubeservicehost in IPNetwork( f"{ip}/{mask}"): should_scan_apiserver = False logger.debug(f"From pod scanning subnet {ip}/{mask}") for ip in IPNetwork(f"{ip}/{mask}"): self.publish_event(NewHostEvent(host=ip, cloud=cloud)) if should_scan_apiserver: self.publish_event( NewHostEvent(host=IPAddress(self.event.kubeservicehost), cloud=cloud))
def execute(self): config = get_config() report = config.reporter.get_report(statistics=config.statistics, mapping=config.mapping) config.dispatcher.dispatch(report) handler.publish_event(ReportDispatched()) handler.publish_event(TablesPrinted())
def test_logs_endpoint(self): config = get_config() logs_url = self.session.get( self.path + KubeletHandlers.LOGS.value.format(path=""), timeout=config.network_timeout, ).text return "<pre>" in logs_url
def get_pods_endpoint(self): config = get_config() logger.debug("Attempting to find pods endpoints") response = requests.get(f"{self.path}/pods", timeout=config.network_timeout) if "items" in response.text: return response.json()
def test_running_pods(self): config = get_config() pods_url = self.path + KubeletHandlers.RUNNINGPODS.value r = self.session.get(pods_url, verify=False, timeout=config.network_timeout) return r.json() if r.status_code == 200 else False
def test_handlers(self): config = get_config() # if kube-hunter runs in a pod, we test with kube-hunter's pod pod = self.kubehunter_pod if config.pod else self.get_random_pod() if pod: debug_handlers = self.DebugHandlers(self.path, pod, self.session) try: # TODO: use named expressions, introduced in python3.8 running_pods = debug_handlers.test_running_pods() if running_pods: self.publish_event( ExposedRunningPodsHandler( count=len(running_pods["items"]))) cmdline = debug_handlers.test_pprof_cmdline() if cmdline: self.publish_event(ExposedKubeletCmdline(cmdline=cmdline)) 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_port_forward(): self.publish_event( ExposedPortForwardHandler()) # not implemented if debug_handlers.test_attach_container(): self.publish_event(ExposedAttachHandler()) if debug_handlers.test_logs_endpoint(): self.publish_event(ExposedSystemLogs()) except Exception: logger.debug("Failed testing debug handlers", exc_info=True)
def execute(self): config = get_config() pods_raw = self.event.session.get( self.base_url + KubeletHandlers.PODS.value, verify=False, timeout=config.network_timeout, ).text if "items" in pods_raw: pods_data = json.loads(pods_raw)["items"] for pod_data in pods_data: container_data = pod_data["spec"]["containers"][0] if container_data: container_name = container_data["name"] output = requests.get( f"{self.base_url}/" + KubeletHandlers.CONTAINERLOGS.value.format( pod_namespace=pod_data["metadata"]["namespace"], pod_id=pod_data["metadata"]["name"], container_name=container_name, ), verify=False, timeout=config.network_timeout, ) if output.status_code == 200 and output.text: self.event.evidence = f"{container_name}: {output.text}" return
def get_pods_endpoint(self): config = get_config() response = self.session.get(f"{self.path}/pods", verify=False, timeout=config.network_timeout) if "items" in response.text: return response.json()
def get_read_only_access(self): config = get_config() endpoint = f"http://{self.event.host}:{self.event.port}/pods" logger.debug(f"Trying to get kubelet read access at {endpoint}") r = requests.get(endpoint, timeout=config.network_timeout) if r.status_code == 200: self.publish_event(ReadOnlyKubeletEvent())
def execute(self): config = get_config() # Attempt to read all hosts from the Kubernetes API for host in list_all_k8s_cluster_nodes(config.kubeconfig): self.publish_event(NewHostEvent(host=host)) # Scan any hosts that the user specified if config.remote or config.cidr: self.publish_event(HostScanEvent()) else: # Discover cluster subnets, we'll scan all these hosts cloud, subnets = None, list() if self.is_azure_pod(): subnets, cloud = self.azure_metadata_discovery() elif self.is_aws_pod_v1(): subnets, cloud = self.aws_metadata_v1_discovery() elif self.is_aws_pod_v2(): subnets, cloud = self.aws_metadata_v2_discovery() subnets += self.gateway_discovery() should_scan_apiserver = False if self.event.kubeservicehost: should_scan_apiserver = True for ip, mask in subnets: if self.event.kubeservicehost and self.event.kubeservicehost in IPNetwork(f"{ip}/{mask}"): should_scan_apiserver = False logger.debug(f"From pod scanning subnet {ip}/{mask}") for ip in IPNetwork(f"{ip}/{mask}"): self.publish_event(NewHostEvent(host=ip, cloud=cloud)) if should_scan_apiserver: self.publish_event(NewHostEvent(host=IPAddress(self.event.kubeservicehost), cloud=cloud))
def publish_event(self, event, caller=None): config = get_config() # setting event chain if caller: event.previous = caller.event event.hunter = caller.__class__ # applying filters on the event, before publishing it to subscribers. # if filter returned None, not proceeding to publish event = self.apply_filters(event) if event: # If event was rewritten, make sure it's linked to its parent ('previous') event if caller: event.previous = caller.event event.hunter = caller.__class__ for hooked_event in self.hooks.keys(): if hooked_event in event.__class__.__mro__: for hook, predicate in self.hooks[hooked_event]: if predicate and not predicate(event): continue if config.statistics and caller: if Vulnerability in event.__class__.__mro__: caller.__class__.publishedVulnerabilities += 1 logger.debug( f"Event {event.__class__} got published with {event}" ) self.put(hook(event))
def get_key_container(self): config = get_config() endpoint = f"{self.base_url}/pods" logger.debug("Trying to find container with access to azure.json file") try: r = requests.get(endpoint, verify=False, timeout=config.network_timeout) except requests.Timeout: logger.debug("failed getting pod info") else: pods_data = r.json().get("items", []) suspicious_volume_names = [] for pod_data in pods_data: for volume in pod_data["spec"].get("volumes", []): if volume.get("hostPath"): path = volume["hostPath"]["path"] if "/etc/kubernetes/azure.json".startswith(path): suspicious_volume_names.append(volume["name"]) for container in pod_data["spec"]["containers"]: for mount in container.get("volumeMounts", []): if mount["name"] in suspicious_volume_names: return { "name": container["name"], "pod": pod_data["metadata"]["name"], "namespace": pod_data["metadata"]["namespace"], }
def test_container_logs(self): config = get_config() logs_url = self.path + KubeletHandlers.CONTAINERLOGS.value.format( pod_namespace=self.pod["namespace"], pod_id=self.pod["name"], container_name=self.pod["container"], ) return self.session.get(logs_url, verify=False, timeout=config.network_timeout).status_code == 200
def test_pprof_cmdline(self): config = get_config() cmd = self.session.get( self.path + KubeletHandlers.PPROF_CMDLINE.value, verify=False, timeout=config.network_timeout, ) return cmd.text if cmd.status_code == 200 else None
def run(self, command, container): config = get_config() run_url = "/".join(self.base_url, "run", container["namespace"], container["pod"], container["name"]) return requests.post(run_url, verify=False, params={"cmd": command}, timeout=config.network_timeout)
def traceroute_discovery(self): config = get_config() node_internal_ip = srp1( Ether() / IP(dst="1.1.1.1", ttl=1) / ICMP(), verbose=0, timeout=config.network_timeout, )[IP].src return [[node_internal_ip, "24"]]
def execute(self): config = get_config() version_metadata = requests.get( f"http://{self.event.host}:{self.event.port}/version", verify=False, timeout=config.network_timeout, ).json() if "buildDate" in version_metadata: self.event.evidence = "build date: {}".format(version_metadata["buildDate"])
def get_request(self, url, verify=False): config = get_config() try: response_text = self.event.session.get(url=url, verify=verify, timeout=config.network_timeout).text.rstrip() return response_text except Exception as ex: logging.debug("Exception: " + str(ex)) return "Exception: " + str(ex)
def execute(self): config = get_config() cve_mapping = { KubectlCpVulnerability: ["1.11.9", "1.12.7", "1.13.5", "1.14.0"], IncompleteFixToKubectlCpVulnerability: ["1.12.9", "1.13.6", "1.14.2"], } logger.debug(f"Checking known CVEs for kubectl version: {self.event.version}") for vulnerability, fix_versions in cve_mapping.items(): if CveUtils.is_vulnerable(fix_versions, self.event.version, not config.include_patched_versions): self.publish_event(vulnerability(binary_version=self.event.version))
def execute(self): config = get_config() if config.cidr: for ip in HostDiscoveryHelpers.generate_hosts(config.cidr): self.publish_event(NewHostEvent(host=ip)) elif config.interface: self.scan_interfaces() elif len(config.remote) > 0: for host in config.remote: self.publish_event(NewHostEvent(host=host))
def insecure_access(self): config = get_config() logger.debug(f"Trying to access etcd insecurely at {self.event.host}") try: r = requests.get( f"http://{self.event.host}:{ETCD_PORT}/version", verify=False, timeout=config.network_timeout, ) return r.content if r.status_code == 200 and r.content else False except requests.exceptions.ConnectionError: return False
def access_api_server(self): config = get_config() logger.debug(f"Passive Hunter is attempting to access the API at {self.path}") try: r = requests.get(f"{self.path}/api", headers=self.headers, verify=False, timeout=config.network_timeout) if r.status_code == 200 and r.content: return r.content except requests.exceptions.ConnectionError: pass return False
def has_api_behaviour(self, protocol): config = get_config() try: r = self.session.get(f"{protocol}://{self.event.host}:{self.event.port}", timeout=config.network_timeout) if ("k8s" in r.text) or ('"code"' in r.text and r.status_code != 200): return True except requests.exceptions.SSLError: logger.debug(f"{[protocol]} protocol not accepted on {self.event.host}:{self.event.port}") except Exception: logger.debug(f"Failed probing {self.event.host}:{self.event.port}", exc_info=True)
def services(self): config = get_config() # map between namespaces and service names services = dict() for namespace in self.namespaces: resource_path = f"{self.api_url}/namespaces/{namespace}/services" resource_json = requests.get(resource_path, timeout=config.network_timeout).json() services[namespace] = self.extract_names(resource_json) logger.debug(f"Enumerated services [{' '.join(services)}]") return services
def get_k8s_version(self): config = get_config() logger.debug("Passive hunter is attempting to find kubernetes version") metrics = requests.get(f"{self.path}/metrics", timeout=config.network_timeout).text for line in metrics.split("\n"): if line.startswith("kubernetes_build_info"): for info in line[line.find("{") + 1 : line.find("}")].split(","): k, v = info.split("=") if k == "gitVersion": return v.strip('"')
def accesible(self): config = get_config() endpoint = f"http://{self.host}:{self.port}/api/v1" logger.debug("Attempting to discover a proxy service") try: r = requests.get(endpoint, timeout=config.network_timeout) if r.status_code == 200 and "APIResourceList" in r.text: return True except requests.Timeout: logger.debug(f"failed to get {endpoint}", exc_info=True) return False