Refactor And Major Bug Fixes in Version and CVE hunting (#162)

* changed version hunting to be on a a new version disclosure vulnerability

* fixed version publish

* added logging and fixed typo

* changed whole way of comparing versions in cve hunter

* changed K8sVersionDisclosure vulnerability to one core vulnerability, that takes an endpoint. changed all usage

* added tests

* merged kubectl cve hunting with apiserver hunting. and simplified the code of apiserver cve hunting

* fixed tests to new names

* changed name of module to cves.py

* drastically improved the cve vulnerble detection utility function. now works with all types of versioning methods

* added packaging in requirementes.txt

* added another test, and improved logic on cve comparison for more complicated versions

* changed CveHunter to subscribe_once, to prevent duplicates duplicates

* fixed tests for new improvements

* removed unnecessary ternary on doc

* removed unnecessary join split

* improved compare function, made it util

* improved cve checking to use mapping
This commit is contained in:
danielsagi
2019-08-27 10:48:47 +03:00
committed by Liz Rice
parent 44e6438d37
commit 259f707ecd
10 changed files with 248 additions and 202 deletions

View File

@@ -2,7 +2,7 @@ import logging
import requests
import json
import threading
from src.core.types import InformationDisclosure, DenialOfService, RemoteCodeExec, IdentityTheft, PrivilegeEscalation, AccessRisk, UnauthenticatedAccess
from src.core.types import InformationDisclosure, DenialOfService, RemoteCodeExec, IdentityTheft, PrivilegeEscalation, AccessRisk, UnauthenticatedAccess, KubernetesCluster
class EventFilterBase(object):
def __init__(self, event):
@@ -139,6 +139,7 @@ class OpenPortEvent(Event):
location = str(self.port)
return location
class HuntFinished(Event):
pass
@@ -149,3 +150,17 @@ class HuntStarted(Event):
class ReportDispatched(Event):
pass
""" Core Vulnerabilites """
class K8sVersionDisclosure(Vulnerability, Event):
"""The kubernetes version could be obtained from the {} endpoint """
def __init__(self, version, from_endpoint, extra_info=""):
Vulnerability.__init__(self, KubernetesCluster, "K8s Version Disclosure", category=InformationDisclosure)
self.version = version
self.from_endpoint = from_endpoint
self.extra_info = extra_info
self.evidence = version
def explain(self):
return self.__doc__.format(self.from_endpoint) + self.extra_info

View File

@@ -5,7 +5,7 @@ import uuid
import copy
from ...core.events import handler
from ...core.events.types import Vulnerability, Event
from ...core.events.types import Vulnerability, Event, K8sVersionDisclosure
from ..discovery.apiserver import ApiServer
from ...core.types import Hunter, ActiveHunter, KubernetesCluster
from ...core.types import RemoteCodeExec, AccessRisk, InformationDisclosure, UnauthenticatedAccess
@@ -13,7 +13,6 @@ from ...core.types import RemoteCodeExec, AccessRisk, InformationDisclosure, Una
""" Vulnerabilities """
class ServerApiAccess(Vulnerability, Event):
""" The API Server port is accessible. Depending on your RBAC settings this could expose access to or control of your cluster. """
@@ -557,3 +556,25 @@ class AccessApiServerActive(ActiveHunter):
# Note: we are not binding any role or cluster role because
# -- in certain cases it might effect the running pod within the cluster (and we don't want to do that).
@handler.subscribe(ApiServer)
class ApiVersionHunter(Hunter):
"""Api Version Hunter
Tries to obtain the Api Server's version directly from /version endpoint
"""
def __init__(self, event):
self.event = event
self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port)
self.session = requests.Session()
self.session.verify = False
if self.event.auth_token:
self.session.headers.update({"Authorization": "Bearer {}".format(self.event.auth_token)})
def execute(self):
if self.event.auth_token:
logging.debug('Passive Hunter is attempting to access the API server version end point using the pod\'s service account token on {}:{} \t'.format(self.event.host, self.event.port))
else:
logging.debug('Passive Hunter is attempting to access the API server version end point anonymously')
version = json.loads(self.session.get(self.path + "/version").text)["gitVersion"]
logging.debug("Discovered version of api server {}".format(version))
self.publish_event(K8sVersionDisclosure(version=version, from_endpoint="/version"))

