1. Added an --active flag, to allow optional "Proof" result, which will do an active hunting of a found vulnerability

2. Added a --remote flag to specify remote clusters/machines for hunting.
3. Improved a bit of the architecture, (Services)

Note:
The reporter module, will gather vulnerabilities before their active hunting will start.
This is not an issue, as we can access all of the attributes of the event directly from the active hunter (event.previous), which we will proccess on the end in the report
This commit is contained in:
daniel_sagi
2018-06-10 19:34:07 +03:00
parent 36e87807e6
commit faa7571127
9 changed files with 73 additions and 47 deletions

View File

@@ -6,8 +6,10 @@ import sys
import time
parser = argparse.ArgumentParser(description='Kube-Hunter, Hunter for weak Kubernetes cluster')
parser.add_argument('--pod', action="store_true", help="set hunter as an insider pod")
parser.add_argument('--remote', nargs='+', metavar="LOGLEVEL", help="one or more remote ip/dns to hunt")
parser.add_argument('--active', action="store_true", help="enables active hunting")
parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set log level, options are:\nDEBUG INFO WARNING")
parser.add_argument('--pod', action="store_true", help="When set, will scan the cluster as a pod, when unset, will scan all network interfaces")
args = parser.parse_args()
try:
loglevel = getattr(logging, args.log.upper())
@@ -25,7 +27,7 @@ from modules.discovery.hosts import HostScanEvent
def main():
logging.info("Started")
try:
handler.publish_event(HostScanEvent(pod=args.pod, active=True))
handler.publish_event(HostScanEvent(pod=args.pod, active=args.active, predefined_hosts=args.remote))
# Blocking to see discovery output
while(True):
time.sleep(100)
@@ -34,7 +36,7 @@ def main():
finally:
handler.free()
logging.debug("Cleaned Queue")
log.print_results()
log.print_results(args.active)
if __name__ == '__main__':
main()

View File

@@ -6,7 +6,6 @@ from modules.discovery.kubelet import KubeletExposedHandler
services = list()
vulnerabilities = list()
informations = list()
@handler.subscribe(Vulnerability)
class VulnerabilityReport(object):
@@ -15,23 +14,11 @@ class VulnerabilityReport(object):
def execute(self):
logging.info("[VULNERABILITY - {name}] {desc}".format(
name=self.vulnerability.name,
name=self.vulnerability.get_name(),
desc=self.vulnerability.explain(),
))
vulnerabilities.append(self.vulnerability)
@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):
@@ -46,16 +33,19 @@ class OpenServiceReport(object):
))
services.append(self.service)
def print_results():
def print_results(active):
services_table = PrettyTable(["Service", "Location", "Description"])
for service in services:
services_table.add_row([service.get_name(), "{}:{}".format(service.host, service.port), service.explain()])
services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()])
vuln_table = PrettyTable(["Location", "From Component", "Vulnerability", "Description"])
column_names = ["Location", "From Component", "Vulnerability", "Description"]
if active: column_names.append("Proof")
vuln_table = PrettyTable(column_names)
for vuln in vulnerabilities:
vuln_table.add_row(["{}:{}".format(vuln.host, vuln.port), vuln.component.name, vuln.get_name(), vuln.explain()])
row = ["{}:{}".format(vuln.host, vuln.port), vuln.component.name, vuln.get_name(), vuln.explain()]
if active: row.append(vuln.attrs)
vuln_table.add_row(row)
print "\nOpen Services:"
print services_table

View File

@@ -8,10 +8,8 @@ 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")
def __init__(self, **kargs):
Service.__init__(self, name="Kubernetes Dashboard", **kargs)
@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 30000)
class KubeDashboard(Hunter):

View File

@@ -13,9 +13,10 @@ from ..events.types import Event, NewHostEvent
from ..types import Hunter
class HostScanEvent(Event):
def __init__(self, pod=False, active=False):
def __init__(self, pod=False, active=False, predefined_hosts=list()):
self.pod = pod
self.active = active # flag to specify whether to get actual data from vulnerabilities
self.predefined_hosts = predefined_hosts
self.auth_token = self.get_auth_token()
self.client_cert = self.get_client_cert()
@@ -37,14 +38,17 @@ class HostDiscovery(Hunter):
def execute(self):
logging.info("Discovering Open Kubernetes Services...")
if self.event.pod:
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
elif len(self.event.predefined_hosts) == 0:
self.scan_interfaces()
else:
for host in self.event.predefined_hosts:
self.publish_event(NewHostEvent(host=host))
def is_azure_cluster(self):
try:

