mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-11 03:37:52 +00:00
Merge branch 'master' into more-service-account-token-hunters
This commit is contained in:
@@ -81,6 +81,8 @@ Install module dependencies:
|
||||
~~~
|
||||
cd ./kube-hunter
|
||||
pip install -r requirements.txt
|
||||
|
||||
In the case where you have python 3.x in the path as your default, and python2 refers to a python 2.7 executable, use "python2 -m pip install -r requirements.txt"
|
||||
~~~
|
||||
Run:
|
||||
`./kube-hunter.py`
|
||||
|
||||
@@ -82,7 +82,7 @@ def list_hunters():
|
||||
print("\nPassive Hunters:\n----------------")
|
||||
for i, (hunter, docs) in enumerate(handler.passive_hunters.items()):
|
||||
name, docs = parse_docs(hunter, docs)
|
||||
print("* {}\n {}\n".format( name, docs))
|
||||
print("* {}\n {}\n".format(name, docs))
|
||||
|
||||
if config.active:
|
||||
print("\n\nActive Hunters:\n---------------")
|
||||
@@ -91,11 +91,13 @@ def list_hunters():
|
||||
print("* {}\n {}\n".format( name, docs))
|
||||
|
||||
|
||||
global hunt_started_lock
|
||||
hunt_started_lock = threading.Lock()
|
||||
hunt_started = False
|
||||
|
||||
|
||||
def main():
|
||||
global hunt_started
|
||||
global hunt_started
|
||||
scan_options = [
|
||||
config.pod,
|
||||
config.cidr,
|
||||
@@ -109,7 +111,10 @@ def main():
|
||||
|
||||
if not any(scan_options):
|
||||
if not interactive_set_config(): return
|
||||
|
||||
hunt_started_lock.acquire()
|
||||
hunt_started = True
|
||||
hunt_started_lock.release()
|
||||
handler.publish_event(HuntStarted())
|
||||
handler.publish_event(HostScanEvent())
|
||||
|
||||
@@ -121,11 +126,16 @@ def main():
|
||||
except EOFError:
|
||||
logging.error("\033[0;31mPlease run again with -it\033[0m")
|
||||
finally:
|
||||
hunt_started_lock.acquire()
|
||||
if hunt_started:
|
||||
hunt_started_lock.release()
|
||||
handler.publish_event(HuntFinished())
|
||||
handler.join()
|
||||
handler.free()
|
||||
logging.debug("Cleaned Queue")
|
||||
else:
|
||||
hunt_started_lock.release()
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -12,7 +12,8 @@ from ..types import ActiveHunter, Hunter
|
||||
from ...core.events.types import HuntFinished
|
||||
import threading
|
||||
|
||||
working_count = 0
|
||||
global queue_lock
|
||||
queue_lock = Lock()
|
||||
|
||||
# Inherits Queue object, handles events asynchronously
|
||||
class EventQueue(Queue, object):
|
||||
@@ -34,12 +35,12 @@ class EventQueue(Queue, object):
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
# decorator wrapping for easy subscription
|
||||
def subscribe(self, event, hook=None, predicate=None):
|
||||
def wrapper(hook):
|
||||
self.subscribe_event(event, hook=hook, predicate=predicate)
|
||||
return hook
|
||||
|
||||
return wrapper
|
||||
|
||||
# getting uninstantiated event object
|
||||
@@ -72,7 +73,9 @@ class EventQueue(Queue, object):
|
||||
# executes callbacks on dedicated thread as a daemon
|
||||
def worker(self):
|
||||
while self.running:
|
||||
queue_lock.acquire()
|
||||
hook = self.get()
|
||||
queue_lock.release()
|
||||
try:
|
||||
hook.execute()
|
||||
except Exception as ex:
|
||||
|
||||
@@ -65,6 +65,9 @@ class Vulnerability(object):
|
||||
def explain(self):
|
||||
return self.__doc__
|
||||
|
||||
|
||||
global event_id_count_lock
|
||||
event_id_count_lock = threading.Lock()
|
||||
event_id_count = 0
|
||||
|
||||
""" Discovery/Hunting Events """
|
||||
@@ -75,8 +78,10 @@ class NewHostEvent(Event):
|
||||
global event_id_count
|
||||
self.host = host
|
||||
self.cloud = cloud
|
||||
event_id_count_lock.acquire()
|
||||
self.event_id = event_id_count
|
||||
event_id_count += 1
|
||||
event_id_count_lock.release()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.host)
|
||||
|
||||
@@ -15,7 +15,6 @@ from ...core.events import handler
|
||||
from ...core.events.types import Event, NewHostEvent, Vulnerability
|
||||
from ...core.types import Hunter, InformationDisclosure, Azure
|
||||
|
||||
|
||||
class RunningAsPodEvent(Event):
|
||||
def __init__(self):
|
||||
self.name = 'Running from within a pod'
|
||||
@@ -54,7 +53,7 @@ class HostDiscovery(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
def execute(self):
|
||||
def execute(self):
|
||||
if config.cidr:
|
||||
try:
|
||||
ip, sn = config.cidr.split('/')
|
||||
|
||||
@@ -7,7 +7,7 @@ from kubelet import ExposedRunHandler
|
||||
|
||||
from ...core.events import handler
|
||||
from ...core.events.types import Event, Vulnerability
|
||||
from ...core.types import Hunter, ActiveHunter, KubernetesCluster, IdentityTheft, Azure
|
||||
from ...core.types import Hunter, ActiveHunter, IdentityTheft, Azure
|
||||
|
||||
|
||||
class AzureSpnExposure(Vulnerability, Event):
|
||||
|
||||
@@ -1,43 +1,11 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from ...core.events import handler
|
||||
from ...core.events.types import Vulnerability, Event, OpenPortEvent
|
||||
from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure, RemoteCodeExec, UnauthenticatedAccess, AccessRisk
|
||||
|
||||
""" Helper functions """
|
||||
|
||||
# Will attempt to do request "req1" with the optional parameters.
|
||||
# If fails it will attempt to do "req2" with the optional parameters.
|
||||
# If once of the request success this method will return True, if both fail- False.
|
||||
def helperFuncDo2Requests(req1, req2, is_verify=False, data=None, req_type="get"):
|
||||
try:
|
||||
r = helperDoRequest(req1, is_verify, data, req_type)
|
||||
has_remote_access_gained = (r.status_code == 200 and r.content != "")
|
||||
if has_remote_access_gained:
|
||||
return r
|
||||
except Exception:
|
||||
try:
|
||||
r = helperDoRequest(req2, is_verify, data, req_type)
|
||||
has_remote_access_gained = (r.status_code == 200 and r.content != "")
|
||||
if has_remote_access_gained:
|
||||
return r
|
||||
except Exception:
|
||||
return False # None of the requests succeded..
|
||||
return False
|
||||
|
||||
|
||||
def helperDoRequest(req, is_verify, data=None, req_type="get"):
|
||||
if req_type == "put":
|
||||
r = requests.put(req, verify=is_verify, timeout=3, data=data)
|
||||
return r
|
||||
elif req_type == "get":
|
||||
r = requests.get(req, verify=is_verify, timeout=3, data=data)
|
||||
return r
|
||||
|
||||
|
||||
from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure, RemoteCodeExec, \
|
||||
UnauthenticatedAccess, AccessRisk
|
||||
""" Vulnerabilities """
|
||||
class EtcdRemoteWriteAccessEvent(Vulnerability, Event):
|
||||
"""Remote write access might grant an attacker full control over the kubernetes cluster"""
|
||||
@@ -57,21 +25,26 @@ class EtcdRemoteVersionDisclosureEvent(Vulnerability, Event):
|
||||
"""Remote version disclosure might give an attacker a valuable data to attack a cluster"""
|
||||
|
||||
def __init__(self, version):
|
||||
Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure", category=InformationDisclosure)
|
||||
|
||||
Vulnerability.__init__(self, KubernetesCluster, name="Etcd Remote version disclosure",
|
||||
category=InformationDisclosure)
|
||||
self.evidence = version
|
||||
|
||||
class EtcdAccessEnabledWithoutAuthEvent(Vulnerability, Event):
|
||||
"""Etcd is accessible without authorization, it would allow a potential attacker to gain access to the etcd"""
|
||||
"""Etcd is accessible using HTTP (without authorization and authentication), it would allow a potential attacker to
|
||||
gain access to the etcd"""
|
||||
|
||||
def __init__(self, version):
|
||||
Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible without authorization", category=UnauthenticatedAccess)
|
||||
Vulnerability.__init__(self, KubernetesCluster, name="Etcd is accessible using insecure connection (HTTP)",
|
||||
category=UnauthenticatedAccess)
|
||||
self.evidence = version
|
||||
|
||||
# Active Hunter
|
||||
@handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379)
|
||||
class EtcdRemoteAccessActive(ActiveHunter):
|
||||
"""Checks for remote write access to etcd"""
|
||||
|
||||
"""Etcd Remote Access
|
||||
Checks for remote write access to etcd"""
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.write_evidence = ''
|
||||
@@ -81,66 +54,69 @@ class EtcdRemoteAccessActive(ActiveHunter):
|
||||
data = {
|
||||
'value': 'remotely written data'
|
||||
}
|
||||
r_secure = "https://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379)
|
||||
r_not_secure = "http://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379)
|
||||
res = helperFuncDo2Requests(r_secure, r_not_secure)
|
||||
if res:
|
||||
self.write_evidence = res.content
|
||||
return True
|
||||
return False
|
||||
try:
|
||||
r = requests.post("{protocol}://{host}:{port}/v2/keys/message".format(host=self.event.host, port=2379,
|
||||
protocol=self.protocol), data=data)
|
||||
self.write_evidence = r.content if r.status_code == 200 and r.content != '' else False
|
||||
return self.write_evidence
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def execute(self):
|
||||
if self.db_keys_write_access():
|
||||
self.publish_event(EtcdRemoteWriteAccessEvent(self.write_evidence))
|
||||
|
||||
|
||||
# Passive Hunter
|
||||
@handler.subscribe(OpenPortEvent, predicate=lambda p: p.port == 2379)
|
||||
class EtcdRemoteAccess(Hunter):
|
||||
"""Etcd Remote Access
|
||||
Checks for remote availability of etcd, version, read access, write access
|
||||
"""
|
||||
# TODO:
|
||||
# Check the etcd hunter on a remote cluster! (currently everything was checked only at 127.0.0.1:2379)
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.version_evidence = ''
|
||||
self.keys_evidence = ''
|
||||
self.protocol = 'https'
|
||||
|
||||
def db_keys_disclosure(self):
|
||||
logging.debug(self.event.host)
|
||||
logging.debug("Passive hunter is attempting to read etcd keys remotely")
|
||||
r_secure = "https://{host}:{port}/v2/keys".format(host=self.event.host, port=2379)
|
||||
r_not_secure = "http://{host}:{port}/v2/keys".format(host=self.event.host, port=2379)
|
||||
res = helperFuncDo2Requests(r_secure, r_not_secure)
|
||||
if res:
|
||||
self.keys_evidence = res.content
|
||||
return True
|
||||
return False
|
||||
logging.debug(self.event.host + " Passive hunter is attempting to read etcd keys remotely")
|
||||
try:
|
||||
r = requests.get(
|
||||
"{protocol}://{host}:{port}/v2/keys".format(protocol=self.protocol, host=self.event.host, port=2379),
|
||||
verify=False)
|
||||
self.keys_evidence = r.content if r.status_code == 200 and r.content != '' else False
|
||||
return self.keys_evidence
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def version_disclosure(self):
|
||||
logging.debug(self.event.host)
|
||||
logging.debug("Passive hunter is attempting to check etcd version remotely")
|
||||
r_secure = "https://{host}:{port}/version".format(host=self.event.host, port=2379)
|
||||
r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379)
|
||||
res = helperFuncDo2Requests(r_secure, r_not_secure)
|
||||
if res:
|
||||
self.version_evidence = res.content
|
||||
return True
|
||||
return False
|
||||
logging.debug(self.event.host + " Passive hunter is attempting to check etcd version remotely")
|
||||
try:
|
||||
r = requests.get(
|
||||
"{protocol}://{host}:{port}/version".format(protocol=self.protocol, host=self.event.host, port=2379),
|
||||
verify=False)
|
||||
self.version_evidence = r.content if r.status_code == 200 and r.content != '' else False
|
||||
return self.version_evidence
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def unauthorized_access(self):
|
||||
logging.debug(self.event.host)
|
||||
logging.debug("Passive hunter is attempting to access etcd without authorization")
|
||||
r_not_secure = "http://{host}:{port}/version".format(host=self.event.host, port=2379)
|
||||
res = helperFuncDo2Requests(r_not_secure, r_not_secure) # We don't have to do 2 requests this time
|
||||
if res:
|
||||
return True
|
||||
return False
|
||||
def insecure_access(self):
|
||||
logging.debug(self.event.host + " Passive hunter is attempting to access etcd insecurely")
|
||||
try:
|
||||
r = requests.get("http://{host}:{port}/version".format(host=self.event.host, port=2379), verify=False)
|
||||
return r.content if r.status_code == 200 and r.content != '' else False
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def execute(self):
|
||||
if self.insecure_access(): # make a decision between http and https protocol
|
||||
self.protocol = 'http'
|
||||
if self.version_disclosure():
|
||||
self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence))
|
||||
if self.unauthorized_access():
|
||||
if self.protocol == 'http':
|
||||
self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence))
|
||||
if self.db_keys_disclosure():
|
||||
self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence))
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@ from src.core.events import handler
|
||||
from src.core.events.types import Event, Service, Vulnerability, HuntFinished, HuntStarted
|
||||
import threading
|
||||
|
||||
|
||||
global services_lock
|
||||
services_lock = threading.Lock()
|
||||
services = list()
|
||||
|
||||
global vulnerabilities_lock
|
||||
vulnerabilities_lock = threading.Lock()
|
||||
vulnerabilities = list()
|
||||
|
||||
|
||||
@@ -38,10 +44,13 @@ class Collector(object):
|
||||
|
||||
def execute(self):
|
||||
"""function is called only when collecting data"""
|
||||
global services, vulnerabilities
|
||||
global services
|
||||
global vulnerabilities
|
||||
bases = self.event.__class__.__mro__
|
||||
if Service in bases:
|
||||
services_lock.acquire()
|
||||
services.append(self.event)
|
||||
services_lock.release()
|
||||
import datetime
|
||||
logging.info("|\n| {name}:\n| type: open service\n| service: {name}\n|_ host: {host}:{port}".format(
|
||||
host=self.event.host,
|
||||
@@ -51,7 +60,9 @@ class Collector(object):
|
||||
))
|
||||
|
||||
elif Vulnerability in bases:
|
||||
vulnerabilities_lock.acquire()
|
||||
vulnerabilities.append(self.event)
|
||||
vulnerabilities_lock.release()
|
||||
logging.info(
|
||||
"|\n| {name}:\n| type: vulnerability\n| host: {host}:{port}\n| description: \n{desc}".format(
|
||||
name=self.event.get_name(),
|
||||
|
||||
@@ -3,8 +3,7 @@ from __future__ import print_function
|
||||
from prettytable import ALL, PrettyTable
|
||||
|
||||
from __main__ import config
|
||||
from collector import services, vulnerabilities
|
||||
import threading
|
||||
from collector import services, vulnerabilities, services_lock, vulnerabilities_lock
|
||||
|
||||
EVIDENCE_PREVIEW = 40
|
||||
MAX_TABLE_WIDTH = 20
|
||||
@@ -15,11 +14,20 @@ class PlainReporter(object):
|
||||
def get_report(self):
|
||||
"""generates report tables"""
|
||||
output = ""
|
||||
if len(services):
|
||||
|
||||
vulnerabilities_lock.acquire()
|
||||
vulnerabilities_len = len(services)
|
||||
vulnerabilities_lock.release()
|
||||
|
||||
services_lock.acquire()
|
||||
services_len = len(vulnerabilities)
|
||||
services_lock.release()
|
||||
|
||||
if services_len:
|
||||
output += self.nodes_table()
|
||||
if not config.mapping:
|
||||
output += self.services_table()
|
||||
if len(vulnerabilities):
|
||||
if vulnerabilities_len:
|
||||
output += self.vulns_table()
|
||||
else:
|
||||
output += "\nNo vulnerabilities were found"
|
||||
@@ -38,11 +46,14 @@ class PlainReporter(object):
|
||||
nodes_table.header_style = "upper"
|
||||
# TODO: replace with sets
|
||||
id_memory = list()
|
||||
services_lock.acquire()
|
||||
for service in services:
|
||||
if service.event_id not in id_memory:
|
||||
nodes_table.add_row(["Node/Master", service.host])
|
||||
id_memory.append(service.event_id)
|
||||
return "\nNodes\n{}\n".format(nodes_table)
|
||||
nodes_ret = "\nNodes\n{}\n".format(nodes_table)
|
||||
services_lock.release()
|
||||
return nodes_ret
|
||||
|
||||
def services_table(self):
|
||||
services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL)
|
||||
@@ -52,9 +63,12 @@ class PlainReporter(object):
|
||||
services_table.sortby = "Service"
|
||||
services_table.reversesort = True
|
||||
services_table.header_style = "upper"
|
||||
services_lock.acquire()
|
||||
for service in services:
|
||||
services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()])
|
||||
return "\nDetected Services\n{}\n".format(services_table)
|
||||
detected_services_ret = "\nDetected Services\n{}\n".format(services_table)
|
||||
services_lock.release()
|
||||
return detected_services_ret
|
||||
|
||||
def vulns_table(self):
|
||||
column_names = ["Location", "Category", "Vulnerability", "Description", "Evidence"]
|
||||
@@ -65,9 +79,12 @@ class PlainReporter(object):
|
||||
vuln_table.reversesort = True
|
||||
vuln_table.padding_width = 1
|
||||
vuln_table.header_style = "upper"
|
||||
|
||||
vulnerabilities_lock.acquire()
|
||||
for vuln in vulnerabilities:
|
||||
row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.category.name, vuln.get_name(), vuln.explain()]
|
||||
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)
|
||||
vulnerabilities_lock.release()
|
||||
return "\nVulnerabilities\n{}\n".format(vuln_table)
|
||||
|
||||
@@ -2,8 +2,7 @@ import StringIO
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from collector import services, vulnerabilities
|
||||
import threading
|
||||
from collector import services, vulnerabilities, services_lock, vulnerabilities_lock
|
||||
|
||||
class YAMLReporter(object):
|
||||
def get_report(self):
|
||||
@@ -20,25 +19,31 @@ class YAMLReporter(object):
|
||||
def get_nodes(self):
|
||||
nodes = list()
|
||||
node_locations = set()
|
||||
services_lock.acquire()
|
||||
for service in services:
|
||||
node_location = str(service.host)
|
||||
if node_location not in node_locations:
|
||||
nodes.append({"type": "Node/Master", "location": str(service.host)})
|
||||
node_locations.add(node_location)
|
||||
services_lock.release()
|
||||
return nodes
|
||||
|
||||
def get_services(self):
|
||||
services_lock.acquire()
|
||||
services_data = [{"service": service.get_name(),
|
||||
"location": "{}:{}{}".format(service.host, service.port, service.get_path()),
|
||||
"description": service.explain()}
|
||||
for service in services]
|
||||
services_lock.release()
|
||||
return services_data
|
||||
|
||||
def get_vulenrabilities(self):
|
||||
vulnerabilities_lock.acquire()
|
||||
vulnerabilities_data = [{"location": "{}:{}".format(vuln.host, vuln.port) if vuln.host else "",
|
||||
"category": vuln.category.name,
|
||||
"vulnerability": vuln.get_name(),
|
||||
"description": vuln.explain(),
|
||||
"evidence": str(vuln.evidence)}
|
||||
for vuln in vulnerabilities]
|
||||
vulnerabilities_lock.release()
|
||||
return vulnerabilities_data
|
||||
|
||||
Reference in New Issue
Block a user