View File

@@ -1,114 +0,0 @@
import logging
import json
import requests
from ...core.events import handler
from ...core.events.types import Vulnerability, Event
from ..discovery.apiserver import ApiServer
from ...core.types import Hunter, ActiveHunter, KubernetesCluster, RemoteCodeExec, AccessRisk, InformationDisclosure, \
PrivilegeEscalation, DenialOfService
""" Vulnerabilities """
class ServerApiVersionEndPointAccessPE(Vulnerability, Event):
"""Node is vulnerable to critical CVE-2018-1002105"""
def __init__(self, evidence):
Vulnerability.__init__(self, KubernetesCluster, name="Critical Privilege Escalation CVE", category=PrivilegeEscalation)
self.evidence = evidence
class ServerApiVersionEndPointAccessDos(Vulnerability, Event):
"""Node not patched for CVE-2019-1002100. Depending on your RBAC settings, a crafted json-patch could cause a Denial of Service."""
def __init__(self, evidence):
Vulnerability.__init__(self, KubernetesCluster, name="Denial of Service to Kubernetes API Server", category=DenialOfService)
self.evidence = evidence
# Passive Hunter
@handler.subscribe(ApiServer)
class IsVulnerableToCVEAttack(Hunter):
"""CVE hunter
Checks if Node is running a Kubernetes version vulnerable to critical CVEs
"""
def __init__(self, event):
self.event = event
self.headers = dict()
# From within a Pod we may have extra credentials
if self.event.auth_token:
self.headers = {'Authorization': 'Bearer ' + self.event.auth_token}
self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port)
self.api_server_evidence = ''
self.k8sVersion = ''
def get_api_server_version_end_point(self):
logging.debug(self.event.host)
if 'Authorization' in self.headers:
logging.debug('Passive Hunter is attempting to access the API server version end point using the pod\'s service account token: \t%s', str(self.headers))
else:
logging.debug('Passive Hunter is attempting to access the API server version end point anonymously')
try:
res = requests.get("{path}/version".format(path=self.path),
headers=self.headers, verify=False)
self.api_server_evidence = res.text
resDict = json.loads(res.text)
version = resDict["gitVersion"].split('.')
first_two_minor_digits = int(version[1])
last_two_minor_digits = int(version[2])
logging.debug('Passive Hunter got version from the API server version end point: %d.%d', first_two_minor_digits, last_two_minor_digits)
return [first_two_minor_digits, last_two_minor_digits]
except (requests.exceptions.ConnectionError, KeyError):
return None
def check_cve_2018_1002105(self, api_version):
first_two_minor_digists = api_version[0]
last_two_minor_digists = api_version[1]
if first_two_minor_digists == 10 and last_two_minor_digists < 11:
return True
elif first_two_minor_digists == 11 and last_two_minor_digists < 5:
return True
elif first_two_minor_digists == 12 and last_two_minor_digists < 3:
return True
elif first_two_minor_digists < 10:
return True
return False
def check_cve_2019_1002100(self, api_version):
"""
Kubernetes v1.0.x-1.10.x
Kubernetes v1.11.0-1.11.7 (fixed in v1.11.8)
Kubernetes v1.12.0-1.12.5 (fixed in v1.12.6)
Kubernetes v1.13.0-1.13.3 (fixed in v1.13.4)
"""
first_two_minor_digists = api_version[0]
last_two_minor_digists = api_version[1]
if first_two_minor_digists == 11 and last_two_minor_digists < 8:
return True
elif first_two_minor_digists == 12 and last_two_minor_digists < 6:
return True
elif first_two_minor_digists == 13 and last_two_minor_digists < 4:
return True
elif first_two_minor_digists < 11:
return True
return False
def execute(self):
api_version = self.get_api_server_version_end_point()
if api_version:
if self.check_cve_2018_1002105(api_version):
self.publish_event(ServerApiVersionEndPointAccessPE(self.api_server_evidence))
if self.check_cve_2019_1002100(api_version):
self.publish_event(ServerApiVersionEndPointAccessDos(self.api_server_evidence))

146
src/modules/hunting/cves.py Normal file
View File

