From 628c2142155884f8bfb40399302e02c8a62f98c1 Mon Sep 17 00:00:00 2001 From: "ori.agmon" Date: Wed, 26 Sep 2018 18:42:06 +0300 Subject: [PATCH 01/30] 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 02/30] 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 03/30] 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 04/30] 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 05/30] 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 06/30] 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 07/30] 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 08/30] 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 09/30] 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 10/30] 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 11/30] 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 12/30] 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 13/30] 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 14/30] 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 15/30] 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 16/30] 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 17/30] 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 18/30] 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 19/30] 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 20/30] 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 21/30] 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 22/30] 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 23/30] 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 24/30] 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 25/30] 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 26/30] 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 27/30] 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 c4a81ca6f999c3ab50653586af2e386195761a36 Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 14:24:07 +0300 Subject: [PATCH 28/30] 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 29/30] 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 8ede3b4f747faf2494392493034fc6c0bb5bcb0e Mon Sep 17 00:00:00 2001 From: oriagmon Date: Wed, 17 Oct 2018 15:54:51 +0300 Subject: [PATCH 30/30] 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