diff --git a/hunting/kubelet.py b/hunting/kubelet.py deleted file mode 100644 index c207c4b..0000000 --- a/hunting/kubelet.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging - -from events import handler, ReadOnlyKubeletEvent, SecureKubeletEvent - -""" dividing ports for seperate hunters """ -@handler.subscribe(ReadOnlyKubeletEvent) -class ReadOnlyKubeletPortHunter(object): - def __init__(self, event): - self.event = event - - def execute(self): - logging.info("[OPEN SERVICE] INSECURED KUBELET API - {}:{}".format(self.event.host, self.event.port)) - -@handler.subscribe(SecureKubeletEvent) -class SecurePortKubeletHunter(object): - def __init__(self, event): - self.event = event - - def execute(self): - logging.info("[OPEN SERVICE] SECURED KUBELET API - {}:{}".format(self.event.host, self.event.port)) \ No newline at end of file diff --git a/kube-hunter.py b/kube-hunter.py index 6664d3c..9e85e30 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -1,12 +1,17 @@ #!/bin/env python +import logging +import sys +import time + import log -from events import handler, HostScanEvent -from discovery import HostDiscovery -import hunting -import time -import sys -import logging +# executes all registrations from sub packages +import modules + +from modules.discovery import HostDiscovery +from modules.events import handler +from modules.events.types import HostScanEvent + def main(): logging.info("Started") @@ -22,4 +27,4 @@ def main(): logging.debug("Cleaned Queue") if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/log/__init__.py b/log/__init__.py index 635cf56..4b6d874 100644 --- a/log/__init__.py +++ b/log/__init__.py @@ -1 +1,2 @@ -from config import * \ No newline at end of file +from config import * +from reporter import * \ No newline at end of file diff --git a/log/reporter.py b/log/reporter.py new file mode 100644 index 0000000..7c54e9f --- /dev/null +++ b/log/reporter.py @@ -0,0 +1,27 @@ +import logging +from modules.events import handler +from modules.events.types import Vulnerability, ServiceEvent + +@handler.subscribe(Vulnerability) +class VulnerabilityReport(object): + def __init__(self, event): + self.event = event + + def execute(self): + vulnerability_type = self.event.__class__.__name__.replace("Vulnerability", "") + logging.info("[VULNERABILITY - {type}] - {desc} | location: {host}:{port}".format(type=vulnerability_type, + desc=self.event.desc, + host=self.event.host, + port=self.event.port)) + +@handler.subscribe(ServiceEvent) +class OpenServiceReport(object): + def __init__(self, event): + self.event = event + + def execute(self): + service_name = self.event.__class__.__name__.replace("Event", "") + logging.info("[OPEN SERVICE - {name}] location: {host}:{port}".format(name=service_name, + desc=self.event.desc, + host=self.event.host, + port=self.event.port)) \ No newline at end of file diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..172cc6b --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,4 @@ +import discovery +import hunting +import events +import types \ No newline at end of file diff --git a/discovery/__init__.py b/modules/discovery/__init__.py similarity index 100% rename from discovery/__init__.py rename to modules/discovery/__init__.py diff --git a/discovery/dashboard.py b/modules/discovery/dashboard.py similarity index 65% rename from discovery/dashboard.py rename to modules/discovery/dashboard.py index beb4389..cd4b9cf 100644 --- a/discovery/dashboard.py +++ b/modules/discovery/dashboard.py @@ -1,8 +1,13 @@ -from events import handler, OpenPortEvent, KubeDashboardEvent +from ..types import Hunter + import requests +from ..events import handler +from ..events.types import KubeDashboardEvent, OpenPortEvent + + @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 30000) -class KubeDashboard(object): +class KubeDashboard(Hunter): def __init__(self, event): self.event = event self.host = event.host @@ -14,4 +19,4 @@ class KubeDashboard(object): return False def execute(self): - handler.publish_event(KubeDashboardEvent()) + self.publish_event(KubeDashboardEvent()) diff --git a/discovery/hosts.py b/modules/discovery/hosts.py similarity index 65% rename from discovery/hosts.py rename to modules/discovery/hosts.py index e151b76..e980265 100644 --- a/discovery/hosts.py +++ b/modules/discovery/hosts.py @@ -2,18 +2,21 @@ import logging import sys import time from enum import Enum +from ..types import Hunter from netaddr import IPNetwork -from events import HostScanEvent, NewHostEvent, handler +from ..events import handler +from ..events.types import HostScanEvent, NewHostEvent from netifaces import AF_INET, ifaddresses, interfaces + # for comparing prefixes class InterfaceTypes(Enum): LOCALHOST = "127.0.0" @handler.subscribe(HostScanEvent) -class HostDiscovery(object): +class HostDiscovery(Hunter): def __init__(self, event): self.event = event # self.external = event.external @@ -21,10 +24,10 @@ class HostDiscovery(object): def execute(self): logging.info("Discovering Open Kubernetes Services...") - handler.publish_event(NewHostEvent(host="acs954agent1.westus2.cloudapp.azure.com")) # test cluster - for ifaceName in interfaces(): - for ip in self.generate_addresses(ifaceName): - handler.publish_event(NewHostEvent(host=ip)) + self.publish_event(NewHostEvent(host="acs954agent1.westus2.cloudapp.azure.com")) # test cluster + # for ifaceName in interfaces(): + # for ip in self.generate_addresses(ifaceName): + # handler.publish_event(NewHostEvent(host=ip)) def generate_addresses(self, ifaceName): for address in [i['addr'] for i in ifaddresses(ifaceName).setdefault(AF_INET, [])]: @@ -32,4 +35,4 @@ class HostDiscovery(object): for ip in IPNetwork(subnet): if not self.event.localhost and InterfaceTypes.LOCALHOST.value in ip.__str__(): continue - yield ip + yield ip \ No newline at end of file diff --git a/discovery/kubelet.py b/modules/discovery/kubelet.py similarity index 69% rename from discovery/kubelet.py rename to modules/discovery/kubelet.py index cb4e91a..e731440 100644 --- a/discovery/kubelet.py +++ b/modules/discovery/kubelet.py @@ -1,24 +1,30 @@ import json import logging -import urllib3 from enum import Enum +from ..types import Hunter import requests +import urllib3 + +from ..events import handler +from ..events.types import (OpenPortEvent, ReadOnlyKubeletEvent, + SecureKubeletEvent) + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -from events import ReadOnlyKubeletEvent, SecureKubeletEvent, OpenPortEvent, handler class KubeletPorts(Enum): SECURED = 10250 READ_ONLY = 10255 @handler.subscribe(OpenPortEvent, predicate= lambda x: x.port == 10255 or x.port == 10250) -class KubeletDiscovery(object): +class KubeletDiscovery(Hunter): def __init__(self, event): - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) self.event = event @property def read_only_access(self): + logging.debug(self.event.host) r = requests.get("http://{host}:{port}/pods".format(host=self.event.host, port=self.event.port)) return r.status_code == 200 @@ -30,6 +36,6 @@ class KubeletDiscovery(object): def execute(self): logging.debug("secure port on {}".format(self.event.port)) if self.event.port == KubeletPorts.SECURED.value and self.secure_access: - handler.publish_event(SecureKubeletEvent()) + self.publish_event(SecureKubeletEvent()) elif self.event.port == KubeletPorts.READ_ONLY.value and self.read_only_access: - handler.publish_event(ReadOnlyKubeletEvent()) \ No newline at end of file + self.publish_event(ReadOnlyKubeletEvent()) \ No newline at end of file diff --git a/discovery/ports.py b/modules/discovery/ports.py similarity index 73% rename from discovery/ports.py rename to modules/discovery/ports.py index ee50985..7e8ae13 100644 --- a/discovery/ports.py +++ b/modules/discovery/ports.py @@ -1,10 +1,14 @@ -from events import handler, NewHostEvent, OpenPortEvent from socket import socket +from ..types import Hunter + +from ..events import handler +from ..events.types import NewHostEvent, OpenPortEvent + default_ports = [8001, 10250, 10255, 30000] @handler.subscribe(NewHostEvent) -class PortDiscovery(object): +class PortDiscovery(Hunter): def __init__(self, event): self.event = event self.host = event.host @@ -13,7 +17,7 @@ class PortDiscovery(object): def execute(self): for single_port in default_ports: if self.test_connection(self.host, single_port): - handler.publish_event(OpenPortEvent(port=single_port)) + self.publish_event(OpenPortEvent(port=single_port)) @staticmethod def test_connection(host, port): diff --git a/discovery/proxy.py b/modules/discovery/proxy.py similarity index 66% rename from discovery/proxy.py rename to modules/discovery/proxy.py index 66fc0f8..db40b1b 100644 --- a/discovery/proxy.py +++ b/modules/discovery/proxy.py @@ -1,10 +1,15 @@ -from events import handler, OpenPortEvent, KubeProxyEvent -from collections import defaultdict -from requests import get import logging +from collections import defaultdict +from ..types import Hunter + +from requests import get + +from ..events import handler +from ..events.types import KubeProxyEvent, OpenPortEvent + @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 8001) -class KubeProxy(object): +class KubeProxy(Hunter): def __init__(self, event): self.event = event self.host = event.host @@ -16,5 +21,4 @@ class KubeProxy(object): def execute(self): if self.accesible: - handler.publish_event(KubeProxyEvent()) - \ No newline at end of file + self.publish_event(KubeProxyEvent()) diff --git a/modules/events/__init__.py b/modules/events/__init__.py new file mode 100644 index 0000000..1dbbdff --- /dev/null +++ b/modules/events/__init__.py @@ -0,0 +1,2 @@ +from handler import * +import types \ No newline at end of file diff --git a/events/handler.py b/modules/events/handler.py similarity index 59% rename from events/handler.py rename to modules/events/handler.py index d9275e7..da987b9 100644 --- a/events/handler.py +++ b/modules/events/handler.py @@ -26,26 +26,22 @@ class EventQueue(Queue, object): # getting uninstantiated event object def subscribe_event(self, event, hook=None, predicate=None): - logging.debug('{} subscribed to {}'.format(event.__name__, hook)) - if hook not in self.hooks[event.__name__]: - self.hooks[event.__name__].append((hook, predicate)) + logging.debug('{} subscribed to {}'.format(event, hook)) + if hook not in self.hooks[event]: + self.hooks[event].append((hook, predicate)) # getting instantiated event object - def publish_event(self, event): - logging.debug('Event {} got published with {}'.format(event.__class__.__name__, event)) - event_name = event.__class__.__name__ - if event_name in self.hooks: - for hook, predicate in self.hooks[event_name]: - if predicate and not predicate(event): - continue + def publish_event(self, event, caller=None): + logging.debug('Event {} got published with {}'.format(event.__class__, event)) + for hooked_event in self.hooks.keys(): + if hooked_event in event.__class__.__mro__: + for hook, predicate in self.hooks[hooked_event]: + if predicate and not predicate(event): + continue - # access to stack frame, can also be implemented by changing the function call to recieve self. - # TODO: decide whether invisibility to the developer is the best approach - last_frame = inspect.stack()[1][0] - if "self" in last_frame.f_locals: - event.previous = last_frame.f_locals["self"].event - - self.put(hook(event)) + if caller: + event.previous = caller.event + self.put(hook(event)) # executes callbacks on dedicated thread as a daemon def worker(self): @@ -53,6 +49,7 @@ class EventQueue(Queue, object): hook = self.get() hook.execute() self.task_done() + logging.debug("closing thread...") # stops execution of all daemons def free(self): @@ -62,4 +59,3 @@ class EventQueue(Queue, object): handler = EventQueue(800) - diff --git a/events/__init__.py b/modules/events/types/__init__.py similarity index 82% rename from events/__init__.py rename to modules/events/types/__init__.py index 498da7f..a01c21a 100644 --- a/events/__init__.py +++ b/modules/events/types/__init__.py @@ -1,8 +1,8 @@ from os.path import dirname, basename, isfile import glob -# explicitly importing the event handler -from handler import handler +from common import * +from information import * # dynamically importing all modules in folder files = glob.glob(dirname(__file__)+"/*.py") diff --git a/events/default_types.py b/modules/events/types/common.py similarity index 82% rename from events/default_types.py rename to modules/events/types/common.py index 88bdad6..323c7a3 100644 --- a/events/default_types.py +++ b/modules/events/types/common.py @@ -21,8 +21,11 @@ class Event(object): previous = previous.previous return history +class ServiceEvent(object): + pass -""" Event Objects """ + +""" Discovery/Hunting Events """ class NewHostEvent(Event): def __init__(self, host): self.host = host @@ -42,20 +45,20 @@ class HostScanEvent(Event): self.internal = interal self.localhost = localhost -class KubeDashboardEvent(Event): +class KubeDashboardEvent(Event, ServiceEvent): def __init__(self, path="/", secure=False): self.path = path self.secure pass -class ReadOnlyKubeletEvent(Event): +class ReadOnlyKubeletEvent(Event, ServiceEvent): def __init__(self): pass -class SecureKubeletEvent(Event): +class SecureKubeletEvent(Event, ServiceEvent): def __init__(self): pass -class KubeProxyEvent(Event): +class KubeProxyEvent(Event, ServiceEvent): def __init__(self): - pass + pass \ No newline at end of file diff --git a/modules/events/types/information.py b/modules/events/types/information.py new file mode 100644 index 0000000..f3d6013 --- /dev/null +++ b/modules/events/types/information.py @@ -0,0 +1,11 @@ +from defaults import Event + +class Vulnerability(object): + """ Information Events """ + # this kind of events will be triggered when important information is discovered + def __init__(self, desc): + self.desc = desc + +class KubeletVulnerability(Vulnerability, Event): + def __init__(self, **kargs): + super(KubeletVulnerability, self).__init__(**kargs) \ No newline at end of file diff --git a/hunting/__init__.py b/modules/hunting/__init__.py similarity index 100% rename from hunting/__init__.py rename to modules/hunting/__init__.py diff --git a/hunting/dashboard.py b/modules/hunting/dashboard.py similarity index 82% rename from hunting/dashboard.py rename to modules/hunting/dashboard.py index 28cb360..9fc470a 100644 --- a/hunting/dashboard.py +++ b/modules/hunting/dashboard.py @@ -1,9 +1,14 @@ -from events import handler, KubeDashboardEvent import logging +from ..types import Hunter + import requests +from ..events import handler +from ..events.types import KubeDashboardEvent + + @handler.subscribe(KubeDashboardEvent) -class KubeDashboard(object): +class KubeDashboard(Hunter): def __init__(self, event): self.event = event @@ -17,7 +22,7 @@ class KubeDashboard(object): if not self.accessible: return - if self.event.secure: + if self.event.secure: logging.info("[OPEN SERVICE] SECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path)) else: logging.info("[OPEN SERVICE] INSECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path)) diff --git a/modules/hunting/kubelet.py b/modules/hunting/kubelet.py new file mode 100644 index 0000000..091f9ea --- /dev/null +++ b/modules/hunting/kubelet.py @@ -0,0 +1,57 @@ +import json +import logging +from ..types import Hunter + +import requests +import urllib3 + +from ..events import handler +from ..events.types import ReadOnlyKubeletEvent, SecureKubeletEvent, KubeletVulnerability + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +""" dividing ports for seperate hunters """ +@handler.subscribe(ReadOnlyKubeletEvent) +class ReadOnlyKubeletPortHunter(Hunter): + def __init__(self, event): + self.event = event + + def execute(self): + pass + +@handler.subscribe(SecureKubeletEvent) +class SecurePortKubeletHunter(Hunter): + def __init__(self, event): + self.event = event + + def execute(self): + self.check_debug_handlers() + + def check_debug_handlers(self): + pod, container = self.get_kubesystem_pod_container() + if self.exec_handler(pod, container): + self.publish_event(KubeletVulnerability(desc="exec enabled")) + + def get_kubesystem_pod_container(self): + pods_data = json.loads(requests.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False).text)['items'] + # filter running kubesystem pod + kubesystem_pod = lambda pod: pod["metadata"]["namespace"] == "kube-system" and pod["status"]["phase"] == "Running" + pod_data = (pod_data for pod_data in pods_data if kubesystem_pod(pod_data)).next() + + container_data = (container_data for container_data in pod_data["spec"]["containers"]).next() + return pod_data["metadata"]["name"], container_data["name"] + + # returns true if successfull + def exec_handler(self, pod, container): + headers = { + "X-Stream-Protocol-Version": "v2.channel.k8s.io", + } + exec_url = "https://{host}:10250/exec/{pod_ns}/{pod}/{cont_name}?command={cmd}&input=1&output=1&tty=1".format( + host = self.event.host, + pod_ns = "kube-system", + pod = pod, + cont_name = container, + cmd = "uname" + ) + stream = requests.post(url=exec_url, headers=headers, verify=False, allow_redirects=False).headers.get("location", default=None) + return bool(stream) diff --git a/hunting/proxy.py b/modules/hunting/proxy.py similarity index 85% rename from hunting/proxy.py rename to modules/hunting/proxy.py index b67d17d..c4e31de 100644 --- a/hunting/proxy.py +++ b/modules/hunting/proxy.py @@ -1,13 +1,16 @@ from enum import Enum -from requests import get -from events import KubeDashboardEvent, KubeProxyEvent, handler +from ..types import Hunter +from requests import get + +from ..events import handler +from ..events.types import KubeDashboardEvent, KubeProxyEvent class Service(Enum): DASHBOARD = "kubernetes-dashboard" @handler.subscribe(KubeProxyEvent) -class KubeProxy(object): +class KubeProxy(Hunter): def __init__(self, event): self.event = event self.api_url = "http://{host}:{port}/api/v1".format(host=self.event.host, port=self.event.port) @@ -17,7 +20,7 @@ class KubeProxy(object): for service in services: curr_path = "api/v1/namespaces/{ns}/services/{sv}/proxy".format(ns=namespace,sv=service) # TODO: check if /proxy is a convention on other services if service == Service.DASHBOARD.value: - handler.publish_event(KubeDashboardEvent(path=curr_path, secure=False)) + self.publish_event(KubeDashboardEvent(path=curr_path, secure=False)) @property def namespaces(self): diff --git a/modules/types/__init__.py b/modules/types/__init__.py new file mode 100644 index 0000000..18d9662 --- /dev/null +++ b/modules/types/__init__.py @@ -0,0 +1 @@ +from defaults import * \ No newline at end of file diff --git a/modules/types/defaults.py b/modules/types/defaults.py new file mode 100644 index 0000000..8475622 --- /dev/null +++ b/modules/types/defaults.py @@ -0,0 +1,8 @@ +from ..events import handler + +class Hunter(object): + def __init__(self): + pass + + def publish_event(self, event): + handler.publish_event(event, caller=self)