@@ -0,0 +1,146 @@
import logging
import json
import requests
from ...core.events import handler
from ...core.events.types import Vulnerability, Event, K8sVersionDisclosure
from ...core.types import Hunter, ActiveHunter, KubernetesCluster, RemoteCodeExec, AccessRisk, InformationDisclosure, \
PrivilegeEscalation, DenialOfService, KubectlClient
from ..discovery.kubectl import KubectlClientEvent
from packaging import version
""" CVE Vulnerabilities """
class ServerApiVersionEndPointAccessPE(Vulnerability, Event):
"""Node is vulnerable to critical CVE-2018-1002105"""
def __init__(self, evidence):
Vulnerability.__init__(self, KubernetesCluster, name="Critical Privilege Escalation CVE", category=PrivilegeEscalation)
self.evidence = evidence
class ServerApiVersionEndPointAccessDos(Vulnerability, Event):
"""Node not patched for CVE-2019-1002100. Depending on your RBAC settings, a crafted json-patch could cause a Denial of Service."""
def __init__(self, evidence):
Vulnerability.__init__(self, KubernetesCluster, name="Denial of Service to Kubernetes API Server", category=DenialOfService)
self.evidence = evidence
class IncompleteFixToKubectlCpVulnerability(Vulnerability, Event):
"""The kubectl client is vulnerable to CVE-2019-11246, an attacker could potentially execute arbitrary code on the client's machine"""
def __init__(self, binary_version):
Vulnerability.__init__(self, KubectlClient, "Kubectl Vulnerable To CVE-2019-11246", category=RemoteCodeExec)
self.binary_version = binary_version
self.evidence = "kubectl version: {}".format(self.binary_version)
class KubectlCpVulnerability(Vulnerability, Event):
"""The kubectl client is vulnerable to CVE-2019-1002101, an attacker could potentially execute arbitrary code on the client's machine"""
def __init__(self, binary_version):
Vulnerability.__init__(self, KubectlClient, "Kubectl Vulnerable To CVE-2019-1002101", category=RemoteCodeExec)
self.binary_version = binary_version
self.evidence = "kubectl version: {}".format(self.binary_version)
class CveUtils:
@staticmethod
def get_base_release(full_ver):
# if LecacyVersion, converting manually to a base version
if type(full_ver) == version.LegacyVersion:
return version.parse('.'.join(full_ver._version.split('.')[:2]))
else:
return version.parse('.'.join(map(str, full_ver._version.release[:2])))
@staticmethod
def to_legacy(full_ver):
# converting version to verison.LegacyVersion
return version.LegacyVersion('.'.join(map(str, full_ver._version.release)))
@staticmethod
def to_raw_version(v):
if type(v) != version.LegacyVersion:
return '.'.join(map(str, v._version.release))
return v._version
@staticmethod
def version_compare(v1, v2):
"""Function compares two versions, handling differences with convertion to LegacyVersion"""
# getting raw version, while striping 'v' char at the start. if exists.
# removing this char lets us safely compare the two version.
v1_raw, v2_raw = CveUtils.to_raw_version(v1).strip('v'), CveUtils.to_raw_version(v2).strip('v')
new_v1 = version.LegacyVersion(v1_raw)
new_v2 = version.LegacyVersion(v2_raw)
return CveUtils.basic_compare(new_v1, new_v2)
@staticmethod
def basic_compare(v1, v2):
return (v1>v2)-(v1<v2)
@staticmethod
def is_vulnerable(fix_versions, check_version):
"""Function determines if a version is vulnerable, by comparing to given fix versions by base release"""
vulnerable = False
check_v = version.parse(check_version)
base_check_v = CveUtils.get_base_release(check_v)
# default to classic compare, unless the check_version is legacy.
version_compare_func = CveUtils.basic_compare
if type(check_v) == version.LegacyVersion:
version_compare_func = CveUtils.version_compare
if check_version not in fix_versions:
# comparing ease base release for a fix
for fix_v in fix_versions:
fix_v = version.parse(fix_v)
base_fix_v = CveUtils.get_base_release(fix_v)
# if the check version and the current fix has the same base release
if base_check_v == base_fix_v:
# when check_version is legacy, we use a custom compare func, to handle differnces between versions.
if version_compare_func(check_v, fix_v) == -1:
# determine vulnerable if smaller and with same base version
vulnerable = True
break
# if we did't find a fix in the fix releases, checking if the version is smaller that the first fix
if not vulnerable and version_compare_func(check_v, version.parse(fix_versions[0])) == -1:
vulnerable = True
return vulnerable
@handler.subscribe_once(K8sVersionDisclosure)
class K8sClusterCveHunter(Hunter):
"""K8s CVE Hunter
Checks if Node is running a Kubernetes version vulnerable to known CVEs
"""
def __init__(self, event):
self.event = event
def execute(self):
logging.debug('Api Cve Hunter determining vulnerable version: {}'.format(self.event.version))
cve_mapping = {
ServerApiVersionEndPointAccessPE: ["1.10.11", "1.11.5", "1.12.3"],
ServerApiVersionEndPointAccessDos: ["1.11.8", "1.12.6", "1.13.4"],
}
for vulnerability, fix_versions in cve_mapping.items():
if CveUtils.is_vulnerable(fix_versions, self.event.version):
self.publish_event(vulnerability(self.event.version))
@handler.subscribe(KubectlClientEvent)
class KubectlCVEHunter(Hunter):
"""Kubectl CVE Hunter
Checks if the kubectl client is vulnerable to known CVEs
"""
def __init__(self, event):
self.event = event
def execute(self):
cve_mapping = {
KubectlCpVulnerability: ['1.11.9', '1.12.7', '1.13.5' '1.14.0'],
IncompleteFixToKubectlCpVulnerability: ['1.12.9', '1.13.6', '1.14.2']
}
logging.debug('Kubectl Cve Hunter determining vulnerable version: {}'.format(self.event.version))
for vulnerability, fix_versions in cve_mapping.items():
if CveUtils.is_vulnerable(fix_versions, self.event.version):
self.publish_event(vulnerability(binary_version=self.event.version))

