mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-12 04:07:11 +00:00
Added a lot of active hunters, using different API Server methods to publish all relevant events from a compromised pod
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user