From a465c3f2ebb9150846c500322157135751bf37dd Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Sun, 27 May 2018 17:45:20 +0300 Subject: [PATCH] 1. Changed order of modules and pacakges in directories. 2. Changed method of hidden stacking of event, to send self as an argument, by inheriting from "Hunter" class. where the publish acts as a proxy to the handler. 3. Added new way of categorizing events, while added an option to subscribe to a father event. if en event gets publish, if its father event is hooked, the hook will be triggered 4. Added a reporter in log/ which listens to parent events, meanwhile Vulnerability and OpenService were added. all logging will be made from reporter from now on --- hunting/kubelet.py | 20 ------- kube-hunter.py | 19 ++++--- log/__init__.py | 3 +- log/reporter.py | 27 +++++++++ modules/__init__.py | 4 ++ {discovery => modules/discovery}/__init__.py | 0 {discovery => modules/discovery}/dashboard.py | 11 +++- {discovery => modules/discovery}/hosts.py | 17 +++--- {discovery => modules/discovery}/kubelet.py | 18 ++++-- {discovery => modules/discovery}/ports.py | 10 +++- {discovery => modules/discovery}/proxy.py | 16 ++++-- modules/events/__init__.py | 2 + {events => modules/events}/handler.py | 32 +++++------ {events => modules/events/types}/__init__.py | 4 +- .../events/types/common.py | 15 +++-- modules/events/types/information.py | 11 ++++ {hunting => modules/hunting}/__init__.py | 0 {hunting => modules/hunting}/dashboard.py | 11 +++- modules/hunting/kubelet.py | 57 +++++++++++++++++++ {hunting => modules/hunting}/proxy.py | 11 ++-- modules/types/__init__.py | 1 + modules/types/defaults.py | 8 +++ 22 files changed, 211 insertions(+), 86 deletions(-) delete mode 100644 hunting/kubelet.py create mode 100644 log/reporter.py create mode 100644 modules/__init__.py rename {discovery => modules/discovery}/__init__.py (100%) rename {discovery => modules/discovery}/dashboard.py (65%) rename {discovery => modules/discovery}/hosts.py (65%) rename {discovery => modules/discovery}/kubelet.py (69%) rename {discovery => modules/discovery}/ports.py (73%) rename {discovery => modules/discovery}/proxy.py (66%) create mode 100644 modules/events/__init__.py rename {events => modules/events}/handler.py (59%) rename {events => modules/events/types}/__init__.py (82%) rename events/default_types.py => modules/events/types/common.py (82%) create mode 100644 modules/events/types/information.py rename {hunting => modules/hunting}/__init__.py (100%) rename {hunting => modules/hunting}/dashboard.py (82%) create mode 100644 modules/hunting/kubelet.py rename {hunting => modules/hunting}/proxy.py (85%) create mode 100644 modules/types/__init__.py create mode 100644 modules/types/defaults.py 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)