View File

@@ -1,64 +0,0 @@
import logging
from ...core.events import handler
from ...core.types import Hunter, RemoteCodeExec, KubectlClient
from ...core.events.types import Vulnerability, Event
from ..discovery.kubectl import KubectlClientEvent
from distutils.version import LooseVersion, StrictVersion
class IncompleteFixToKubectlCpVulnerability(Vulnerability, Event):
"""The kubectl client is vulnerable to CVE-2019-11246, an attacker could potentially execute arbitrary code on the client's machine"""
def __init__(self, binary_version):
Vulnerability.__init__(self, KubectlClient, "Kubectl Vulnerable To CVE-2019-11246", category=RemoteCodeExec)
self.binary_version = binary_version
self.evidence = "kubectl version: {}".format(self.binary_version)
class KubectlCpVulnerability(Vulnerability, Event):
"""The kubectl client is vulnerable to CVE-2019-1002101, an attacker could potentially execute arbitrary code on the client's machine"""
def __init__(self, binary_version):
Vulnerability.__init__(self, KubectlClient, "Kubectl Vulnerable To CVE-2019-1002101", category=RemoteCodeExec)
self.binary_version = binary_version
self.evidence = "kubectl version: {}".format(self.binary_version)
@handler.subscribe(KubectlClientEvent)
class KubectlCVEHunter(Hunter):
"""Kubectl CVE Hunter
Compares version of the kubectl binary to known CVE affected versions
"""
def __init__(self, event):
self.event = event
def is_older_than(self, fix_versions, check_version):
"""Function determines if a version is vulnerable, by comparing to given fix versions"""
logging.debug("Passive hunter is comparing the kubectl binary version to vulnerable versions")
# in case version is in short version, converting
if len(LooseVersion(check_version).version) < 3:
check_version += '.0'
vulnerable = False
if check_version not in fix_versions:
for fix_v in fix_versions:
fix_v = LooseVersion(fix_v)
base_v = '.'.join(map(lambda x: str(x), fix_v.version[:2]) )
if check_version.startswith(base_v):
if LooseVersion(check_version) < fix_v:
vulnerable = True
break
# if version is smaller than smaller fix version
if not vulnerable and LooseVersion(check_version) < LooseVersion(fix_versions[0]):
vulnerable = True
return vulnerable
def execute(self):
cve_2019_1002101_fix_versions = ['1.11.9', '1.12.7', '1.13.5' '1.14.0']
cve_2019_11246_fix_versions = ['1.12.9', '1.13.6', '1.14.2']
if self.is_older_than(fix_versions=cve_2019_1002101_fix_versions, check_version=self.event.version):
self.publish_event(KubectlCpVulnerability(binary_version=self.event.version))
if self.is_older_than(fix_versions=cve_2019_11246_fix_versions, check_version=self.event.version):
self.publish_event(IncompleteFixToKubectlCpVulnerability(binary_version=self.event.version))