View File

@@ -23,6 +23,7 @@ class SecureKubeletEvent(Service, Event):
self.token = token
Service.__init__(self, name="Kubelet API")
""" Vulnerabilities """
class PodsHandler:
"""Exposes sensitive information about pods that are bound to the node"""
@@ -47,6 +48,12 @@ class AnonymousAuthEnabled(Vulnerability, Event):
def proof(self):
pass # TODO: decide on an appropriate proof
class KubeletPorts(Enum):
SECURED = 10250
READ_ONLY = 10255
@handler.subscribe(OpenPortEvent, predicate= lambda x: x.port == 10255 or x.port == 10250)
class KubeletDiscovery(Hunter):
def __init__(self, event):
@@ -89,9 +96,4 @@ class KubeletDiscovery(Hunter):
if self.event.port == KubeletPorts.SECURED.value:
self.get_secure_access()
elif self.event.port == KubeletPorts.READ_ONLY.value:
self.get_read_only_access()
class KubeletPorts(Enum):
SECURED = 10250
READ_ONLY = 10255
self.get_read_only_access()

View File

@@ -8,6 +8,7 @@ from ..events import handler
from ..events.types import Service, Event, OpenPortEvent
class KubeProxyEvent(Event, Service):
"""proxies from a localhost address to the Kubernetes apiserver"""
def __init__(self):
Service.__init__(self, name="Kubernetes Proxy")

View File

@@ -35,12 +35,17 @@ class Kubelet(KubernetesCluster):
""" Event Types """
# TODO: make proof an abstract method.
class Service(object):
def __init__(self, name):
def __init__(self, name, path="", secure=False):
self.name = name
self.secure = secure
self.path = path
def get_name(self):
return self.name
def get_path(self):
return "/" + self.path if self.path else ""
def explain(self):
return self.__doc__

View File

@@ -14,7 +14,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class ContainerLogsHandler:
"""Outputs logs from a running container"""
name="/containerlogs"
name="/containerLogs"
remediation="--enable-debugging-handlers=False On Kubelet"
class RunningPodsHandler:
@@ -248,24 +248,37 @@ class SecureKubeletPortHunter(Hunter):
""" Active Hunting Of Handlers"""
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler=="exec" and x.active)
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==ExecHandler.name and x.active)
class ActiveExecHandler(Hunter):
def __init__(self, event):
self.event = Event
self.event = event
def execute(self):
pass
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler=="run" and x.active)
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==RunHandler.name and x.active)
class ActiveRunHandler(Hunter):
def __init__(self, event):
self.event = Event
self.event = event
def execute(self):
pass
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==ContainerLogsHandler.name and x.active)
class ActiveContainerLogs(Hunter):
def __init__(self, event):
self.event = event
def execute(self):
pass
@handler.subscribe(KubeletExposedHandler, predicate=lambda x: x.handler.name==AttachHandler.name and x.active)
class ActiveContainerLogs(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

View File

@@ -1,15 +1,23 @@
import logging
from enum import Enum
from ..types import Hunter
from requests import get
from ..events import handler
from ..discovery.dashboard import KubeDashboardEvent
from ..discovery.proxy import KubeProxyEvent
from ..events import handler
from ..events.types import Vulnerability, Event, KubernetesCluster
from ..types import Hunter
class Service(Enum):
DASHBOARD = "kubernetes-dashboard"
class KubeProxyExposed(Vulnerability, Event):
"""Exposes all oprations on the cluster"""
def __init__(self):
Vulnerability.__init__(self, KubernetesCluster, "Proxy Exposed")
@handler.subscribe(KubeProxyEvent)
class KubeProxy(Hunter):
def __init__(self, event):
@@ -17,10 +25,12 @@ class KubeProxy(Hunter):
self.api_url = "http://{host}:{port}/api/v1".format(host=self.event.host, port=self.event.port)
def execute(self):
self.publish_event(KubeProxyExposed())
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:
if service == Service.DASHBOARD.value:
logging.debug(service)
curr_path = "api/v1/namespaces/{ns}/services/{sv}/proxy".format(ns=namespace,sv=service) # TODO: check if /proxy is a convention on other services
self.publish_event(KubeDashboardEvent(path=curr_path, secure=False))
@property
@@ -36,6 +46,7 @@ class KubeProxy(Hunter):
resource_path = "/namespaces/{ns}/services".format(ns=namespace)
resource_json = get(self.api_url + resource_path).json()
services[namespace] = self.extract_names(resource_json)
logging.debug(services)
return services
@staticmethod