From cd880ec50ee4304b24064800ce4e44c873a1b887 Mon Sep 17 00:00:00 2001 From: Shir Date: Sun, 6 May 2018 19:50:00 +0300 Subject: [PATCH 1/8] Delete everything of Kube Hunter 1.0 --- README.md | 39 ---------------- discovery.py | 79 -------------------------------- hunters/__init__.py | 3 -- hunters/dashboard.py | 104 ------------------------------------------- hunters/hunter.py | 9 ---- hunters/kubelet.py | 9 ---- hunters/proxy.py | 9 ---- kube-hunter.py | 79 -------------------------------- requirements.txt | 4 -- services.py | 87 ------------------------------------ validation.py | 40 ----------------- 11 files changed, 462 deletions(-) delete mode 100644 README.md delete mode 100644 discovery.py delete mode 100644 hunters/__init__.py delete mode 100644 hunters/dashboard.py delete mode 100644 hunters/hunter.py delete mode 100644 hunters/kubelet.py delete mode 100644 hunters/proxy.py delete mode 100755 kube-hunter.py delete mode 100644 requirements.txt delete mode 100644 services.py delete mode 100644 validation.py diff --git a/README.md b/README.md deleted file mode 100644 index f2e146b..0000000 --- a/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Kube Hunter - -Insecure Kubernetes clusters detection tool. - -## Installation - -Run the following commands to clone and install pre-requisites: - -```bash -git clone git@bitbucket.org:scalock/kube-hunter.git -cd kube-hunter -pip install -R requirements.txt -./kube-hunter -h -``` - -## Current Features - -The following action are currently supported: - -### Hunt - -Supplied a host IP, the tool will search for open Kubernetes services, -listening to default ports. -For each service found, it will check if it is insecure and grants -capabilities. - -```bash -./kube-hunter hunt 127.0.0.1 -``` - -### Scan - -Supplied a subnet address (CIDR notation), the tool will scan for -hosts with open Kubernetes services. - -## Supported Kubernetes Services - -The tool currently supports the following services: -* Kubernetes Dashboard diff --git a/discovery.py b/discovery.py deleted file mode 100644 index cf82b82..0000000 --- a/discovery.py +++ /dev/null @@ -1,79 +0,0 @@ -from logging import debug, info -from multiprocessing import Process, Queue -from socket import socket - -from netaddr import IPNetwork - - -KUBE_PROXY_PORT = 8001 -KUBELET_PORT = 10250 -KUBELET_READONLY_PORT = 10255 -DASHBOARD_PORT = 30000 - -DEFAULT_PORTS = [ - KUBE_PROXY_PORT, - KUBELET_PORT, - KUBELET_READONLY_PORT, - DASHBOARD_PORT -] - - -def cidr_to_list(cidr): - host_list = list(IPNetwork(cidr)) - return host_list - - -def test_connection(host, ports): - result = [] - - for port in ports: - s = socket() - s.settimeout(1) - success = s.connect_ex((str(host), port)) - s.close() - if success == 0: - info("{}:{} is open".format(host, port)) - result.append("{}:{}".format(host, port)) - - return result - - -class Worker(Process): - _count = 0 - - def __init__(self, queue): - super(Worker, self).__init__() - self.queue = queue - self.name = "Worker #{}".format(Worker._count) - Worker._count += 1 - - def run(self): - for host, ports, callback in iter(self.queue.get, None): - debug("{}: Checking host {}".format(self.name, host)) - for result in test_connection(host, ports): - callback(result) - - -class HostScanner(object): - def __init__(self, threads=1): - self.threads = threads - - def scan(self, cidr, ports, callback): - queue = Queue() - workers = [] - - debug("Starting workers") - for i in range(self.threads): - workers.append(Worker(queue)) - workers[-1].start() - - for host in cidr_to_list(cidr): - queue.put((host, ports, callback)) - - for i in range(self.threads): - queue.put(None) - - debug("Waiting for workers to finish") - for worker in workers: - worker.join() - debug("Workers finished") diff --git a/hunters/__init__.py b/hunters/__init__.py deleted file mode 100644 index ba7370c..0000000 --- a/hunters/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .dashboard import * -from .kubelet import * -from .proxy import * diff --git a/hunters/dashboard.py b/hunters/dashboard.py deleted file mode 100644 index ec4f509..0000000 --- a/hunters/dashboard.py +++ /dev/null @@ -1,104 +0,0 @@ -from io import BytesIO -from logging import info, warning, debug - -from PIL import Image -from requests import get -from selenium import webdriver -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.ui import WebDriverWait - -from hunters.hunter import Hunter - - - -DASHBOARD_PATHS = [ - "", - "/ui", - "/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy" -] - -API = { - "overview": "/api/v1/overview/default?itemsPerPage=100", - "nodes": "/api/v1/node?itemsPerPage=100" -} - -XPATH = { - "login_skip": "/html/body/kd-login/form/kd-content-card/div/div/div/kd-content/button[2]" -} - - -def test_url(url): - r = get(url) - if r.status_code == 200: - return r.url - - -class Dashboard(Hunter): - def __init__(self, host): - self.host = host - if "://" not in host: - self.host_url = "http://{}".format(host) - else: - self.host_url = host - self._is_auth_required = None - self._base_path = None - - def format_url(self, path): - return self.base_path + path - - def list_nodes(self): - return [str(n["objectMeta"]["name"]) for n in get(self.format_url(API["nodes"])).json()["nodes"]] - - @property - def base_path(self): - if self._base_path: - return self._base_path - for path in DASHBOARD_PATHS: - path = test_url(self.host_url + path) - if path: - self._base_path = path - return path - raise Exception("User interface URL path was not found") - - @property - def is_auth_required(self): - if not self._is_auth_required: - overview = get(self.format_url(API["overview"])).json() - if "errors" in overview and overview["errors"]: - self._is_auth_required = any([e["ErrStatus"]["code"] == 403 for e in overview["errors"]]) - else: - self._is_auth_required = False - return self._is_auth_required - - def take_screenshot(self): - driver = webdriver.Chrome() - driver.fullscreen_window() - waiter = WebDriverWait(driver, 5) - - driver.get(self.base_path) - waiter.until(lambda d: "Overview" in d.title or "Sign" in d.title) - - skip_buttons = driver.find_elements_by_xpath(XPATH["login_skip"]) - if skip_buttons: - skip_buttons[0].click() - waiter.until(expected_conditions.title_contains("Overview")) - - result = driver.get_screenshot_as_png() - driver.quit() - - return result - - def hunt(self, *args, **kwargs): - debug("Hunting dashboard at {}".format(self.host)) - - debug("Checking authentication...") - if self.is_auth_required: - warning("Authentication is required") - return - - debug("Authentication is not required") - debug("Listing nodes on the cluster...") - debug("Nodes: {}".format(self.list_nodes())) - - debug("Taking a screenshot...") - Image.open(BytesIO(self.take_screenshot())).show() diff --git a/hunters/hunter.py b/hunters/hunter.py deleted file mode 100644 index f4db0a7..0000000 --- a/hunters/hunter.py +++ /dev/null @@ -1,9 +0,0 @@ -from abc import ABCMeta, abstractmethod - - -class Hunter(object): - __metaclass__ = ABCMeta - - @abstractmethod - def hunt(self, *args, **kwargs): - pass diff --git a/hunters/kubelet.py b/hunters/kubelet.py deleted file mode 100644 index c7a9f69..0000000 --- a/hunters/kubelet.py +++ /dev/null @@ -1,9 +0,0 @@ -from hunters.hunter import Hunter - - -class Kubelet(Hunter): - def __init__(self, host): - self.host = host - - def hunt(self, *args, **kwargs): - raise NotImplementedError() diff --git a/hunters/proxy.py b/hunters/proxy.py deleted file mode 100644 index 7784e64..0000000 --- a/hunters/proxy.py +++ /dev/null @@ -1,9 +0,0 @@ -from hunters.hunter import Hunter - - -class Proxy(Hunter): - def __init__(self, host): - self.host = host - - def hunt(self, *args, **kwargs): - raise NotImplementedError() diff --git a/kube-hunter.py b/kube-hunter.py deleted file mode 100755 index f1d9897..0000000 --- a/kube-hunter.py +++ /dev/null @@ -1,79 +0,0 @@ -#! /usr/bin/python - -from __future__ import print_function - -from argparse import ArgumentParser -from logging import DEBUG, basicConfig, info, warning - -from discovery import DEFAULT_PORTS, HostScanner -from hunters import Dashboard, Kubelet, Proxy -from services import * -from validation import ip, subnet -import chromedriver_binary - -HUNT_MODE = "hunt" -SCAN_MODE = "scan" - - -def hunt_callback(host): - hunters = { - KUBERNETES_DASHBOARD: Dashboard, - KUBERNETES_KUBELET_HTTPS: Kubelet, - KUBERNETES_KUBELET_HTTP: Kubelet, - KUBERNETES_PROXY: Proxy - } - - service_type = identify_service(host) - if service_type == UNKNOWN: - return - - if service_type not in hunters: - warning("Unsupported service type: {}".format(describe_service_type(service_type))) - else: - try: - hunters[service_type](host).hunt() - except NotImplementedError: - pass - -def scan_callback(host): - print("{} - {}".format(host, describe_service_type(identify_service(host)))) - - -def hunt(*args, **kwargs): - target = args[0] - info("Hunting target {}".format(target)) - scanner = HostScanner(threads=1) - scanner.scan(target, DEFAULT_PORTS, hunt_callback) - - -def scan(*args, **kwargs): - target = args[0] - info("Scanning for targets on {}".format(target)) - scanner = HostScanner(threads=20) - scanner.scan(target, DEFAULT_PORTS, scan_callback) - - -def main(mode, *args, **kwargs): - actions = { - SCAN_MODE: scan, - HUNT_MODE: hunt - } - - actions[mode](*args, **kwargs) - - -if __name__ == "__main__": - basicConfig(level=DEBUG) - parser = ArgumentParser() - - subparsers = parser.add_subparsers(dest="action", description="Available actions") - - hunt_parser = subparsers.add_parser(HUNT_MODE) - hunt_parser.add_argument("host", type=ip, help="host to hunt") - - scan_parser = subparsers.add_parser(SCAN_MODE) - scan_parser.add_argument("subnet", type=subnet, help="subnet to scan (CIDR notation)") - - arguments = parser.parse_args() - - main(arguments.action, *([i[1] for i in arguments._get_kwargs()[1:]])) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9f2bd78..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -selenium -pillow -netaddr -chromedriver_binary \ No newline at end of file diff --git a/services.py b/services.py deleted file mode 100644 index 4e93e34..0000000 --- a/services.py +++ /dev/null @@ -1,87 +0,0 @@ -from requests import get -from urllib3 import disable_warnings - -UNKNOWN = 0 -KUBERNETES_DASHBOARD = 1 -KUBERNETES_PROXY = 2 -KUBERNETES_KUBELET_HTTPS = 3 -KUBERNETES_KUBELET_HTTP = 4 - -disable_warnings() - - -def describe_service_type(service_type): - if service_type == KUBERNETES_DASHBOARD: - return "Kubernetes Dashboard" - - if service_type == KUBERNETES_PROXY: - return "Kubernetes Proxy" - - if service_type == KUBERNETES_KUBELET_HTTPS: - return "Kubernetes Kubelet" - - if service_type == KUBERNETES_KUBELET_HTTP: - return "Kubernetes Kubelet (Read only)" - - return "Unknown Service" - - -def is_dashboard(host): - try: - r = get("http://{}/api/v1/login/status".format(host)).json() - return all([ - "tokenPresent" in r, - "headerPresent" in r, - "httpsMode" in r - ]) - except: - return False - - -def is_proxy(host): - try: - r = get("http://{}/".format(host)).json() - return all([ - "paths" in r, - "/api" in r["paths"] - ]) - except: - return False - - -def is_kubelet_https(host): - try: - r = get("https://{}/pods".format(host), verify=False).json() - return all([ - "kind" in r, - "items" in r - ]) - except: - return False - - -def is_kubelet_http(host): - try: - r = get("http://{}/pods".format(host)).json() - return all([ - "kind" in r, - "items" in r - ]) - except: - return False - - -def identify_service(host): - if is_dashboard(host): - return KUBERNETES_DASHBOARD - - if is_proxy(host): - return KUBERNETES_PROXY - - if is_kubelet_https(host): - return KUBERNETES_KUBELET_HTTPS - - if is_kubelet_http(host): - return KUBERNETES_KUBELET_HTTP - - return UNKNOWN diff --git a/validation.py b/validation.py deleted file mode 100644 index 27550b4..0000000 --- a/validation.py +++ /dev/null @@ -1,40 +0,0 @@ -from argparse import ArgumentTypeError - - -def ip(string): - error = ArgumentTypeError("{} is not a valid IP address".format(string)) - octets = string.split(".") - - if len(octets) != 4: - raise error - - try: - for o in octets: - o = int(o) - if o < 0 or o > 255: - raise error - except ValueError: - raise error - - return string - - -def subnet(string): - parts = string.split("/") - - if len(parts) != 2: - raise ArgumentTypeError("{} is not a valid subnet".format(string)) - - host, mask = parts - - ip(host) - - try: - mask = int(mask) - except ValueError: - raise ArgumentTypeError("{} is not an integer".format(mask)) - - if mask < 0 or mask > 32: - raise ArgumentTypeError("{} is not valid host identifier".format(mask)) - - return string From eb6cbc263608b1203534cb27de7614d703d9ff12 Mon Sep 17 00:00:00 2001 From: Shir Date: Sun, 6 May 2018 19:51:36 +0300 Subject: [PATCH 2/8] Initial Commit --- .gitignore | 3 -- __init__.py | 6 +++ events.py | 18 +++++++++ host_discovery.py | 16 ++++++++ kube_open_dashboard.py | 31 +++++++++++++++ main.py | 4 ++ modules/__init__.py | 6 +++ modules/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 315 bytes .../__pycache__/host_discovery.cpython-36.pyc | Bin 0 -> 1071 bytes .../kube_open_dashboard.cpython-36.pyc | Bin 0 -> 1155 bytes .../__pycache__/port_discovery.cpython-36.pyc | Bin 0 -> 1332 bytes modules/host_discovery.py | 16 ++++++++ modules/kube_open_dashboard.py | 31 +++++++++++++++ modules/port_discovery.py | 36 ++++++++++++++++++ port_discovery.py | 36 ++++++++++++++++++ 15 files changed, 200 insertions(+), 3 deletions(-) delete mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 events.py create mode 100644 host_discovery.py create mode 100644 kube_open_dashboard.py create mode 100644 main.py create mode 100644 modules/__init__.py create mode 100644 modules/__pycache__/__init__.cpython-36.pyc create mode 100644 modules/__pycache__/host_discovery.cpython-36.pyc create mode 100644 modules/__pycache__/kube_open_dashboard.cpython-36.pyc create mode 100644 modules/__pycache__/port_discovery.cpython-36.pyc create mode 100644 modules/host_discovery.py create mode 100644 modules/kube_open_dashboard.py create mode 100644 modules/port_discovery.py create mode 100644 port_discovery.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8c75de4..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.idea/ -__pycache__/ -*.pyc \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..440ff65 --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +from .kube_open_dashboard import KubeOpenDashboard +from .port_discovery import PortDiscovery +from .host_discovery import HostDiscovery + +__all__ = [HostDiscovery, KubeOpenDashboard, PortDiscovery] + diff --git a/events.py b/events.py new file mode 100644 index 0000000..cc9f506 --- /dev/null +++ b/events.py @@ -0,0 +1,18 @@ +hooks = {} + + +def trigger_event(name, item): + print('Event Lookup: ', name, item) + if name in hooks: + for single_hook in hooks[name]: + print("Event triggerd!", single_hook, item) + single_hook(item).execute() + + +def register_event(name, callback): + print('NEW Event: ', name, callback) + if name not in hooks: + # default dict + hooks[name] = [] + if callback not in hooks[name]: + hooks[name].append(callback) diff --git a/host_discovery.py b/host_discovery.py new file mode 100644 index 0000000..e26e5b7 --- /dev/null +++ b/host_discovery.py @@ -0,0 +1,16 @@ +from netifaces import interfaces, ifaddresses, AF_INET +from netaddr import IPNetwork +import events + + +class HostDiscovery(object): + def __init__(self, task): + pass + + def execute(self): + 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.trigger_event('NEW_HOST', {'host': single_ip}) diff --git a/kube_open_dashboard.py b/kube_open_dashboard.py new file mode 100644 index 0000000..56f2092 --- /dev/null +++ b/kube_open_dashboard.py @@ -0,0 +1,31 @@ +import events +import requests + + +class KubeOpenDashboard(object): + def __init__(self, task): + self.task = task + self.host = task['host'] + self.port = task['port'] or 80 + + pass + + def execute(self): + try: + r = requests.get("http://{host}:{port}/api/v1/node?itemsPerPage=100".format(host=self.host, port=self.port)) + except requests.exceptions.ConnectionError: + return None + + ret = r.json() + if 'listMeta' in ret: + print("KubeOpenDashboard :: Open Dashboard!", self.host) + + +events.register_event('OPEN_PORT_30000', KubeOpenDashboard) + +if __name__ == "__main__": + queue = list() + queue.append(KubeOpenDashboard({'host': '192.168.1.117', 'port': 30000})) + queue.append(KubeOpenDashboard({'host': '192.168.1.117', 'port': None})) + for i in queue: + i.execute() diff --git a/main.py b/main.py new file mode 100644 index 0000000..346e4c7 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +import modules + +modules.HostDiscovery({}).execute() + diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..440ff65 --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,6 @@ +from .kube_open_dashboard import KubeOpenDashboard +from .port_discovery import PortDiscovery +from .host_discovery import HostDiscovery + +__all__ = [HostDiscovery, KubeOpenDashboard, PortDiscovery] + diff --git a/modules/__pycache__/__init__.cpython-36.pyc b/modules/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94eb4c7a13ca9a418c50602c278949c53f982d23 GIT binary patch literal 315 zcmXr!<>jhZejjz3fq~&M5W@j8kmUfx#YR9Pg&~D8has0Sijfh-X3Am8WsYJ7vzc>P za#^ET85vSpQyEj4)0v~#Qdoi+G+AFV0ySzf-V*dKO-l7INX>IeEY3*EPb^9S3i1Zz z7nQhV7ANPIr506!1U&MK5dwai>`}tmKz;G~K#lP!Fr7tAKr^HG3V@p9Q((G_Ku(R~ z%K#dHB*+33yu}_LpO}*qAHR~Jhz-aG6TkGFtzyFSi?U-93riDYK(^>+l;)MB7U>$r nlIYq;;?~;+JQV&3^Ip@5eS(8Jxo@O literal 0 HcmV?d00001 diff --git a/modules/__pycache__/host_discovery.cpython-36.pyc b/modules/__pycache__/host_discovery.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4881d0beb08ea71fb8339629763a8acc700b6af1 GIT binary patch literal 1071 zcmZuvO;6iE5Z(3KjuVm|D%y&whe|#9K#G7<6`>+N3KzE$sFAE7*LXLD6=88-Ou!Bh(-PB_g- zPD7eop_Q|cQ9QFUJ9k2-^jYSH?gzqc?%WXWh{}wG9%o0ycYi?!;xkiA2U^H(oCsx> z(r(PT6iUISvb`T227AXoHPypUfzV$Ic^)Jfwip0!4(Am(f`pWl(BhO^HzZ`7aT``B zjpa<^pip`@RY`FvBPfP+&Leeiq*3bl2~2uL|iQsw_{(&mVwlE}kTa z2!{jaf|cmH;9PytWJY$$$>%AZ6V-G=XS8o=)*~}(m(1wYnpo1)HfMcjVsU!~&ft_? zz1H@G%_u}KPi^h?E8OAk#7^nVqGY(=CPS}1=U)l#ARtoI)V8+IuD+L1Ktmc}b>AhyM27$M=R+cKA6#2zg9kXF-myG>~B8V%IjI?OBvLcBy zwFS;1YV3Yno4#G&l&FvKg1vTh@bT!_*eJK}NlYji-qn=QTy*16rX`}6sHAbbh0J4Z zTyZG|TA7BHX|E?_RPKF8dhjZ&LKh|?h;w18ckLLuYIHgfI;^U6(91-WUi?STRQ@7I zt_lefTBk3mO`p;ybo}g5mVxhJXAGcN6q#BS%^L{(dK5u>W%C=k1li;ZobLK??qM;B zErM1yutz;sp7_Ry{t4^x4g^2bmZiT`Tc;B(zFO@#G_I{91v;LB8pZ;);69tyGeGpwo|of zdLdl-2mAs4lCPXNA#voyIH44&!qJS!v!3zH_f6KPr%ONQeuN(#LcWoImWTExiW;I4 zM9?k?Cp!)4Dj@U zxb)F0-{~S0UXTw{VG)W+X+$VFZtvl%yQEzFjuntb%j(O;(AY##%cy{aRFIGfD%d9y zI>Hey?ym4K+Bb8r`v$-t5?(-cQXUm!-*k!_oB*EU0KK2>uxuAO_z2y@8>Lq@sBp*_z%2r76K_3vwN2sWP)$S(-x29$reB$cAwIG?bM_7vwh3*E^VS;Cxh z-YU!RKgzx@?V#F!rLqHtFBZn0aCW=-vca3%J8${=DlYp{FN#yn2h+8UwMy;b<4Ubk zt38TW`*tv|kSX5?eK>(sD`|(r_}M{%*i6#SNH{_&K=SbwxnOU&mYvW;y!&AMC`U4c gq49rfHo=ogtIzBtD%%`8zV1>}KKY%z)A`&T*HK^rvS0OI8mAFNv+WjhUf2`Ndc=~Qw^$8JxC>03&%ut1 z?>}R#h(1SkkJQ74v7CH1%fro66oa=67}m{Fp|}#)EcK`deEZO&POe$Y(?InxPv{1R zwYDz`lh?07zsUuDO^eO*!gF*u?g3NU?QAvpgeWH&CL8S zru%68_si!`CMUI?PqK?9o6MV6@ZhYeEa(T1CW~4%C78)Mrk84ye|Y}Jg;Ew(VWkWy zcUKT!-oI7maO0A5^r1y=0ZLczA#N^zwhY02cEwk0Mj}?c6*{oq%vU#OCH@pNy3Xd! zdf21?$DO;207pU{_r~eg!QsoXJUsa2I1ydj&ZqL1yYKAsH!@ryDiPL@1a6?|3&zU4t}4iFQCI6I0lbC^ZwFQv(qTZUR}X`w+oq_dC3KNGeAkLl za(%djys@y22SceDkN6JXM_GP&8@e0Mcax^^5qUb~p5Vr^mR*UJnBl~UxVRJ6TOnyy z{8Y63%0r7!bX|E$Amtw`?}#bC!{{vyM3XxHA?axvi;E0cTP&b%tcz}XmvHGkW3}t$ zWo;nwJ0v>P95-p-G Date: Thu, 10 May 2018 14:15:12 +0300 Subject: [PATCH 3/8] Added EventQueue class, to handle events. Events now gets processed asynchronously. --- __init__.py | 6 ---- events.py | 55 ++++++++++++++++++++++++-------- events.pyc | Bin 0 -> 2175 bytes host_discovery.py | 16 ---------- kube-hunter.py | 17 ++++++++++ kube_open_dashboard.py | 31 ------------------ main.py | 4 --- modules/__init__.pyc | Bin 0 -> 396 bytes modules/host_discovery.py | 4 +-- modules/host_discovery.pyc | Bin 0 -> 1315 bytes modules/kube_open_dashboard.py | 6 ++-- modules/kube_open_dashboard.pyc | Bin 0 -> 1609 bytes modules/port_discovery.py | 6 ++-- modules/port_discovery.pyc | Bin 0 -> 1867 bytes port_discovery.py | 36 --------------------- requirements.txt | 5 +++ 16 files changed, 70 insertions(+), 116 deletions(-) delete mode 100644 __init__.py create mode 100644 events.pyc delete mode 100644 host_discovery.py create mode 100644 kube-hunter.py delete mode 100644 kube_open_dashboard.py delete mode 100644 main.py create mode 100644 modules/__init__.pyc create mode 100644 modules/host_discovery.pyc create mode 100644 modules/kube_open_dashboard.pyc create mode 100644 modules/port_discovery.pyc delete mode 100644 port_discovery.py create mode 100644 requirements.txt diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 440ff65..0000000 --- a/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .kube_open_dashboard import KubeOpenDashboard -from .port_discovery import PortDiscovery -from .host_discovery import HostDiscovery - -__all__ = [HostDiscovery, KubeOpenDashboard, PortDiscovery] - diff --git a/events.py b/events.py index cc9f506..938dc09 100644 --- a/events.py +++ b/events.py @@ -1,18 +1,45 @@ -hooks = {} +from Queue import Queue +from threading import Thread +from collections import defaultdict +from threading import Lock -def trigger_event(name, item): - print('Event Lookup: ', name, item) - if name in hooks: - for single_hook in hooks[name]: - print("Event triggerd!", single_hook, item) - single_hook(item).execute() +# Inherits Queue object, handles events asynchronously +class EventQueue(Queue, object): + def __init__(self, num_worker=10): + super(EventQueue, self).__init__() + self.hooks = defaultdict(list) + + for i in range(num_worker): + t = Thread(target=self.worker) + t.daemon = True + t.start() + + def publish_event(self, name, item): + if name in self.hooks: + safe_print('Event {} got published with {}'.format(name, item)) + for single_hook in self.hooks[name]: + self.put(single_hook(item)) + + def subscribe_event(self, name, callback): + safe_print('Subscribed: {} to {} '.format(name, callback)) + if callback not in self.hooks[name]: + self.hooks[name].append(callback) + + # executes callbacks on dedicated thread + def worker(self): + while True: + hook = self.get() + hook.execute() + self.task_done() -def register_event(name, callback): - print('NEW Event: ', name, callback) - if name not in hooks: - # default dict - hooks[name] = [] - if callback not in hooks[name]: - hooks[name].append(callback) + +print_lock = Lock() +def safe_print(*args, **kargs): + with print_lock: + print(args, kargs) + + +handler = EventQueue(500) + \ No newline at end of file diff --git a/events.pyc b/events.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e98d5aa1a0a6eb5c5d79b0fd9d5fddd0b15dded GIT binary patch literal 2175 zcmb_d-;dKq5T3R3;~b72;wU0gg-+^2BWOX0ClG2;xvG7ruBwA{trfI#yqm;{W9Pi< zaus(^@XnvoKd!vD-;6_#z9;9Mo1I67iUmh6$~ zvo(px_Q~wgTSdu$Ohn_+0RHoT@Ea*;z6w^m4*~xdt$QkKrCdAkai@Gk%9K5OE-}g=9ieI%GPcbSWN>IG~&l z&iEL1#p4Brn?iUziY}0GK+Ax$RX!x&;S^!jT{?e4zDE~|Eyu6!(`7&&Ajt9(P>{OB z3lUv}qz5dQ*W{S9hjeNP2(jN2905;hJ-2?e&nm~|nvJbvj>qpoXq}h2*E;4m5CTz= zJ1;vIRkd=W)oD4mv0%+dzOL$(t!4Aey0PNMq;_4Etp~XDBk{n(*A`Hg~lR8uf!J)dTB9(=hb38nx6sJ%CF%Vx8t$I|Cxhwz~HDNm{gGZ4% zzrWoC>+ty1kN`fv1&K%Y@V2wZbXv;A*y8W*!h>+@=*{KPyz)nzW(s&0)*QXg{Q_2j zDdvC$BA3pr-qd;N1=LwpuTu|h5N(v>wxZkDvn(MY(RQ+{{ zKw{T|^-Xri{2_IqZmU}@ydNPJc#p9NrZb)Z^HV$l8N(+i4_AGvpMNrER9o2pkLBk$ zZrn^=R_9Y|o-(4oV%b7?h~@Ye3jvd9nr=3>G$Rg5P>XLv6Vii0#Wc-!jS`o}eGIY1 z=?N9(jzG6_ZX=9Ye4~bp6F5#+xCW*YOt!aaC^P^^^-Pnx!9Z| zZaI1cn$@|@8gJ1ZkZtN#+Ek^LhH%B5-BToH0y5vXtccXU8mN(UAm)kI%vY_2=31{S z(}1d4?rS{6hWHB>e99GfMcYYR;0wcHNOV25A4R>Wm%$*_;3$9LRgM9rcz=TwPzP!8 zar*J6Q-klzZxCbocAT_d5bp1CRW-LG0a&gh`fb}aSFBwnvy(iS=3jB7oDiwL%0GY$ zCnLr!W>Z^s`TC(7v^|>_v#Ka;=JTp_(&}TTj5L{Qy2OHf5#ldljM;}UX_1zuu=Q06 cJFAD>!e=cBzGX7I&me9n`RfetMk?z53!`+*r2qf` literal 0 HcmV?d00001 diff --git a/host_discovery.py b/host_discovery.py deleted file mode 100644 index e26e5b7..0000000 --- a/host_discovery.py +++ /dev/null @@ -1,16 +0,0 @@ -from netifaces import interfaces, ifaddresses, AF_INET -from netaddr import IPNetwork -import events - - -class HostDiscovery(object): - def __init__(self, task): - pass - - def execute(self): - 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.trigger_event('NEW_HOST', {'host': single_ip}) diff --git a/kube-hunter.py b/kube-hunter.py new file mode 100644 index 0000000..eae7ca4 --- /dev/null +++ b/kube-hunter.py @@ -0,0 +1,17 @@ +import modules +import threading +import time +import sys + +def main(): + try: + modules.HostDiscovery({}).execute() + # Blocking to see discovery output + while(True): + time.sleep(1) + except KeyboardInterrupt: + print('User stopped kubehunter') + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/kube_open_dashboard.py b/kube_open_dashboard.py deleted file mode 100644 index 56f2092..0000000 --- a/kube_open_dashboard.py +++ /dev/null @@ -1,31 +0,0 @@ -import events -import requests - - -class KubeOpenDashboard(object): - def __init__(self, task): - self.task = task - self.host = task['host'] - self.port = task['port'] or 80 - - pass - - def execute(self): - try: - r = requests.get("http://{host}:{port}/api/v1/node?itemsPerPage=100".format(host=self.host, port=self.port)) - except requests.exceptions.ConnectionError: - return None - - ret = r.json() - if 'listMeta' in ret: - print("KubeOpenDashboard :: Open Dashboard!", self.host) - - -events.register_event('OPEN_PORT_30000', KubeOpenDashboard) - -if __name__ == "__main__": - queue = list() - queue.append(KubeOpenDashboard({'host': '192.168.1.117', 'port': 30000})) - queue.append(KubeOpenDashboard({'host': '192.168.1.117', 'port': None})) - for i in queue: - i.execute() diff --git a/main.py b/main.py deleted file mode 100644 index 346e4c7..0000000 --- a/main.py +++ /dev/null @@ -1,4 +0,0 @@ -import modules - -modules.HostDiscovery({}).execute() - diff --git a/modules/__init__.pyc b/modules/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..928d88dd4f1c9f215d72a59474aee97ebc9b737a GIT binary patch literal 396 zcmYk0-%7(U6vj`Qb&4=ie2Lz;2M`fqyHJEF^R5IAZ4%njv?)od>}`BS@dZ4w8#RBv zNlg|ACnI5kBpir%aX+r~@#q1|D&pRAwlU3L~p zHzX~Q&=LyOiM1yi!X?Q-EL11h{_p$f*980LC-HqGA{NFVW^{#F{4xV*W zQCZ=g6lD|2QMNj)t*eBWg|V^~v7_61?1OfqvuZTj3&f^x0^)i&gma?ZPr*a%$S+B* H7|rAtDnDJN literal 0 HcmV?d00001 diff --git a/modules/host_discovery.py b/modules/host_discovery.py index e26e5b7..263ca78 100644 --- a/modules/host_discovery.py +++ b/modules/host_discovery.py @@ -1,6 +1,6 @@ from netifaces import interfaces, ifaddresses, AF_INET from netaddr import IPNetwork -import events +import events class HostDiscovery(object): @@ -13,4 +13,4 @@ class HostDiscovery(object): if addresses: subnet = IPNetwork('{0}/24'.format(addresses[0])) for single_ip in IPNetwork(subnet): - events.trigger_event('NEW_HOST', {'host': single_ip}) + events.handler.publish_event('NEW_HOST', {'host': single_ip}) \ No newline at end of file diff --git a/modules/host_discovery.pyc b/modules/host_discovery.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b366fd0eeb0aeb818172cba28a05187a4f9f068b GIT binary patch literal 1315 zcmcgs!EVz)5PfSWX&YLp65>$Afnz|T1%x;tgn*`~y`;#gR1vwX9D9>EvE$UcF0DvT z<-naAKf%Yr9cCtlenGLklbxOM?#z2{Gw4a8<0upRmgx`p{J00 z=y}L}^n56edB|$W>*&>&zK^VdUIR%DW(5OeAZCGerQio{2^)u3(D-}JQjB-dnj z>P$I^Vq@Jcb_Y?Cl*U@}SIB?!P9Jpkj#`TI*T@{a?-=)~C?_4aLGFHGT|2dLac0Vk ze|>QFTN?H*83Qh#DyCwn1L}zGIu2Nq4P@dX zJ6Xm!!5)&?eGzhHc1G=l!y>e09QIS!ui}Yu+eJAHZ5(AqJP9XN-#i*s+a=DsESCutbEOO=uSvz$GzPS9?je8oaxNPsEK6)qLV z9-PlBmmbUSqd?avzI2 zTm$13jEUMBQaY*5gg>Vf)aeArr%lj%EI4wl9RZEWwd{U=yx4jAETtKw&CcGj-aqUf zNfsl~VzOC=2!wmqiZw1VgQ&_}z(@olBHnFKlzHT+AY%K>Ox;pDqiC9BrWBpbs(zN* zk(RLNFsM_uNQyKUn@*G)!iy9rYu75|O>Nbm8YcmpPKTM%>FkENXKu~8i7RKM^>y;0 zHr0-5s4aC{wOEW-3Yk*vTFZ6PdMPKZ1KuIHL*}|tncaj`1FlApl8IXLg+D=zkQ0RE tPi4ub&?onsf(*4#8SsS`5{v%W#7;K-55)_LFPFHzVhM;s?-AJR{vU;KE0O>J literal 0 HcmV?d00001 diff --git a/modules/kube_open_dashboard.py b/modules/kube_open_dashboard.py index 56f2092..99f8273 100644 --- a/modules/kube_open_dashboard.py +++ b/modules/kube_open_dashboard.py @@ -1,13 +1,11 @@ import events import requests - class KubeOpenDashboard(object): def __init__(self, task): self.task = task self.host = task['host'] self.port = task['port'] or 80 - pass def execute(self): @@ -18,10 +16,10 @@ class KubeOpenDashboard(object): ret = r.json() if 'listMeta' in ret: - print("KubeOpenDashboard :: Open Dashboard!", self.host) + events.safe_print("KubeOpenDashboard :: Open Dashboard!", self.host) -events.register_event('OPEN_PORT_30000', KubeOpenDashboard) +events.handler.subscribe_event('OPEN_PORT_30000', KubeOpenDashboard) if __name__ == "__main__": queue = list() diff --git a/modules/kube_open_dashboard.pyc b/modules/kube_open_dashboard.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be5cf263a67b56cb57452654e75730f826704799 GIT binary patch literal 1609 zcmcgrOK;Oa5T11&P12S}DV)B3`2H36Vx_B_Ik~Irg-%li2R=y0mI@ zD*uC@z>Ncc3+^zpeIdb>I^M~6Jv)!@`<(kRTlxCnXV-_bt3dy+XzA}{0=fl^0J;~x zfUbbR0d!!8oE&@xI05+3%>e>Ld58)SJ z?whQSn<>WYo*wiQPX!kP3wrySmcB!VM&R*)r|MF7!a6x{x2g`k_Su}TH9oXL=2*Zn zfV2*sSl|cRvM?>A_ZTGJqI3XT)jXYO8@7W)8|$QrGGT{ysyQ-t%XoU|a%(P^v&HSc z#?g+;QB_)}MsJh1*6Mvq)KI6MkGMyTzcOb8wsoa0kV4^4>@OBu$Yk&<_dQ%!+y8b9dZHk(R~cfSki+FeC$- zT|h(=J3M-nJHvqFaRLreJQ2jyZOC(M0iq%dE1)(&2*DV=C+wmUQ#zp(c{p}pSOjIj zlp(6HqE3^t!kBM{mAh!`CfR{8X{%m8Vt6O5BL;U;_tLPw->k<;fGz|oqwPl;PUt`V`AA#`VmoTs~O%LcyTa7WsB*ouYDEvQCd#jc|OGLu=or^LPE;W q=J8UJTj0)c2s0dBDOwhXXQynv;PRTeB5IvmbMr)TSuD|SR{RFR&TAh4 literal 0 HcmV?d00001 diff --git a/modules/port_discovery.py b/modules/port_discovery.py index 196854a..714e0ee 100644 --- a/modules/port_discovery.py +++ b/modules/port_discovery.py @@ -11,8 +11,8 @@ class PortDiscovery(object): def execute(self): for single_port in default_ports: if self.test_connection(self.host, single_port): - events.trigger_event('OPEN_PORT', {'host': self.host, 'port': single_port}) - events.trigger_event('OPEN_PORT_{port}'.format(port=single_port), + events.handler.publish_event('OPEN_PORT', {'host': self.host, 'port': single_port}) + events.handler.publish_event('OPEN_PORT_{port}'.format(port=single_port), {'host': self.host, 'port': single_port}) @staticmethod @@ -26,7 +26,7 @@ class PortDiscovery(object): return False -events.register_event('NEW_HOST', PortDiscovery) +events.handler.subscribe_event('NEW_HOST', PortDiscovery) if __name__ == "__main__": queue = list() diff --git a/modules/port_discovery.pyc b/modules/port_discovery.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34704db7b7ff380dc65a612ca421ff974e3d0a96 GIT binary patch literal 1867 zcmcgsO>f&q5S=9@%aWzWNt+-ahhBPM14M{&aMBtbR8X*>$dyHjB4zF} zbsGB+IR7Qb{DU0&mzsO>X5=(!fgW9%91mxPvor6_(EqE`dVl@nFs9RA9slp+wtqq- zxD;hX3!)*B;%ARCMMFi2Pf3lEIwfngU!$Q%NrQ|}6S&stEzt?lZ^IfHMYB51)+lL` zsnLl?$}}ix(cGi*1DSOW9KzS6S&Ji5B-o&&Op^fwteu0AK*0{AIo*UOsAN&YcD}Xr1;${VZ2mYO+bdOUT7cWiu z>Th5?cpmcGKSLOlj53M))GxOreyr*IJXh>rQ)FZUzPaUS9b2^3v9BGQ8N zAmEU`t3gf+>!je2ZQ>BSHQ7WI7uh+AhL45ahIn*$^m|dx?J|nZXulZQG9HanH?Fwb zZc*-!Y#e3ieKfDe=Ek(johe6)BB?TCM@uxVlP_I&m#^G9a%!FCsneQu%cYQt8Anzb zN@?Z|l*2#B5tu0HSP@#9t)$0Yg7UIK&Zk+8M*&Tzup6GP zC-qcD{8UTXT{P(faSTNiqc!#VxuErYY(uFB1Cy%FK zYjcRB;B_VPV1Kd#fn4ftSs5`sjWTNjHVG}E(8#B(Xt7ls8*9%N-;jsx0-wnr8S9Ph;)jUQ|82h63ph|H4w>R` z9YVID7|*ata%#x)C`)Fmv5m`gY*uR%US Date: Thu, 10 May 2018 19:02:07 +0300 Subject: [PATCH 4/8] Added .gitignore file, ignoring .pyc files, Also changed modules structure --- .gitignore | 1 + discovery/__init__.py | 6 ++++ discovery/dashboard.py | 16 ++++++++++ .../host_discovery.py => discovery/hosts.py | 0 .../port_discovery.py => discovery/ports.py | 6 ++-- events.py | 2 +- events.pyc | Bin 2175 -> 0 bytes hunting/__init__.py | 4 +++ hunting/dashboard.py | 29 ++++++++++++++++++ kube-hunter.py | 5 +-- modules/__init__.py | 6 ---- modules/__init__.pyc | Bin 396 -> 0 bytes modules/__pycache__/__init__.cpython-36.pyc | Bin 315 -> 0 bytes .../__pycache__/host_discovery.cpython-36.pyc | Bin 1071 -> 0 bytes .../kube_open_dashboard.cpython-36.pyc | Bin 1155 -> 0 bytes .../__pycache__/port_discovery.cpython-36.pyc | Bin 1332 -> 0 bytes modules/host_discovery.pyc | Bin 1315 -> 0 bytes modules/kube_open_dashboard.py | 29 ------------------ modules/kube_open_dashboard.pyc | Bin 1609 -> 0 bytes modules/port_discovery.pyc | Bin 1867 -> 0 bytes 20 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 .gitignore create mode 100644 discovery/__init__.py create mode 100644 discovery/dashboard.py rename modules/host_discovery.py => discovery/hosts.py (100%) rename modules/port_discovery.py => discovery/ports.py (81%) delete mode 100644 events.pyc create mode 100644 hunting/__init__.py create mode 100644 hunting/dashboard.py delete mode 100644 modules/__init__.py delete mode 100644 modules/__init__.pyc delete mode 100644 modules/__pycache__/__init__.cpython-36.pyc delete mode 100644 modules/__pycache__/host_discovery.cpython-36.pyc delete mode 100644 modules/__pycache__/kube_open_dashboard.cpython-36.pyc delete mode 100644 modules/__pycache__/port_discovery.cpython-36.pyc delete mode 100644 modules/host_discovery.pyc delete mode 100644 modules/kube_open_dashboard.py delete mode 100644 modules/kube_open_dashboard.pyc delete mode 100644 modules/port_discovery.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/discovery/__init__.py b/discovery/__init__.py new file mode 100644 index 0000000..7d881ec --- /dev/null +++ b/discovery/__init__.py @@ -0,0 +1,6 @@ +from .dashboard import KubeDashboard +from .ports import PortDiscovery +from .hosts import HostDiscovery + +__all__ = [HostDiscovery, KubeDashboard, PortDiscovery] + diff --git a/discovery/dashboard.py b/discovery/dashboard.py new file mode 100644 index 0000000..6dcd8ec --- /dev/null +++ b/discovery/dashboard.py @@ -0,0 +1,16 @@ +import events +import requests + +class KubeDashboard(object): + def __init__(self, task): + self.task = task + self.host = task['host'] + self.port = task['port'] or 80 + pass + + def execute(self): + # TODO: insert logic for detremining dashboard/insecure dashboard is there + events.handler.publish_event('KUBE_DASHBOARD', {'host': self.host, 'port': self.port}) + + +events.handler.subscribe_event('OPEN_PORT_30000', KubeDashboard) diff --git a/modules/host_discovery.py b/discovery/hosts.py similarity index 100% rename from modules/host_discovery.py rename to discovery/hosts.py diff --git a/modules/port_discovery.py b/discovery/ports.py similarity index 81% rename from modules/port_discovery.py rename to discovery/ports.py index 714e0ee..7f66eab 100644 --- a/modules/port_discovery.py +++ b/discovery/ports.py @@ -1,8 +1,8 @@ from socket import socket import events -default_ports = [8001, 10250, 10255, 30000] +default_ports = [8001, 10250, 10255, 30000] class PortDiscovery(object): def __init__(self, task): @@ -11,9 +11,7 @@ class PortDiscovery(object): def execute(self): for single_port in default_ports: if self.test_connection(self.host, single_port): - events.handler.publish_event('OPEN_PORT', {'host': self.host, 'port': single_port}) - events.handler.publish_event('OPEN_PORT_{port}'.format(port=single_port), - {'host': self.host, 'port': single_port}) + events.handler.publish_event('OPEN_PORT_{port}'.format(port=single_port), {'host': self.host, 'port': single_port}) @staticmethod def test_connection(host, port): diff --git a/events.py b/events.py index 938dc09..79c9f8a 100644 --- a/events.py +++ b/events.py @@ -16,8 +16,8 @@ class EventQueue(Queue, object): t.start() def publish_event(self, name, item): + safe_print('Event {} got published with {}'.format(name, item)) if name in self.hooks: - safe_print('Event {} got published with {}'.format(name, item)) for single_hook in self.hooks[name]: self.put(single_hook(item)) diff --git a/events.pyc b/events.pyc deleted file mode 100644 index 6e98d5aa1a0a6eb5c5d79b0fd9d5fddd0b15dded..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2175 zcmb_d-;dKq5T3R3;~b72;wU0gg-+^2BWOX0ClG2;xvG7ruBwA{trfI#yqm;{W9Pi< zaus(^@XnvoKd!vD-;6_#z9;9Mo1I67iUmh6$~ zvo(px_Q~wgTSdu$Ohn_+0RHoT@Ea*;z6w^m4*~xdt$QkKrCdAkai@Gk%9K5OE-}g=9ieI%GPcbSWN>IG~&l z&iEL1#p4Brn?iUziY}0GK+Ax$RX!x&;S^!jT{?e4zDE~|Eyu6!(`7&&Ajt9(P>{OB z3lUv}qz5dQ*W{S9hjeNP2(jN2905;hJ-2?e&nm~|nvJbvj>qpoXq}h2*E;4m5CTz= zJ1;vIRkd=W)oD4mv0%+dzOL$(t!4Aey0PNMq;_4Etp~XDBk{n(*A`Hg~lR8uf!J)dTB9(=hb38nx6sJ%CF%Vx8t$I|Cxhwz~HDNm{gGZ4% zzrWoC>+ty1kN`fv1&K%Y@V2wZbXv;A*y8W*!h>+@=*{KPyz)nzW(s&0)*QXg{Q_2j zDdvC$BA3pr-qd;N1=LwpuTu|h5N(v>wxZkDvn(MY(RQ+{{ zKw{T|^-Xri{2_IqZmU}@ydNPJc#p9NrZb)Z^HV$l8N(+i4_AGvpMNrER9o2pkLBk$ zZrn^=R_9Y|o-(4oV%b7?h~@Ye3jvd9nr=3>G$Rg5P>XLv6Vii0#Wc-!jS`o}eGIY1 z=?N9(jzG6_ZX=9Ye4~bp6F5#+xCW*YOt!aaC^P^^^-Pnx!9Z| zZaI1cn$@|@8gJ1ZkZtN#+Ek^LhH%B5-BToH0y5vXtccXU8mN(UAm)kI%vY_2=31{S z(}1d4?rS{6hWHB>e99GfMcYYR;0wcHNOV25A4R>Wm%$*_;3$9LRgM9rcz=TwPzP!8 zar*J6Q-klzZxCbocAT_d5bp1CRW-LG0a&gh`fb}aSFBwnvy(iS=3jB7oDiwL%0GY$ zCnLr!W>Z^s`TC(7v^|>_v#Ka;=JTp_(&}TTj5L{Qy2OHf5#ldljM;}UX_1zuu=Q06 cJFAD>!e=cBzGX7I&me9n`RfetMk?z53!`+*r2qf` diff --git a/hunting/__init__.py b/hunting/__init__.py new file mode 100644 index 0000000..d73b2a6 --- /dev/null +++ b/hunting/__init__.py @@ -0,0 +1,4 @@ +from .dashboard import KubeDashboard + +__all__ = [KubeDashboard] + diff --git a/hunting/dashboard.py b/hunting/dashboard.py new file mode 100644 index 0000000..3cff369 --- /dev/null +++ b/hunting/dashboard.py @@ -0,0 +1,29 @@ +import events +import requests +from events import safe_print + +class KubeDashboard(object): + def __init__(self, task): + self.host = task['host'] + self.port = task['port'] or 30000 + + def execute(self): + print("KUBEDASHBOARD At: {} {}".format(self.host, self.port)) + if self.secured: + safe_print("SECURED DASHBOARD") + else: + safe_print("INSECURE DASHBOARD") + + @property + def secured(self): + try: + r = requests.get("http://{host}:{port}/api/v1/node?itemsPerPage=100".format(host=self.host, port=self.port)) + except requests.exceptions.ConnectionError: + return True + + ret = r.json() + if 'listMeta' in ret: + return False + return True + +events.handler.subscribe_event('KUBE_DASHBOARD', KubeDashboard) \ No newline at end of file diff --git a/kube-hunter.py b/kube-hunter.py index eae7ca4..4fbcd42 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -1,11 +1,12 @@ -import modules +import discovery +import hunting import threading import time import sys def main(): try: - modules.HostDiscovery({}).execute() + discovery.HostDiscovery({}).execute() # Blocking to see discovery output while(True): time.sleep(1) diff --git a/modules/__init__.py b/modules/__init__.py deleted file mode 100644 index 440ff65..0000000 --- a/modules/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .kube_open_dashboard import KubeOpenDashboard -from .port_discovery import PortDiscovery -from .host_discovery import HostDiscovery - -__all__ = [HostDiscovery, KubeOpenDashboard, PortDiscovery] - diff --git a/modules/__init__.pyc b/modules/__init__.pyc deleted file mode 100644 index 928d88dd4f1c9f215d72a59474aee97ebc9b737a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 396 zcmYk0-%7(U6vj`Qb&4=ie2Lz;2M`fqyHJEF^R5IAZ4%njv?)od>}`BS@dZ4w8#RBv zNlg|ACnI5kBpir%aX+r~@#q1|D&pRAwlU3L~p zHzX~Q&=LyOiM1yi!X?Q-EL11h{_p$f*980LC-HqGA{NFVW^{#F{4xV*W zQCZ=g6lD|2QMNj)t*eBWg|V^~v7_61?1OfqvuZTj3&f^x0^)i&gma?ZPr*a%$S+B* H7|rAtDnDJN diff --git a/modules/__pycache__/__init__.cpython-36.pyc b/modules/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 94eb4c7a13ca9a418c50602c278949c53f982d23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 315 zcmXr!<>jhZejjz3fq~&M5W@j8kmUfx#YR9Pg&~D8has0Sijfh-X3Am8WsYJ7vzc>P za#^ET85vSpQyEj4)0v~#Qdoi+G+AFV0ySzf-V*dKO-l7INX>IeEY3*EPb^9S3i1Zz z7nQhV7ANPIr506!1U&MK5dwai>`}tmKz;G~K#lP!Fr7tAKr^HG3V@p9Q((G_Ku(R~ z%K#dHB*+33yu}_LpO}*qAHR~Jhz-aG6TkGFtzyFSi?U-93riDYK(^>+l;)MB7U>$r nlIYq;;?~;+JQV&3^Ip@5eS(8Jxo@O diff --git a/modules/__pycache__/host_discovery.cpython-36.pyc b/modules/__pycache__/host_discovery.cpython-36.pyc deleted file mode 100644 index 4881d0beb08ea71fb8339629763a8acc700b6af1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1071 zcmZuvO;6iE5Z(3KjuVm|D%y&whe|#9K#G7<6`>+N3KzE$sFAE7*LXLD6=88-Ou!Bh(-PB_g- zPD7eop_Q|cQ9QFUJ9k2-^jYSH?gzqc?%WXWh{}wG9%o0ycYi?!;xkiA2U^H(oCsx> z(r(PT6iUISvb`T227AXoHPypUfzV$Ic^)Jfwip0!4(Am(f`pWl(BhO^HzZ`7aT``B zjpa<^pip`@RY`FvBPfP+&Leeiq*3bl2~2uL|iQsw_{(&mVwlE}kTa z2!{jaf|cmH;9PytWJY$$$>%AZ6V-G=XS8o=)*~}(m(1wYnpo1)HfMcjVsU!~&ft_? zz1H@G%_u}KPi^h?E8OAk#7^nVqGY(=CPS}1=U)l#ARtoI)V8+IuD+L1Ktmc}b>AhyM27$M=R+cKA6#2zg9kXF-myG>~B8V%IjI?OBvLcBy zwFS;1YV3Yno4#G&l&FvKg1vTh@bT!_*eJK}NlYji-qn=QTy*16rX`}6sHAbbh0J4Z zTyZG|TA7BHX|E?_RPKF8dhjZ&LKh|?h;w18ckLLuYIHgfI;^U6(91-WUi?STRQ@7I zt_lefTBk3mO`p;ybo}g5mVxhJXAGcN6q#BS%^L{(dK5u>W%C=k1li;ZobLK??qM;B zErM1yutz;sp7_Ry{t4^x4g^2bmZiT`Tc;B(zFO@#G_I{91v;LB8pZ;);69tyGeGpwo|of zdLdl-2mAs4lCPXNA#voyIH44&!qJS!v!3zH_f6KPr%ONQeuN(#LcWoImWTExiW;I4 zM9?k?Cp!)4Dj@U zxb)F0-{~S0UXTw{VG)W+X+$VFZtvl%yQEzFjuntb%j(O;(AY##%cy{aRFIGfD%d9y zI>Hey?ym4K+Bb8r`v$-t5?(-cQXUm!-*k!_oB*EU0KK2>uxuAO_z2y@8>Lq@sBp*_z%2r76K_3vwN2sWP)$S(-x29$reB$cAwIG?bM_7vwh3*E^VS;Cxh z-YU!RKgzx@?V#F!rLqHtFBZn0aCW=-vca3%J8${=DlYp{FN#yn2h+8UwMy;b<4Ubk zt38TW`*tv|kSX5?eK>(sD`|(r_}M{%*i6#SNH{_&K=SbwxnOU&mYvW;y!&AMC`U4c gq49rfHo=ogtIzBtD%%`8zV1>}KKY%z)A`&T*HK^rvS0OI8mAFNv+WjhUf2`Ndc=~Qw^$8JxC>03&%ut1 z?>}R#h(1SkkJQ74v7CH1%fro66oa=67}m{Fp|}#)EcK`deEZO&POe$Y(?InxPv{1R zwYDz`lh?07zsUuDO^eO*!gF*u?g3NU?QAvpgeWH&CL8S zru%68_si!`CMUI?PqK?9o6MV6@ZhYeEa(T1CW~4%C78)Mrk84ye|Y}Jg;Ew(VWkWy zcUKT!-oI7maO0A5^r1y=0ZLczA#N^zwhY02cEwk0Mj}?c6*{oq%vU#OCH@pNy3Xd! zdf21?$DO;207pU{_r~eg!QsoXJUsa2I1ydj&ZqL1yYKAsH!@ryDiPL@1a6?|3&zU4t}4iFQCI6I0lbC^ZwFQv(qTZUR}X`w+oq_dC3KNGeAkLl za(%djys@y22SceDkN6JXM_GP&8@e0Mcax^^5qUb~p5Vr^mR*UJnBl~UxVRJ6TOnyy z{8Y63%0r7!bX|E$Amtw`?}#bC!{{vyM3XxHA?axvi;E0cTP&b%tcz}XmvHGkW3}t$ zWo;nwJ0v>P95-p-G$Afnz|T1%x;tgn*`~y`;#gR1vwX9D9>EvE$UcF0DvT z<-naAKf%Yr9cCtlenGLklbxOM?#z2{Gw4a8<0upRmgx`p{J00 z=y}L}^n56edB|$W>*&>&zK^VdUIR%DW(5OeAZCGerQio{2^)u3(D-}JQjB-dnj z>P$I^Vq@Jcb_Y?Cl*U@}SIB?!P9Jpkj#`TI*T@{a?-=)~C?_4aLGFHGT|2dLac0Vk ze|>QFTN?H*83Qh#DyCwn1L}zGIu2Nq4P@dX zJ6Xm!!5)&?eGzhHc1G=l!y>e09QIS!ui}Yu+eJAHZ5(AqJP9XN-#i*s+a=DsESCutbEOO=uSvz$GzPS9?je8oaxNPsEK6)qLV z9-PlBmmbUSqd?avzI2 zTm$13jEUMBQaY*5gg>Vf)aeArr%lj%EI4wl9RZEWwd{U=yx4jAETtKw&CcGj-aqUf zNfsl~VzOC=2!wmqiZw1VgQ&_}z(@olBHnFKlzHT+AY%K>Ox;pDqiC9BrWBpbs(zN* zk(RLNFsM_uNQyKUn@*G)!iy9rYu75|O>Nbm8YcmpPKTM%>FkENXKu~8i7RKM^>y;0 zHr0-5s4aC{wOEW-3Yk*vTFZ6PdMPKZ1KuIHL*}|tncaj`1FlApl8IXLg+D=zkQ0RE tPi4ub&?onsf(*4#8SsS`5{v%W#7;K-55)_LFPFHzVhM;s?-AJR{vU;KE0O>J diff --git a/modules/kube_open_dashboard.py b/modules/kube_open_dashboard.py deleted file mode 100644 index 99f8273..0000000 --- a/modules/kube_open_dashboard.py +++ /dev/null @@ -1,29 +0,0 @@ -import events -import requests - -class KubeOpenDashboard(object): - def __init__(self, task): - self.task = task - self.host = task['host'] - self.port = task['port'] or 80 - pass - - def execute(self): - try: - r = requests.get("http://{host}:{port}/api/v1/node?itemsPerPage=100".format(host=self.host, port=self.port)) - except requests.exceptions.ConnectionError: - return None - - ret = r.json() - if 'listMeta' in ret: - events.safe_print("KubeOpenDashboard :: Open Dashboard!", self.host) - - -events.handler.subscribe_event('OPEN_PORT_30000', KubeOpenDashboard) - -if __name__ == "__main__": - queue = list() - queue.append(KubeOpenDashboard({'host': '192.168.1.117', 'port': 30000})) - queue.append(KubeOpenDashboard({'host': '192.168.1.117', 'port': None})) - for i in queue: - i.execute() diff --git a/modules/kube_open_dashboard.pyc b/modules/kube_open_dashboard.pyc deleted file mode 100644 index be5cf263a67b56cb57452654e75730f826704799..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1609 zcmcgrOK;Oa5T11&P12S}DV)B3`2H36Vx_B_Ik~Irg-%li2R=y0mI@ zD*uC@z>Ncc3+^zpeIdb>I^M~6Jv)!@`<(kRTlxCnXV-_bt3dy+XzA}{0=fl^0J;~x zfUbbR0d!!8oE&@xI05+3%>e>Ld58)SJ z?whQSn<>WYo*wiQPX!kP3wrySmcB!VM&R*)r|MF7!a6x{x2g`k_Su}TH9oXL=2*Zn zfV2*sSl|cRvM?>A_ZTGJqI3XT)jXYO8@7W)8|$QrGGT{ysyQ-t%XoU|a%(P^v&HSc z#?g+;QB_)}MsJh1*6Mvq)KI6MkGMyTzcOb8wsoa0kV4^4>@OBu$Yk&<_dQ%!+y8b9dZHk(R~cfSki+FeC$- zT|h(=J3M-nJHvqFaRLreJQ2jyZOC(M0iq%dE1)(&2*DV=C+wmUQ#zp(c{p}pSOjIj zlp(6HqE3^t!kBM{mAh!`CfR{8X{%m8Vt6O5BL;U;_tLPw->k<;fGz|oqwPl;PUt`V`AA#`VmoTs~O%LcyTa7WsB*ouYDEvQCd#jc|OGLu=or^LPE;W q=J8UJTj0)c2s0dBDOwhXXQynv;PRTeB5IvmbMr)TSuD|SR{RFR&TAh4 diff --git a/modules/port_discovery.pyc b/modules/port_discovery.pyc deleted file mode 100644 index 34704db7b7ff380dc65a612ca421ff974e3d0a96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1867 zcmcgsO>f&q5S=9@%aWzWNt+-ahhBPM14M{&aMBtbR8X*>$dyHjB4zF} zbsGB+IR7Qb{DU0&mzsO>X5=(!fgW9%91mxPvor6_(EqE`dVl@nFs9RA9slp+wtqq- zxD;hX3!)*B;%ARCMMFi2Pf3lEIwfngU!$Q%NrQ|}6S&stEzt?lZ^IfHMYB51)+lL` zsnLl?$}}ix(cGi*1DSOW9KzS6S&Ji5B-o&&Op^fwteu0AK*0{AIo*UOsAN&YcD}Xr1;${VZ2mYO+bdOUT7cWiu z>Th5?cpmcGKSLOlj53M))GxOreyr*IJXh>rQ)FZUzPaUS9b2^3v9BGQ8N zAmEU`t3gf+>!je2ZQ>BSHQ7WI7uh+AhL45ahIn*$^m|dx?J|nZXulZQG9HanH?Fwb zZc*-!Y#e3ieKfDe=Ek(johe6)BB?TCM@uxVlP_I&m#^G9a%!FCsneQu%cYQt8Anzb zN@?Z|l*2#B5tu0HSP@#9t)$0Yg7UIK&Zk+8M*&Tzup6GP zC-qcD{8UTXT{P(faSTNiqc!#VxuErYY(uFB1Cy%FK zYjcRB;B_VPV1Kd#fn4ftSs5`sjWTNjHVG}E(8#B(Xt7ls8*9%N-;jsx0-wnr8S9Ph;)jUQ|82h63ph|H4w>R` z9YVID7|*ata%#x)C`)Fmv5m`gY*uR%US Date: Sun, 13 May 2018 17:02:39 +0300 Subject: [PATCH 5/8] Started adding KubeProxy hunting, basic services detection mechanism was added. The idea is discovering sub paths of resources in the api and sending them for hunting. Added Dashboard detection from Proxy. Also improved architecture of discovery/hunting from a "secure" perspective --- discovery/__init__.py | 5 +++-- discovery/dashboard.py | 11 +++++++---- discovery/proxy.py | 19 ++++++++++++++++++ hunting/__init__.py | 3 ++- hunting/dashboard.py | 31 ++++++++++++++--------------- hunting/proxy.py | 44 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- 7 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 discovery/proxy.py create mode 100644 hunting/proxy.py diff --git a/discovery/__init__.py b/discovery/__init__.py index 7d881ec..47e8904 100644 --- a/discovery/__init__.py +++ b/discovery/__init__.py @@ -1,6 +1,7 @@ -from .dashboard import KubeDashboard -from .ports import PortDiscovery from .hosts import HostDiscovery +from .ports import PortDiscovery +from .dashboard import KubeDashboard +from .proxy import KubeProxy __all__ = [HostDiscovery, KubeDashboard, PortDiscovery] diff --git a/discovery/dashboard.py b/discovery/dashboard.py index 6dcd8ec..6bd15a3 100644 --- a/discovery/dashboard.py +++ b/discovery/dashboard.py @@ -5,12 +5,15 @@ class KubeDashboard(object): def __init__(self, task): self.task = task self.host = task['host'] - self.port = task['port'] or 80 - pass + self.port = task['port'] if 'port' in task else 80 + @property + def secure(self): + # TODO: insert logic for detremining a secure/insecure dashboard is there + return False + def execute(self): - # TODO: insert logic for detremining dashboard/insecure dashboard is there - events.handler.publish_event('KUBE_DASHBOARD', {'host': self.host, 'port': self.port}) + events.handler.publish_event('KUBE_DASHBOARD', {"host": self.host, "port": self.port, "secure": self.secure}) events.handler.subscribe_event('OPEN_PORT_30000', KubeDashboard) diff --git a/discovery/proxy.py b/discovery/proxy.py new file mode 100644 index 0000000..5738936 --- /dev/null +++ b/discovery/proxy.py @@ -0,0 +1,19 @@ +import events +from collections import defaultdict +from requests import get + +class KubeProxy(object): + def __init__(self, task): + self.task = task + self.host = task['host'] + self.port = task['port'] or 8001 + + @property + def accesible(self): + return True + + def execute(self): + if self.accesible: + events.handler.publish_event('KUBE_PROXY', self.task) + +events.handler.subscribe_event('OPEN_PORT_8001', KubeProxy) \ No newline at end of file diff --git a/hunting/__init__.py b/hunting/__init__.py index d73b2a6..ee4f108 100644 --- a/hunting/__init__.py +++ b/hunting/__init__.py @@ -1,4 +1,5 @@ from .dashboard import KubeDashboard +from .proxy import KubeProxy -__all__ = [KubeDashboard] +__all__ = [KubeDashboard, KubeProxy] diff --git a/hunting/dashboard.py b/hunting/dashboard.py index 3cff369..35fc6ab 100644 --- a/hunting/dashboard.py +++ b/hunting/dashboard.py @@ -6,24 +6,23 @@ class KubeDashboard(object): def __init__(self, task): self.host = task['host'] self.port = task['port'] or 30000 + self.secure = task['secure'] if 'secure' in task else False + self.location = task["location"] if "location" in task else "" + + @property + def accessible(self): + protocol = "https" if self.secure else "http" + r = requests.get("{protocol}://{host}:{port}/{loc}".format(protocol=protocol, host=self.host, port=self.port, loc=self.location)) + return r.status_code == 200 def execute(self): - print("KUBEDASHBOARD At: {} {}".format(self.host, self.port)) - if self.secured: - safe_print("SECURED DASHBOARD") + if not self.accessible: + return + + if self.secure: + safe_print("SECURED DASHBOARD AT {}:{}/{}".format(self.host, self.port, self.location)) else: - safe_print("INSECURE DASHBOARD") + safe_print("INSECURE DASHBOARD AT {}:{}/{}".format(self.host, self.port, self.location)) - @property - def secured(self): - try: - r = requests.get("http://{host}:{port}/api/v1/node?itemsPerPage=100".format(host=self.host, port=self.port)) - except requests.exceptions.ConnectionError: - return True - ret = r.json() - if 'listMeta' in ret: - return False - return True - -events.handler.subscribe_event('KUBE_DASHBOARD', KubeDashboard) \ No newline at end of file +events.handler.subscribe_event('KUBE_DASHBOARD', KubeDashboard) diff --git a/hunting/proxy.py b/hunting/proxy.py new file mode 100644 index 0000000..406186e --- /dev/null +++ b/hunting/proxy.py @@ -0,0 +1,44 @@ +import events +from requests import get +from enum import Enum + +class Service(Enum): + DASHBOARD = "kubernetes-dashboard" + +class KubeProxy(object): + def __init__(self, task): + self.host = task['host'] + self.port = task['port'] or 8001 + + self.api_url = "http://{host}:{port}/api/v1".format(host=self.host, port=self.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: + events.handler.publish_event('KUBE_DASHBOARD', {"host": self.host, "port": self.port, "location": curr_path, 'secure': False}) + + @property + def namespaces(self): + resource_json = get(self.api_url + "/namespaces").json() + return self.extract_names(resource_json) + + @property + def services(self): + # map between namespaces and service names + services = dict() + for namespace in self.namespaces: + resource_path = "/namespaces/{ns}/services".format(ns=namespace) + resource_json = get(self.api_url + resource_path).json() + services[namespace] = self.extract_names(resource_json) + return services + + @staticmethod + def extract_names(resource_json): + names = list() + for item in resource_json["items"]: + names.append(item["metadata"]["name"]) + return names + +events.handler.subscribe_event('KUBE_PROXY', KubeProxy) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b5de6b8..a0951dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ selenium pillow netaddr chromedriver_binary -netifaces \ No newline at end of file +netifaces +enum \ No newline at end of file From 376519614058ee073c2135e155860ed2dd08c056 Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Mon, 14 May 2018 17:13:13 +0300 Subject: [PATCH 6/8] Changed events architecture, using class objects for all events Added new subscription method for events, using a "subscribe" decorator. Added an optional predicate function to events, for optional filtering on triggered time. used mainly for OpenPortEvent. Added proper logging and argparse Added cleanup of threads when exiting/finishing --- discovery/dashboard.py | 14 ++++---- discovery/hosts.py | 2 +- discovery/ports.py | 19 +++------- discovery/proxy.py | 12 +++---- events.py | 82 ++++++++++++++++++++++++++++++++++-------- hunting/dashboard.py | 24 ++++++------- hunting/proxy.py | 13 +++---- kube-hunter.py | 13 ++++--- log.py | 14 ++++++++ 9 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 log.py diff --git a/discovery/dashboard.py b/discovery/dashboard.py index 6bd15a3..2a2cd5e 100644 --- a/discovery/dashboard.py +++ b/discovery/dashboard.py @@ -1,19 +1,17 @@ -import events +from events import handler, OpenPortEvent, KubeDashboardEvent 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'] if 'port' in task else 80 + self.host = task.host + self.port = task.port @property def secure(self): # TODO: insert logic for detremining a secure/insecure dashboard is there return False - + def execute(self): - events.handler.publish_event('KUBE_DASHBOARD', {"host": self.host, "port": self.port, "secure": self.secure}) - - -events.handler.subscribe_event('OPEN_PORT_30000', KubeDashboard) + handler.publish_event(KubeDashboardEvent(host=self.host, port=self.port, secure=self.secure)) diff --git a/discovery/hosts.py b/discovery/hosts.py index 263ca78..b024694 100644 --- a/discovery/hosts.py +++ b/discovery/hosts.py @@ -13,4 +13,4 @@ class HostDiscovery(object): if addresses: subnet = IPNetwork('{0}/24'.format(addresses[0])) for single_ip in IPNetwork(subnet): - events.handler.publish_event('NEW_HOST', {'host': single_ip}) \ No newline at end of file + events.handler.publish_event(events.NewHostEvent(host=single_ip)) \ No newline at end of file diff --git a/discovery/ports.py b/discovery/ports.py index 7f66eab..72ead53 100644 --- a/discovery/ports.py +++ b/discovery/ports.py @@ -1,17 +1,18 @@ +from events import handler, NewHostEvent, OpenPortEvent from socket import socket -import events - default_ports = [8001, 10250, 10255, 30000] +@handler.subscribe(NewHostEvent) class PortDiscovery(object): def __init__(self, task): - self.host = task['host'] + self.host = task.host + self.port = task.port def execute(self): for single_port in default_ports: if self.test_connection(self.host, single_port): - events.handler.publish_event('OPEN_PORT_{port}'.format(port=single_port), {'host': self.host, 'port': single_port}) + handler.publish_event(OpenPortEvent(host=self.host, port=single_port)) @staticmethod def test_connection(host, port): @@ -22,13 +23,3 @@ class PortDiscovery(object): if success == 0: return True return False - - -events.handler.subscribe_event('NEW_HOST', PortDiscovery) - -if __name__ == "__main__": - queue = list() - queue.append(PortDiscovery({'host': '192.168.1.117'})) - queue.append(PortDiscovery({'host': '192.168.1.101'})) - for i in queue: - i.execute() diff --git a/discovery/proxy.py b/discovery/proxy.py index 5738936..b215f1a 100644 --- a/discovery/proxy.py +++ b/discovery/proxy.py @@ -1,12 +1,13 @@ -import events +from events import handler, OpenPortEvent, KubeProxyEvent from collections import defaultdict from requests import get +@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 + self.host = task.host + self.port = task.port or 8001 @property def accesible(self): @@ -14,6 +15,5 @@ class KubeProxy(object): def execute(self): if self.accesible: - events.handler.publish_event('KUBE_PROXY', self.task) - -events.handler.subscribe_event('OPEN_PORT_8001', KubeProxy) \ No newline at end of file + handler.publish_event(KubeProxyEvent(host=self.host, port=self.port)) + \ No newline at end of file diff --git a/events.py b/events.py index 79c9f8a..b9dbb0e 100644 --- a/events.py +++ b/events.py @@ -1,45 +1,99 @@ +import logging from Queue import Queue from threading import Thread from collections import defaultdict from threading import Lock - +from abc import ABCMeta # Inherits Queue object, handles events asynchronously class EventQueue(Queue, object): def __init__(self, num_worker=10): super(EventQueue, self).__init__() self.hooks = defaultdict(list) + self.running = True for i in range(num_worker): t = Thread(target=self.worker) t.daemon = True t.start() - def publish_event(self, name, item): - safe_print('Event {} got published with {}'.format(name, item)) - if name in self.hooks: - for single_hook in self.hooks[name]: - self.put(single_hook(item)) + # decorator wrapping for easy subscription + def subscribe(self, event, hook=None, predicate=None): + def wrapper(hook): + self.subscribe_event(event, hook=hook, predicate=predicate) + return hook + return wrapper + + # getting uninstantiated event object + def subscribe_event(self, event, hook=None, predicate=None): + logging.debug('{} subscribed to {}'.format(event.__name__, hook)) + if hook not in self.hooks[event.__name__]: + self.hooks[event.__name__].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 + self.put(hook(event)) - def subscribe_event(self, name, callback): - safe_print('Subscribed: {} to {} '.format(name, callback)) - if callback not in self.hooks[name]: - self.hooks[name].append(callback) - # executes callbacks on dedicated thread def worker(self): - while True: + while self.running: hook = self.get() hook.execute() self.task_done() + def free(self): + self.running = False + with self.mutex: + self.queue.clear() +handler = EventQueue(500) + print_lock = Lock() def safe_print(*args, **kargs): with print_lock: print(args, kargs) + -handler = EventQueue(500) - \ No newline at end of file +""" Parent Event Objects """ +class NetworkEvent(object): + def __init__(self, host, port): + self.host = host + self.port = port + +class ServiceEvent(NetworkEvent): + 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) + + def __str__(self): + return str(self.host) + +class OpenPortEvent(NetworkEvent): + def __init__(self, host, port): + super(OpenPortEvent, self).__init__(port=port, host=host) + + 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 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 diff --git a/hunting/dashboard.py b/hunting/dashboard.py index 35fc6ab..a576f53 100644 --- a/hunting/dashboard.py +++ b/hunting/dashboard.py @@ -1,28 +1,24 @@ -import events +from events import handler, KubeDashboardEvent +import logging import requests -from events import safe_print +@handler.subscribe(KubeDashboardEvent) class KubeDashboard(object): def __init__(self, task): - self.host = task['host'] - self.port = task['port'] or 30000 - self.secure = task['secure'] if 'secure' in task else False - self.location = task["location"] if "location" in task else "" - + self.task = task + @property def accessible(self): - protocol = "https" if self.secure else "http" - r = requests.get("{protocol}://{host}:{port}/{loc}".format(protocol=protocol, host=self.host, port=self.port, loc=self.location)) + 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)) return r.status_code == 200 def execute(self): if not self.accessible: return - if self.secure: - safe_print("SECURED DASHBOARD AT {}:{}/{}".format(self.host, self.port, self.location)) + if self.task.secure: + logging.info("SECURED DASHBOARD AT {}:{}/{}".format(self.task.host, self.task.port, self.task.location)) else: - safe_print("INSECURE DASHBOARD AT {}:{}/{}".format(self.host, self.port, self.location)) + logging.info("INSECURED DASHBOARD AT {}:{}/{}".format(self.task.host, self.task.port, self.task.location)) - -events.handler.subscribe_event('KUBE_DASHBOARD', KubeDashboard) diff --git a/hunting/proxy.py b/hunting/proxy.py index 406186e..95ca0cc 100644 --- a/hunting/proxy.py +++ b/hunting/proxy.py @@ -1,23 +1,22 @@ -import events +from events import handler, KubeProxyEvent, KubeDashboardEvent from requests import get from enum import Enum class Service(Enum): DASHBOARD = "kubernetes-dashboard" +@handler.subscribe(KubeProxyEvent) class KubeProxy(object): def __init__(self, task): - self.host = task['host'] - self.port = task['port'] or 8001 - - self.api_url = "http://{host}:{port}/api/v1".format(host=self.host, port=self.port) + self.task = task + self.api_url = "http://{host}:{port}/api/v1".format(host=self.task.host, port=self.task.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: - events.handler.publish_event('KUBE_DASHBOARD', {"host": self.host, "port": self.port, "location": curr_path, 'secure': False}) + handler.publish_event(KubeDashboardEvent(host=self.task.host, port=self.task.port, location=curr_path, secure=False)) @property def namespaces(self): @@ -40,5 +39,3 @@ class KubeProxy(object): for item in resource_json["items"]: names.append(item["metadata"]["name"]) return names - -events.handler.subscribe_event('KUBE_PROXY', KubeProxy) \ No newline at end of file diff --git a/kube-hunter.py b/kube-hunter.py index 4fbcd42..5c0c8c5 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -1,18 +1,23 @@ +import log +import logging +from events import handler import discovery import hunting -import threading import time import sys def main(): + logging.info("Started") try: discovery.HostDiscovery({}).execute() # Blocking to see discovery output while(True): - time.sleep(1) + time.sleep(100) except KeyboardInterrupt: - print('User stopped kubehunter') - sys.exit(1) + logging.info("Kube-Hunter Stopped") + finally: + handler.free() + logging.debug("Cleaned Queue") if __name__ == '__main__': main() \ No newline at end of file diff --git a/log.py b/log.py new file mode 100644 index 0000000..dc9526b --- /dev/null +++ b/log.py @@ -0,0 +1,14 @@ +import logging +import argparse + +parser = argparse.ArgumentParser(description='Kubehunter, hunting weak kubernetes clusters') +parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='WARNING', help="set output level, options are:\nDEBUG INFO WARNING") + +args = parser.parse_args() + +try: + loglevel = getattr(logging, args.log.upper()) +except: + loglevel = logging.INFO + +logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s') From 672c59f576a08774d26ff18238c4c17d3b2432a4 Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Tue, 15 May 2018 15:57:36 +0300 Subject: [PATCH 7/8] Added events/ folder. Added dynamic imports for all modules inside: 'events/', 'discovery/', 'hunting/' (you can now add new files with new implementations, and not worry about imports.) Changed port timeout to be 1.5 seconds, more reliable results. Changed default log level to INFO --- discovery/__init__.py | 12 ++++----- discovery/ports.py | 2 +- discovery/proxy.py | 1 + events/__init__.py | 11 ++++++++ events/default_types.py | 41 +++++++++++++++++++++++++++++ events.py => events/handler.py | 48 +++------------------------------- hunting/__init__.py | 10 ++++--- hunting/proxy.py | 5 ++-- kube-hunter.py | 3 ++- log.py | 4 +-- 10 files changed, 76 insertions(+), 61 deletions(-) create mode 100644 events/__init__.py create mode 100644 events/default_types.py rename events.py => events/handler.py (55%) diff --git a/discovery/__init__.py b/discovery/__init__.py index 47e8904..e1e1462 100644 --- a/discovery/__init__.py +++ b/discovery/__init__.py @@ -1,7 +1,7 @@ -from .hosts import HostDiscovery -from .ports import PortDiscovery -from .dashboard import KubeDashboard -from .proxy import KubeProxy - -__all__ = [HostDiscovery, KubeDashboard, PortDiscovery] +from os.path import dirname, basename, isfile +import glob +# dynamically importing all modules in folder +files = glob.glob(dirname(__file__)+"/*.py") +for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')): + exec('from {} import *'.format(module_name)) \ No newline at end of file diff --git a/discovery/ports.py b/discovery/ports.py index 72ead53..19acc09 100644 --- a/discovery/ports.py +++ b/discovery/ports.py @@ -17,7 +17,7 @@ class PortDiscovery(object): @staticmethod def test_connection(host, port): s = socket() - s.settimeout(1) + s.settimeout(1.5) success = s.connect_ex((str(host), port)) s.close() if success == 0: diff --git a/discovery/proxy.py b/discovery/proxy.py index b215f1a..bbcc5c6 100644 --- a/discovery/proxy.py +++ b/discovery/proxy.py @@ -1,6 +1,7 @@ from events import handler, OpenPortEvent, KubeProxyEvent from collections import defaultdict from requests import get +import logging @handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 8001) class KubeProxy(object): diff --git a/events/__init__.py b/events/__init__.py new file mode 100644 index 0000000..498da7f --- /dev/null +++ b/events/__init__.py @@ -0,0 +1,11 @@ +from os.path import dirname, basename, isfile +import glob + +# explicitly importing the event handler +from handler import handler + +# dynamically importing all modules in folder +files = glob.glob(dirname(__file__)+"/*.py") +for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')): + if module_name != "handler": + exec('from {} import *'.format(module_name)) \ No newline at end of file diff --git a/events/default_types.py b/events/default_types.py new file mode 100644 index 0000000..e5076f4 --- /dev/null +++ b/events/default_types.py @@ -0,0 +1,41 @@ +# class EventMeta(type): +# def __init__(cls, name, bases, dct): +# super(EventMeta, cls).__init__(name, bases, dct) + +""" Parent Event Objects """ +class NetworkEvent(object): + # __metaclass__ = EventMeta + def __init__(self, host, port): + self.host = host + self.port = port + +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) + + def __str__(self): + return str(self.host) + +class OpenPortEvent(NetworkEvent): + def __init__(self, host, port): + super(OpenPortEvent, self).__init__(port=port, host=host) + + 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 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 diff --git a/events.py b/events/handler.py similarity index 55% rename from events.py rename to events/handler.py index b9dbb0e..408a3bf 100644 --- a/events.py +++ b/events/handler.py @@ -40,60 +40,18 @@ class EventQueue(Queue, object): continue self.put(hook(event)) - # executes callbacks on dedicated thread + # executes callbacks on dedicated thread as a daemon def worker(self): while self.running: hook = self.get() hook.execute() self.task_done() + # stops execution of all daemons def free(self): self.running = False with self.mutex: self.queue.clear() -handler = EventQueue(500) - -print_lock = Lock() -def safe_print(*args, **kargs): - with print_lock: - print(args, kargs) - - - -""" Parent Event Objects """ -class NetworkEvent(object): - def __init__(self, host, port): - self.host = host - self.port = port - -class ServiceEvent(NetworkEvent): - 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) - - def __str__(self): - return str(self.host) - -class OpenPortEvent(NetworkEvent): - def __init__(self, host, port): - super(OpenPortEvent, self).__init__(port=port, host=host) - - 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 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 +handler = EventQueue(800) \ No newline at end of file diff --git a/hunting/__init__.py b/hunting/__init__.py index ee4f108..e1e1462 100644 --- a/hunting/__init__.py +++ b/hunting/__init__.py @@ -1,5 +1,7 @@ -from .dashboard import KubeDashboard -from .proxy import KubeProxy - -__all__ = [KubeDashboard, KubeProxy] +from os.path import dirname, basename, isfile +import glob +# dynamically importing all modules in folder +files = glob.glob(dirname(__file__)+"/*.py") +for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')): + exec('from {} import *'.format(module_name)) \ No newline at end of file diff --git a/hunting/proxy.py b/hunting/proxy.py index 95ca0cc..4627ecc 100644 --- a/hunting/proxy.py +++ b/hunting/proxy.py @@ -1,6 +1,7 @@ -from events import handler, KubeProxyEvent, KubeDashboardEvent -from requests import get from enum import Enum +from requests import get +from events import KubeDashboardEvent, KubeProxyEvent, handler + class Service(Enum): DASHBOARD = "kubernetes-dashboard" diff --git a/kube-hunter.py b/kube-hunter.py index 5c0c8c5..47f917b 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -1,10 +1,11 @@ import log -import logging + from events import handler import discovery import hunting import time import sys +import logging def main(): logging.info("Started") diff --git a/log.py b/log.py index dc9526b..829d2fa 100644 --- a/log.py +++ b/log.py @@ -2,13 +2,13 @@ import logging import argparse parser = argparse.ArgumentParser(description='Kubehunter, hunting weak kubernetes clusters') -parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='WARNING', help="set output level, options are:\nDEBUG INFO WARNING") +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: - loglevel = logging.INFO + pass logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s') From 290f87de70c7806033cf382af9a9ca5fa03e1297 Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Thu, 24 May 2018 15:25:43 +0300 Subject: [PATCH 8/8] 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: