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:
daniel_sagi
2018-06-10 16:27:37 +03:00
parent 4a98d698a1
commit 36e87807e6
14 changed files with 336 additions and 144 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,4 +2,5 @@ netaddr
netifaces
enum34
scapy
requests
requests
PrettyTable