mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-10 11:17:05 +00:00
Delete everything of Kube Hunter 1.0
This commit is contained in:
39
README.md
39
README.md
@@ -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
|
||||
79
discovery.py
79
discovery.py
@@ -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")
|
||||
@@ -1,3 +0,0 @@
|
||||
from .dashboard import *
|
||||
from .kubelet import *
|
||||
from .proxy import *
|
||||
@@ -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()
|
||||
@@ -1,9 +0,0 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class Hunter(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def hunt(self, *args, **kwargs):
|
||||
pass
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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:]]))
|
||||
@@ -1,4 +0,0 @@
|
||||
selenium
|
||||
pillow
|
||||
netaddr
|
||||
chromedriver_binary
|
||||
87
services.py
87
services.py
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user