mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-22 09:04:32 +00:00
Merge branch 'report_token_feature' of bitbucket.org:scalock/kube-hunter
This commit is contained in:
@@ -6,42 +6,81 @@ import sys
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser(description='Kube-Hunter - hunts for security weaknesses in Kubernetes clusters')
|
||||
parser.add_argument('--internal', action="store_true", help="set hunting of all internal network interfaces")
|
||||
parser.add_argument('--pod', action="store_true", help="set hunter as an insider pod")
|
||||
parser.add_argument('--cidr', type=str, help="set manual cidr to scan, example: 192.168.0.0/16")
|
||||
parser.add_argument('--quick', action="store_true", help="scanning only known small sections of the subnet")
|
||||
parser.add_argument('--mapping', action="store_true", help="outputs only mapping of cluster's nodes")
|
||||
parser.add_argument('--mapping', action="store_true", help="outputs only a mapping of the cluster's nodes")
|
||||
parser.add_argument('--remote', nargs='+', metavar="HOST", default=list(), 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('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set log level, options are: debug, info, warn, none")
|
||||
parser.add_argument('--token', type=str, metavar="AQUA_TOKEN", help="specify the token retrieved from Aqua, after finished executing, the report will be visible on kube-hunter's site")
|
||||
|
||||
config = parser.parse_args()
|
||||
try:
|
||||
loglevel = getattr(logging, config.log.upper())
|
||||
except:
|
||||
pass
|
||||
logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s')
|
||||
if config.log.lower() != "none":
|
||||
logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s')
|
||||
|
||||
import log
|
||||
from report import reporter
|
||||
from src.core.events import handler
|
||||
from src.modules.discovery import HostDiscovery
|
||||
from src.modules.discovery.hosts import HostScanEvent
|
||||
|
||||
|
||||
def interactive_set_config():
|
||||
"""Sets config manually, returns True for success"""
|
||||
options = {
|
||||
"Remote scanning": "scans one or more specific IPs or DNS names",
|
||||
"Internal scanning": "scans all network interfaces",
|
||||
"CIDR scanning": "scans a spesific cidr"
|
||||
} # maps between option and its explanation
|
||||
|
||||
print "Choose one of the options below:"
|
||||
for i, (option, explanation) in enumerate(options.items()):
|
||||
print "{}. {} ({})".format(i+1, option.ljust(20), explanation)
|
||||
choice = raw_input("Your choice: ")
|
||||
if choice == '1':
|
||||
config.remote = raw_input("Remotes (seperated by a ','): ").replace(' ', '').split(',')
|
||||
elif choice == '2':
|
||||
config.internal = True
|
||||
elif choice == '3':
|
||||
config.cidr = raw_input("CIDR (example - 192.168.1.0/24): ").replace(' ', '')
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def main():
|
||||
logging.info("Started")
|
||||
scan_options = [
|
||||
config.pod,
|
||||
config.cidr,
|
||||
config.remote,
|
||||
config.internal
|
||||
]
|
||||
hunt_started = False
|
||||
try:
|
||||
handler.publish_event(HostScanEvent(predefined_hosts=config.remote))
|
||||
if not any(scan_options):
|
||||
if not interactive_set_config(): return
|
||||
hunt_started = True
|
||||
logging.info("Started")
|
||||
handler.publish_event(HostScanEvent())
|
||||
|
||||
# Blocking to see discovery output
|
||||
handler.join()
|
||||
except KeyboardInterrupt:
|
||||
logging.debug("Kube-Hunter stopped by user")
|
||||
finally:
|
||||
handler.free()
|
||||
logging.debug("Cleaned Queue")
|
||||
log.print_results()
|
||||
if hunt_started:
|
||||
handler.free()
|
||||
logging.debug("Cleaned Queue")
|
||||
if config.token:
|
||||
reporter.send_report(token=config.token)
|
||||
else:
|
||||
reporter.print_tables()
|
||||
|
||||
if config.pod:
|
||||
while True: time.sleep(5)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
# Proof -> Evidence
|
||||
main()
|
||||
101
log/reporter.py
101
log/reporter.py
@@ -1,101 +0,0 @@
|
||||
import logging
|
||||
|
||||
from prettytable import ALL, PrettyTable
|
||||
|
||||
from __main__ import config
|
||||
from src.core.events import handler
|
||||
from src.core.events.types import Service, Vulnerability
|
||||
|
||||
services = list()
|
||||
vulnerabilities = list()
|
||||
|
||||
EVIDENCE_PREVIEW = 40
|
||||
MAX_WIDTH_VULNS = 70
|
||||
MAX_WIDTH_SERVICES = 60
|
||||
|
||||
@handler.subscribe(Vulnerability)
|
||||
class VulnerabilityReport(object):
|
||||
def __init__(self, event):
|
||||
self.vulnerability = event
|
||||
|
||||
def execute(self):
|
||||
logging.info("[VULNERABILITY - {name}] {desc}".format(
|
||||
name=self.vulnerability.get_name(),
|
||||
desc=self.vulnerability.explain(),
|
||||
))
|
||||
vulnerabilities.append(self.vulnerability)
|
||||
# TODO: Add ActiveHunter replacement by id, when a vulnerability comes from active hunter, it replaces it's predecessor
|
||||
|
||||
@handler.subscribe(Service)
|
||||
class OpenServiceReport(object):
|
||||
def __init__(self, event):
|
||||
self.service = event
|
||||
|
||||
def execute(self):
|
||||
logging.info("[OPEN SERVICE - {name}] IP:{host} PORT:{port}".format(
|
||||
name=self.service.name,
|
||||
desc=self.service.desc,
|
||||
host=self.service.host,
|
||||
port=self.service.port
|
||||
))
|
||||
services.append(self.service)
|
||||
|
||||
def print_nodes():
|
||||
nodes_table = PrettyTable(["Type", "Location"], hrules=ALL)
|
||||
nodes_table.align="l"
|
||||
nodes_table.max_width=MAX_WIDTH_SERVICES
|
||||
nodes_table.padding_width=1
|
||||
nodes_table.sortby="Type"
|
||||
nodes_table.reversesort=True
|
||||
nodes_table.header_style="upper"
|
||||
|
||||
# TODO: replace with sets
|
||||
id_memory = list()
|
||||
for service in services:
|
||||
if service.id not in id_memory:
|
||||
nodes_table.add_row(["Slave/Master", service.host])
|
||||
id_memory.append(service.id)
|
||||
print "Nodes:"
|
||||
print nodes_table
|
||||
print
|
||||
|
||||
def print_services():
|
||||
services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL)
|
||||
services_table.align="l"
|
||||
services_table.max_width=MAX_WIDTH_SERVICES
|
||||
services_table.padding_width=1
|
||||
services_table.sortby="Service"
|
||||
services_table.reversesort=True
|
||||
services_table.header_style="upper"
|
||||
for service in services:
|
||||
services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()])
|
||||
print "Open Services:"
|
||||
print services_table
|
||||
print
|
||||
|
||||
def print_vulnerabilities():
|
||||
column_names = ["Location", "Category", "Vulnerability", "Description"]
|
||||
if config.active: column_names.append("Evidence")
|
||||
vuln_table = PrettyTable(column_names, hrules=ALL)
|
||||
vuln_table.align="l"
|
||||
vuln_table.max_width=MAX_WIDTH_VULNS
|
||||
vuln_table.sortby="Category"
|
||||
vuln_table.reversesort=True
|
||||
vuln_table.padding_width=1
|
||||
vuln_table.header_style="upper"
|
||||
for vuln in vulnerabilities:
|
||||
row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.component.name, vuln.get_name(), vuln.explain()]
|
||||
if config.active:
|
||||
evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence)
|
||||
row.append(evidence)
|
||||
vuln_table.add_row(row)
|
||||
print "Vulnerabilities:"
|
||||
print vuln_table
|
||||
print
|
||||
|
||||
|
||||
def print_results():
|
||||
print_nodes()
|
||||
if not config.mapping:
|
||||
print_services()
|
||||
print_vulnerabilities()
|
||||
209
report/reporter.py
Normal file
209
report/reporter.py
Normal file
@@ -0,0 +1,209 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
import requests
|
||||
from prettytable import ALL, PrettyTable
|
||||
|
||||
from __main__ import config
|
||||
from src.core.events import handler
|
||||
from src.core.events.types import Service, Vulnerability
|
||||
|
||||
# [event, ...]
|
||||
services = list()
|
||||
|
||||
# [(TypeClass, event), ...]
|
||||
insights = list()
|
||||
|
||||
vulnerabilities = list()
|
||||
|
||||
EVIDENCE_PREVIEW = 40
|
||||
MAX_WIDTH_VULNS = 70
|
||||
MAX_WIDTH_SERVICES = 60
|
||||
|
||||
AQUA_PUSH_URL = "https://qlyscbqwl7.execute-api.us-east-1.amazonaws.com/Prod/submit?token={token}"
|
||||
AQUA_RESULTS_URL = "https://qlyscbqwl7.execute-api.us-east-1.amazonaws.com/Prod/result?token={token}"
|
||||
|
||||
@handler.subscribe(Service)
|
||||
@handler.subscribe(Vulnerability)
|
||||
class Reporter(object):
|
||||
"""Reportes can be initiated by the event handler, and by regular decaration. for usage on end of runtime"""
|
||||
def __init__(self, event=None):
|
||||
self.event = event
|
||||
self.insights_by_id = defaultdict(list)
|
||||
self.services_by_id = defaultdict(list)
|
||||
|
||||
def execute(self):
|
||||
"""function is called only when collecting data"""
|
||||
global services, insights
|
||||
bases = self.event.__class__.__mro__
|
||||
if Service in bases:
|
||||
services.append(self.event)
|
||||
logging.info("[OPEN SERVICE - {name}] IP:{host} PORT:{port}".format(
|
||||
host=self.event.host,
|
||||
port=self.event.port,
|
||||
name=self.event.get_name(),
|
||||
desc=self.event.explain()
|
||||
))
|
||||
elif Vulnerability in bases:
|
||||
insights.append((Vulnerability, self.event))
|
||||
vulnerabilities.append(self.event)
|
||||
logging.info("[VULNERABILITY - {name}] {desc}".format(
|
||||
name=self.event.get_name(),
|
||||
desc=self.event.explain(),
|
||||
))
|
||||
|
||||
if config.token:
|
||||
self.send_report(token=config.token)
|
||||
|
||||
def print_tables(self):
|
||||
"""generates report tables and outputs to stdout"""
|
||||
if len(services):
|
||||
print_nodes()
|
||||
if not config.mapping:
|
||||
print_services()
|
||||
print_vulnerabilities()
|
||||
else:
|
||||
print "\nKube Hunter couldn't find any clusters"
|
||||
# print "\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "")
|
||||
|
||||
def build_sub_services(self, services_list):
|
||||
# correlation functions
|
||||
def get_insights_by_service(service):
|
||||
"""generates list of insights related to a given service"""
|
||||
insights = list()
|
||||
for insight_type, insight in self.insights_by_id[service.event_id]:
|
||||
if service in insight.history:
|
||||
insights.append((insight_type, insight))
|
||||
return insights
|
||||
|
||||
def get_services_by_service(parent_service):
|
||||
"""generates list of insights related to a given service"""
|
||||
services = list()
|
||||
for service in self.services_by_id[parent_service.event_id]:
|
||||
if service != parent_service and parent_service in service.history:
|
||||
services.append(service)
|
||||
self.services_by_id[parent_service.event_id].remove(service)
|
||||
return services
|
||||
|
||||
current_list = list()
|
||||
for service in services_list:
|
||||
current_list.append(
|
||||
{
|
||||
"type": service.get_name(),
|
||||
"metadata": {
|
||||
"port": service.port,
|
||||
"path": service.get_path()
|
||||
},
|
||||
"description": service.explain()
|
||||
})
|
||||
next_services = get_services_by_service(service)
|
||||
if next_services:
|
||||
current_list[-1]["services"] = self.build_sub_services(next_services)
|
||||
current_list[-1]["insights"] = [{
|
||||
"type": insight_type.__name__,
|
||||
"name": insight.get_name(),
|
||||
"description": insight.explain(),
|
||||
"evidence": insight.evidence if insight_type == Vulnerability else ""
|
||||
} for insight_type, insight in get_insights_by_service(service)]
|
||||
return current_list
|
||||
|
||||
def send_report(self, token):
|
||||
def generate_report():
|
||||
"""function generates a report corresponding to specifications of the frontend of kubehunter"""
|
||||
for service in services:
|
||||
self.services_by_id[service.event_id].append(service)
|
||||
for insight_type, insight in insights:
|
||||
self.insights_by_id[insight.event_id].append((insight_type, insight))
|
||||
|
||||
# building first layer of services (nodes)
|
||||
report = defaultdict(list)
|
||||
for _, services_list in self.services_by_id.items():
|
||||
service_report = {
|
||||
"type": "Node", # on future, determine if slave or master
|
||||
"metadata": {
|
||||
"host": str(services_list[0].host)
|
||||
},
|
||||
# then constructing their sub services tree
|
||||
"services": self.build_sub_services(services_list)
|
||||
}
|
||||
report["services"].append(service_report)
|
||||
return report
|
||||
|
||||
finished = (not handler.unfinished_tasks)
|
||||
logging.debug("generating report")
|
||||
report = {
|
||||
'results': generate_report(),
|
||||
'metadata': {
|
||||
'finished': finished
|
||||
}
|
||||
}
|
||||
logging.debug("uploading report")
|
||||
r = requests.put(AQUA_PUSH_URL.format(token=token), json=report)
|
||||
|
||||
if r.status_code == 201: # created status
|
||||
logging.debug("report was uploaded successfully")
|
||||
if finished:
|
||||
print "\nYour report: \n{}".format(AQUA_RESULTS_URL.format(token=token))
|
||||
else:
|
||||
logging.debug("Failed sending report with:{}, {}".format(r.status_code, r.text))
|
||||
if finished:
|
||||
print "\nCould not send report.\n{}".format(json.loads(r.text).get("status", ""))
|
||||
|
||||
reporter = Reporter()
|
||||
|
||||
|
||||
""" Tables Generation """
|
||||
def print_nodes():
|
||||
nodes_table = PrettyTable(["Type", "Location"], hrules=ALL)
|
||||
nodes_table.align="l"
|
||||
nodes_table.max_width=MAX_WIDTH_SERVICES
|
||||
nodes_table.padding_width=1
|
||||
nodes_table.sortby="Type"
|
||||
nodes_table.reversesort=True
|
||||
nodes_table.header_style="upper"
|
||||
|
||||
# TODO: replace with sets
|
||||
id_memory = list()
|
||||
for service in services:
|
||||
if service.event_id not in id_memory:
|
||||
nodes_table.add_row(["Slave/Master", service.host])
|
||||
id_memory.append(service.event_id)
|
||||
print "Nodes:"
|
||||
print nodes_table
|
||||
print
|
||||
|
||||
def print_services():
|
||||
services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL)
|
||||
services_table.align="l"
|
||||
services_table.max_width=MAX_WIDTH_SERVICES
|
||||
services_table.padding_width=1
|
||||
services_table.sortby="Service"
|
||||
services_table.reversesort=True
|
||||
services_table.header_style="upper"
|
||||
for service in services:
|
||||
services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()])
|
||||
print "Open Services:"
|
||||
print services_table
|
||||
print
|
||||
|
||||
def print_vulnerabilities():
|
||||
column_names = ["Location", "Category", "Vulnerability", "Description"]
|
||||
if config.active: column_names.append("Evidence")
|
||||
vuln_table = PrettyTable(column_names, hrules=ALL)
|
||||
vuln_table.align="l"
|
||||
vuln_table.max_width=MAX_WIDTH_VULNS
|
||||
vuln_table.sortby="Category"
|
||||
vuln_table.reversesort=True
|
||||
vuln_table.padding_width=1
|
||||
vuln_table.header_style="upper"
|
||||
for vuln in vulnerabilities:
|
||||
row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.component.name, vuln.get_name(), vuln.explain()]
|
||||
if config.active:
|
||||
evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence)
|
||||
row.append(evidence)
|
||||
vuln_table.add_row(row)
|
||||
print "Vulnerabilities:"
|
||||
print vuln_table
|
||||
print
|
||||
@@ -41,9 +41,6 @@ class Service(object):
|
||||
def explain(self):
|
||||
return self.__doc__
|
||||
|
||||
def proof(self):
|
||||
return self.name
|
||||
|
||||
class Vulnerability(object):
|
||||
def __init__(self, component, name):
|
||||
self.component = component
|
||||
@@ -64,7 +61,7 @@ class NewHostEvent(Event):
|
||||
def __init__(self, host, cloud=None):
|
||||
global event_id_count
|
||||
self.host = host
|
||||
self.id = event_id_count
|
||||
self.event_id = event_id_count
|
||||
self.cloud = cloud
|
||||
event_id_count += 1
|
||||
|
||||
|
||||
@@ -33,14 +33,14 @@ class HostScanEvent(Event):
|
||||
|
||||
def get_auth_token(self):
|
||||
if config.pod:
|
||||
with open("/run/secrets/kubernetes.io/serviceaccount/token") as token_file:
|
||||
return token_file.read()
|
||||
return None
|
||||
|
||||
try:
|
||||
with open("/run/secrets/kubernetes.io/serviceaccount/token") as token_file:
|
||||
return token_file.read()
|
||||
except IOError:
|
||||
pass
|
||||
def get_client_cert(self):
|
||||
if config.pod:
|
||||
return "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
return None
|
||||
|
||||
@handler.subscribe(HostScanEvent)
|
||||
class HostDiscovery(Hunter):
|
||||
@@ -58,16 +58,16 @@ class HostDiscovery(Hunter):
|
||||
self.publish_event(NewHostEvent(host=ip, cloud=cloud))
|
||||
except:
|
||||
logging.error("unable to parse cidr")
|
||||
elif config.internal:
|
||||
self.scan_interfaces()
|
||||
elif len(config.remote) > 0:
|
||||
for host in config.remote:
|
||||
self.publish_event(NewHostEvent(host=host, cloud=self.get_cloud(host)))
|
||||
elif config.pod:
|
||||
if self.is_azure_pod():
|
||||
self.azure_metadata_discovery()
|
||||
else:
|
||||
self.traceroute_discovery()
|
||||
elif len(self.event.predefined_hosts) == 0:
|
||||
self.scan_interfaces()
|
||||
else:
|
||||
for host in self.event.predefined_hosts:
|
||||
self.publish_event(NewHostEvent(host=host, cloud=self.get_cloud(host)))
|
||||
|
||||
def get_cloud(self, host):
|
||||
metadata = requests.get("http://www.azurespeed.com/api/region?ipOrUrl={ip}".format(ip=host)).text
|
||||
@@ -76,11 +76,10 @@ class HostDiscovery(Hunter):
|
||||
|
||||
def is_azure_pod(self):
|
||||
try:
|
||||
if requests.get("http://169.254.169.254/metadata/instance?api-version=2017-08-01", headers={"Metadata":"true"}).status_code == 200:
|
||||
if requests.get("http://169.254.169.254/metadata/instance?api-version=2017-08-01", headers={"Metadata":"true"}, timeout=5).status_code == 200:
|
||||
return True
|
||||
except Exception as ex:
|
||||
logging.debug("Not azure cluster " + str(ex.message))
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
# for pod scanning
|
||||
def traceroute_discovery(self):
|
||||
|
||||
@@ -24,22 +24,6 @@ class SecureKubeletEvent(Service, Event):
|
||||
Service.__init__(self, name="Kubelet API")
|
||||
|
||||
|
||||
""" Vulnerabilities """
|
||||
class ExposedPodsHandler(Vulnerability, Event):
|
||||
"""Exposes sensitive information about pods that are bound to the node"""
|
||||
def __init__(self):
|
||||
Vulnerability.__init__(self, Kubelet, "Exposed /pods")
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
class KubeletPorts(Enum):
|
||||
SECURED = 10250
|
||||
READ_ONLY = 10255
|
||||
@@ -53,21 +37,18 @@ 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(ExposedPodsHandler())
|
||||
self.publish_event(ReadOnlyKubeletEvent())
|
||||
|
||||
def get_secure_access(self):
|
||||
event = SecureKubeletEvent()
|
||||
if self.ping_kubelet(authenticate=False) == 200:
|
||||
self.publish_event(ExposedPodsHandler())
|
||||
self.publish_event(AnonymousAuthEnabled())
|
||||
event.anonymous_auth = True
|
||||
event.secure = False
|
||||
# anonymous authentication is disabled
|
||||
elif self.ping_kubelet(authenticate=True) == 200:
|
||||
event.anonymous_auth = False
|
||||
event.secure = True
|
||||
self.publish_event(event)
|
||||
|
||||
def ping_kubelet(self, authenticate=False):
|
||||
def ping_kubelet(self, authenticate):
|
||||
r = requests.Session()
|
||||
if authenticate:
|
||||
if self.event.auth_token:
|
||||
|
||||
@@ -8,12 +8,22 @@ import urllib3
|
||||
from __main__ import config
|
||||
from ...core.events import handler
|
||||
from ...core.events.types import Vulnerability, Event
|
||||
from ..discovery.kubelet import ReadOnlyKubeletEvent, SecureKubeletEvent, ExposedPodsHandler
|
||||
from ..discovery.kubelet import ReadOnlyKubeletEvent, SecureKubeletEvent
|
||||
from ...core.types import Hunter, ActiveHunter, KubernetesCluster, Kubelet
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
""" Vulnerabilities """
|
||||
class ExposedPodsHandler(Vulnerability, Event):
|
||||
"""Exposes all complete PodSpecs bound to a node"""
|
||||
def __init__(self):
|
||||
Vulnerability.__init__(self, Kubelet, "Exposed /pods")
|
||||
|
||||
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")
|
||||
|
||||
class ExposedContainerLogsHandler(Vulnerability, Event):
|
||||
"""Outputs logs from a running container"""
|
||||
def __init__(self):
|
||||
@@ -50,6 +60,11 @@ class ExposedAttachHandler(Vulnerability, Event):
|
||||
Vulnerability.__init__(self, Kubelet, "Exposed /attach")
|
||||
self.remediation="--enable-debugging-handlers=False On Kubelet"
|
||||
|
||||
class ExposedHealthzHandler(Vulnerability, Event):
|
||||
"""By accessing open /healthz handler, an attacker could get the cluster health state"""
|
||||
def __init__(self):
|
||||
Vulnerability.__init__(self, Kubelet, "Cluster Health Disclosure")
|
||||
|
||||
class K8sVersionDisclosure(Vulnerability, Event):
|
||||
"""Discloses the kubernetes version, exposed from a log on the /metrics endpoint"""
|
||||
def __init__(self, version):
|
||||
@@ -70,6 +85,7 @@ class ReadOnlyKubeletPortHunter(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.path = "http://{}:{}/".format(self.event.host, self.event.port)
|
||||
self.pods_endpoint_data = ""
|
||||
|
||||
def get_k8s_version(self):
|
||||
metrics = requests.get(self.path + "metrics").text
|
||||
@@ -82,23 +98,35 @@ class ReadOnlyKubeletPortHunter(Hunter):
|
||||
|
||||
# returns list of tuples of Privileged 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"]:
|
||||
if self.pods_endpoint_data:
|
||||
for pod in self.pods_endpoint_data["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 get_pods_endpoint(self):
|
||||
response = requests.get(self.path + "pods")
|
||||
if "items" in response.text:
|
||||
return json.loads(response.text)
|
||||
|
||||
def check_healthz_endpoint(self):
|
||||
return requests.get(self.path + "healthz", verify=False).status_code == 200
|
||||
|
||||
def execute(self):
|
||||
self.pods_endpoint_data = self.get_pods_endpoint()
|
||||
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))
|
||||
|
||||
if self.pods_endpoint_data:
|
||||
self.publish_event(ExposedPodsHandler())
|
||||
if self.check_healthz_endpoint():
|
||||
self.publish_event(ExposedHealthzHandler())
|
||||
|
||||
@handler.subscribe(SecureKubeletEvent)
|
||||
class SecureKubeletPortHunter(Hunter):
|
||||
class DebugHandlers(object):
|
||||
@@ -186,55 +214,69 @@ class SecureKubeletPortHunter(Hunter):
|
||||
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.cert = self.event.client_cert
|
||||
self.path = "https://{}:{}/".format(self.event.host, 10250)
|
||||
self.kubehunter_pod = {"name": "kube-hunter", "namespace": "default", "container": "kube-hunter"}
|
||||
self.pods_endpoint_data = ""
|
||||
|
||||
def get_pods_endpoint(self):
|
||||
response = self.session.get(self.path + "pods", verify=False)
|
||||
if "items" in response.text:
|
||||
return json.loads(response.text)
|
||||
|
||||
def check_healthz_endpoint(self):
|
||||
return requests.get(self.path + "healthz", verify=False).status_code == 200
|
||||
|
||||
def execute(self):
|
||||
self.test_debugging_handlers()
|
||||
self.pods_endpoint_data = self.get_pods_endpoint()
|
||||
if not self.event.secure:
|
||||
self.publish_event(AnonymousAuthEnabled())
|
||||
if self.pods_endpoint_data:
|
||||
self.publish_event(ExposedPodsHandler())
|
||||
if self.check_healthz_endpoint():
|
||||
self.publish_event(ExposedHealthzHandler())
|
||||
self.test_handlers()
|
||||
|
||||
def test_debugging_handlers(self):
|
||||
def test_handlers(self):
|
||||
# if kube-hunter runs in a pod, we test with kube-hunter's pod
|
||||
pod = self.get_self_pod() if config.pod else self.get_random_pod()
|
||||
debug_handlers = self.DebugHandlers(self.path, pod=pod, session=self.session)
|
||||
|
||||
try:
|
||||
if debug_handlers.test_container_logs():
|
||||
self.publish_event(ExposedContainerLogsHandler())
|
||||
if debug_handlers.test_exec_container():
|
||||
self.publish_event(ExposedExecHandler())
|
||||
if debug_handlers.test_run_container():
|
||||
self.publish_event(ExposedRunHandler())
|
||||
if debug_handlers.test_running_pods():
|
||||
self.publish_event(ExposedRunningPodsHandler())
|
||||
if debug_handlers.test_port_forward():
|
||||
self.publish_event(ExposedPortForwardHandler()) # not implemented
|
||||
if debug_handlers.test_attach_container():
|
||||
self.publish_event(ExposedAttachHandler())
|
||||
except Exception as ex:
|
||||
logging.debug(str(ex.message))
|
||||
|
||||
def get_self_pod(self):
|
||||
return {"name": "kube-hunter",
|
||||
"namespace": "default",
|
||||
"container": "kube-hunter"}
|
||||
pod = self.kubehunter_pod if config.pod else self.get_random_pod()
|
||||
if pod:
|
||||
debug_handlers = self.DebugHandlers(self.path, pod=pod, session=self.session)
|
||||
try:
|
||||
if debug_handlers.test_container_logs():
|
||||
self.publish_event(ExposedContainerLogsHandler())
|
||||
if debug_handlers.test_exec_container():
|
||||
self.publish_event(ExposedExecHandler())
|
||||
if debug_handlers.test_run_container():
|
||||
self.publish_event(ExposedRunHandler())
|
||||
if debug_handlers.test_running_pods():
|
||||
self.publish_event(ExposedRunningPodsHandler())
|
||||
if debug_handlers.test_port_forward():
|
||||
self.publish_event(ExposedPortForwardHandler()) # not implemented
|
||||
if debug_handlers.test_attach_container():
|
||||
self.publish_event(ExposedAttachHandler())
|
||||
except Exception as ex:
|
||||
logging.debug(str(ex.message))
|
||||
else:
|
||||
pass # no pod to check on.
|
||||
|
||||
# trying to get a pod from default namespace, if doesnt exist, gets a kube-system one
|
||||
def get_random_pod(self):
|
||||
pods_data = json.loads(self.session.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False).text)['items']
|
||||
# filter running kubesystem pod
|
||||
is_default_pod = lambda pod: pod["metadata"]["namespace"] == "default" and pod["status"]["phase"] == "Running"
|
||||
is_kubesystem_pod = lambda pod: pod["metadata"]["namespace"] == "kube-system" and pod["status"]["phase"] == "Running"
|
||||
pod_data = next((pod_data for pod_data in pods_data if is_default_pod(pod_data)), None)
|
||||
if not pod_data:
|
||||
pod_data = next((pod_data for pod_data in pods_data if is_kubesystem_pod(pod_data)), None)
|
||||
|
||||
container_data = (container_data for container_data in pod_data["spec"]["containers"]).next()
|
||||
return {
|
||||
"name": pod_data["metadata"]["name"],
|
||||
"container": container_data["name"],
|
||||
"namespace": pod_data["metadata"]["namespace"]
|
||||
}
|
||||
|
||||
if self.pods_endpoint_data:
|
||||
pods_data = self.pods_endpoint_data["items"]
|
||||
# filter running kubesystem pod
|
||||
is_default_pod = lambda pod: pod["metadata"]["namespace"] == "default" and pod["status"]["phase"] == "Running"
|
||||
is_kubesystem_pod = lambda pod: pod["metadata"]["namespace"] == "kube-system" and pod["status"]["phase"] == "Running"
|
||||
pod_data = next((pod_data for pod_data in pods_data if is_default_pod(pod_data)), None)
|
||||
if not pod_data:
|
||||
pod_data = next((pod_data for pod_data in pods_data if is_kubesystem_pod(pod_data)), None)
|
||||
|
||||
container_data = (container_data for container_data in pod_data["spec"]["containers"]).next()
|
||||
return {
|
||||
"name": pod_data["metadata"]["name"],
|
||||
"container": container_data["name"],
|
||||
"namespace": pod_data["metadata"]["namespace"]
|
||||
}
|
||||
|
||||
@handler.subscribe(ExposedRunHandler)
|
||||
class ProveRunHandler(ActiveHunter):
|
||||
@@ -267,6 +309,16 @@ class ProveRunHandler(ActiveHunter):
|
||||
self.event.evidence = "uname: " + output
|
||||
break
|
||||
|
||||
|
||||
@handler.subscribe(ExposedHealthzHandler)
|
||||
class ProveHealthzHandler(ActiveHunter):
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
def execute(self):
|
||||
protocol = "https" if self.event.port == 10250 else "http"
|
||||
self.event.evidence = requests.get("{protocol}://{host}:{port}/healthz".format(protocol=protocol, host=self.event.host, port=self.event.port), verify=False).text
|
||||
|
||||
@handler.subscribe(ExposedPodsHandler)
|
||||
class ProvePodsHandler(ActiveHunter):
|
||||
def __init__(self, event):
|
||||
|
||||
Reference in New Issue
Block a user