diff --git a/src/core/events/handler.py b/src/core/events/handler.py index d75403b..24a8233 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -12,8 +12,6 @@ from ..types import ActiveHunter, Hunter, HunterBase from ...core.events.types import HuntFinished, Vulnerability import threading -global queue_lock -queue_lock = Lock() # Inherits Queue object, handles events asynchronously class EventQueue(Queue, object): @@ -32,6 +30,7 @@ class EventQueue(Queue, object): t.daemon = True t.start() self.workers.append(t) + t = Thread(target=self.notifier) t.daemon = True t.start() @@ -83,21 +82,23 @@ class EventQueue(Queue, object): # executes callbacks on dedicated thread as a daemon def worker(self): while self.running: - queue_lock.acquire() - hook = self.get() - queue_lock.release() try: + hook = self.get() hook.execute() except Exception as ex: logging.debug("Exception: {} - {}".format(hook.__class__, ex)) - self.task_done() + finally: + self.task_done() logging.debug("closing thread...") def notifier(self): time.sleep(2) + # should consider locking on unfinished_tasks while self.unfinished_tasks > 0: logging.debug("{} tasks left".format(self.unfinished_tasks)) time.sleep(3) + if self.unfinished_tasks == 1: + logging.debug("final hook is hanging") # stops execution of all daemons def free(self): diff --git a/src/core/types.py b/src/core/types.py index fc4b4ab..4616e3a 100644 --- a/src/core/types.py +++ b/src/core/types.py @@ -37,6 +37,9 @@ class KubernetesCluster(): """Kubernetes Cluster""" name = "Kubernetes Cluster" +class KubectlClient(): + """The kubectl client binary is used by the user to interact with the cluster""" + name = "Kubectl Client" class Kubelet(KubernetesCluster): """The kubelet is the primary "node agent" that runs on each node""" diff --git a/src/modules/discovery/kubectl.py b/src/modules/discovery/kubectl.py new file mode 100644 index 0000000..3c8bada --- /dev/null +++ b/src/modules/discovery/kubectl.py @@ -0,0 +1,46 @@ + +import logging +import subprocess +import json + +from ...core.types import Discovery +from ...core.events import handler +from ...core.events.types import HuntStarted, Event + + +class KubectlClientEvent(Event): + """The API server is in charge of all operations on the cluster.""" + def __init__(self, version): + self.version = version + + def location(self): + return "local machine" + +# Will be triggered on start of every hunt +@handler.subscribe(HuntStarted) +class KubectlClientDiscovery(Discovery): + """Kubectl Client Discovery + Checks for the existence of a local kubectl client + """ + def __init__(self, event): + self.event = event + + def get_kubectl_binary_version(self): + version = None + try: + # kubectl version --client does not make any connection to the cluster/internet whatsoever. + versionInfo = subprocess.check_output("kubectl version --client", stderr=subprocess.STDOUT) + if b"GitVersion" in versionInfo: + # extracting version from kubectl output + versionInfo = versionInfo.decode() + start = versionInfo.find('GitVersion') + version = versionInfo[start + len("GitVersion':\"") : versionInfo.find("\",", start)] + except Exception as x: + logging.debug("Could not find kubectl client") + return version + + def execute(self): + logging.debug("Attempting to discover a local kubectl client") + version = self.get_kubectl_binary_version() + if version: + self.publish_event(KubectlClientEvent(version=version)) \ No newline at end of file diff --git a/src/modules/hunting/kubectl.py b/src/modules/hunting/kubectl.py new file mode 100644 index 0000000..5ac8454 --- /dev/null +++ b/src/modules/hunting/kubectl.py @@ -0,0 +1,64 @@ +import logging + +from ...core.events import handler +from ...core.types import Hunter, RemoteCodeExec, KubectlClient +from ...core.events.types import Vulnerability, Event +from ..discovery.kubectl import KubectlClientEvent + +from distutils.version import LooseVersion, StrictVersion + +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) + + +@handler.subscribe(KubectlClientEvent) +class KubectlCVEHunter(Hunter): + """Kubectl CVE Hunter + Compares version of the kubectl binary to known CVE affected versions + """ + def __init__(self, event): + self.event = event + + def is_older_than(self, fix_versions, check_version): + """Function determines if a version is vulnerable, by comparing to given fix versions""" + logging.debug("Passive hunter is comparing the kubectl binary version to vulnerable versions") + # in case version is in short version, converting + if len(LooseVersion(check_version).version) < 3: + check_version += '.0' + + vulnerable = False + if check_version not in fix_versions: + for fix_v in fix_versions: + fix_v = LooseVersion(fix_v) + base_v = '.'.join(map(lambda x: str(x), fix_v.version[:2]) ) + + if check_version.startswith(base_v): + if LooseVersion(check_version) < fix_v: + vulnerable = True + break + # if version is smaller than smaller fix version + if not vulnerable and LooseVersion(check_version) < LooseVersion(fix_versions[0]): + vulnerable = True + + return vulnerable + + def execute(self): + cve_2019_1002101_fix_versions = ['1.11.9', '1.12.7', '1.13.5' '1.14.0'] + cve_2019_11246_fix_versions = ['1.12.9', '1.13.6', '1.14.2'] + + if self.is_older_than(fix_versions=cve_2019_1002101_fix_versions, check_version=self.event.version): + self.publish_event(KubectlCpVulnerability(binary_version=self.event.version)) + + if self.is_older_than(fix_versions=cve_2019_11246_fix_versions, check_version=self.event.version): + self.publish_event(IncompleteFixToKubectlCpVulnerability(binary_version=self.event.version)) diff --git a/src/modules/hunting/kubelet.py b/src/modules/hunting/kubelet.py index dafc575..9495873 100644 --- a/src/modules/hunting/kubelet.py +++ b/src/modules/hunting/kubelet.py @@ -213,8 +213,9 @@ class SecureKubeletPortHunter(Hunter): containerName=self.pod["container"], cmd = "" ) - status_code = requests.post(run_url, allow_redirects=False, verify=False).status_code - return (status_code != 404 and status_code != 401) + status_code = requests.post(run_url, allow_redirects=False, verify=False).status_code + # check if return value is 4xx + return not 400 <= status_code < 500 # returns list of currently running pods def test_running_pods(self):