From f22b89775dc7d60333c0bb95d13e95fe6c27fe61 Mon Sep 17 00:00:00 2001 From: jmichealson <31416532+jmichealson@users.noreply.github.com> Date: Tue, 11 Sep 2018 08:03:43 -0500 Subject: [PATCH 01/44] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94b0156..4b6de13 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ There are three methods for deploying kube-hunter: You can run the kube-hunter python code directly on your machine. #### Prerequisites -You will need the following installed: +You will need the following installed and assumed for path: * python 2.7 * pip @@ -79,6 +79,7 @@ Install module dependencies: ~~~ cd ./kube-hunter pip install -r requirements.txt +In the case you have python 3.x in the path as your default, use "python2 -m pip install -r requirements.txt" ~~~ Run: `./kube-hunter.py` From 628c2142155884f8bfb40399302e02c8a62f98c1 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 26 Sep 2018 18:42:06 +0300 Subject: [PATCH 02/44] Added some remote access to etcd checks. --- src/modules/discovery/etcd.py | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/modules/discovery/etcd.py diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py new file mode 100644 index 0000000..116207b --- /dev/null +++ b/src/modules/discovery/etcd.py @@ -0,0 +1,67 @@ +import json +import logging + +import requests + +from ...core.events import handler +from ...core.events.types import Event, OpenPortEvent, Service +from ...core.types import Hunter + +"""Etcd is a DB that stores cluster's data, + it contains configuration and current state information, and might contain secrets""" +#services: + +class etcdRemoteWriteAccessEvent(Service, Event): + """Remote write from anonymous user can give him full control over the kubernetes cluster""" + def __init__(self): + Service.__init__(self, name="Etcd Remote Write Access Event") +class etcdRemoteReadAccessEvent(Service, Event): + """Remote read access from anonymous user might expose cluster exploits and secrets, more.""" + def __init__(self): + Service.__init__(self, name="Etcd Remote Read Access Event") + +"event handlers" +@handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379 or p.port == 2370 or p.port == 2380) +class etcdRemoteAccess(Hunter): + """etcd Remote Access + Checks for availability of etcd, version, read access, write access + """ + #TODO: + #db_keys_write_access: Convert that curl command to a uri: curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello world" + #If we've got a read access-> check if data is encryption. + #Read Liz's book & etcd's rest api and check if I've missed important commands to check + #Do we need to add a auth check and remote connection?->>> + #->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display it? + #Add proper logs + def __init__(self, event): + self.event = event + + def db_keys_disclosure(self): + logging.debug(self.event.host) + r = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) + if r.status_code == 200: + self.publish_event(etcdRemoteReadAccessEvent(secure=False)) + return True + return False + + def db_keys_write_access(self): + logging.debug(self.event.host) + r = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) + if r.status_code == 200: + self.publish_event(etcdRemoteWriteAccessEvent(secure=False)) + return True + return False + + def version_disclosure(self): + logging.debug(self.event.host) + r = requests.get("https://{host}:{port}/version".format(host=self.event.host, port=2379)) # decide which port to choose (maybe the host's port?) + if r.status_code == 200: + self.publish_event(etcdRemoteReadAccessEvent(secure=False)) + return True + return False + + + def execute(self): + if (self.version_disclosure()): + self.db_keys_disclosure() + self.db_keys_write_access() From a506ed5b9cd38c10b867cffc791b6b2fe7e0b7f2 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 30 Sep 2018 14:03:42 +0300 Subject: [PATCH 03/44] Edited some of the etcd checking & added 2379 port checking --- src/modules/discovery/etcd.py | 34 ++++++++++++++++++++++------------ src/modules/discovery/ports.py | 2 +- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 116207b..8926d8e 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -21,46 +21,56 @@ class etcdRemoteReadAccessEvent(Service, Event): Service.__init__(self, name="Etcd Remote Read Access Event") "event handlers" -@handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379 or p.port == 2370 or p.port == 2380) +@handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) class etcdRemoteAccess(Hunter): - """etcd Remote Access - Checks for availability of etcd, version, read access, write access + """Etcd Remote Access + Checks for remote availability of etcd, version, read access, write access """ #TODO: - #db_keys_write_access: Convert that curl command to a uri: curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello world" - #If we've got a read access-> check if data is encryption. + #If we've got a read access-> check if data is encrypted. #Read Liz's book & etcd's rest api and check if I've missed important commands to check #Do we need to add a auth check and remote connection?->>> #->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display it? #Add proper logs + #Check why the execute() isn't being called def __init__(self, event): self.event = event def db_keys_disclosure(self): logging.debug(self.event.host) - r = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) - if r.status_code == 200: + r_secure = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) + r_not_secure = requests.get("http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) + has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") + if has_remote_access_gained: self.publish_event(etcdRemoteReadAccessEvent(secure=False)) return True return False def db_keys_write_access(self): logging.debug(self.event.host) - r = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) - if r.status_code == 200: + data = { + 'value': 'remote write access penetration' + } + r_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data)#decide which port to choose (maybe the host's port?) + r_not_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data)#decide which port to choose (maybe the host's port?) + + has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") + if has_remote_access_gained: self.publish_event(etcdRemoteWriteAccessEvent(secure=False)) return True return False def version_disclosure(self): logging.debug(self.event.host) - r = requests.get("https://{host}:{port}/version".format(host=self.event.host, port=2379)) # decide which port to choose (maybe the host's port?) - if r.status_code == 200: + r_secure = requests.get("https://{host}:{port}/version".format(host=self.event.host, port=2379)) # decide which port to choose (maybe the host's port?) + r_not_secure = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379)) # decide which port to choose (maybe the host's port?) + + has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") + if has_remote_access_gained: self.publish_event(etcdRemoteReadAccessEvent(secure=False)) return True return False - def execute(self): if (self.version_disclosure()): self.db_keys_disclosure() diff --git a/src/modules/discovery/ports.py b/src/modules/discovery/ports.py index d0fb619..e3660ea 100644 --- a/src/modules/discovery/ports.py +++ b/src/modules/discovery/ports.py @@ -7,7 +7,7 @@ from ...core.events import handler from ...core.events.types import NewHostEvent, OpenPortEvent -default_ports = [8001, 10250, 10255, 30000, 443, 6443] +default_ports = [8001, 10250, 10255, 30000, 443, 6443, 2379] @handler.subscribe(NewHostEvent) class PortDiscovery(Hunter): From e2c04b2a7cade7db997de3a8b0f3e8210bcaa882 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Tue, 2 Oct 2018 11:59:25 +0300 Subject: [PATCH 04/44] Added timeout for each request. Finished with some of the TODOS tasks (added logs). Added another TODO task for this branch. --- src/modules/discovery/etcd.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 8926d8e..3ea9e19 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -27,19 +27,19 @@ class etcdRemoteAccess(Hunter): Checks for remote availability of etcd, version, read access, write access """ #TODO: - #If we've got a read access-> check if data is encrypted. #Read Liz's book & etcd's rest api and check if I've missed important commands to check #Do we need to add a auth check and remote connection?->>> - #->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display it? - #Add proper logs + #->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display the no auth event anyway? #Check why the execute() isn't being called + #Decide if I should move db_keys_write_access to hunting/etcd.py as an active hunter def __init__(self, event): self.event = event def db_keys_disclosure(self): logging.debug(self.event.host) - r_secure = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) - r_not_secure = requests.get("http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379))#decide which port to choose (maybe the host's port?) + logging.debug("Passive hunter is attempting to read etcd keys remotely") + r_secure = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379), timeout=5)#decide which port to choose (maybe the host's port?) + r_not_secure = requests.get("http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379), timeout=5)#decide which port to choose (maybe the host's port?) has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") if has_remote_access_gained: self.publish_event(etcdRemoteReadAccessEvent(secure=False)) @@ -48,11 +48,12 @@ class etcdRemoteAccess(Hunter): def db_keys_write_access(self): logging.debug(self.event.host) + logging.debug("Active hunter* is attempting to write keys remotely") data = { 'value': 'remote write access penetration' } - r_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data)#decide which port to choose (maybe the host's port?) - r_not_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data)#decide which port to choose (maybe the host's port?) + r_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data, timeout=5)#decide which port to choose (maybe the host's port?) + r_not_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data, timeout=5)#decide which port to choose (maybe the host's port?) has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") if has_remote_access_gained: @@ -62,8 +63,9 @@ class etcdRemoteAccess(Hunter): def version_disclosure(self): logging.debug(self.event.host) - r_secure = requests.get("https://{host}:{port}/version".format(host=self.event.host, port=2379)) # decide which port to choose (maybe the host's port?) - r_not_secure = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379)) # decide which port to choose (maybe the host's port?) + logging.debug("Passive hunter is attempting to check etcd version remotely") + r_secure = requests.get("https://{host}:{port}/version".format(host=self.event.host, port=2379), timeout=5) # decide which port to choose (maybe the host's port?) + r_not_secure = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), timeout=5) # decide which port to choose (maybe the host's port?) has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") if has_remote_access_gained: From 64722ea1b47535ea22b8493c43a7ea8c28911cfa Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Tue, 2 Oct 2018 17:57:50 +0300 Subject: [PATCH 05/44] Solved some exception bugs & did some refactoring to code --- src/modules/discovery/etcd.py | 65 ++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 3ea9e19..2437e32 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -12,15 +12,19 @@ from ...core.types import Hunter #services: class etcdRemoteWriteAccessEvent(Service, Event): - """Remote write from anonymous user can give him full control over the kubernetes cluster""" + """Remote write access might grant an attacker full control over the kubernetes cluster""" def __init__(self): Service.__init__(self, name="Etcd Remote Write Access Event") class etcdRemoteReadAccessEvent(Service, Event): - """Remote read access from anonymous user might expose cluster exploits and secrets, more.""" + """Remote read access might expose to an attacker cluster's possible exploits, secrets and more.""" def __init__(self): Service.__init__(self, name="Etcd Remote Read Access Event") +class etcdRemoteVersionDisclosureEvent(Service, Event): + """Remote version disclosure might give an attacker a valuable data to attack a cluster""" + def __init__(self): + Service.__init__(self, name="Etcd Remote version disclosure") + -"event handlers" @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) class etcdRemoteAccess(Hunter): """Etcd Remote Access @@ -38,11 +42,11 @@ class etcdRemoteAccess(Hunter): def db_keys_disclosure(self): logging.debug(self.event.host) logging.debug("Passive hunter is attempting to read etcd keys remotely") - r_secure = requests.get("https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379), timeout=5)#decide which port to choose (maybe the host's port?) - r_not_secure = requests.get("http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379), timeout=5)#decide which port to choose (maybe the host's port?) - has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") - if has_remote_access_gained: - self.publish_event(etcdRemoteReadAccessEvent(secure=False)) + r_secure = "https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) + r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) + + if self.helperFuncDo2Requests(r_secure, r_not_secure): + self.publish_event(etcdRemoteReadAccessEvent()) return True return False @@ -52,28 +56,49 @@ class etcdRemoteAccess(Hunter): data = { 'value': 'remote write access penetration' } - r_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data, timeout=5)#decide which port to choose (maybe the host's port?) - r_not_secure = requests.put("https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379), data=data, timeout=5)#decide which port to choose (maybe the host's port?) + r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) + r_not_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") - if has_remote_access_gained: - self.publish_event(etcdRemoteWriteAccessEvent(secure=False)) + if self.helperFuncDo2Requests(r_secure, r_not_secure, data=data, reqType="put"): + self.publish_event(etcdRemoteWriteAccessEvent()) return True return False def version_disclosure(self): logging.debug(self.event.host) logging.debug("Passive hunter is attempting to check etcd version remotely") - r_secure = requests.get("https://{host}:{port}/version".format(host=self.event.host, port=2379), timeout=5) # decide which port to choose (maybe the host's port?) - r_not_secure = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), timeout=5) # decide which port to choose (maybe the host's port?) - - has_remote_access_gained = (r_secure.status_code == 200 and r_secure.content != "") or (r_not_secure.status_code == 200 and r_not_secure.content != "") - if has_remote_access_gained: - self.publish_event(etcdRemoteReadAccessEvent(secure=False)) + r_secure = "https://{host}:{port}/version".format(host=self.event.host, port=2379) + r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) + if self.helperFuncDo2Requests( r_secure, r_not_secure ): + self.publish_event(etcdRemoteVersionDisclosureEvent()) return True return False def execute(self): - if (self.version_disclosure()): + if (self.version_disclosure()): self.db_keys_disclosure() self.db_keys_write_access() + + def helperFuncDo2Requests(self, req1, req2, isVerify=False, data=None, reqType="get"): + try: + r = self.helperDoRequest(req1, isVerify, data, reqType) + has_remote_access_gained = (r.status_code == 200 and r.content != "") + if has_remote_access_gained: + return True + except Exception: + try: + r = self.helperDoRequest(req2, isVerify, data, reqType) + has_remote_access_gained = (r.status_code == 200 and r.content != "") + if has_remote_access_gained: + return True + except Exception: + return False #None of the requests succeded.. + return False + + def helperDoRequest(self, req, isVerify, data=None, reqType="get"): + if reqType == "put": + r = requests.put(req, verify=isVerify, timeout=3, data=data) + return r + elif reqType == "get": + r = requests.get(req, verify=isVerify, timeout=3, data=data) + return r \ No newline at end of file From f4ff44012ec88100b5ac1c94da3ac34371e881aa Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Tue, 2 Oct 2018 18:02:12 +0300 Subject: [PATCH 06/44] Solved some exception bugs & did some refactoring to code & Added event --- src/modules/discovery/etcd.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 2437e32..174bf31 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -23,6 +23,11 @@ class etcdRemoteVersionDisclosureEvent(Service, Event): """Remote version disclosure might give an attacker a valuable data to attack a cluster""" def __init__(self): Service.__init__(self, name="Etcd Remote version disclosure") +class etcdAccessEnabledWithoutAuthEvent(Service, Event): + """Remote version disclosure might give an attacker a valuable data to attack a cluster""" + def __init__(self): + Service.__init__(self, name="Etcd is accessible without authorization") + @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) @@ -76,6 +81,7 @@ class etcdRemoteAccess(Hunter): def execute(self): if (self.version_disclosure()): + self.publish_event(etcdAccessEnabledWithoutAuthEvent())#if version is accessible we can publish "no auth event". self.db_keys_disclosure() self.db_keys_write_access() From 7201f5e236cb515e63a4ae188a8fac9ff327768e Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Tue, 2 Oct 2018 18:55:50 +0300 Subject: [PATCH 07/44] Solved some exception bugs & did some refactoring to code & Added event & splited active & passive hunter --- src/core/events/handler.py | 12 +++---- src/modules/discovery/etcd.py | 35 +++++++------------ src/modules/hunting/etcd.py | 65 +++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 src/modules/hunting/etcd.py diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 051087d..f5b4595 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -41,7 +41,7 @@ class EventQueue(Queue, object): self.subscribe_event(event, hook=hook, predicate=predicate) return hook return wrapper - + # getting uninstantiated event object def subscribe_event(self, event, hook=None, predicate=None): if ActiveHunter in hook.__mro__: @@ -50,7 +50,7 @@ class EventQueue(Queue, object): else: self.active_hunters[hook] = hook.__doc__ elif Hunter in hook.__mro__: - self.passive_hunters[hook] = hook.__doc__ + self.passive_hunters[hook] = hook.__doc__ if hook not in self.hooks[event]: self.hooks[event].append((hook, predicate)) @@ -75,13 +75,13 @@ class EventQueue(Queue, object): hook = self.get() try: hook.execute() - except Exception as ex: + except Exception as ex: logging.debug(ex.message) self.task_done() logging.debug("closing thread...") def notifier(self): - time.sleep(2) + time.sleep(2) while self.unfinished_tasks > 0: logging.debug("{} tasks left".format(self.unfinished_tasks)) time.sleep(3) @@ -91,5 +91,5 @@ class EventQueue(Queue, object): self.running = False with self.mutex: self.queue.clear() - -handler = EventQueue(800) + +handler = EventQueue(800) \ No newline at end of file diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 174bf31..3c0119e 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -24,7 +24,7 @@ class etcdRemoteVersionDisclosureEvent(Service, Event): def __init__(self): Service.__init__(self, name="Etcd Remote version disclosure") class etcdAccessEnabledWithoutAuthEvent(Service, Event): - """Remote version disclosure might give an attacker a valuable data to attack a cluster""" + """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" def __init__(self): Service.__init__(self, name="Etcd is accessible without authorization") @@ -55,20 +55,6 @@ class etcdRemoteAccess(Hunter): return True return False - def db_keys_write_access(self): - logging.debug(self.event.host) - logging.debug("Active hunter* is attempting to write keys remotely") - data = { - 'value': 'remote write access penetration' - } - r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - r_not_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - - if self.helperFuncDo2Requests(r_secure, r_not_secure, data=data, reqType="put"): - self.publish_event(etcdRemoteWriteAccessEvent()) - return True - return False - def version_disclosure(self): logging.debug(self.event.host) logging.debug("Passive hunter is attempting to check etcd version remotely") @@ -85,15 +71,18 @@ class etcdRemoteAccess(Hunter): self.db_keys_disclosure() self.db_keys_write_access() - def helperFuncDo2Requests(self, req1, req2, isVerify=False, data=None, reqType="get"): + # Will attempt to do request "req1" with the optional parameters. + # If fails it will attempt to do "req2" with the optional parameters. + # If once of the request success this method will return True, if both fail- False. + def helperFuncDo2Requests(self, req1, req2, is_verify=False, data=None, req_type="get"): try: - r = self.helperDoRequest(req1, isVerify, data, reqType) + r = self.helperDoRequest(req1, is_verify, data, req_type) has_remote_access_gained = (r.status_code == 200 and r.content != "") if has_remote_access_gained: return True except Exception: try: - r = self.helperDoRequest(req2, isVerify, data, reqType) + r = self.helperDoRequest(req2, is_verify, data, req_type) has_remote_access_gained = (r.status_code == 200 and r.content != "") if has_remote_access_gained: return True @@ -101,10 +90,10 @@ class etcdRemoteAccess(Hunter): return False #None of the requests succeded.. return False - def helperDoRequest(self, req, isVerify, data=None, reqType="get"): - if reqType == "put": - r = requests.put(req, verify=isVerify, timeout=3, data=data) + def helperDoRequest(self, req, is_verify, data=None, req_type="get"): + if req_type == "put": + r = requests.put(req, verify=is_verify, timeout=3, data=data) return r - elif reqType == "get": - r = requests.get(req, verify=isVerify, timeout=3, data=data) + elif req_type == "get": + r = requests.get(req, verify=is_verify, timeout=3, data=data) return r \ No newline at end of file diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py new file mode 100644 index 0000000..de1589f --- /dev/null +++ b/src/modules/hunting/etcd.py @@ -0,0 +1,65 @@ +import json +import logging + +import requests + +from ...core.events import handler +from ...core.events.types import Vulnerability, Event, Service, OpenPortEvent +from ...core.types import ActiveHunter + +"""Etcd is a DB that stores cluster's data, + it contains configuration and current state information, and might contain secrets""" +# Vulnerability: +class etcdRemoteWriteAccessEvent(Vulnerability, Event): + """Remote write access might grant an attacker full control over the kubernetes cluster""" + def __init__(self): + Vulnerability.__init__(self, name="Etcd Remote Write Access Event") + +@handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) +class etcdRemoteAccess(ActiveHunter): + """Etcd Remote Access + Checks for remote write access to etcd + """ + def db_keys_write_access(self): + logging.debug(self.event.host) + logging.debug("Active hunter is attempting to write keys remotely") + data = { + 'value': 'remote write access penetration' + } + r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) + r_not_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) + + if self.helperFuncDo2Requests(r_secure, r_not_secure, data=data, req_type="put"): + self.publish_event(etcdRemoteWriteAccessEvent()) + return True + return False + + def execute(self): + self.db_keys_write_access() + + # Will attempt to do request "req1" with the optional parameters. + # If fails it will attempt to do "req2" with the optional parameters. + # If once of the request success this method will return True, if both fail- False. + def helperFuncDo2Requests(self, req1, req2, is_verify=False, data=None, req_type="get"): + try: + r = self.helperDoRequest(req1, is_verify, data, req_type) + has_remote_access_gained = (r.status_code == 200 and r.content != "") + if has_remote_access_gained: + return True + except Exception: + try: + r = self.helperDoRequest(req2, is_verify, data, req_type) + has_remote_access_gained = (r.status_code == 200 and r.content != "") + if has_remote_access_gained: + return True + except Exception: + return False #None of the requests succeded.. + return False + + def helperDoRequest(self, req, is_verify, data=None, req_type="get"): + if req_type == "put": + r = requests.put(req, verify=is_verify, timeout=3, data=data) + return r + elif req_type == "get": + r = requests.get(req, verify=is_verify, timeout=3, data=data) + return r \ No newline at end of file From d0633ee3c1ffb3213bbb19230ee87dc16733fdda Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Tue, 2 Oct 2018 19:32:32 +0300 Subject: [PATCH 08/44] Added init method to the etcd active hunter --- src/modules/hunting/etcd.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index de1589f..46faabe 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -4,7 +4,7 @@ import logging import requests from ...core.events import handler -from ...core.events.types import Vulnerability, Event, Service, OpenPortEvent +from ...core.events.types import Vulnerability, Event, OpenPortEvent from ...core.types import ActiveHunter """Etcd is a DB that stores cluster's data, @@ -16,10 +16,14 @@ class etcdRemoteWriteAccessEvent(Vulnerability, Event): Vulnerability.__init__(self, name="Etcd Remote Write Access Event") @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) -class etcdRemoteAccess(ActiveHunter): +class etcdRemoteAccessActive(ActiveHunter): """Etcd Remote Access Checks for remote write access to etcd """ + + def __init__(self, event): + self.event = event + def db_keys_write_access(self): logging.debug(self.event.host) logging.debug("Active hunter is attempting to write keys remotely") From 4f9d1e2c45c9ed70ec0acf7cc2384f0957ab75cf Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 3 Oct 2018 18:20:35 +0300 Subject: [PATCH 09/44] I've Split the etcd hunters to hunting & discovery dirs --- src/modules/discovery/etcd.py | 78 ++------------------- src/modules/hunting/etcd.py | 127 +++++++++++++++++++++++++++------- 2 files changed, 108 insertions(+), 97 deletions(-) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 3c0119e..2f27d08 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -11,22 +11,10 @@ from ...core.types import Hunter it contains configuration and current state information, and might contain secrets""" #services: -class etcdRemoteWriteAccessEvent(Service, Event): - """Remote write access might grant an attacker full control over the kubernetes cluster""" +class etcdAccessEvent(Service, Event): + """Etcd is a DB that stores cluster's data, it contains configuration and current state information, and might contain secrets""" def __init__(self): - Service.__init__(self, name="Etcd Remote Write Access Event") -class etcdRemoteReadAccessEvent(Service, Event): - """Remote read access might expose to an attacker cluster's possible exploits, secrets and more.""" - def __init__(self): - Service.__init__(self, name="Etcd Remote Read Access Event") -class etcdRemoteVersionDisclosureEvent(Service, Event): - """Remote version disclosure might give an attacker a valuable data to attack a cluster""" - def __init__(self): - Service.__init__(self, name="Etcd Remote version disclosure") -class etcdAccessEnabledWithoutAuthEvent(Service, Event): - """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" - def __init__(self): - Service.__init__(self, name="Etcd is accessible without authorization") + Service.__init__(self, name="Etcd Access") @@ -35,65 +23,13 @@ class etcdRemoteAccess(Hunter): """Etcd Remote Access Checks for remote availability of etcd, version, read access, write access """ - #TODO: - #Read Liz's book & etcd's rest api and check if I've missed important commands to check - #Do we need to add a auth check and remote connection?->>> - #->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display the no auth event anyway? - #Check why the execute() isn't being called - #Decide if I should move db_keys_write_access to hunting/etcd.py as an active hunter def __init__(self, event): self.event = event - def db_keys_disclosure(self): - logging.debug(self.event.host) - logging.debug("Passive hunter is attempting to read etcd keys remotely") - r_secure = "https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) - r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) - - if self.helperFuncDo2Requests(r_secure, r_not_secure): - self.publish_event(etcdRemoteReadAccessEvent()) - return True - return False - - def version_disclosure(self): - logging.debug(self.event.host) - logging.debug("Passive hunter is attempting to check etcd version remotely") - r_secure = "https://{host}:{port}/version".format(host=self.event.host, port=2379) - r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) - if self.helperFuncDo2Requests( r_secure, r_not_secure ): - self.publish_event(etcdRemoteVersionDisclosureEvent()) - return True - return False - def execute(self): - if (self.version_disclosure()): - self.publish_event(etcdAccessEnabledWithoutAuthEvent())#if version is accessible we can publish "no auth event". - self.db_keys_disclosure() - self.db_keys_write_access() - - # Will attempt to do request "req1" with the optional parameters. - # If fails it will attempt to do "req2" with the optional parameters. - # If once of the request success this method will return True, if both fail- False. - def helperFuncDo2Requests(self, req1, req2, is_verify=False, data=None, req_type="get"): try: - r = self.helperDoRequest(req1, is_verify, data, req_type) - has_remote_access_gained = (r.status_code == 200 and r.content != "") - if has_remote_access_gained: - return True - except Exception: - try: - r = self.helperDoRequest(req2, is_verify, data, req_type) - has_remote_access_gained = (r.status_code == 200 and r.content != "") - if has_remote_access_gained: - return True - except Exception: - return False #None of the requests succeded.. - return False + self.publish_event(etcdAccessEvent()) + except Exception as e: + import traceback + traceback.print_exc() - def helperDoRequest(self, req, is_verify, data=None, req_type="get"): - if req_type == "put": - r = requests.put(req, verify=is_verify, timeout=3, data=data) - return r - elif req_type == "get": - r = requests.get(req, verify=is_verify, timeout=3, data=data) - return r \ No newline at end of file diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 46faabe..fab89a0 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -5,15 +5,73 @@ import requests from ...core.events import handler from ...core.events.types import Vulnerability, Event, OpenPortEvent -from ...core.types import ActiveHunter +from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure + +# Helper functions: + +# Will attempt to do request "req1" with the optional parameters. +# If fails it will attempt to do "req2" with the optional parameters. +# If once of the request success this method will return True, if both fail- False. +def helperFuncDo2Requests(req1, req2, is_verify=False, data=None, req_type="get"): + try: + r = helperDoRequest(req1, is_verify, data, req_type) + has_remote_access_gained = (r.status_code == 200 and r.content != "") + if has_remote_access_gained: + return True + except Exception: + try: + r = helperDoRequest(req2, is_verify, data, req_type) + has_remote_access_gained = (r.status_code == 200 and r.content != "") + if has_remote_access_gained: + return True + except Exception: + return False # None of the requests succeded.. + return False + + +def helperDoRequest(req, is_verify, data=None, req_type="get"): + if req_type == "put": + r = requests.put(req, verify=is_verify, timeout=3, data=data) + return r + elif req_type == "get": + r = requests.get(req, verify=is_verify, timeout=3, data=data) + return r + """Etcd is a DB that stores cluster's data, it contains configuration and current state information, and might contain secrets""" + # Vulnerability: class etcdRemoteWriteAccessEvent(Vulnerability, Event): """Remote write access might grant an attacker full control over the kubernetes cluster""" def __init__(self): - Vulnerability.__init__(self, name="Etcd Remote Write Access Event") + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Write Access Event") + print 'mro: ' + str(self.__mro__) + +class etcdRemoteWriteAccessEvent(Vulnerability, Event): + """Remote write access might grant an attacker full control over the kubernetes cluster""" + + def __init__(self): + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Write Access Event") + +class etcdRemoteReadAccessEvent(Vulnerability, Event): + """Remote read access might expose to an attacker cluster's possible exploits, secrets and more.""" + + def __init__(self): + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Read Access Event") + +class etcdRemoteVersionDisclosureEvent(Vulnerability, Event): + """Remote version disclosure might give an attacker a valuable data to attack a cluster""" + + def __init__(self): + Vulnerability.__init__(self, KubernetesCluster, category="boii", name="Etcd Remote version disclosure") + +class etcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): + """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" + + def __init__(self): + Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization") + @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) class etcdRemoteAccessActive(ActiveHunter): @@ -22,6 +80,7 @@ class etcdRemoteAccessActive(ActiveHunter): """ def __init__(self, event): + print 'mro: ' + str(self.__mro__) self.event = event def db_keys_write_access(self): @@ -39,31 +98,47 @@ class etcdRemoteAccessActive(ActiveHunter): return False def execute(self): + print 'execute scope!! ACTIVEEE~~~~~~\n' self.db_keys_write_access() - # Will attempt to do request "req1" with the optional parameters. - # If fails it will attempt to do "req2" with the optional parameters. - # If once of the request success this method will return True, if both fail- False. - def helperFuncDo2Requests(self, req1, req2, is_verify=False, data=None, req_type="get"): - try: - r = self.helperDoRequest(req1, is_verify, data, req_type) - has_remote_access_gained = (r.status_code == 200 and r.content != "") - if has_remote_access_gained: - return True - except Exception: - try: - r = self.helperDoRequest(req2, is_verify, data, req_type) - has_remote_access_gained = (r.status_code == 200 and r.content != "") - if has_remote_access_gained: - return True - except Exception: - return False #None of the requests succeded.. +@handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) +class etcdRemoteAccess(Hunter): + """Etcd Remote Access + Checks for remote availability of etcd, version, read access, write access + """ + + # TODO: + # Read Liz's book & etcd's rest api and check if I've missed important commands to check + # Do we need to add a auth check and remote connection?->>> + # ->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display the no auth event anyway? + # Check why the execute() isn't being called + # Decide if I should move db_keys_write_access to hunting/etcd.py as an active hunter + def __init__(self, event): + self.event = event + + def db_keys_disclosure(self): + logging.debug(self.event.host) + logging.debug("Passive hunter is attempting to read etcd keys remotely") + r_secure = "https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) + r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) + + if helperFuncDo2Requests(r_secure, r_not_secure): + self.publish_event(etcdRemoteReadAccessEvent()) + return True return False - def helperDoRequest(self, req, is_verify, data=None, req_type="get"): - if req_type == "put": - r = requests.put(req, verify=is_verify, timeout=3, data=data) - return r - elif req_type == "get": - r = requests.get(req, verify=is_verify, timeout=3, data=data) - return r \ No newline at end of file + def version_disclosure(self): + logging.debug(self.event.host) + logging.debug("Passive hunter is attempting to check etcd version remotely") + r_secure = "https://{host}:{port}/version".format(host=self.event.host, port=2379) + r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) + if helperFuncDo2Requests(r_secure, r_not_secure): + self.publish_event(etcdRemoteVersionDisclosureEvent()) + return True + return False + + def execute(self): + if (self.version_disclosure()): + self.publish_event(etcdAccessEnabledWithoutAuthEvent()) # if version is accessible we can publish "no auth event". + self.db_keys_disclosure() + self.db_keys_write_access() \ No newline at end of file From 0d980fa0efde5118964429aaefa4a7a4e5a4454b Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 3 Oct 2018 18:32:31 +0300 Subject: [PATCH 10/44] Added some evidences to events & deleted unused code --- src/modules/hunting/etcd.py | 41 +++++++++++++++---------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index fab89a0..11924c1 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -7,7 +7,7 @@ from ...core.events import handler from ...core.events.types import Vulnerability, Event, OpenPortEvent from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure -# Helper functions: +""" Helper functions """ # Will attempt to do request "req1" with the optional parameters. # If fails it will attempt to do "req2" with the optional parameters. @@ -17,13 +17,13 @@ def helperFuncDo2Requests(req1, req2, is_verify=False, data=None, req_type="get" r = helperDoRequest(req1, is_verify, data, req_type) has_remote_access_gained = (r.status_code == 200 and r.content != "") if has_remote_access_gained: - return True + return r except Exception: try: r = helperDoRequest(req2, is_verify, data, req_type) has_remote_access_gained = (r.status_code == 200 and r.content != "") if has_remote_access_gained: - return True + return r except Exception: return False # None of the requests succeded.. return False @@ -38,16 +38,7 @@ def helperDoRequest(req, is_verify, data=None, req_type="get"): return r -"""Etcd is a DB that stores cluster's data, - it contains configuration and current state information, and might contain secrets""" - -# Vulnerability: -class etcdRemoteWriteAccessEvent(Vulnerability, Event): - """Remote write access might grant an attacker full control over the kubernetes cluster""" - def __init__(self): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Write Access Event") - print 'mro: ' + str(self.__mro__) - +""" Vulnerabilities """ class etcdRemoteWriteAccessEvent(Vulnerability, Event): """Remote write access might grant an attacker full control over the kubernetes cluster""" @@ -57,14 +48,16 @@ class etcdRemoteWriteAccessEvent(Vulnerability, Event): class etcdRemoteReadAccessEvent(Vulnerability, Event): """Remote read access might expose to an attacker cluster's possible exploits, secrets and more.""" - def __init__(self): + def __init__(self, keys): Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Read Access Event") + self.evidence = keys class etcdRemoteVersionDisclosureEvent(Vulnerability, Event): """Remote version disclosure might give an attacker a valuable data to attack a cluster""" - def __init__(self): + def __init__(self, version): Vulnerability.__init__(self, KubernetesCluster, category="boii", name="Etcd Remote version disclosure") + self.evidence = version class etcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" @@ -75,12 +68,9 @@ class etcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) class etcdRemoteAccessActive(ActiveHunter): - """Etcd Remote Access - Checks for remote write access to etcd - """ + """Checks for remote write access to etcd""" def __init__(self, event): - print 'mro: ' + str(self.__mro__) self.event = event def db_keys_write_access(self): @@ -98,7 +88,7 @@ class etcdRemoteAccessActive(ActiveHunter): return False def execute(self): - print 'execute scope!! ACTIVEEE~~~~~~\n' + print 'Active hunter execute() scope~~~~~\n' self.db_keys_write_access() @handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) @@ -121,9 +111,9 @@ class etcdRemoteAccess(Hunter): logging.debug("Passive hunter is attempting to read etcd keys remotely") r_secure = "https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) - - if helperFuncDo2Requests(r_secure, r_not_secure): - self.publish_event(etcdRemoteReadAccessEvent()) + res = helperFuncDo2Requests(r_secure, r_not_secure) + if res: + self.publish_event(etcdRemoteReadAccessEvent(res.content)) return True return False @@ -132,8 +122,9 @@ class etcdRemoteAccess(Hunter): logging.debug("Passive hunter is attempting to check etcd version remotely") r_secure = "https://{host}:{port}/version".format(host=self.event.host, port=2379) r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) - if helperFuncDo2Requests(r_secure, r_not_secure): - self.publish_event(etcdRemoteVersionDisclosureEvent()) + res = helperFuncDo2Requests(r_secure, r_not_secure) + if res: + self.publish_event(etcdRemoteReadAccessEvent(res.content)) return True return False From 1e13ab09852a16c66dc1374e5ee3d9d37e32f446 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 3 Oct 2018 18:34:43 +0300 Subject: [PATCH 11/44] Updated the TODOS list (Only 2 left!) --- src/modules/hunting/etcd.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 11924c1..d4484a2 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -99,10 +99,7 @@ class etcdRemoteAccess(Hunter): # TODO: # Read Liz's book & etcd's rest api and check if I've missed important commands to check - # Do we need to add a auth check and remote connection?->>> - # ->>>if we are able to get the version remotely it means there was no auth check and we were able to connect remotely but maybe we should display the no auth event anyway? - # Check why the execute() isn't being called - # Decide if I should move db_keys_write_access to hunting/etcd.py as an active hunter + # Check the etcd hunter on a remote cluster! (currently everything was checked only at 127.0.0.1:2379) def __init__(self, event): self.event = event From da9a97dfd890bc36cb017bc13f4791f25c5f1f51 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 3 Oct 2018 18:46:48 +0300 Subject: [PATCH 12/44] Fixed a small bug in the active hunter & passive hunter --- src/modules/hunting/etcd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index d4484a2..1ff367a 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -82,13 +82,13 @@ class etcdRemoteAccessActive(ActiveHunter): r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) r_not_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - if self.helperFuncDo2Requests(r_secure, r_not_secure, data=data, req_type="put"): - self.publish_event(etcdRemoteWriteAccessEvent()) + res = helperFuncDo2Requests(r_secure, r_not_secure) + if res: + self.publish_event(etcdRemoteReadAccessEvent(res.content)) return True return False def execute(self): - print 'Active hunter execute() scope~~~~~\n' self.db_keys_write_access() @handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) @@ -121,7 +121,7 @@ class etcdRemoteAccess(Hunter): r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) res = helperFuncDo2Requests(r_secure, r_not_secure) if res: - self.publish_event(etcdRemoteReadAccessEvent(res.content)) + self.publish_event(etcdRemoteVersionDisclosureEvent(res.content)) return True return False From 29f4d94ca32579408589bc0e1256e5668ec96353 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 3 Oct 2018 18:48:54 +0300 Subject: [PATCH 13/44] Fixed a small bug in the active hunter --- src/modules/hunting/etcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 1ff367a..4e83082 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -84,7 +84,7 @@ class etcdRemoteAccessActive(ActiveHunter): res = helperFuncDo2Requests(r_secure, r_not_secure) if res: - self.publish_event(etcdRemoteReadAccessEvent(res.content)) + self.publish_event(etcdRemoteWriteAccessEvent(res.content)) return True return False From 440ee5cf2ba3f479a077de1b3c3bb6790e423a4c Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 3 Oct 2018 18:50:53 +0300 Subject: [PATCH 14/44] Fixed a small bug in the active hunter --- src/modules/hunting/etcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 4e83082..e4a88db 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -84,7 +84,7 @@ class etcdRemoteAccessActive(ActiveHunter): res = helperFuncDo2Requests(r_secure, r_not_secure) if res: - self.publish_event(etcdRemoteWriteAccessEvent(res.content)) + self.publish_event(etcdRemoteWriteAccessEvent()) return True return False From 3984b5ad32e741018e0fe8e5b157a712fbea11d1 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 7 Oct 2018 11:42:21 +0300 Subject: [PATCH 15/44] Added categories to all vulnerabilities --- src/modules/hunting/etcd.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index e4a88db..d0e6383 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -5,7 +5,7 @@ import requests from ...core.events import handler from ...core.events.types import Vulnerability, Event, OpenPortEvent -from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure +from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure, RemoteCodeExec, UnauthenticatedAccess, AccessRisk """ Helper functions """ @@ -43,27 +43,27 @@ class etcdRemoteWriteAccessEvent(Vulnerability, Event): """Remote write access might grant an attacker full control over the kubernetes cluster""" def __init__(self): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Write Access Event") + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Write Access Event", category=RemoteCodeExec) class etcdRemoteReadAccessEvent(Vulnerability, Event): """Remote read access might expose to an attacker cluster's possible exploits, secrets and more.""" def __init__(self, keys): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Read Access Event") + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Read Access Event", category=AccessRisk) self.evidence = keys class etcdRemoteVersionDisclosureEvent(Vulnerability, Event): """Remote version disclosure might give an attacker a valuable data to attack a cluster""" def __init__(self, version): - Vulnerability.__init__(self, KubernetesCluster, category="boii", name="Etcd Remote version disclosure") + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure", category=AccessRisk) self.evidence = version class etcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" def __init__(self): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization") + Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization", category=UnauthenticatedAccess) @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) From 7e24471e08c628f1097a93dfbe18f9c8ece20bbc Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 7 Oct 2018 11:45:52 +0300 Subject: [PATCH 16/44] Updated the todos list --- src/modules/hunting/etcd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index d0e6383..7bf41e7 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -98,7 +98,6 @@ class etcdRemoteAccess(Hunter): """ # TODO: - # Read Liz's book & etcd's rest api and check if I've missed important commands to check # Check the etcd hunter on a remote cluster! (currently everything was checked only at 127.0.0.1:2379) def __init__(self, event): self.event = event From b9ce66e3727c5913378784a83c3df93e316a9c31 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 7 Oct 2018 16:59:49 +0300 Subject: [PATCH 17/44] Added evidence to the no auth event & tested it on a vulnerable remote cluster (and it worked!) --- src/modules/discovery/etcd.py | 2 +- src/modules/hunting/etcd.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 2f27d08..a6486cc 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -14,7 +14,7 @@ from ...core.types import Hunter class etcdAccessEvent(Service, Event): """Etcd is a DB that stores cluster's data, it contains configuration and current state information, and might contain secrets""" def __init__(self): - Service.__init__(self, name="Etcd Access") + Service.__init__(self, name="Etcd") diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 7bf41e7..cf3d957 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -62,8 +62,9 @@ class etcdRemoteVersionDisclosureEvent(Vulnerability, Event): class etcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" - def __init__(self): + def __init__(self, version): Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization", category=UnauthenticatedAccess) + self.evidence = version @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) @@ -89,14 +90,13 @@ class etcdRemoteAccessActive(ActiveHunter): return False def execute(self): - self.db_keys_write_access() + self.db_keys_write_access() @handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) class etcdRemoteAccess(Hunter): """Etcd Remote Access Checks for remote availability of etcd, version, read access, write access """ - # TODO: # Check the etcd hunter on a remote cluster! (currently everything was checked only at 127.0.0.1:2379) def __init__(self, event): @@ -120,12 +120,13 @@ class etcdRemoteAccess(Hunter): r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) res = helperFuncDo2Requests(r_secure, r_not_secure) if res: + self.no_auth_evidence = res.content self.publish_event(etcdRemoteVersionDisclosureEvent(res.content)) return True return False def execute(self): if (self.version_disclosure()): - self.publish_event(etcdAccessEnabledWithoutAuthEvent()) # if version is accessible we can publish "no auth event". + self.publish_event(etcdAccessEnabledWithoutAuthEvent(self.no_auth_evidence)) # if version is accessible we can publish "no auth event". self.db_keys_disclosure() self.db_keys_write_access() \ No newline at end of file From 4573fe408932b846ad1e599e5c3d1cf6a7d77820 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 7 Oct 2018 17:16:07 +0300 Subject: [PATCH 18/44] Improved unauthorized access false positive on edge case (where user is running using https & 127.0.0.1 & needed certificates) --- src/modules/hunting/etcd.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index cf3d957..774524b 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -125,8 +125,18 @@ class etcdRemoteAccess(Hunter): return True return False + def unauthorized_access(self): + logging.debug(self.event.host) + logging.debug("Passive hunter is attempting to access etcd without authorization") + r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) + res = helperFuncDo2Requests(r_not_secure, r_not_secure)# We dont have to do 2 requests this time + if res: + self.publish_event(etcdAccessEnabledWithoutAuthEvent(res.content)) + return True + return False + def execute(self): - if (self.version_disclosure()): - self.publish_event(etcdAccessEnabledWithoutAuthEvent(self.no_auth_evidence)) # if version is accessible we can publish "no auth event". + if self.version_disclosure(): + self.unauthorized_access() self.db_keys_disclosure() - self.db_keys_write_access() \ No newline at end of file + self.db_keys_write_access() From 19c10fd8e975dd700e613df102ef8ef0de42a02b Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Mon, 8 Oct 2018 17:18:19 +0300 Subject: [PATCH 19/44] Fixed the PR comments :-) --- src/core/events/handler.py | 4 +-- src/modules/discovery/etcd.py | 10 +++---- src/modules/hunting/etcd.py | 54 +++++++++++++++++++---------------- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index f5b4595..4e8f686 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -50,7 +50,7 @@ class EventQueue(Queue, object): else: self.active_hunters[hook] = hook.__doc__ elif Hunter in hook.__mro__: - self.passive_hunters[hook] = hook.__doc__ + self.passive_hunters[hook] = hook.__doc__ if hook not in self.hooks[event]: self.hooks[event].append((hook, predicate)) @@ -92,4 +92,4 @@ class EventQueue(Queue, object): with self.mutex: self.queue.clear() -handler = EventQueue(800) \ No newline at end of file +handler = EventQueue(800) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index a6486cc..6b226ad 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -7,11 +7,9 @@ from ...core.events import handler from ...core.events.types import Event, OpenPortEvent, Service from ...core.types import Hunter -"""Etcd is a DB that stores cluster's data, - it contains configuration and current state information, and might contain secrets""" -#services: +# Service: -class etcdAccessEvent(Service, Event): +class EtcdAccessEvent(Service, Event): """Etcd is a DB that stores cluster's data, it contains configuration and current state information, and might contain secrets""" def __init__(self): Service.__init__(self, name="Etcd") @@ -19,7 +17,7 @@ class etcdAccessEvent(Service, Event): @handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) -class etcdRemoteAccess(Hunter): +class EtcdRemoteAccess(Hunter): """Etcd Remote Access Checks for remote availability of etcd, version, read access, write access """ @@ -28,7 +26,7 @@ class etcdRemoteAccess(Hunter): def execute(self): try: - self.publish_event(etcdAccessEvent()) + self.publish_event(EtcdAccessEvent()) except Exception as e: import traceback traceback.print_exc() diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 774524b..7070141 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -39,61 +39,63 @@ def helperDoRequest(req, is_verify, data=None, req_type="get"): """ Vulnerabilities """ -class etcdRemoteWriteAccessEvent(Vulnerability, Event): +class EtcdRemoteWriteAccessEvent(Vulnerability, Event): """Remote write access might grant an attacker full control over the kubernetes cluster""" - def __init__(self): + def __init__(self, write_res): Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Write Access Event", category=RemoteCodeExec) + self.evidence = write_res -class etcdRemoteReadAccessEvent(Vulnerability, Event): +class EtcdRemoteReadAccessEvent(Vulnerability, Event): """Remote read access might expose to an attacker cluster's possible exploits, secrets and more.""" def __init__(self, keys): Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote Read Access Event", category=AccessRisk) self.evidence = keys -class etcdRemoteVersionDisclosureEvent(Vulnerability, Event): +class EtcdRemoteVersionDisclosureEvent(Vulnerability, Event): """Remote version disclosure might give an attacker a valuable data to attack a cluster""" def __init__(self, version): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure", category=AccessRisk) + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure", category=InformationDisclosure) self.evidence = version -class etcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): +class EtcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" def __init__(self, version): Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization", category=UnauthenticatedAccess) self.evidence = version - -@handler.subscribe(OpenPortEvent, predicate= lambda p: p.port == 2379) -class etcdRemoteAccessActive(ActiveHunter): +# Active Hunter +@handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) +class EtcdRemoteAccessActive(ActiveHunter): """Checks for remote write access to etcd""" def __init__(self, event): self.event = event + self.write_evidence = '' def db_keys_write_access(self): - logging.debug(self.event.host) - logging.debug("Active hunter is attempting to write keys remotely") + logging.debug("Active hunter is attempting to write keys remotely on host " + self.event.host) data = { - 'value': 'remote write access penetration' + 'value': 'remotely written data' } r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - r_not_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - + r_not_secure = "http://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) res = helperFuncDo2Requests(r_secure, r_not_secure) if res: - self.publish_event(etcdRemoteWriteAccessEvent()) + self.write_evidence = res.content return True return False def execute(self): - self.db_keys_write_access() + if self.db_keys_write_access(): + self.publish_event(EtcdRemoteWriteAccessEvent(self.write_evidence)) +# Passive Hunter @handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) -class etcdRemoteAccess(Hunter): +class EtcdRemoteAccess(Hunter): """Etcd Remote Access Checks for remote availability of etcd, version, read access, write access """ @@ -101,6 +103,8 @@ class etcdRemoteAccess(Hunter): # Check the etcd hunter on a remote cluster! (currently everything was checked only at 127.0.0.1:2379) def __init__(self, event): self.event = event + self.version_evidence = '' + self.keys_evidence = '' def db_keys_disclosure(self): logging.debug(self.event.host) @@ -109,7 +113,7 @@ class etcdRemoteAccess(Hunter): r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) res = helperFuncDo2Requests(r_secure, r_not_secure) if res: - self.publish_event(etcdRemoteReadAccessEvent(res.content)) + self.keys_evidence = res.content return True return False @@ -120,8 +124,7 @@ class etcdRemoteAccess(Hunter): r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) res = helperFuncDo2Requests(r_secure, r_not_secure) if res: - self.no_auth_evidence = res.content - self.publish_event(etcdRemoteVersionDisclosureEvent(res.content)) + self.version_evidence = res.content return True return False @@ -129,14 +132,15 @@ class etcdRemoteAccess(Hunter): logging.debug(self.event.host) logging.debug("Passive hunter is attempting to access etcd without authorization") r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) - res = helperFuncDo2Requests(r_not_secure, r_not_secure)# We dont have to do 2 requests this time + res = helperFuncDo2Requests(r_not_secure, r_not_secure) # We don't have to do 2 requests this time if res: - self.publish_event(etcdAccessEnabledWithoutAuthEvent(res.content)) return True return False def execute(self): if self.version_disclosure(): - self.unauthorized_access() - self.db_keys_disclosure() - self.db_keys_write_access() + self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence)) + if self.unauthorized_access(): + self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence)) + if self.db_keys_disclosure(): + self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence)) From 28b7910588fb0a263a936601e9b2a9bff34ba4e7 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Mon, 8 Oct 2018 17:21:29 +0300 Subject: [PATCH 20/44] Fixed the PR comments :-) --- src/modules/discovery/etcd.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/modules/discovery/etcd.py b/src/modules/discovery/etcd.py index 6b226ad..3e92540 100644 --- a/src/modules/discovery/etcd.py +++ b/src/modules/discovery/etcd.py @@ -25,9 +25,4 @@ class EtcdRemoteAccess(Hunter): self.event = event def execute(self): - try: - self.publish_event(EtcdAccessEvent()) - except Exception as e: - import traceback - traceback.print_exc() - + self.publish_event(EtcdAccessEvent()) From 5f5f411aff5152c659f6a44060329cf71765ed6b Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Tue, 9 Oct 2018 21:27:11 +0300 Subject: [PATCH 21/44] Added informative description for the EtcdAccessEnabledWithoutAuthEvent event --- src/modules/hunting/etcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 7070141..25aa47a 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -61,7 +61,7 @@ class EtcdRemoteVersionDisclosureEvent(Vulnerability, Event): self.evidence = version class EtcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): - """Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd""" + """Etcd is accessible using HTTP (without authorization and authentication), it would allow a potential attacker to gain access to the etcd""" def __init__(self, version): Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization", category=UnauthenticatedAccess) From 7041280f2c7457fcdc45141861cfc266e0a3daed Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 10 Oct 2018 19:11:04 +0300 Subject: [PATCH 22/44] Removed the helpers functions --- src/modules/hunting/etcd.py | 86 ++++++++----------------------------- 1 file changed, 19 insertions(+), 67 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 25aa47a..b96d18d 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -7,37 +7,6 @@ from ...core.events import handler from ...core.events.types import Vulnerability, Event, OpenPortEvent from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure, RemoteCodeExec, UnauthenticatedAccess, AccessRisk -""" Helper functions """ - -# Will attempt to do request "req1" with the optional parameters. -# If fails it will attempt to do "req2" with the optional parameters. -# If once of the request success this method will return True, if both fail- False. -def helperFuncDo2Requests(req1, req2, is_verify=False, data=None, req_type="get"): - try: - r = helperDoRequest(req1, is_verify, data, req_type) - has_remote_access_gained = (r.status_code == 200 and r.content != "") - if has_remote_access_gained: - return r - except Exception: - try: - r = helperDoRequest(req2, is_verify, data, req_type) - has_remote_access_gained = (r.status_code == 200 and r.content != "") - if has_remote_access_gained: - return r - except Exception: - return False # None of the requests succeded.. - return False - - -def helperDoRequest(req, is_verify, data=None, req_type="get"): - if req_type == "put": - r = requests.put(req, verify=is_verify, timeout=3, data=data) - return r - elif req_type == "get": - r = requests.get(req, verify=is_verify, timeout=3, data=data) - return r - - """ Vulnerabilities """ class EtcdRemoteWriteAccessEvent(Vulnerability, Event): """Remote write access might grant an attacker full control over the kubernetes cluster""" @@ -81,13 +50,9 @@ class EtcdRemoteAccessActive(ActiveHunter): data = { 'value': 'remotely written data' } - r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - r_not_secure = "http://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379) - res = helperFuncDo2Requests(r_secure, r_not_secure) - if res: - self.write_evidence = res.content - return True - return False + r = "{protocol}://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379, protocol=self.protocol, data=data) + self.write_evidence = r.content if r.status_code == '200' and r.content != '' else False + return self.write_evidence def execute(self): if self.db_keys_write_access(): @@ -99,48 +64,35 @@ class EtcdRemoteAccess(Hunter): """Etcd Remote Access Checks for remote availability of etcd, version, read access, write access """ - # TODO: - # Check the etcd hunter on a remote cluster! (currently everything was checked only at 127.0.0.1:2379) def __init__(self, event): self.event = event self.version_evidence = '' self.keys_evidence = '' + self.protocol = 'https' def db_keys_disclosure(self): - logging.debug(self.event.host) - logging.debug("Passive hunter is attempting to read etcd keys remotely") - r_secure = "https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) - r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379) - res = helperFuncDo2Requests(r_secure, r_not_secure) - if res: - self.keys_evidence = res.content - return True - return False + logging.debug(self.event.host + " Passive hunter is attempting to read etcd keys remotely") + r = requests.get("{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379), verify=False) + self.keys_evidence = r.content if r.status_code == '200' and r.content != '' else False + return self.version_evidence - def version_disclosure(self): - logging.debug(self.event.host) - logging.debug("Passive hunter is attempting to check etcd version remotely") - r_secure = "https://{host}:{port}/version".format(host=self.event.host, port=2379) - r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) - res = helperFuncDo2Requests(r_secure, r_not_secure) - if res: - self.version_evidence = res.content - return True - return False + def version_disclosure(self, protocol): + logging.debug(self.event.host + " Passive hunter is attempting to check etcd version remotely") + r = requests.get("{protocol}://{host}:{port}/version".format(protocol=self.protocol, host=self.event.host, port=2379), verify=False) + self.version_evidence = r.content if r.status_code == '200' and r.content != '' else False + return self.version_evidence def unauthorized_access(self): - logging.debug(self.event.host) - logging.debug("Passive hunter is attempting to access etcd without authorization") - r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379) - res = helperFuncDo2Requests(r_not_secure, r_not_secure) # We don't have to do 2 requests this time - if res: - return True - return False + logging.debug(self.event.host + " Passive hunter is attempting to access etcd without authorization") + r = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), verify=False) + return r.content if r.status_code == '200' and r.content != '' else False def execute(self): + if self.unauthorized_access(): # inits http/https protocol + self.protocol = 'http' if self.version_disclosure(): self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence)) - if self.unauthorized_access(): + if self.protocol == 'http' and self.unauthorized_access(): self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence)) if self.db_keys_disclosure(): self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence)) From e3d45d5d88d8999c8d9c1bddc9e2e87cf8912253 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 10 Oct 2018 19:12:16 +0300 Subject: [PATCH 23/44] improved comment --- src/modules/hunting/etcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index b96d18d..bd0ccb6 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -88,7 +88,7 @@ class EtcdRemoteAccess(Hunter): return r.content if r.status_code == '200' and r.content != '' else False def execute(self): - if self.unauthorized_access(): # inits http/https protocol + if self.unauthorized_access(): # decide between http and https protocol self.protocol = 'http' if self.version_disclosure(): self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence)) From 46cbfbc5dc07a8ae7f69c14f25d1fcff252c8a70 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 10 Oct 2018 20:10:06 +0300 Subject: [PATCH 24/44] Fixed status code bug & some intending. --- src/modules/hunting/etcd.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index bd0ccb6..149970d 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -50,20 +50,23 @@ class EtcdRemoteAccessActive(ActiveHunter): data = { 'value': 'remotely written data' } - r = "{protocol}://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379, protocol=self.protocol, data=data) - self.write_evidence = r.content if r.status_code == '200' and r.content != '' else False + r = "{protocol}://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379, protocol=self.protocol, + data=data) + self.write_evidence = r.content if r.status_code == 200 and r.content != '' else False return self.write_evidence def execute(self): if self.db_keys_write_access(): self.publish_event(EtcdRemoteWriteAccessEvent(self.write_evidence)) + # Passive Hunter @handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) class EtcdRemoteAccess(Hunter): """Etcd Remote Access Checks for remote availability of etcd, version, read access, write access """ + def __init__(self, event): self.event = event self.version_evidence = '' @@ -72,20 +75,24 @@ class EtcdRemoteAccess(Hunter): def db_keys_disclosure(self): logging.debug(self.event.host + " Passive hunter is attempting to read etcd keys remotely") - r = requests.get("{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379), verify=False) - self.keys_evidence = r.content if r.status_code == '200' and r.content != '' else False + r = requests.get( + "{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379), + verify=False) + self.keys_evidence = r.content if r.status_code == 200 and r.content != '' else False return self.version_evidence - def version_disclosure(self, protocol): + def version_disclosure(self): logging.debug(self.event.host + " Passive hunter is attempting to check etcd version remotely") - r = requests.get("{protocol}://{host}:{port}/version".format(protocol=self.protocol, host=self.event.host, port=2379), verify=False) - self.version_evidence = r.content if r.status_code == '200' and r.content != '' else False + r = requests.get( + "{protocol}://{host}:{port}/version".format(protocol=self.protocol, host=self.event.host, port=2379), + verify=False) + self.version_evidence = r.content if r.status_code == 200 and r.content != '' else False return self.version_evidence def unauthorized_access(self): logging.debug(self.event.host + " Passive hunter is attempting to access etcd without authorization") r = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), verify=False) - return r.content if r.status_code == '200' and r.content != '' else False + return r.content if r.status_code == 200 and r.content != '' else False def execute(self): if self.unauthorized_access(): # decide between http and https protocol @@ -96,3 +103,4 @@ class EtcdRemoteAccess(Hunter): self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence)) if self.db_keys_disclosure(): self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence)) + From b5117fb315ec43e4d33160cdb2646445e3a5bed7 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 14 Oct 2018 14:51:15 +0300 Subject: [PATCH 25/44] Had to remove the Azure component form the hunting/aks since it made a circular dependency bug! --- src/core/types.py | 3 +++ src/modules/discovery/hosts.py | 3 +-- src/modules/hunting/aks.py | 5 +---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/types.py b/src/core/types.py index ac3d385..626ebc9 100644 --- a/src/core/types.py +++ b/src/core/types.py @@ -17,6 +17,9 @@ class Kubelet(KubernetesCluster): """The kubelet is the primary "node agent" that runs on each node""" name = "Kubelet" +class Azure(KubernetesCluster): + """Azure Cluster""" + name = "Azure" """ Categories """ class InformationDisclosure(object): diff --git a/src/modules/discovery/hosts.py b/src/modules/discovery/hosts.py index e255407..fcde41f 100644 --- a/src/modules/discovery/hosts.py +++ b/src/modules/discovery/hosts.py @@ -13,8 +13,7 @@ from netifaces import AF_INET, ifaddresses, interfaces from ...core.events import handler from ...core.events.types import Event, NewHostEvent, Vulnerability -from ...core.types import Hunter, InformationDisclosure -from ..hunting.aks import Azure +from ...core.types import Hunter, InformationDisclosure, Azure class AzureMetadataApi(Vulnerability, Event): diff --git a/src/modules/hunting/aks.py b/src/modules/hunting/aks.py index 3275fb6..2fdaf0c 100644 --- a/src/modules/hunting/aks.py +++ b/src/modules/hunting/aks.py @@ -8,10 +8,7 @@ from kubelet import ExposedRunHandler from ...core.events import handler from ...core.events.types import Event, Vulnerability from ...core.types import Hunter, ActiveHunter, KubernetesCluster, IdentityTheft - -class Azure(KubernetesCluster): - """Azure Cluster""" - name = "Azure" + class AzureSpnExposure(Vulnerability, Event): """The SPN is exposed, potentially allowing an attacker to gain access to the Azure subscription""" From f771da5f5eb690836af5ebae63bfb51aa6be7fd3 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 14 Oct 2018 15:25:46 +0300 Subject: [PATCH 26/44] Added the Azure import to aks.py --- src/modules/hunting/aks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/hunting/aks.py b/src/modules/hunting/aks.py index 2fdaf0c..3fbda93 100644 --- a/src/modules/hunting/aks.py +++ b/src/modules/hunting/aks.py @@ -7,7 +7,7 @@ from kubelet import ExposedRunHandler from ...core.events import handler from ...core.events.types import Event, Vulnerability -from ...core.types import Hunter, ActiveHunter, KubernetesCluster, IdentityTheft +from ...core.types import Hunter, ActiveHunter, IdentityTheft, Azure class AzureSpnExposure(Vulnerability, Event): From 99dc62ee9d7a9a5541adda675b79356c0c2d163d Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Sun, 14 Oct 2018 16:08:40 +0300 Subject: [PATCH 27/44] Added top to the readme for dev so the mistake won't happen again --- src/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/README.md b/src/README.md index 80f3612..0bf676a 100644 --- a/src/README.md +++ b/src/README.md @@ -72,6 +72,9 @@ _The file's (module's) content is imported automatically"_ The second step is to determine what events your Hunter will subscribe to, and from where you can get them. `Convention:` Events should be declared in their corresponding module. for example, a KubeDashboardEvent event is declared in the dashboard discovery module. + `Notice:` An hunter located under the `disovery` folder should not import any modules located under the `hunting` folder +in order to prevent circular dependency bug. + Following the above example, let's figure out the imports: ```python from ...core.types import Hunter @@ -114,7 +117,7 @@ relative import: `...core.types` ## Creating Events -As discussed above, we know there are alot of different types of events that can be created. but at the end, they all need to inherit from the base class `Event` +As discussed above, we know there are a lot of different types of events that can be created. but at the end, they all need to inherit from the base class `Event` lets see some examples of creating different types of events: ### Vulnerability ```python From e679c93d4a4853a2d023de97fab3f89e4fd03a00 Mon Sep 17 00:00:00 2001 From: Liz Rice Date: Tue, 16 Oct 2018 20:13:21 +0100 Subject: [PATCH 28/44] Tiny typo correction --- src/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/README.md b/src/README.md index 0bf676a..1b78d68 100644 --- a/src/README.md +++ b/src/README.md @@ -72,7 +72,7 @@ _The file's (module's) content is imported automatically"_ The second step is to determine what events your Hunter will subscribe to, and from where you can get them. `Convention:` Events should be declared in their corresponding module. for example, a KubeDashboardEvent event is declared in the dashboard discovery module. - `Notice:` An hunter located under the `disovery` folder should not import any modules located under the `hunting` folder + `Note:` An hunter located under the `discovery` folder should not import any modules located under the `hunting` folder in order to prevent circular dependency bug. Following the above example, let's figure out the imports: From a8762a4adabe30ae1983ba239b883d527b04229a Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 11:32:50 +0300 Subject: [PATCH 29/44] Fixed PR: Used the same lock for each variable & changed locks names --- kube-hunter.py | 19 +++++++++---------- src/core/events/types/common.py | 13 +++++++------ src/modules/report/collector.py | 20 +++++++++++++++++--- src/modules/report/plain.py | 23 ++++++++++------------- src/modules/report/yaml.py | 20 ++++++++++---------- 5 files changed, 53 insertions(+), 42 deletions(-) diff --git a/kube-hunter.py b/kube-hunter.py index 77fa06c..90b7c99 100755 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -82,7 +82,7 @@ def list_hunters(): print("\nPassive Hunters:\n----------------") for i, (hunter, docs) in enumerate(handler.passive_hunters.items()): name, docs = parse_docs(hunter, docs) - print("* {}\n {}\n".format( name, docs)) + print("* {}\n {}\n".format(name, docs)) if config.active: print("\n\nActive Hunters:\n---------------") @@ -91,10 +91,11 @@ def list_hunters(): print("* {}\n {}\n".format( name, docs)) -tlock3 = threading.Lock() -tlock3.acquire() +global hunt_started_lock +hunt_started_lock = threading.Lock() +hunt_started_lock.acquire() hunt_started = False -tlock3.release() +hunt_started_lock.release() def main(): @@ -112,10 +113,9 @@ def main(): if not any(scan_options): if not interactive_set_config(): return - tlock = threading.Lock() - tlock.acquire() + hunt_started_lock.acquire() hunt_started = True - tlock.release() + hunt_started_lock.release() handler.publish_event(HuntStarted()) handler.publish_event(HostScanEvent()) @@ -127,14 +127,13 @@ def main(): except EOFError: logging.error("\033[0;31mPlease run again with -it\033[0m") finally: - tlock2 = threading.Lock() - tlock2.acquire() + hunt_started_lock.acquire() if hunt_started: handler.publish_event(HuntFinished()) handler.join() handler.free() logging.debug("Cleaned Queue") - tlock2.release() + hunt_started_lock.release() if __name__ == '__main__': diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index d19dc2f..1822f12 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -65,10 +65,12 @@ class Vulnerability(object): def explain(self): return self.__doc__ -tlock1 = threading.Lock() -tlock1.acquire() + +global event_id_count_lock +event_id_count_lock = threading.Lock() +event_id_count_lock.acquire() event_id_count = 0 -tlock1.release() +event_id_count_lock.release() """ Discovery/Hunting Events """ @@ -78,11 +80,10 @@ class NewHostEvent(Event): global event_id_count self.host = host self.cloud = cloud - tlock = threading.Lock() - tlock.acquire() + event_id_count_lock.acquire() self.event_id = event_id_count event_id_count += 1 - tlock.release() + event_id_count_lock.release() def __str__(self): return str(self.host) diff --git a/src/modules/report/collector.py b/src/modules/report/collector.py index fa50cb0..5bf0a25 100644 --- a/src/modules/report/collector.py +++ b/src/modules/report/collector.py @@ -5,8 +5,21 @@ from src.core.events import handler from src.core.events.types import Event, Service, Vulnerability, HuntFinished, HuntStarted import threading + +global services_lock +services_lock = threading.Lock() +services_lock.acquire() services = list() +services_lock.release() + +global vulnerabilities_lock +vulnerabilities_lock = threading.Lock() +vulnerabilities_lock.acquire() vulnerabilities = list() +vulnerabilities_lock.release() + + + def console_trim(text, prefix=' '): @@ -38,12 +51,12 @@ class Collector(object): def execute(self): """function is called only when collecting data""" - tlock = threading.Lock() - tlock.acquire() global services, vulnerabilities bases = self.event.__class__.__mro__ if Service in bases: + services_lock.acquire() services.append(self.event) + services_lock.release() import datetime logging.info("|\n| {name}:\n| type: open service\n| service: {name}\n|_ host: {host}:{port}".format( host=self.event.host, @@ -53,7 +66,9 @@ class Collector(object): )) elif Vulnerability in bases: + vulnerabilities_lock.acquire() vulnerabilities.append(self.event) + vulnerabilities_lock.release() logging.info( "|\n| {name}:\n| type: vulnerability\n| host: {host}:{port}\n| description: \n{desc}".format( name=self.event.get_name(), @@ -61,7 +76,6 @@ class Collector(object): port=self.event.port, desc=wrap_last_line(console_trim(self.event.explain(), '| ')) )) - tlock.release() class TablesPrinted(Event): diff --git a/src/modules/report/plain.py b/src/modules/report/plain.py index 64d2dda..0464e6e 100644 --- a/src/modules/report/plain.py +++ b/src/modules/report/plain.py @@ -3,7 +3,7 @@ from __future__ import print_function from prettytable import ALL, PrettyTable from __main__ import config -from collector import services, vulnerabilities +from collector import services, vulnerabilities,services_lock, vulnerabilities_lock import threading EVIDENCE_PREVIEW = 40 @@ -15,8 +15,7 @@ class PlainReporter(object): def get_report(self): """generates report tables""" output = "" - tlock = threading.Lock - tlock.acquire() + services_lock.acquire() if len(services): output += self.nodes_table() if not config.mapping: @@ -27,7 +26,7 @@ class PlainReporter(object): output += "\nNo vulnerabilities were found" else: print("\nKube Hunter couldn't find any clusters") - tlock.release() + services_lock.release() # print("\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "")) return output @@ -41,13 +40,12 @@ class PlainReporter(object): nodes_table.header_style = "upper" # TODO: replace with sets id_memory = list() - tlock = threading.Lock - tlock.acquire() + services_lock.acquire() for service in services: if service.event_id not in id_memory: nodes_table.add_row(["Node/Master", service.host]) id_memory.append(service.event_id) - tlock.release() + services_lock.acquire() return "\nNodes\n{}\n".format(nodes_table) def services_table(self): @@ -58,11 +56,10 @@ class PlainReporter(object): services_table.sortby = "Service" services_table.reversesort = True services_table.header_style = "upper" - tlock = threading.Lock - tlock.acquire() + services_lock.acquire() for service in services: services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) - tlock.release() + services_lock.release() return "\nDetected Services\n{}\n".format(services_table) def vulns_table(self): @@ -74,12 +71,12 @@ class PlainReporter(object): vuln_table.reversesort = True vuln_table.padding_width = 1 vuln_table.header_style = "upper" - tlock = threading.Lock - tlock.acquire() + + vulnerabilities_lock.acquire() for vuln in vulnerabilities: row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.category.name, vuln.get_name(), vuln.explain()] evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence) row.append(evidence) vuln_table.add_row(row) - tlock.release() + vulnerabilities_lock.release() return "\nVulnerabilities\n{}\n".format(vuln_table) diff --git a/src/modules/report/yaml.py b/src/modules/report/yaml.py index 2a0b372..80ae1fd 100644 --- a/src/modules/report/yaml.py +++ b/src/modules/report/yaml.py @@ -2,17 +2,20 @@ import StringIO from ruamel.yaml import YAML -from collector import services, vulnerabilities +from collector import services, vulnerabilities, services_lock, vulnerabilities_lock import threading + class YAMLReporter(object): def get_report(self): yaml = YAML() + vulnerabilities_lock.acquire() report = { "nodes": self.get_nodes(), "services": self.get_services(), "vulnerabilities": self.get_vulenrabilities() } + vulnerabilities_lock.release() output = StringIO.StringIO() yaml.dump(report, output) return output.getvalue() @@ -20,34 +23,31 @@ class YAMLReporter(object): def get_nodes(self): nodes = list() node_locations = set() - tlock = threading.Lock - tlock.acquire() + services_lock.acquire() for service in services: node_location = str(service.host) if node_location not in node_locations: nodes.append({"type": "Node/Master", "location": str(service.host)}) node_locations.add(node_location) - tlock.release() + services_lock.release() return nodes def get_services(self): - tlock = threading.Lock - tlock.acquire() + services_lock.acquire() services_data = [{"service": service.get_name(), "location": "{}:{}{}".format(service.host, service.port, service.get_path()), "description": service.explain()} for service in services] - tlock.release() + services_lock.release() return services_data def get_vulenrabilities(self): - tlock = threading.Lock - tlock.acquire() + vulnerabilities_lock.acquire() vulnerabilities_data = [{"location": "{}:{}".format(vuln.host, vuln.port) if vuln.host else "", "category": vuln.category.name, "vulnerability": vuln.get_name(), "description": vuln.explain(), "evidence": str(vuln.evidence)} for vuln in vulnerabilities] - tlock.release() + vulnerabilities_lock.release() return vulnerabilities_data From 87232a12cdbc2827e3a417750c1c785e794f3963 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 12:36:04 +0300 Subject: [PATCH 30/44] Fixed PR: Used the same lock for each variable & changed locks names --- kube-hunter.py | 6 ++++-- src/core/events/handler.py | 4 +++- src/modules/discovery/hosts.py | 2 +- src/modules/report/collector.py | 3 --- src/modules/report/plain.py | 13 ++++++++++--- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/kube-hunter.py b/kube-hunter.py index 90b7c99..afea10b 100755 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -129,15 +129,17 @@ def main(): finally: hunt_started_lock.acquire() if hunt_started: + hunt_started_lock.release() handler.publish_event(HuntFinished()) handler.join() handler.free() logging.debug("Cleaned Queue") - hunt_started_lock.release() + else: + hunt_started_lock.release() if __name__ == '__main__': - for i in range(6): + for i in range(1): try: main() except: diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 8e3b109..017cb0f 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -75,7 +75,9 @@ class EventQueue(Queue, object): hook = self.get() try: hook.execute() - except Exception as ex: + except Exception as ex: + import traceback + traceback.print_exc() logging.debug(ex.message) self.task_done() logging.debug("closing thread...") diff --git a/src/modules/discovery/hosts.py b/src/modules/discovery/hosts.py index bdc6b39..7e432ef 100644 --- a/src/modules/discovery/hosts.py +++ b/src/modules/discovery/hosts.py @@ -50,7 +50,7 @@ class HostDiscovery(Hunter): def __init__(self, event): self.event = event - def execute(self): + def execute(self): if config.cidr: try: ip, sn = config.cidr.split('/') diff --git a/src/modules/report/collector.py b/src/modules/report/collector.py index 5bf0a25..fd41bd2 100644 --- a/src/modules/report/collector.py +++ b/src/modules/report/collector.py @@ -19,9 +19,6 @@ vulnerabilities = list() vulnerabilities_lock.release() - - - def console_trim(text, prefix=' '): a = text.split(" ") b = a[:] diff --git a/src/modules/report/plain.py b/src/modules/report/plain.py index 0464e6e..e6cbf5e 100644 --- a/src/modules/report/plain.py +++ b/src/modules/report/plain.py @@ -17,14 +17,19 @@ class PlainReporter(object): output = "" services_lock.acquire() if len(services): + services_lock.release() output += self.nodes_table() if not config.mapping: output += self.services_table() + vulnerabilities_lock.acquire() if len(vulnerabilities): + vulnerabilities_lock.release() output += self.vulns_table() else: + vulnerabilities_lock.release() output += "\nNo vulnerabilities were found" else: + services_lock.release() print("\nKube Hunter couldn't find any clusters") services_lock.release() # print("\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "")) @@ -45,8 +50,9 @@ class PlainReporter(object): if service.event_id not in id_memory: nodes_table.add_row(["Node/Master", service.host]) id_memory.append(service.event_id) - services_lock.acquire() - return "\nNodes\n{}\n".format(nodes_table) + nodes_ret = "\nNodes\n{}\n".format(nodes_table) + services_lock.release() + return nodes_ret def services_table(self): services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) @@ -59,8 +65,9 @@ class PlainReporter(object): services_lock.acquire() for service in services: services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) + detected_services_ret = "\nDetected Services\n{}\n".format(services_table) services_lock.release() - return "\nDetected Services\n{}\n".format(services_table) + return detected_services_ret def vulns_table(self): column_names = ["Location", "Category", "Vulnerability", "Description", "Evidence"] From 655467c27abdd34457b20f0186d906fe25411fac Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 12:53:31 +0300 Subject: [PATCH 31/44] Solved hidden exception that stopped program normal run. --- src/core/events/handler.py | 2 -- src/modules/report/plain.py | 1 - 2 files changed, 3 deletions(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 017cb0f..476ea48 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -76,8 +76,6 @@ class EventQueue(Queue, object): try: hook.execute() except Exception as ex: - import traceback - traceback.print_exc() logging.debug(ex.message) self.task_done() logging.debug("closing thread...") diff --git a/src/modules/report/plain.py b/src/modules/report/plain.py index e6cbf5e..c474656 100644 --- a/src/modules/report/plain.py +++ b/src/modules/report/plain.py @@ -31,7 +31,6 @@ class PlainReporter(object): else: services_lock.release() print("\nKube Hunter couldn't find any clusters") - services_lock.release() # print("\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "")) return output From c4a81ca6f999c3ab50653586af2e386195761a36 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 14:24:07 +0300 Subject: [PATCH 32/44] Added proper exception handling --- src/modules/hunting/etcd.py | 44 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 149970d..9c6188a 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -50,10 +50,13 @@ class EtcdRemoteAccessActive(ActiveHunter): data = { 'value': 'remotely written data' } - r = "{protocol}://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379, protocol=self.protocol, - data=data) - self.write_evidence = r.content if r.status_code == 200 and r.content != '' else False - return self.write_evidence + try: + r = requests.post("{protocol}://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379, + protocol=self.protocol), data=data) + self.write_evidence = r.content if r.status_code == 200 and r.content != '' else False + return self.write_evidence + except requests.exceptions.ConnectionError: + return False def execute(self): if self.db_keys_write_access(): @@ -75,24 +78,33 @@ class EtcdRemoteAccess(Hunter): def db_keys_disclosure(self): logging.debug(self.event.host + " Passive hunter is attempting to read etcd keys remotely") - r = requests.get( - "{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379), - verify=False) - self.keys_evidence = r.content if r.status_code == 200 and r.content != '' else False - return self.version_evidence + try: + r = requests.get( + "{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379), + verify=False) + self.keys_evidence = r.content if r.status_code == 200 and r.content != '' else False + return self.version_evidence + except requests.exceptions.ConnectionError: + return False def version_disclosure(self): logging.debug(self.event.host + " Passive hunter is attempting to check etcd version remotely") - r = requests.get( - "{protocol}://{host}:{port}/version".format(protocol=self.protocol, host=self.event.host, port=2379), - verify=False) - self.version_evidence = r.content if r.status_code == 200 and r.content != '' else False - return self.version_evidence + try: + r = requests.get( + "{protocol}://{host}:{port}/version".format(protocol=self.protocol, host=self.event.host, port=2379), + verify=False) + self.version_evidence = r.content if r.status_code == 200 and r.content != '' else False + return self.version_evidence + except requests.exceptions.ConnectionError: + return False def unauthorized_access(self): logging.debug(self.event.host + " Passive hunter is attempting to access etcd without authorization") - r = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), verify=False) - return r.content if r.status_code == 200 and r.content != '' else False + try: + r = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), verify=False) + return r.content if r.status_code == 200 and r.content != '' else False + except requests.exceptions.ConnectionError: + return False def execute(self): if self.unauthorized_access(): # decide between http and https protocol From 73e533624a3b72316e3efcee22c9d5b82f520f86 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 14:57:27 +0300 Subject: [PATCH 33/44] Fixed all PR comments! & improved commenting & indenting --- src/modules/hunting/etcd.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 9c6188a..1461a72 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -1,11 +1,11 @@ -import json import logging import requests from ...core.events import handler from ...core.events.types import Vulnerability, Event, OpenPortEvent -from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure, RemoteCodeExec, UnauthenticatedAccess, AccessRisk +from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure, RemoteCodeExec, \ + UnauthenticatedAccess, AccessRisk """ Vulnerabilities """ class EtcdRemoteWriteAccessEvent(Vulnerability, Event): @@ -26,20 +26,24 @@ class EtcdRemoteVersionDisclosureEvent(Vulnerability, Event): """Remote version disclosure might give an attacker a valuable data to attack a cluster""" def __init__(self, version): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure", category=InformationDisclosure) + Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure", + category=InformationDisclosure) self.evidence = version class EtcdAccessEnabledWithoutAuthEvent(Vulnerability, Event): - """Etcd is accessible using HTTP (without authorization and authentication), it would allow a potential attacker to gain access to the etcd""" + """Etcd is accessible using HTTP (without authorization and authentication), it would allow a potential attacker to + gain access to the etcd""" def __init__(self, version): - Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization", category=UnauthenticatedAccess) + Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible using insecure connection (HTTP)", + category=UnauthenticatedAccess) self.evidence = version # Active Hunter @handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379) class EtcdRemoteAccessActive(ActiveHunter): - """Checks for remote write access to etcd""" + """Etcd Remote Access + Checks for remote write access to etcd""" def __init__(self, event): self.event = event @@ -98,8 +102,8 @@ class EtcdRemoteAccess(Hunter): except requests.exceptions.ConnectionError: return False - def unauthorized_access(self): - logging.debug(self.event.host + " Passive hunter is attempting to access etcd without authorization") + def insecure_access(self): + logging.debug(self.event.host + " Passive hunter is attempting to access etcd insecurely") try: r = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), verify=False) return r.content if r.status_code == 200 and r.content != '' else False @@ -107,11 +111,11 @@ class EtcdRemoteAccess(Hunter): return False def execute(self): - if self.unauthorized_access(): # decide between http and https protocol + if self.insecure_access(): # make a decision between http and https protocol self.protocol = 'http' if self.version_disclosure(): self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence)) - if self.protocol == 'http' and self.unauthorized_access(): + if self.protocol == 'http': self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence)) if self.db_keys_disclosure(): self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence)) From 26f3878f7c18157ca139b28fe555af26268abc54 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 15:28:18 +0300 Subject: [PATCH 34/44] Using stricter locking "policy" --- kube-hunter.py | 4 +++- src/core/events/handler.py | 4 +++- src/core/events/types/common.py | 2 ++ src/modules/report/collector.py | 7 ++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/kube-hunter.py b/kube-hunter.py index afea10b..6099921 100755 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -99,7 +99,9 @@ hunt_started_lock.release() def main(): - global hunt_started + hunt_started_lock.acquire() + global hunt_started + hunt_started_lock.release() scan_options = [ config.pod, config.cidr, diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 476ea48..012415a 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -91,5 +91,7 @@ class EventQueue(Queue, object): self.running = False with self.mutex: self.queue.clear() - + + handler = EventQueue(800) + diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index 1822f12..efb778c 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -77,7 +77,9 @@ event_id_count_lock.release() class NewHostEvent(Event): def __init__(self, host, cloud=None): + event_id_count_lock.acquire() global event_id_count + event_id_count_lock.release() self.host = host self.cloud = cloud event_id_count_lock.acquire() diff --git a/src/modules/report/collector.py b/src/modules/report/collector.py index fd41bd2..c816039 100644 --- a/src/modules/report/collector.py +++ b/src/modules/report/collector.py @@ -48,7 +48,12 @@ class Collector(object): def execute(self): """function is called only when collecting data""" - global services, vulnerabilities + services_lock.acquire() + global services + services_lock.release() + vulnerabilities_lock.acquire() + global vulnerabilities + vulnerabilities_lock.release() bases = self.event.__class__.__mro__ if Service in bases: services_lock.acquire() From 8ede3b4f747faf2494392493034fc6c0bb5bcb0e Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 15:54:51 +0300 Subject: [PATCH 35/44] return self.version_evidence --> return self.keys_evidence --- src/modules/hunting/etcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/hunting/etcd.py b/src/modules/hunting/etcd.py index 1461a72..a9d8e1a 100644 --- a/src/modules/hunting/etcd.py +++ b/src/modules/hunting/etcd.py @@ -87,7 +87,7 @@ class EtcdRemoteAccess(Hunter): "{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379), verify=False) self.keys_evidence = r.content if r.status_code == 200 and r.content != '' else False - return self.version_evidence + return self.keys_evidence except requests.exceptions.ConnectionError: return False From a109dd956b3a6e3cd609f2990ca51c9cc261bdc3 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 17:54:11 +0300 Subject: [PATCH 36/44] found another variable I think should be locked --- src/core/events/handler.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 012415a..662b65e 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -10,9 +10,10 @@ from __main__ import config from ..types import ActiveHunter, Hunter from ...core.events.types import HuntFinished -import threading -working_count = 0 +global is_running_lock +is_running_lock = Lock() + # Inherits Queue object, handles events asynchronously class EventQueue(Queue, object): @@ -22,7 +23,9 @@ class EventQueue(Queue, object): self.active_hunters = dict() self.hooks = defaultdict(list) + is_running_lock.acquire() self.running = True + is_running_lock.release() self.workers = list() for i in range(num_worker): @@ -34,14 +37,14 @@ class EventQueue(Queue, object): t.daemon = True t.start() - # decorator wrapping for easy subscription def subscribe(self, event, hook=None, predicate=None): def wrapper(hook): self.subscribe_event(event, hook=hook, predicate=predicate) return hook + return wrapper - + # getting uninstantiated event object def subscribe_event(self, event, hook=None, predicate=None): if ActiveHunter in hook.__mro__: @@ -71,6 +74,7 @@ class EventQueue(Queue, object): # executes callbacks on dedicated thread as a daemon def worker(self): + is_running_lock.acquire() while self.running: hook = self.get() try: @@ -78,17 +82,20 @@ class EventQueue(Queue, object): except Exception as ex: logging.debug(ex.message) self.task_done() + is_running_lock.release() logging.debug("closing thread...") def notifier(self): - time.sleep(2) + time.sleep(2) while self.unfinished_tasks > 0: logging.debug("{} tasks left".format(self.unfinished_tasks)) time.sleep(3) # stops execution of all daemons def free(self): + is_running_lock.acquire() self.running = False + is_running_lock.release() with self.mutex: self.queue.clear() From 6602821a80e1842f9e581a48989a5b2bc176d58e Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 20:21:27 +0300 Subject: [PATCH 37/44] DONE --- src/core/events/handler.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 662b65e..2ce090a 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -11,6 +11,9 @@ from ..types import ActiveHunter, Hunter from ...core.events.types import HuntFinished +global queue_lock +queue_lock = Lock() + global is_running_lock is_running_lock = Lock() @@ -23,9 +26,10 @@ class EventQueue(Queue, object): self.active_hunters = dict() self.hooks = defaultdict(list) - is_running_lock.acquire() + #is_running_lock.acquire() + self.running = True - is_running_lock.release() + #is_running_lock.release() self.workers = list() for i in range(num_worker): @@ -74,15 +78,15 @@ class EventQueue(Queue, object): # executes callbacks on dedicated thread as a daemon def worker(self): - is_running_lock.acquire() while self.running: + queue_lock.acquire() hook = self.get() + queue_lock.release() try: hook.execute() except Exception as ex: logging.debug(ex.message) self.task_done() - is_running_lock.release() logging.debug("closing thread...") def notifier(self): @@ -93,9 +97,9 @@ class EventQueue(Queue, object): # stops execution of all daemons def free(self): - is_running_lock.acquire() + #is_running_lock.acquire() self.running = False - is_running_lock.release() + #is_running_lock.release() with self.mutex: self.queue.clear() From 7e324c5374951fcb2838f628bdf79956598dbfc3 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Sun, 21 Oct 2018 10:47:48 +0300 Subject: [PATCH 38/44] Fixed safe to change PR comments. the next fixes of these PR would need to be tested --- kube-hunter.py | 9 ++------- src/core/events/handler.py | 8 -------- src/modules/report/yaml.py | 1 - 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/kube-hunter.py b/kube-hunter.py index 6099921..2d8548c 100755 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -141,10 +141,5 @@ def main(): if __name__ == '__main__': - for i in range(1): - try: - main() - except: - import traceback - print ('\n\n\n\n\n\n\n\n\n') - traceback.print_exc() + main() + diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 2ce090a..af5ddb1 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -14,9 +14,6 @@ from ...core.events.types import HuntFinished global queue_lock queue_lock = Lock() -global is_running_lock -is_running_lock = Lock() - # Inherits Queue object, handles events asynchronously class EventQueue(Queue, object): @@ -26,10 +23,7 @@ class EventQueue(Queue, object): self.active_hunters = dict() self.hooks = defaultdict(list) - #is_running_lock.acquire() - self.running = True - #is_running_lock.release() self.workers = list() for i in range(num_worker): @@ -97,9 +91,7 @@ class EventQueue(Queue, object): # stops execution of all daemons def free(self): - #is_running_lock.acquire() self.running = False - #is_running_lock.release() with self.mutex: self.queue.clear() diff --git a/src/modules/report/yaml.py b/src/modules/report/yaml.py index 80ae1fd..fe96af9 100644 --- a/src/modules/report/yaml.py +++ b/src/modules/report/yaml.py @@ -3,7 +3,6 @@ import StringIO from ruamel.yaml import YAML from collector import services, vulnerabilities, services_lock, vulnerabilities_lock -import threading class YAMLReporter(object): From 76692a36d76fe1b085b4b9915744017090e2b37e Mon Sep 17 00:00:00 2001 From: oriagmon Date: Sun, 21 Oct 2018 10:53:50 +0300 Subject: [PATCH 39/44] Finished with the left of the PR comments. --- kube-hunter.py | 4 ---- src/core/events/types/common.py | 2 -- src/modules/report/collector.py | 4 ---- src/modules/report/plain.py | 1 - 4 files changed, 11 deletions(-) diff --git a/kube-hunter.py b/kube-hunter.py index 2d8548c..cd43b88 100755 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -93,15 +93,11 @@ def list_hunters(): global hunt_started_lock hunt_started_lock = threading.Lock() -hunt_started_lock.acquire() hunt_started = False -hunt_started_lock.release() def main(): - hunt_started_lock.acquire() global hunt_started - hunt_started_lock.release() scan_options = [ config.pod, config.cidr, diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index efb778c..1822f12 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -77,9 +77,7 @@ event_id_count_lock.release() class NewHostEvent(Event): def __init__(self, host, cloud=None): - event_id_count_lock.acquire() global event_id_count - event_id_count_lock.release() self.host = host self.cloud = cloud event_id_count_lock.acquire() diff --git a/src/modules/report/collector.py b/src/modules/report/collector.py index c816039..a26ef6b 100644 --- a/src/modules/report/collector.py +++ b/src/modules/report/collector.py @@ -48,12 +48,8 @@ class Collector(object): def execute(self): """function is called only when collecting data""" - services_lock.acquire() global services - services_lock.release() - vulnerabilities_lock.acquire() global vulnerabilities - vulnerabilities_lock.release() bases = self.event.__class__.__mro__ if Service in bases: services_lock.acquire() diff --git a/src/modules/report/plain.py b/src/modules/report/plain.py index c474656..d9ff4cd 100644 --- a/src/modules/report/plain.py +++ b/src/modules/report/plain.py @@ -4,7 +4,6 @@ from prettytable import ALL, PrettyTable from __main__ import config from collector import services, vulnerabilities,services_lock, vulnerabilities_lock -import threading EVIDENCE_PREVIEW = 40 MAX_TABLE_WIDTH = 20 From a7e4ebf3693c315752c6b2be1413be9a9216387d Mon Sep 17 00:00:00 2001 From: oriagmon Date: Sun, 21 Oct 2018 11:47:15 +0300 Subject: [PATCH 40/44] Solved spacing conflict --- src/core/events/handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index af5ddb1..6f22a0a 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -96,5 +96,7 @@ class EventQueue(Queue, object): self.queue.clear() -handler = EventQueue(800) + + +handler = EventQueue(800) From 3b0d2233fd0ec6930db17497ac6f48518dce4872 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Sun, 21 Oct 2018 11:50:42 +0300 Subject: [PATCH 41/44] Solved spacing conflict --- src/core/events/handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 6f22a0a..13978e8 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -96,7 +96,4 @@ class EventQueue(Queue, object): self.queue.clear() - - - handler = EventQueue(800) From 812edafe80328ad2c6b1aafc63e235d496ce2fff Mon Sep 17 00:00:00 2001 From: oriagmon Date: Sun, 21 Oct 2018 11:51:44 +0300 Subject: [PATCH 42/44] Solved spacing conflict --- src/core/events/handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 13978e8..a3da29f 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -95,5 +95,4 @@ class EventQueue(Queue, object): with self.mutex: self.queue.clear() - handler = EventQueue(800) From 951efab5e05573de3e099042a750e34e5e741392 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Mon, 22 Oct 2018 11:39:53 +0300 Subject: [PATCH 43/44] Fixed PR comments. --- src/core/events/types/common.py | 2 -- src/modules/report/collector.py | 4 ---- src/modules/report/plain.py | 17 ++++++++++------- src/modules/report/yaml.py | 2 -- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index 1822f12..ffe60a4 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -68,9 +68,7 @@ class Vulnerability(object): global event_id_count_lock event_id_count_lock = threading.Lock() -event_id_count_lock.acquire() event_id_count = 0 -event_id_count_lock.release() """ Discovery/Hunting Events """ diff --git a/src/modules/report/collector.py b/src/modules/report/collector.py index a26ef6b..b23e454 100644 --- a/src/modules/report/collector.py +++ b/src/modules/report/collector.py @@ -8,15 +8,11 @@ import threading global services_lock services_lock = threading.Lock() -services_lock.acquire() services = list() -services_lock.release() global vulnerabilities_lock vulnerabilities_lock = threading.Lock() -vulnerabilities_lock.acquire() vulnerabilities = list() -vulnerabilities_lock.release() def console_trim(text, prefix=' '): diff --git a/src/modules/report/plain.py b/src/modules/report/plain.py index d9ff4cd..ca107f5 100644 --- a/src/modules/report/plain.py +++ b/src/modules/report/plain.py @@ -14,21 +14,24 @@ class PlainReporter(object): def get_report(self): """generates report tables""" output = "" + + vulnerabilities_lock.acquire() + vulnerabilities_len = len(services) + vulnerabilities_lock.release() + services_lock.acquire() - if len(services): - services_lock.release() + services_len = len(vulnerabilities) + services_lock.release() + + if services_len: output += self.nodes_table() if not config.mapping: output += self.services_table() - vulnerabilities_lock.acquire() - if len(vulnerabilities): - vulnerabilities_lock.release() + if vulnerabilities_len: output += self.vulns_table() else: - vulnerabilities_lock.release() output += "\nNo vulnerabilities were found" else: - services_lock.release() print("\nKube Hunter couldn't find any clusters") # print("\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "")) return output diff --git a/src/modules/report/yaml.py b/src/modules/report/yaml.py index fe96af9..824f621 100644 --- a/src/modules/report/yaml.py +++ b/src/modules/report/yaml.py @@ -8,13 +8,11 @@ from collector import services, vulnerabilities, services_lock, vulnerabilities_ class YAMLReporter(object): def get_report(self): yaml = YAML() - vulnerabilities_lock.acquire() report = { "nodes": self.get_nodes(), "services": self.get_services(), "vulnerabilities": self.get_vulenrabilities() } - vulnerabilities_lock.release() output = StringIO.StringIO() yaml.dump(report, output) return output.getvalue() From 6d73a2cde385e9510bab9a23c1a186f0cc8dc8e6 Mon Sep 17 00:00:00 2001 From: Liz Rice Date: Tue, 6 Nov 2018 10:32:44 +0000 Subject: [PATCH 44/44] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b6de13..7124bc6 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ There are three methods for deploying kube-hunter: You can run the kube-hunter python code directly on your machine. #### Prerequisites -You will need the following installed and assumed for path: +You will need the following installed: * python 2.7 * pip @@ -79,7 +79,8 @@ Install module dependencies: ~~~ cd ./kube-hunter pip install -r requirements.txt -In the case you have python 3.x in the path as your default, use "python2 -m pip install -r requirements.txt" + +In the case where you have python 3.x in the path as your default, and python2 refers to a python 2.7 executable, use "python2 -m pip install -r requirements.txt" ~~~ Run: `./kube-hunter.py`