From 259f707ecd50f0ca5194ae8bb86a015d6d9c4635 Mon Sep 17 00:00:00 2001 From: danielsagi Date: Tue, 27 Aug 2019 10:48:47 +0300 Subject: [PATCH] Refactor And Major Bug Fixes in Version and CVE hunting (#162) * changed version hunting to be on a a new version disclosure vulnerability * fixed version publish * added logging and fixed typo * changed whole way of comparing versions in cve hunter * changed K8sVersionDisclosure vulnerability to one core vulnerability, that takes an endpoint. changed all usage * added tests * merged kubectl cve hunting with apiserver hunting. and simplified the code of apiserver cve hunting * fixed tests to new names * changed name of module to cves.py * drastically improved the cve vulnerble detection utility function. now works with all types of versioning methods * added packaging in requirementes.txt * added another test, and improved logic on cve comparison for more complicated versions * changed CveHunter to subscribe_once, to prevent duplicates duplicates * fixed tests for new improvements * removed unnecessary ternary on doc * removed unnecessary join split * improved compare function, made it util * improved cve checking to use mapping --- requirements.txt | 1 + src/core/events/types/common.py | 17 ++- src/modules/hunting/apiserver.py | 25 ++++- src/modules/hunting/cvehunter.py | 114 ------------------- src/modules/hunting/cves.py | 146 +++++++++++++++++++++++++ src/modules/hunting/kubectl.py | 64 ----------- src/modules/hunting/kubelet.py | 11 +- src/modules/hunting/proxy.py | 15 +-- tests/hunting/test_apiserver_hunter.py | 5 +- tests/hunting/test_cvehunting.py | 52 +++++++++ 10 files changed, 248 insertions(+), 202 deletions(-) delete mode 100644 src/modules/hunting/cvehunter.py create mode 100644 src/modules/hunting/cves.py delete mode 100644 src/modules/hunting/kubectl.py create mode 100644 tests/hunting/test_cvehunting.py diff --git a/requirements.txt b/requirements.txt index 78b341e..3c5e3bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ urllib3>=1.24.2,<1.25 ruamel.yaml requests_mock future +packaging \ No newline at end of file diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index 1819cc1..fac9c98 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -2,7 +2,7 @@ import logging import requests import json import threading -from src.core.types import InformationDisclosure, DenialOfService, RemoteCodeExec, IdentityTheft, PrivilegeEscalation, AccessRisk, UnauthenticatedAccess +from src.core.types import InformationDisclosure, DenialOfService, RemoteCodeExec, IdentityTheft, PrivilegeEscalation, AccessRisk, UnauthenticatedAccess, KubernetesCluster class EventFilterBase(object): def __init__(self, event): @@ -139,6 +139,7 @@ class OpenPortEvent(Event): location = str(self.port) return location + class HuntFinished(Event): pass @@ -149,3 +150,17 @@ class HuntStarted(Event): class ReportDispatched(Event): pass + + +""" Core Vulnerabilites """ +class K8sVersionDisclosure(Vulnerability, Event): + """The kubernetes version could be obtained from the {} endpoint """ + def __init__(self, version, from_endpoint, extra_info=""): + Vulnerability.__init__(self, KubernetesCluster, "K8s Version Disclosure", category=InformationDisclosure) + self.version = version + self.from_endpoint = from_endpoint + self.extra_info = extra_info + self.evidence = version + + def explain(self): + return self.__doc__.format(self.from_endpoint) + self.extra_info diff --git a/src/modules/hunting/apiserver.py b/src/modules/hunting/apiserver.py index a081cc2..30ee32c 100644 --- a/src/modules/hunting/apiserver.py +++ b/src/modules/hunting/apiserver.py @@ -5,7 +5,7 @@ import uuid import copy from ...core.events import handler -from ...core.events.types import Vulnerability, Event +from ...core.events.types import Vulnerability, Event, K8sVersionDisclosure from ..discovery.apiserver import ApiServer from ...core.types import Hunter, ActiveHunter, KubernetesCluster from ...core.types import RemoteCodeExec, AccessRisk, InformationDisclosure, UnauthenticatedAccess @@ -13,7 +13,6 @@ from ...core.types import RemoteCodeExec, AccessRisk, InformationDisclosure, Una """ Vulnerabilities """ - class ServerApiAccess(Vulnerability, Event): """ The API Server port is accessible. Depending on your RBAC settings this could expose access to or control of your cluster. """ @@ -557,3 +556,25 @@ class AccessApiServerActive(ActiveHunter): # Note: we are not binding any role or cluster role because # -- in certain cases it might effect the running pod within the cluster (and we don't want to do that). + +@handler.subscribe(ApiServer) +class ApiVersionHunter(Hunter): + """Api Version Hunter + Tries to obtain the Api Server's version directly from /version endpoint + """ + def __init__(self, event): + self.event = event + self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port) + self.session = requests.Session() + self.session.verify = False + if self.event.auth_token: + self.session.headers.update({"Authorization": "Bearer {}".format(self.event.auth_token)}) + + def execute(self): + if self.event.auth_token: + logging.debug('Passive Hunter is attempting to access the API server version end point using the pod\'s service account token on {}:{} \t'.format(self.event.host, self.event.port)) + else: + logging.debug('Passive Hunter is attempting to access the API server version end point anonymously') + version = json.loads(self.session.get(self.path + "/version").text)["gitVersion"] + logging.debug("Discovered version of api server {}".format(version)) + self.publish_event(K8sVersionDisclosure(version=version, from_endpoint="/version")) \ No newline at end of file diff --git a/src/modules/hunting/cvehunter.py b/src/modules/hunting/cvehunter.py deleted file mode 100644 index 3b61876..0000000 --- a/src/modules/hunting/cvehunter.py +++ /dev/null @@ -1,114 +0,0 @@ -import logging -import json -import requests - -from ...core.events import handler -from ...core.events.types import Vulnerability, Event -from ..discovery.apiserver import ApiServer -from ...core.types import Hunter, ActiveHunter, KubernetesCluster, RemoteCodeExec, AccessRisk, InformationDisclosure, \ - PrivilegeEscalation, DenialOfService - -""" Vulnerabilities """ - - -class ServerApiVersionEndPointAccessPE(Vulnerability, Event): - """Node is vulnerable to critical CVE-2018-1002105""" - - def __init__(self, evidence): - Vulnerability.__init__(self, KubernetesCluster, name="Critical Privilege Escalation CVE", category=PrivilegeEscalation) - self.evidence = evidence - - -class ServerApiVersionEndPointAccessDos(Vulnerability, Event): - """Node not patched for CVE-2019-1002100. Depending on your RBAC settings, a crafted json-patch could cause a Denial of Service.""" - - def __init__(self, evidence): - Vulnerability.__init__(self, KubernetesCluster, name="Denial of Service to Kubernetes API Server", category=DenialOfService) - self.evidence = evidence - - -# Passive Hunter -@handler.subscribe(ApiServer) -class IsVulnerableToCVEAttack(Hunter): - """CVE hunter - Checks if Node is running a Kubernetes version vulnerable to critical CVEs - """ - - def __init__(self, event): - self.event = event - self.headers = dict() - # From within a Pod we may have extra credentials - if self.event.auth_token: - self.headers = {'Authorization': 'Bearer ' + self.event.auth_token} - self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port) - self.api_server_evidence = '' - self.k8sVersion = '' - - def get_api_server_version_end_point(self): - logging.debug(self.event.host) - if 'Authorization' in self.headers: - logging.debug('Passive Hunter is attempting to access the API server version end point using the pod\'s service account token: \t%s', str(self.headers)) - else: - logging.debug('Passive Hunter is attempting to access the API server version end point anonymously') - try: - res = requests.get("{path}/version".format(path=self.path), - headers=self.headers, verify=False) - self.api_server_evidence = res.text - resDict = json.loads(res.text) - version = resDict["gitVersion"].split('.') - first_two_minor_digits = int(version[1]) - last_two_minor_digits = int(version[2]) - logging.debug('Passive Hunter got version from the API server version end point: %d.%d', first_two_minor_digits, last_two_minor_digits) - return [first_two_minor_digits, last_two_minor_digits] - - except (requests.exceptions.ConnectionError, KeyError): - return None - - def check_cve_2018_1002105(self, api_version): - first_two_minor_digists = api_version[0] - last_two_minor_digists = api_version[1] - - if first_two_minor_digists == 10 and last_two_minor_digists < 11: - return True - elif first_two_minor_digists == 11 and last_two_minor_digists < 5: - return True - elif first_two_minor_digists == 12 and last_two_minor_digists < 3: - return True - elif first_two_minor_digists < 10: - return True - - return False - - def check_cve_2019_1002100(self, api_version): - """ - Kubernetes v1.0.x-1.10.x - Kubernetes v1.11.0-1.11.7 (fixed in v1.11.8) - Kubernetes v1.12.0-1.12.5 (fixed in v1.12.6) - Kubernetes v1.13.0-1.13.3 (fixed in v1.13.4) - """ - - first_two_minor_digists = api_version[0] - last_two_minor_digists = api_version[1] - - if first_two_minor_digists == 11 and last_two_minor_digists < 8: - return True - elif first_two_minor_digists == 12 and last_two_minor_digists < 6: - return True - elif first_two_minor_digists == 13 and last_two_minor_digists < 4: - return True - elif first_two_minor_digists < 11: - return True - - return False - - def execute(self): - api_version = self.get_api_server_version_end_point() - - if api_version: - if self.check_cve_2018_1002105(api_version): - self.publish_event(ServerApiVersionEndPointAccessPE(self.api_server_evidence)) - - if self.check_cve_2019_1002100(api_version): - self.publish_event(ServerApiVersionEndPointAccessDos(self.api_server_evidence)) - - diff --git a/src/modules/hunting/cves.py b/src/modules/hunting/cves.py new file mode 100644 index 0000000..a090d5b --- /dev/null +++ b/src/modules/hunting/cves.py @@ -0,0 +1,146 @@ +import logging +import json +import requests + +from ...core.events import handler +from ...core.events.types import Vulnerability, Event, K8sVersionDisclosure +from ...core.types import Hunter, ActiveHunter, KubernetesCluster, RemoteCodeExec, AccessRisk, InformationDisclosure, \ + PrivilegeEscalation, DenialOfService, KubectlClient +from ..discovery.kubectl import KubectlClientEvent + +from packaging import version + +""" CVE Vulnerabilities """ + +class ServerApiVersionEndPointAccessPE(Vulnerability, Event): + """Node is vulnerable to critical CVE-2018-1002105""" + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Critical Privilege Escalation CVE", category=PrivilegeEscalation) + self.evidence = evidence + +class ServerApiVersionEndPointAccessDos(Vulnerability, Event): + """Node not patched for CVE-2019-1002100. Depending on your RBAC settings, a crafted json-patch could cause a Denial of Service.""" + def __init__(self, evidence): + Vulnerability.__init__(self, KubernetesCluster, name="Denial of Service to Kubernetes API Server", category=DenialOfService) + self.evidence = evidence + +class IncompleteFixToKubectlCpVulnerability(Vulnerability, Event): + """The kubectl client is vulnerable to CVE-2019-11246, an attacker could potentially execute arbitrary code on the client's machine""" + def __init__(self, binary_version): + Vulnerability.__init__(self, KubectlClient, "Kubectl Vulnerable To CVE-2019-11246", category=RemoteCodeExec) + self.binary_version = binary_version + self.evidence = "kubectl version: {}".format(self.binary_version) + +class KubectlCpVulnerability(Vulnerability, Event): + """The kubectl client is vulnerable to CVE-2019-1002101, an attacker could potentially execute arbitrary code on the client's machine""" + def __init__(self, binary_version): + Vulnerability.__init__(self, KubectlClient, "Kubectl Vulnerable To CVE-2019-1002101", category=RemoteCodeExec) + self.binary_version = binary_version + self.evidence = "kubectl version: {}".format(self.binary_version) + + +class CveUtils: + @staticmethod + def get_base_release(full_ver): + # if LecacyVersion, converting manually to a base version + if type(full_ver) == version.LegacyVersion: + return version.parse('.'.join(full_ver._version.split('.')[:2])) + else: + return version.parse('.'.join(map(str, full_ver._version.release[:2]))) + + @staticmethod + def to_legacy(full_ver): + # converting version to verison.LegacyVersion + return version.LegacyVersion('.'.join(map(str, full_ver._version.release))) + + @staticmethod + def to_raw_version(v): + if type(v) != version.LegacyVersion: + return '.'.join(map(str, v._version.release)) + return v._version + + @staticmethod + def version_compare(v1, v2): + """Function compares two versions, handling differences with convertion to LegacyVersion""" + # getting raw version, while striping 'v' char at the start. if exists. + # removing this char lets us safely compare the two version. + v1_raw, v2_raw = CveUtils.to_raw_version(v1).strip('v'), CveUtils.to_raw_version(v2).strip('v') + new_v1 = version.LegacyVersion(v1_raw) + new_v2 = version.LegacyVersion(v2_raw) + + return CveUtils.basic_compare(new_v1, new_v2) + + @staticmethod + def basic_compare(v1, v2): + return (v1>v2)-(v1