View File

@@ -8,7 +8,7 @@ import urllib3
from __main__ import config
from ...core.events import handler
from ...core.events.types import Vulnerability, Event
from ...core.events.types import Vulnerability, Event, K8sVersionDisclosure
from ..discovery.kubelet import ReadOnlyKubeletEvent, SecureKubeletEvent
from ...core.types import Hunter, ActiveHunter, KubernetesCluster, Kubelet, InformationDisclosure, RemoteCodeExec, AccessRisk
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -75,13 +75,6 @@ class ExposedHealthzHandler(Vulnerability, Event):
self.evidence = "status: {}".format(self.status)
class K8sVersionDisclosure(Vulnerability, Event):
"""The kubernetes version could be obtained from logs in the /metrics endpoint"""
def __init__(self, version):
Vulnerability.__init__(self, Kubelet, "K8s Version Disclosure", category=InformationDisclosure)
self.evidence = version
class PrivilegedContainers(Vulnerability, Event):
"""A Privileged container exist on a node. could expose the node/cluster to unwanted root operations"""
def __init__(self, containers):
@@ -165,7 +158,7 @@ class ReadOnlyKubeletPortHunter(Hunter):
privileged_containers = self.find_privileged_containers()
healthz = self.check_healthz_endpoint()
if k8s_version:
self.publish_event(K8sVersionDisclosure(version=k8s_version))
self.publish_event(K8sVersionDisclosure(version=k8s_version, from_endpoint="/metrics", extra_info="on the Kubelet"))
if privileged_containers:
self.publish_event(PrivilegedContainers(containers=privileged_containers))
if healthz:

View File

@@ -5,7 +5,7 @@ import requests
import json
from ...core.events import handler
from ...core.events.types import Event, Vulnerability
from ...core.events.types import Event, Vulnerability, K8sVersionDisclosure
from ...core.types import ActiveHunter, Hunter, KubernetesCluster, InformationDisclosure
from ..discovery.dashboard import KubeDashboardEvent
from ..discovery.proxy import KubeProxyEvent
@@ -16,12 +16,6 @@ class KubeProxyExposed(Vulnerability, Event):
def __init__(self):
Vulnerability.__init__(self, KubernetesCluster, "Proxy Exposed", category=InformationDisclosure)
class K8sVersionDisclosure(Vulnerability, Event):
"""The Kubernetes version is exposed from kube-proxy"""
def __init__(self):
Vulnerability.__init__(self, KubernetesCluster, "K8s Version Disclosure", category=InformationDisclosure)
class Service(Enum):
DASHBOARD = "kubernetes-dashboard"
@@ -36,7 +30,6 @@ class KubeProxy(Hunter):
def execute(self):
self.publish_event(KubeProxyExposed())
self.publish_event(K8sVersionDisclosure())
for namespace, services in self.services.items():
for service in services:
if service == Service.DASHBOARD.value:
@@ -83,8 +76,8 @@ class ProveProxyExposed(ActiveHunter):
if "buildDate" in version_metadata:
self.event.evidence = "build date: {}".format(version_metadata["buildDate"])
@handler.subscribe(K8sVersionDisclosure)
class ProveK8sVersionDisclosure(ActiveHunter):
@handler.subscribe(KubeProxyExposed)
class K8sVersionDisclosureProve(ActiveHunter):
"""K8s Version Hunter
Hunts Proxy when exposed, extracts the version
"""
@@ -97,4 +90,4 @@ class ProveK8sVersionDisclosure(ActiveHunter):
port=self.event.port,
), verify=False).text)
if "gitVersion" in version_metadata:
self.event.evidence = "version: {}".format(version_metadata["gitVersion"])
self.publish_event(K8sVersionDisclosure(version=version_metadata["gitVersion"], from_endpoint="/version", extra_info="on the kube-proxy"))