From cd880ec50ee4304b24064800ce4e44c873a1b887 Mon Sep 17 00:00:00 2001 From: Shir Date: Sun, 6 May 2018 19:50:00 +0300 Subject: [PATCH] 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