Merge branch 'master' into more-service-account-token-hunters

This commit is contained in:
Ori Agmon
2018-11-07 23:44:16 +02:00
committed by GitHub
10 changed files with 119 additions and 91 deletions

View File

@@ -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`

View File

@@ -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__':

View File

@@ -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:

View File

@@ -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)

View File

@@ -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('/')

View File

@@ -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):

View File

@@ -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))

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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