From b95feebcbaf68c59875d4978be6b07fda6034103 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Mon, 15 Oct 2018 20:12:59 +0300 Subject: [PATCH] Added a lot of active hunters, using different API Server methods to publish all relevant events from a compromised pod --- src/modules/hunting/apiserver.py | 247 ++++++++++++++++++++++++++++++- 1 file changed, 240 insertions(+), 7 deletions(-) diff --git a/src/modules/hunting/apiserver.py b/src/modules/hunting/apiserver.py index b0373f1..e91f55c 100644 --- a/src/modules/hunting/apiserver.py +++ b/src/modules/hunting/apiserver.py @@ -5,7 +5,7 @@ import requests from ...core.events import handler from ...core.events.types import Vulnerability, Event, OpenPortEvent -from ...core.types import Hunter, KubernetesCluster, RemoteCodeExec, AccessRisk, InformationDisclosure +from ...core.types import Hunter, ActiveHunter, KubernetesCluster, RemoteCodeExec, AccessRisk, InformationDisclosure """ Vulnerabilities """ @@ -16,6 +16,7 @@ class ServerApiAccess(Vulnerability, Event): Vulnerability.__init__(self, KubernetesCluster, name="Accessed to server API", category=RemoteCodeExec) self.evidence = evidence + class ServiceAccountTokenAccess(Vulnerability, Event): """ Accessing the pod's service account token gives an attacker the option to use the server API """ @@ -24,7 +25,8 @@ class ServiceAccountTokenAccess(Vulnerability, Event): category=AccessRisk) self.evidence = evidence -class podListAccessDefaultNamespace(Vulnerability, Event): + +class PodListUnderDefaultNamespace(Vulnerability, Event): """ Accessing the pods list under default namespace within a compromised pod might grant an attacker a valuable information to harm the cluster """ @@ -33,7 +35,8 @@ class podListAccessDefaultNamespace(Vulnerability, Event): category=InformationDisclosure) self.evidence = evidence -class podListAccessAllNamespaces(Vulnerability, Event): + +class PodListUnderAllNamespaces(Vulnerability, Event): """ Accessing the pods list under ALL of the namespaces within a compromised pod might grant an attacker a valuable information to harm the cluster """ @@ -43,11 +46,80 @@ class podListAccessAllNamespaces(Vulnerability, Event): self.evidence = evidence +class ListAllNamespaces(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + + +class CreateARole(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + + +class CreateAClusterRole(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + + +class PatchARole(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + + +class PatchAClusterRole(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + + +class CreateARole(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + + +class CreateAClusterRole(Vulnerability, Event): + """ Accessing all of the namespaces within a compromised pod might grant an attacker a valuable information + """ + + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Access to the all namespaces list", + category=InformationDisclosure) + self.evidence = evidence + # Passive Hunter @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 443 or x.port == 6443) class AccessApiServerViaServiceAccountToken(Hunter): - """ - Accessing the api server might grant an attacker full control over the cluster + """ API Server Hunter + Accessing the api server within a compromised pod might grant an attacker full control over the cluster """ def __init__(self, event): @@ -110,8 +182,169 @@ class AccessApiServerViaServiceAccountToken(Hunter): if self.access_api_server(): self.publish_event(ServerApiAccess(self.api_server_evidence)) if self.get_pods_list_under_all_namespace(): - self.publish_event(podListAccessAllNamespaces(self.pod_list_under_all_namespaces_evidence)) + self.publish_event(PodListUnderAllNamespaces(self.pod_list_under_all_namespaces_evidence)) if self.get_pods_list_under_default_namespace(): - self.publish_event(podListAccessDefaultNamespace(self.pod_list_under_default_namespace_evidence)) + self.publish_event(PodListUnderDefaultNamespace(self.pod_list_under_default_namespace_evidence)) + + +# Active Hunter +@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 443 or x.port == 6443) +class AccessApiServerViaServiceAccountTokenActive(ActiveHunter): + """API server hunter + Accessing the api server might grant an attacker full control over the cluster + """ + + def __init__(self, event): + self.event = event + self.api_server_evidence = '' + self.service_account_token_evidence = '' + self.all_namespaces_evidence = '' + self.namespace_roles_evidence = '' + self.all_roles_evidence = '' + self.cluster_roles_evidence = '' + + self.namespaces_and_their_pod_names = {} + + def get_service_account_token(self): + logging.debug(self.event.host) + logging.debug('Passive Hunter is attempting to access pod\'s service account token') + try: + with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r') as token: + data = token.read() + self.service_account_token_evidence = data + return True + except IOError: # Couldn't read file + return False + + def get_pods_list_under_default_namespace(self): + try: + res = requests.get("https://{host}:{port}/api/v1/namespaces/{namespace}/pods".format(host=self.event.host, + port=self.event.port, namespace='default'), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + + parsed_response_content = json.loads(res.content) + for item in parsed_response_content["items"]: + self.namespaces_and_their_pod_names[item["metadata"]["namespace"]] = item["metadata"]["name"] + + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: # e.g. DNS failure, refused connection, etc + return False + + def get_pods_list_under_all_namespace(self): + try: + res = requests.get("https://{host}:{port}/api/v1/pods".format(host=self.event.host, port=self.event.port), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + parsed_response_content = json.loads(res.content) + for item in parsed_response_content["items"]: + self.namespaces_and_their_pod_names[item["metadata"]["namespace"]] = item["metadata"]["name"] + + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: # e.g. DNS failure, refused connection, etc + return False + + def create_a_pod(self): + pass + + # would be used on our newly created pod only + def delete_a_pod(self, pod_namespace, pod_name): + try: + res = requests.delete("https://{host}:{port}/api/v1/namespaces/{namespace}/pods/{name}".format( + host=self.event.host, port=self.event.port, namespace=pod_namespace, name=pod_name), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: + return False + + # would be used on our newly created pod only + def patch_a_pod(self, pod_namespace, pod_name): + try: + patch_data = {} + res = requests.patch("https://{host}:{port}/api/v1/namespaces/{namespace}/pods/{name}".format( + host=self.event.host, port=self.event.port, namespace=pod_namespace, name=pod_name), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False, data=patch_data) + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: + return False + + #Namespaces methods: + + def get_all_namespaces(self): + try: + res = requests.get("https://{host}:{port}/api/v1/namespaces".format(host=self.event.host, + port=self.event.port), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + + parsed_response_content = json.loads(res.content) + # Parse content after creating RBAC roles that would return 200 OK so I can see the data myself and understand how to parse it + # for item in parsed_response_content["items"]: + # self.namespaces_and_their_pod_names[item["metadata"]["namespace"]] = item["metadata"]["name"] + + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: # e.g. DNS failure, refused connection, etc + return False + + def create_namespace(self): + pass + + + #Roles & Cluster roles Methods: + + def get_roles_for_namespace(self, namespace): + try: + res = requests.get("https://{host}:{port}/apis/rbac.authorization.k8s.io/v1/namespaces/{namespace}/roles".format( + host=self.event.host, port=self.event.port, namespace=namespace), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + self.namespace_roles_evidence = res.content + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: + return False + + def get_cluster_roles(self): + try: + res = requests.get("https://{host}:{port}/apis/rbac.authorization.k8s.io/v1/clusterroles".format( + host=self.event.host, port=self.event.port), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + self.namespace_roles_evidence = res.content + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: + return False + + def get_all_roles(self): + try: + res = requests.get("https://{host}:{port}/apis/rbac.authorization.k8s.io/v1/roles".format( + host=self.event.host, port=self.event.port), + headers={'Authorization': 'Bearer ' + self.service_account_token_evidence}, verify=False) + self.namespace_roles_evidence = res.content + return res.status_code == 200 and res.content != '' + except requests.exceptions.ConnectionError: + return False + + def create_role(self): + pass + + def create_cluster_role(self): + pass + + # would be use on an newly create role only + def delete_a_role(self): + pass + + # would be use on an newly create cluster role only + def delete_a_cluster_role(self): + pass + + # would be use on an newly create role only + def patch_a_role(self): + pass + + # would be use on an newly create role only + def patch_a_cluster_role(self): + pass + + def execute(self): + if self.get_service_account_token(): + # Do I need these data for other hunters (the onces that uses pod name or namespace as an argument)? + self.get_pods_list_under_all_namespace() + self.get_pods_list_under_default_namespace()