From 290f87de70c7806033cf382af9a9ca5fa03e1297 Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Thu, 24 May 2018 15:25:43 +0300 Subject: [PATCH] 1. added log/ 2. Started adding kubelet scanning. 3. Changed events architecture. All events are inheriting from "Event" class. when instantiating and defining a new event class, attributes other than what is important for that perticular event are not needed. the event handler will be stacking the events, so that each event will have all the attributes of its successors. This proccess is invisible to the developer, but needs to be acknowledged. *note: from now on, all executors needs to set self.event to given arg on init* Example (pseudo): @subscribe(NewHostEvent) def PortScan(event): publish(OpenPortEvent(port="8080")) @subscribe(OpenPortEvent) def print(event): print(event.host) publish(NewHostEvent(host="0.0.0.0")) >> output: 0.0.0.0 the print function recieves an open port event. even though when publishing the OpenPortEvent we did not specify a host, the print function can access the "host" attribute, as the OpenPortEvent successor was NewHostEvent. if "host" was not defined on the succesors, it is "None" --- discovery/dashboard.py | 10 +++--- discovery/hosts.py | 37 ++++++++++++++----- discovery/kubelet.py | 35 ++++++++++++++++++ discovery/ports.py | 9 ++--- discovery/proxy.py | 10 +++--- events/default_types.py | 78 ++++++++++++++++++++++++++--------------- events/handler.py | 18 +++++++--- hunting/dashboard.py | 15 ++++---- hunting/kubelet.py | 20 +++++++++++ hunting/proxy.py | 8 ++--- kube-hunter.py | 6 ++-- log/__init__.py | 1 + log.py => log/config.py | 1 - 13 files changed, 175 insertions(+), 73 deletions(-) create mode 100644 discovery/kubelet.py create mode 100644 hunting/kubelet.py create mode 100644 log/__init__.py rename log.py => log/config.py (99%) diff --git a/discovery/dashboard.py b/discovery/dashboard.py index 2a2cd5e..beb4389 100644 --- a/discovery/dashboard.py +++ b/discovery/dashboard.py @@ -3,10 +3,10 @@ import requests @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 30000) class KubeDashboard(object): - def __init__(self, task): - self.task = task - self.host = task.host - self.port = task.port + def __init__(self, event): + self.event = event + self.host = event.host + self.port = event.port @property def secure(self): @@ -14,4 +14,4 @@ class KubeDashboard(object): return False def execute(self): - handler.publish_event(KubeDashboardEvent(host=self.host, port=self.port, secure=self.secure)) + handler.publish_event(KubeDashboardEvent()) diff --git a/discovery/hosts.py b/discovery/hosts.py index b024694..e151b76 100644 --- a/discovery/hosts.py +++ b/discovery/hosts.py @@ -1,16 +1,35 @@ -from netifaces import interfaces, ifaddresses, AF_INET +import logging +import sys +import time +from enum import Enum + from netaddr import IPNetwork -import events +from events import HostScanEvent, NewHostEvent, handler +from netifaces import AF_INET, ifaddresses, interfaces +# for comparing prefixes +class InterfaceTypes(Enum): + LOCALHOST = "127.0.0" + +@handler.subscribe(HostScanEvent) class HostDiscovery(object): - def __init__(self, task): - pass + def __init__(self, event): + self.event = event + # self.external = event.external 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(): - addresses = [i['addr'] for i in ifaddresses(ifaceName).setdefault(AF_INET, [])] - if addresses: - subnet = IPNetwork('{0}/24'.format(addresses[0])) - for single_ip in IPNetwork(subnet): - events.handler.publish_event(events.NewHostEvent(host=single_ip)) \ No newline at end of file + 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, [])]: + subnet = IPNetwork('{0}/24'.format(address)) + for ip in IPNetwork(subnet): + if not self.event.localhost and InterfaceTypes.LOCALHOST.value in ip.__str__(): + continue + yield ip diff --git a/discovery/kubelet.py b/discovery/kubelet.py new file mode 100644 index 0000000..cb4e91a --- /dev/null +++ b/discovery/kubelet.py @@ -0,0 +1,35 @@ +import json +import logging +import urllib3 +from enum import Enum + +import requests + +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): + def __init__(self, event): + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + self.event = event + + @property + def read_only_access(self): + r = requests.get("http://{host}:{port}/pods".format(host=self.event.host, port=self.event.port)) + return r.status_code == 200 + + @property + def secure_access(self): + r = requests.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False) + return r.status_code == 200 + + 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()) + elif self.event.port == KubeletPorts.READ_ONLY.value and self.read_only_access: + handler.publish_event(ReadOnlyKubeletEvent()) \ No newline at end of file diff --git a/discovery/ports.py b/discovery/ports.py index 19acc09..ee50985 100644 --- a/discovery/ports.py +++ b/discovery/ports.py @@ -5,14 +5,15 @@ default_ports = [8001, 10250, 10255, 30000] @handler.subscribe(NewHostEvent) class PortDiscovery(object): - def __init__(self, task): - self.host = task.host - self.port = task.port + def __init__(self, event): + self.event = event + self.host = event.host + self.port = event.port def execute(self): for single_port in default_ports: if self.test_connection(self.host, single_port): - handler.publish_event(OpenPortEvent(host=self.host, port=single_port)) + handler.publish_event(OpenPortEvent(port=single_port)) @staticmethod def test_connection(host, port): diff --git a/discovery/proxy.py b/discovery/proxy.py index bbcc5c6..66fc0f8 100644 --- a/discovery/proxy.py +++ b/discovery/proxy.py @@ -5,10 +5,10 @@ import logging @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 8001) class KubeProxy(object): - def __init__(self, task): - self.task = task - self.host = task.host - self.port = task.port or 8001 + def __init__(self, event): + self.event = event + self.host = event.host + self.port = event.port or 8001 @property def accesible(self): @@ -16,5 +16,5 @@ class KubeProxy(object): def execute(self): if self.accesible: - handler.publish_event(KubeProxyEvent(host=self.host, port=self.port)) + handler.publish_event(KubeProxyEvent()) \ No newline at end of file diff --git a/events/default_types.py b/events/default_types.py index e5076f4..88bdad6 100644 --- a/events/default_types.py +++ b/events/default_types.py @@ -1,41 +1,61 @@ -# class EventMeta(type): -# def __init__(cls, name, bases, dct): -# super(EventMeta, cls).__init__(name, bases, dct) +import logging -""" Parent Event Objects """ -class NetworkEvent(object): - # __metaclass__ = EventMeta - def __init__(self, host, port): - self.host = host - self.port = port +class Event(object): + def __init__(self): + self.previous = None + + # newest attribute gets selected first + def __getattr__(self, name): + if name == "previous": + return None + for event in self.history: + if name in event.__dict__: + return event.__dict__[name] + + # returns the event history ordered from newest to oldest + @property + def history(self): + previous, history = self.previous, list() + while previous: + history.append(previous) + previous = previous.previous + return history -class ServiceEvent(NetworkEvent): - # __metaclass__ = EventMeta - def __init__(self, secure, location, host, port): - super(ServiceEvent, self).__init__(host=host, port=port) - self.secure = secure - self.location = location """ Event Objects """ -class NewHostEvent(NetworkEvent): - def __init__(self, host, port=0): - super(NewHostEvent, self).__init__(port=port, host=host) +class NewHostEvent(Event): + def __init__(self, host): + self.host = host def __str__(self): return str(self.host) -class OpenPortEvent(NetworkEvent): - def __init__(self, host, port): - super(OpenPortEvent, self).__init__(port=port, host=host) - +class OpenPortEvent(Event): + def __init__(self, port): + self.port = port + def __str__(self): return str(self.port) -class KubeProxyEvent(ServiceEvent): - def __init__(self, host, port=8001, secure=True, location=""): - super(KubeProxyEvent, self).__init__(secure=secure, location=location, host=host, port=port) +class HostScanEvent(Event): + def __init__(self, interal=True, localhost=True): + self.internal = interal + self.localhost = localhost -class KubeDashboardEvent(ServiceEvent): - def __init__(self, host, secure=True, port=30000, location=""): - super(KubeDashboardEvent, self).__init__(location=location, secure=secure, host=host, port=port) - \ No newline at end of file +class KubeDashboardEvent(Event): + def __init__(self, path="/", secure=False): + self.path = path + self.secure + pass + +class ReadOnlyKubeletEvent(Event): + def __init__(self): + pass + +class SecureKubeletEvent(Event): + def __init__(self): + pass + +class KubeProxyEvent(Event): + def __init__(self): + pass diff --git a/events/handler.py b/events/handler.py index 408a3bf..d9275e7 100644 --- a/events/handler.py +++ b/events/handler.py @@ -1,9 +1,9 @@ +import inspect import logging -from Queue import Queue -from threading import Thread -from collections import defaultdict -from threading import Lock from abc import ABCMeta +from collections import defaultdict +from Queue import Queue +from threading import Lock, Thread # Inherits Queue object, handles events asynchronously class EventQueue(Queue, object): @@ -38,6 +38,13 @@ class EventQueue(Queue, object): for hook, predicate in self.hooks[event_name]: 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)) # executes callbacks on dedicated thread as a daemon @@ -54,4 +61,5 @@ class EventQueue(Queue, object): self.queue.clear() -handler = EventQueue(800) \ No newline at end of file +handler = EventQueue(800) + diff --git a/hunting/dashboard.py b/hunting/dashboard.py index a576f53..28cb360 100644 --- a/hunting/dashboard.py +++ b/hunting/dashboard.py @@ -4,21 +4,20 @@ import requests @handler.subscribe(KubeDashboardEvent) class KubeDashboard(object): - def __init__(self, task): - self.task = task + def __init__(self, event): + self.event = event @property def accessible(self): - protocol = "https" if self.task.secure else "http" - r = requests.get("{protocol}://{host}:{port}/{loc}".format(protocol=protocol, host=self.task.host, port=self.task.port, loc=self.task.location)) + protocol = "https" if self.event.secure else "http" + r = requests.get("{protocol}://{host}:{port}{loc}".format(protocol=protocol, host=self.event.host, port=self.event.port, loc=self.event.path)) return r.status_code == 200 def execute(self): if not self.accessible: return - if self.task.secure: - logging.info("SECURED DASHBOARD AT {}:{}/{}".format(self.task.host, self.task.port, self.task.location)) + if self.event.secure: + logging.info("[OPEN SERVICE] SECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path)) else: - logging.info("INSECURED DASHBOARD AT {}:{}/{}".format(self.task.host, self.task.port, self.task.location)) - + logging.info("[OPEN SERVICE] INSECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path)) diff --git a/hunting/kubelet.py b/hunting/kubelet.py new file mode 100644 index 0000000..c207c4b --- /dev/null +++ b/hunting/kubelet.py @@ -0,0 +1,20 @@ +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/hunting/proxy.py b/hunting/proxy.py index 4627ecc..b67d17d 100644 --- a/hunting/proxy.py +++ b/hunting/proxy.py @@ -8,16 +8,16 @@ class Service(Enum): @handler.subscribe(KubeProxyEvent) class KubeProxy(object): - def __init__(self, task): - self.task = task - self.api_url = "http://{host}:{port}/api/v1".format(host=self.task.host, port=self.task.port) + def __init__(self, event): + self.event = event + self.api_url = "http://{host}:{port}/api/v1".format(host=self.event.host, port=self.event.port) def execute(self): for namespace, services in self.services.items(): 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(host=self.task.host, port=self.task.port, location=curr_path, secure=False)) + handler.publish_event(KubeDashboardEvent(path=curr_path, secure=False)) @property def namespaces(self): diff --git a/kube-hunter.py b/kube-hunter.py index 47f917b..0c5fd17 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -1,7 +1,7 @@ import log -from events import handler -import discovery +from events import handler, HostScanEvent +from discovery import HostDiscovery import hunting import time import sys @@ -10,7 +10,7 @@ import logging def main(): logging.info("Started") try: - discovery.HostDiscovery({}).execute() + handler.publish_event(HostScanEvent(interal=True, localhost=False)) # Blocking to see discovery output while(True): time.sleep(100) diff --git a/log/__init__.py b/log/__init__.py new file mode 100644 index 0000000..635cf56 --- /dev/null +++ b/log/__init__.py @@ -0,0 +1 @@ +from config import * \ No newline at end of file diff --git a/log.py b/log/config.py similarity index 99% rename from log.py rename to log/config.py index 829d2fa..3c74159 100644 --- a/log.py +++ b/log/config.py @@ -5,7 +5,6 @@ parser = argparse.ArgumentParser(description='Kubehunter, hunting weak kubernete parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set output level, options are:\nDEBUG INFO WARNING") args = parser.parse_args() - try: loglevel = getattr(logging, args.log.upper()) except: