mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-08 18:26:48 +00:00
1. completely transferred all event types to their corresponding module
2. started working on results table. 3. *added convention* from now on, every vulnerability/service event, should have a __doc__ that describes them. notice the new get_name(), component, and explain() attributes that needs to be implemented as well.
This commit is contained in:
@@ -20,12 +20,12 @@ import log
|
||||
import modules
|
||||
from modules.discovery import HostDiscovery
|
||||
from modules.events import handler
|
||||
from modules.events.types import HostScanEvent
|
||||
from modules.discovery.hosts import HostScanEvent
|
||||
|
||||
def main():
|
||||
logging.info("Started")
|
||||
try:
|
||||
handler.publish_event(HostScanEvent(pod=args.pod))
|
||||
handler.publish_event(HostScanEvent(pod=args.pod, active=True))
|
||||
# Blocking to see discovery output
|
||||
while(True):
|
||||
time.sleep(100)
|
||||
@@ -34,6 +34,7 @@ def main():
|
||||
finally:
|
||||
handler.free()
|
||||
logging.debug("Cleaned Queue")
|
||||
log.print_results()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import logging
|
||||
from prettytable import PrettyTable
|
||||
from modules.events import handler
|
||||
from modules.events.types import Vulnerability, ServiceEvent
|
||||
from modules.events.types import Vulnerability, Information, Service
|
||||
from modules.discovery.kubelet import KubeletExposedHandler
|
||||
|
||||
services = list()
|
||||
vulnerabilities = list()
|
||||
informations = list()
|
||||
|
||||
@handler.subscribe(Vulnerability)
|
||||
class VulnerabilityReport(object):
|
||||
@@ -9,11 +15,24 @@ class VulnerabilityReport(object):
|
||||
|
||||
def execute(self):
|
||||
logging.info("[VULNERABILITY - {name}] {desc}".format(
|
||||
name=self.vulnerability.name,
|
||||
desc=self.vulnerability.explain(),
|
||||
name=self.vulnerability.name,
|
||||
desc=self.vulnerability.explain(),
|
||||
))
|
||||
vulnerabilities.append(self.vulnerability)
|
||||
|
||||
@handler.subscribe(ServiceEvent)
|
||||
@handler.subscribe(Information)
|
||||
class ClusterInformation(object):
|
||||
def __init__(self, event):
|
||||
self.information = event
|
||||
|
||||
def execute(self):
|
||||
logging.info("[INFORMATION - {name}] {desc}".format(
|
||||
name=self.information.get_name(),
|
||||
desc=self.information.explain(),
|
||||
))
|
||||
informations.append(self.information)
|
||||
|
||||
@handler.subscribe(Service)
|
||||
class OpenServiceReport(object):
|
||||
def __init__(self, event):
|
||||
self.service = event
|
||||
@@ -24,4 +43,22 @@ class OpenServiceReport(object):
|
||||
desc=self.service.desc,
|
||||
host=self.service.host,
|
||||
port=self.service.port
|
||||
))
|
||||
))
|
||||
services.append(self.service)
|
||||
|
||||
|
||||
|
||||
def print_results():
|
||||
services_table = PrettyTable(["Service", "Location", "Description"])
|
||||
for service in services:
|
||||
services_table.add_row([service.get_name(), "{}:{}".format(service.host, service.port), service.explain()])
|
||||
|
||||
vuln_table = PrettyTable(["Location", "From Component", "Vulnerability", "Description"])
|
||||
for vuln in vulnerabilities:
|
||||
vuln_table.add_row(["{}:{}".format(vuln.host, vuln.port), vuln.component.name, vuln.get_name(), vuln.explain()])
|
||||
|
||||
print "\nOpen Services:"
|
||||
print services_table
|
||||
print "\nVulnerabilities:"
|
||||
print vuln_table
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
from ..types import Hunter
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import KubeDashboardEvent, OpenPortEvent
|
||||
from ..events.types import Event, Service, OpenPortEvent
|
||||
from ..types import Hunter
|
||||
|
||||
class KubeDashboardEvent(Service, Event):
|
||||
"""Allows multiple arbitrary operations on the cluster from all connections"""
|
||||
def __init__(self, path="/", secure=False):
|
||||
self.path = path
|
||||
self.secure
|
||||
Service.__init__(self, name="Kubernetes Dashboard")
|
||||
|
||||
@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 30000)
|
||||
class KubeDashboard(Hunter):
|
||||
@@ -15,8 +22,11 @@ class KubeDashboard(Hunter):
|
||||
|
||||
@property
|
||||
def secure(self):
|
||||
# TODO: insert logic for detremining a secure/insecure dashboard is there
|
||||
default = json.loads(requests.get("http://{}:{}/api/v1/service/default".format(self.host, self.port)).text)
|
||||
if "errors" in default and len(default["errors"]) == 0:
|
||||
return False
|
||||
return False
|
||||
|
||||
def execute(self):
|
||||
self.publish_event(KubeDashboardEvent())
|
||||
if not self.secure:
|
||||
self.publish_event(KubeDashboardEvent())
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
import requests
|
||||
from netaddr import IPNetwork
|
||||
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) # disables scapy's warnings
|
||||
from scapy.all import ICMP, IP, Ether, srp1
|
||||
|
||||
from netifaces import AF_INET, ifaddresses, interfaces
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import HostScanEvent, NewHostEvent
|
||||
from ..events.types import Event, NewHostEvent
|
||||
from ..types import Hunter
|
||||
|
||||
# for comparing prefixes
|
||||
class InterfaceTypes(Enum):
|
||||
LOCALHOST = "127"
|
||||
class HostScanEvent(Event):
|
||||
def __init__(self, pod=False, active=False):
|
||||
self.pod = pod
|
||||
self.active = active # flag to specify whether to get actual data from vulnerabilities
|
||||
self.auth_token = self.get_auth_token()
|
||||
self.client_cert = self.get_client_cert()
|
||||
|
||||
def get_auth_token(self):
|
||||
if self.pod:
|
||||
with open("/run/secrets/kubernetes.io/serviceaccount/token") as token_file:
|
||||
return token_file.read()
|
||||
return None
|
||||
|
||||
def get_client_cert(self):
|
||||
if self.pod:
|
||||
return "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
return None
|
||||
|
||||
@handler.subscribe(HostScanEvent)
|
||||
class HostDiscovery(Hunter):
|
||||
@@ -26,22 +38,50 @@ class HostDiscovery(Hunter):
|
||||
def execute(self):
|
||||
logging.info("Discovering Open Kubernetes Services...")
|
||||
if self.event.pod:
|
||||
self.scan_nodes()
|
||||
if self.is_azure_cluster():
|
||||
self.azure_metadata_discovery()
|
||||
else:
|
||||
self.traceroute_discovery()
|
||||
else:
|
||||
# self.publish_event(NewHostEvent(host="acs954agent1.westus2.cloudapp.azure.com")) # test cluster
|
||||
self.scan_interfaces()
|
||||
|
||||
def is_azure_cluster(self):
|
||||
try:
|
||||
if requests.get("http://169.254.169.254/metadata/instance?api-version=2017-08-01", headers={"Metadata":"true"}).status_code == 200:
|
||||
return True
|
||||
except Exception as ex:
|
||||
logging.debug("Not azure cluster " + ex.message)
|
||||
return False
|
||||
|
||||
# for pod scanning
|
||||
def scan_nodes(self):
|
||||
def traceroute_discovery(self):
|
||||
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) # disables scapy's warnings
|
||||
from scapy.all import ICMP, IP, Ether, srp1
|
||||
|
||||
node_internal_ip = srp1(Ether() / IP(dst="google.com" , ttl=1) / ICMP(), verbose=0)[IP].src
|
||||
for ip in self.generate_subnet(ip=node_internal_ip, sn="24"):
|
||||
self.publish_event(NewHostEvent(host=ip))
|
||||
|
||||
# quering azure's interface metadata api | works only from a pod
|
||||
def azure_metadata_discovery(self):
|
||||
machine_metadata = json.loads(requests.get("http://169.254.169.254/metadata/instance?api-version=2017-08-01", headers={"Metadata":"true"}).text)
|
||||
for interface in machine_metadata["network"]["interface"]:
|
||||
address, subnet = interface["ipv4"]["subnet"][0]["address"], interface["ipv4"]["subnet"][0]["prefix"]
|
||||
for ip in self.generate_subnet(address, sn=subnet):
|
||||
self.publish_event(NewHostEvent(host=ip))
|
||||
|
||||
# for normal scanning
|
||||
def scan_interfaces(self):
|
||||
for ip in self.generate_interfaces_subnet():
|
||||
handler.publish_event(NewHostEvent(host=ip))
|
||||
|
||||
# generator, generating a subnet by given a cidr
|
||||
def generate_subnet(self, ip, sn="24"):
|
||||
subnet = IPNetwork('{ip}/{sn}'.format(ip=ip, sn=sn))
|
||||
for ip in IPNetwork(subnet):
|
||||
yield ip
|
||||
|
||||
# generate all subnets from all internal network interfaces
|
||||
def generate_interfaces_subnet(self, sn='24'):
|
||||
for ifaceName in interfaces():
|
||||
@@ -50,9 +90,7 @@ class HostDiscovery(Hunter):
|
||||
continue
|
||||
for ip in self.generate_subnet(ip, sn):
|
||||
yield ip
|
||||
|
||||
# generator, generating a subnet by given a cidr
|
||||
def generate_subnet(self, ip, sn="24"):
|
||||
subnet = IPNetwork('{ip}/{sn}'.format(ip=ip, sn=sn))
|
||||
for ip in IPNetwork(subnet):
|
||||
yield ip
|
||||
|
||||
# for comparing prefixes
|
||||
class InterfaceTypes(Enum):
|
||||
LOCALHOST = "127"
|
||||
@@ -7,10 +7,45 @@ import requests
|
||||
import urllib3
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import (OpenPortEvent, ReadOnlyKubeletEvent,
|
||||
SecureKubeletEvent, Vulnerability, Event)
|
||||
from ..events.types import OpenPortEvent, Kubelet, Vulnerability, Event, Service
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
""" Services """
|
||||
class ReadOnlyKubeletEvent(Service, Event):
|
||||
"""Exposes specific handlers which disclose sensitive information about the cluster"""
|
||||
def __init__(self):
|
||||
Service.__init__(self, name="Kubelet API (readonly)")
|
||||
|
||||
class SecureKubeletEvent(Service, Event):
|
||||
"""Exposes handlers that can perform unwanted operations on pods/containers"""
|
||||
def __init__(self, cert=False, token=False):
|
||||
self.cert = cert
|
||||
self.token = token
|
||||
Service.__init__(self, name="Kubelet API")
|
||||
|
||||
""" Vulnerabilities """
|
||||
class PodsHandler:
|
||||
"""Exposes sensitive information about pods that are bound to the node"""
|
||||
name="/pods"
|
||||
|
||||
class KubeletExposedHandler(Vulnerability, Event):
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
Vulnerability.__init__(self, Kubelet, "Handler Exposure")
|
||||
|
||||
def get_name(self):
|
||||
return "{} - {}".format(self.name, self.handler.name)
|
||||
|
||||
def explain(self):
|
||||
return self.handler.__doc__
|
||||
|
||||
class AnonymousAuthEnabled(Vulnerability, Event):
|
||||
"""Anonymous Auth to the kubelet, exposes secure access to all requests on the kubelet"""
|
||||
def __init__(self):
|
||||
Vulnerability.__init__(self, Kubelet, "Anonymous Authentication")
|
||||
|
||||
def proof(self):
|
||||
pass # TODO: decide on an appropriate proof
|
||||
|
||||
@handler.subscribe(OpenPortEvent, predicate= lambda x: x.port == 10255 or x.port == 10250)
|
||||
class KubeletDiscovery(Hunter):
|
||||
@@ -21,13 +56,13 @@ class KubeletDiscovery(Hunter):
|
||||
logging.debug(self.event.host)
|
||||
r = requests.get("http://{host}:{port}/pods".format(host=self.event.host, port=self.event.port))
|
||||
if r.status_code == 200:
|
||||
self.publish_event(KubeletOpenHandler(handler="pods"))
|
||||
self.publish_event(KubeletExposedHandler(handler=PodsHandler))
|
||||
self.publish_event(ReadOnlyKubeletEvent())
|
||||
|
||||
def get_secure_access(self):
|
||||
event = SecureKubeletEvent()
|
||||
if self.ping_kubelet(authenticate=False) == 200:
|
||||
self.publish_event(KubeletOpenHandler(handler="pods"))
|
||||
self.publish_event(KubeletExposedHandler(handler=PodsHandler))
|
||||
self.publish_event(AnonymousAuthEnabled())
|
||||
event.anonymous_auth = True
|
||||
# anonymous authentication is disabled
|
||||
@@ -45,8 +80,11 @@ class KubeletDiscovery(Hunter):
|
||||
if self.event.client_cert:
|
||||
r.cert = self.event.client_cert
|
||||
r.verify = False
|
||||
return r.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port)).status_code
|
||||
|
||||
try:
|
||||
return r.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port)).status_code
|
||||
except Exception as ex:
|
||||
logging.debug("Failed pinging secured kubelet {} : {}".format(self.event.host, ex.message))
|
||||
|
||||
def execute(self):
|
||||
if self.event.port == KubeletPorts.SECURED.value:
|
||||
self.get_secure_access()
|
||||
@@ -54,23 +92,6 @@ class KubeletDiscovery(Hunter):
|
||||
self.get_read_only_access()
|
||||
|
||||
|
||||
""" Types """
|
||||
class KubeletOpenHandler(Vulnerability, Event):
|
||||
def __init__(self, handler, **kargs):
|
||||
self.handler = handler
|
||||
Vulnerability.__init__(self, name="Kubelet Exposure", **kargs)
|
||||
|
||||
def explain(self):
|
||||
return "Handler - {}/ Kubelet Api - {}:{}".format(self.handler, self.host, self.port)
|
||||
|
||||
|
||||
class AnonymousAuthEnabled(Vulnerability, Event):
|
||||
def __init__(self, **kargs):
|
||||
Vulnerability.__init__(self, name="Anonymous Authentication", **kargs)
|
||||
|
||||
def explain(self):
|
||||
return "Kubelet - {}:{}".format(self.host, self.port)
|
||||
|
||||
class KubeletPorts(Enum):
|
||||
SECURED = 10250
|
||||
READ_ONLY = 10255
|
||||
|
||||
@@ -5,8 +5,11 @@ from ..types import Hunter
|
||||
from requests import get
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import KubeProxyEvent, OpenPortEvent
|
||||
from ..events.types import Service, Event, OpenPortEvent
|
||||
|
||||
class KubeProxyEvent(Event, Service):
|
||||
def __init__(self):
|
||||
Service.__init__(self, name="Kubernetes Proxy")
|
||||
|
||||
@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 8001)
|
||||
class KubeProxy(Hunter):
|
||||
|
||||
@@ -5,6 +5,9 @@ from collections import defaultdict
|
||||
from Queue import Queue
|
||||
from threading import Lock, Thread
|
||||
|
||||
working_count = 0
|
||||
lock = Lock()
|
||||
|
||||
# Inherits Queue object, handles events asynchronously
|
||||
class EventQueue(Queue, object):
|
||||
def __init__(self, num_worker=10):
|
||||
|
||||
@@ -2,7 +2,6 @@ from os.path import dirname, basename, isfile
|
||||
import glob
|
||||
|
||||
from common import *
|
||||
from information import *
|
||||
|
||||
# dynamically importing all modules in folder
|
||||
files = glob.glob(dirname(__file__)+"/*.py")
|
||||
|
||||
@@ -22,31 +22,68 @@ class Event(object):
|
||||
return history
|
||||
|
||||
|
||||
""" Information Fathers """
|
||||
# TODO: make explain an abstract method.
|
||||
class ServiceEvent(object):
|
||||
def __init__(self, name, data=""):
|
||||
"""Kubernetes Components"""
|
||||
class KubernetesCluster():
|
||||
"""Kubernetes Cluster"""
|
||||
name = "Kubernetes Cluster"
|
||||
|
||||
class Kubelet(KubernetesCluster):
|
||||
"""The kubelet is the primary "node agent" that runs on each node"""
|
||||
name = "Kubelet"
|
||||
|
||||
|
||||
""" Event Types """
|
||||
# TODO: make proof an abstract method.
|
||||
class Service(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.data = data
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def explain(self):
|
||||
return self.data
|
||||
return self.__doc__
|
||||
|
||||
def proof(self):
|
||||
return self.name
|
||||
|
||||
class Vulnerability(object):
|
||||
def __init__(self, name, data=""):
|
||||
def __init__(self, component, name):
|
||||
self.component = component
|
||||
self.name = name
|
||||
self.data = data
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def explain(self):
|
||||
return self.data
|
||||
|
||||
return self.__doc__
|
||||
|
||||
def proof(self):
|
||||
return self.name
|
||||
|
||||
class Information(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def explain(self):
|
||||
return self.__doc__
|
||||
|
||||
def proof(self):
|
||||
return self.name
|
||||
|
||||
|
||||
event_id_count = 0
|
||||
""" Discovery/Hunting Events """
|
||||
class NewHostEvent(Event):
|
||||
def __init__(self, host):
|
||||
global event_id_count
|
||||
self.host = host
|
||||
|
||||
self.id = event_id_count
|
||||
event_id_count += 1
|
||||
|
||||
def __str__(self):
|
||||
return str(self.host)
|
||||
|
||||
@@ -56,40 +93,3 @@ class OpenPortEvent(Event):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.port)
|
||||
|
||||
class HostScanEvent(Event):
|
||||
def __init__(self, pod=False):
|
||||
self.pod = pod
|
||||
self.auth_token = self.get_auth_token()
|
||||
self.client_cert = self.get_client_cert()
|
||||
|
||||
def get_auth_token(self):
|
||||
if self.pod:
|
||||
with open("/run/secrets/kubernetes.io/serviceaccount/token") as token_file:
|
||||
return token_file.read()
|
||||
return None
|
||||
|
||||
def get_client_cert(self):
|
||||
if self.pod:
|
||||
return "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
return None
|
||||
|
||||
|
||||
class KubeDashboardEvent(Event, ServiceEvent):
|
||||
def __init__(self, path="/", secure=False):
|
||||
self.path = path
|
||||
self.secure
|
||||
|
||||
class ReadOnlyKubeletEvent(Event, ServiceEvent):
|
||||
def __init__(self):
|
||||
ServiceEvent.__init__(self, name="Kubelet API (readonly)")
|
||||
|
||||
class SecureKubeletEvent(Event, ServiceEvent):
|
||||
def __init__(self, cert=False, token=False):
|
||||
self.cert = cert
|
||||
self.token = token
|
||||
ServiceEvent.__init__(self, name="Kubelet API")
|
||||
|
||||
class KubeProxyEvent(Event, ServiceEvent):
|
||||
def __init__(self):
|
||||
ServiceEvent.__init__(self, name="Kubernetes Proxy")
|
||||
@@ -1,11 +0,0 @@
|
||||
from common import Event
|
||||
|
||||
class Vulnerability(object):
|
||||
""" Information Events """
|
||||
# this kind of events will be triggered when important information is discovered
|
||||
def __init__(self, name, data=""):
|
||||
self.name = name
|
||||
self.data = data
|
||||
|
||||
def explain(self):
|
||||
return self.data
|
||||
@@ -4,24 +4,13 @@ from ..types import Hunter
|
||||
import requests
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import Vulnerability, Event, KubeDashboardEvent
|
||||
from ..discovery.dashboard import KubeDashboardEvent
|
||||
|
||||
@handler.subscribe(KubeDashboardEvent)
|
||||
class KubeDashboard(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
@property
|
||||
def accessible(self):
|
||||
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.event.secure:
|
||||
# logging.info("[OPEN SERVICE] SECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path))
|
||||
# else:
|
||||
# logging.info("[OPEN SERVICE] INSECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path))
|
||||
# TODO: implement dashboard hunting
|
||||
pass
|
||||
@@ -6,21 +6,97 @@ import requests
|
||||
import urllib3
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import (Vulnerability, Event, ReadOnlyKubeletEvent,
|
||||
SecureKubeletEvent)
|
||||
from ..discovery.kubelet import KubeletOpenHandler
|
||||
from ..events.types import (KubernetesCluster, Kubelet, Vulnerability, Information, Event)
|
||||
from ..discovery.kubelet import KubeletExposedHandler, ReadOnlyKubeletEvent, SecureKubeletEvent
|
||||
from ..types import Hunter
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
class ContainerLogsHandler:
|
||||
"""Outputs logs from a running container"""
|
||||
name="/containerlogs"
|
||||
remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
class RunningPodsHandler:
|
||||
"""Outputs a list of currently runnning pods, and some of their metadata"""
|
||||
name="/runningpods"
|
||||
remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
class ExecHandler:
|
||||
"""Opens a websocket that enables running and executing arbitrary commands on a container"""
|
||||
name="/exec"
|
||||
remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
class RunHandler:
|
||||
"""Allows remote arbitrary execution inside a container"""
|
||||
name="/run"
|
||||
remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
class PortForwardHandler:
|
||||
"""Setting a port forwaring rule on a pod"""
|
||||
name="/portForward"
|
||||
remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
class AttachHandler:
|
||||
"""Opens a websocket that enables running and executing arbitrary commands on a container"""
|
||||
name="/attach"
|
||||
remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
|
||||
""" Vulnerabilities """
|
||||
class K8sVersionDisclosure(Vulnerability, Event):
|
||||
"""Discloses the kubernetes version, exposed from a log on the /metrics endpoint"""
|
||||
def __init__(self, version):
|
||||
Vulnerability.__init__(self, Kubelet, "Version Disclosure")
|
||||
self.version = version
|
||||
|
||||
def proof(self):
|
||||
return self.version
|
||||
|
||||
class PrivilegedContainers(Vulnerability, Event):
|
||||
"""A priviledged container on a node, can expose the node/cluster to unwanted root operations"""
|
||||
def __init__(self, containers):
|
||||
Vulnerability.__init__(self, KubernetesCluster, "Priviledged Container")
|
||||
self.containers = containers
|
||||
|
||||
def proof(self):
|
||||
return self.containers
|
||||
|
||||
|
||||
""" dividing ports for seperate hunters """
|
||||
@handler.subscribe(ReadOnlyKubeletEvent)
|
||||
class ReadOnlyKubeletPortHunter(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.path = "http://{}:{}/".format(self.event.host, self.event.port)
|
||||
|
||||
def get_k8s_version(self):
|
||||
metrics = requests.get(self.path + "metrics").text
|
||||
for line in metrics.split("\n"):
|
||||
if line.startswith("kubernetes_build_info"):
|
||||
for info in line[line.find('{') + 1: line.find('}')].split(','):
|
||||
k, v = info.split("=")
|
||||
if k == "gitVersion":
|
||||
return v.strip("\"")
|
||||
|
||||
# returns list of tuples of priviledged container and their pod.
|
||||
def find_privileged_containers(self):
|
||||
pods = json.loads(requests.get(self.path + "pods").text)
|
||||
privileged_containers = list()
|
||||
if "items" in pods:
|
||||
for pod in pods["items"]:
|
||||
for container in pod["spec"]["containers"]:
|
||||
if "securityContext" in container and "privileged" in container["securityContext"] and container["securityContext"]["privileged"]:
|
||||
privileged_containers.append((pod["metadata"]["name"], container["name"]))
|
||||
return privileged_containers if len(privileged_containers) > 0 else None
|
||||
|
||||
def execute(self):
|
||||
pass
|
||||
k8s_version = self.get_k8s_version()
|
||||
privileged_containers = self.find_privileged_containers()
|
||||
if k8s_version:
|
||||
self.publish_event(K8sVersionDisclosure(version=k8s_version))
|
||||
if privileged_containers:
|
||||
self.publish_event(PrivilegedContainers(containers=privileged_containers))
|
||||
|
||||
@handler.subscribe(SecureKubeletEvent)
|
||||
class SecureKubeletPortHunter(Hunter):
|
||||
@@ -53,7 +129,7 @@ class SecureKubeletPortHunter(Hunter):
|
||||
containerName=self.pod.container
|
||||
)
|
||||
if self.session.get(logs_url, verify=False).status_code == 200:
|
||||
return self.Handlers.CONTAINERLOGS.name
|
||||
return ContainerLogsHandler
|
||||
|
||||
# need further investigation on websockets protocol for further implementation
|
||||
def test_exec_container(self):
|
||||
@@ -63,10 +139,10 @@ class SecureKubeletPortHunter(Hunter):
|
||||
podNamespace = self.pod.namespace,
|
||||
podID = self.pod.name,
|
||||
containerName = self.pod.container,
|
||||
cmd = "uname"
|
||||
cmd = "uname -a"
|
||||
)
|
||||
if "/cri/exec/" in self.session.get(exec_url, headers=headers, allow_redirects=False ,verify=False).text:
|
||||
return self.Handlers.EXEC.name
|
||||
return ExecHandler
|
||||
|
||||
# need further investigation on websockets protocol for further implementation
|
||||
def test_port_forward(self):
|
||||
@@ -92,17 +168,17 @@ class SecureKubeletPortHunter(Hunter):
|
||||
podNamespace = self.pod.namespace,
|
||||
podID = self.pod.name,
|
||||
containerName = self.pod.container,
|
||||
cmd = "echo check"
|
||||
cmd = "uname -a"
|
||||
)
|
||||
output = requests.post(run_url, allow_redirects=False ,verify=False).text
|
||||
if "echo" not in output and "check" in output:
|
||||
return self.Handlers.EXEC.name
|
||||
return RunHandler
|
||||
|
||||
# returns list of currently running pods
|
||||
def test_running_pods(self):
|
||||
pods_url = self.path + self.Handlers.RUNNINGPODS.value
|
||||
if 'items' in json.loads(self.session.get(pods_url, verify=False).text).keys():
|
||||
return self.Handlers.RUNNINGPODS.name
|
||||
return RunningPodsHandler
|
||||
|
||||
# need further investigation on the differences between attach and exec
|
||||
def test_attach_container(self):
|
||||
@@ -111,17 +187,17 @@ class SecureKubeletPortHunter(Hunter):
|
||||
podNamespace = self.pod.namespace,
|
||||
podID = self.pod.name,
|
||||
containerName = self.pod.container,
|
||||
cmd = "uname"
|
||||
cmd = "uname -a"
|
||||
)
|
||||
if "/cri/attach/" in self.session.get(attach_url, allow_redirects=False ,verify=False).text:
|
||||
return self.Handlers.ATTACH.name
|
||||
return AttachHandler
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.session = requests.Session()
|
||||
if self.event.secure:
|
||||
self.session.headers.update({"Authorization": "Bearer {}".format(self.event.auth_token)})
|
||||
self.session.cert = self.event.client_cert
|
||||
self.session.headers.update({"Authorization": "Bearer {}".format(self.event.auth_token)})
|
||||
self.session.cert = self.event.client_cert
|
||||
self.path = "https://{}:{}/".format(self.event.host, 10250)
|
||||
|
||||
def execute(self):
|
||||
@@ -140,10 +216,14 @@ class SecureKubeletPortHunter(Hunter):
|
||||
debug_handlers.test_attach_container
|
||||
]
|
||||
for test_handler in test_list:
|
||||
handler_name = test_handler()
|
||||
if handler_name:
|
||||
self.publish_event(KubeletOpenHandler(handler=handler_name.lower()))
|
||||
|
||||
try:
|
||||
handler = test_handler()
|
||||
if handler:
|
||||
self.publish_event(KubeletExposedHandler(handler=handler))
|
||||
except Exception as ex:
|
||||
logging.debug("Failed getting handler: {}".format(test_handler))
|
||||
pass
|
||||
|
||||
def get_self_pod(self):
|
||||
return {"name": "kube-hunter",
|
||||
"namespace": "default",
|
||||
@@ -166,6 +246,26 @@ class SecureKubeletPortHunter(Hunter):
|
||||
"namespace": "default"
|
||||
}
|
||||
|
||||
|
||||
""" Active Hunting Of Handlers"""
|
||||
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler=="exec" and x.active)
|
||||
class ActiveExecHandler(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = Event
|
||||
|
||||
def execute(self):
|
||||
pass
|
||||
|
||||
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler=="run" and x.active)
|
||||
class ActiveRunHandler(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = Event
|
||||
|
||||
def execute(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
# def get_kubesystem_pod_container(self):
|
||||
# pods_data = json.loads(requests.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False).text)['items']
|
||||
# # filter running kubesystem pod
|
||||
|
||||
@@ -4,7 +4,8 @@ from ..types import Hunter
|
||||
from requests import get
|
||||
|
||||
from ..events import handler
|
||||
from ..events.types import KubeDashboardEvent, KubeProxyEvent
|
||||
from ..discovery.dashboard import KubeDashboardEvent
|
||||
from ..discovery.proxy import KubeProxyEvent
|
||||
|
||||
class Service(Enum):
|
||||
DASHBOARD = "kubernetes-dashboard"
|
||||
|
||||
@@ -2,4 +2,5 @@ netaddr
|
||||
netifaces
|
||||
enum34
|
||||
scapy
|
||||
requests
|
||||
requests
|
||||
PrettyTable
|
||||
Reference in New Issue
Block a user