Delete everything of Kube Hunter 1.0

This commit is contained in:
Shir
2018-05-06 19:50:00 +03:00
parent c8c8cd9ebd
commit cd880ec50e
11 changed files with 0 additions and 462 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -1,3 +0,0 @@
from .dashboard import *
from .kubelet import *
from .proxy import *

View File

@@ -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()

View File

@@ -1,9 +0,0 @@
from abc import ABCMeta, abstractmethod
class Hunter(object):
__metaclass__ = ABCMeta
@abstractmethod
def hunt(self, *args, **kwargs):
pass

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:]]))

View File

@@ -1,4 +0,0 @@
selenium
pillow
netaddr
chromedriver_binary

View File

@@ -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

View File

@@ -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