mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-08-23 22:26:23 +00:00
Merge branch 'master' into api-server-hunt-improvements
This commit is contained in:
+2
-5
@@ -2,16 +2,13 @@ group: travis_latest
|
||||
language: python
|
||||
cache: pip
|
||||
matrix:
|
||||
allow_failures:
|
||||
# Python 3 tests are failing due to an import error
|
||||
- python: 3.6
|
||||
include:
|
||||
- python: 2.7
|
||||
#- python: 3.4
|
||||
#- python: 3.5
|
||||
- python: 3.6
|
||||
#- python: 3.7
|
||||
# dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
|
||||
- python: 3.7
|
||||
dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
|
||||
# sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:2.7.15-alpine3.8
|
||||
FROM python:3.7.2-alpine3.9
|
||||
|
||||
RUN apk add --update \
|
||||
linux-headers \
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||

|
||||
|
||||
[](https://travis-ci.org/aquasecurity/kube-hunter)
|
||||
|
||||
Kube-hunter hunts for security weaknesses in Kubernetes clusters. The tool was developed to increase awareness and visibility for security issues in Kubernetes environments. **You should NOT run kube-hunter on a Kubernetes cluster you don't own!**
|
||||
|
||||
**Run kube-hunter**: kube-hunter is available as a container (aquasec/kube-hunter), and we also offer a web site at [kube-hunter.aquasec.com](https://kube-hunter.aquasec.com) where you can register online to receive a token allowing you see and share the results online. You can also run the Python code yourself as described below.
|
||||
@@ -72,7 +74,7 @@ You can run the kube-hunter python code directly on your machine.
|
||||
#### Prerequisites
|
||||
|
||||
You will need the following installed:
|
||||
* python 2.7
|
||||
* python 2.7 or python 3.x
|
||||
* pip
|
||||
|
||||
Clone the repository:
|
||||
@@ -84,9 +86,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`
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ PrettyTable
|
||||
urllib3
|
||||
ruamel.yaml
|
||||
requests_mock
|
||||
future
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
import core
|
||||
import modules
|
||||
from . import core
|
||||
from . import modules
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import types
|
||||
import events
|
||||
from . import types
|
||||
from . import events
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
from handler import *
|
||||
import types
|
||||
from .handler import *
|
||||
from . import types
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import time
|
||||
from abc import ABCMeta
|
||||
from collections import defaultdict
|
||||
from Queue import Queue
|
||||
from queue import Queue
|
||||
from threading import Lock, Thread
|
||||
|
||||
from __main__ import config
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from os.path import dirname, basename, isfile
|
||||
import glob
|
||||
|
||||
from common import *
|
||||
from .common import *
|
||||
|
||||
# dynamically importing all modules in folder
|
||||
files = glob.glob(dirname(__file__)+"/*.py")
|
||||
for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')):
|
||||
if module_name != "handler":
|
||||
exec('from {} import *'.format(module_name))
|
||||
exec('from .{} import *'.format(module_name))
|
||||
|
||||
@@ -16,6 +16,17 @@ class Event(object):
|
||||
if name in event.__dict__:
|
||||
return event.__dict__[name]
|
||||
|
||||
# Event's logical location to be used mainly for reports.
|
||||
# If event don't implement it check previous event
|
||||
# This is because events are composed (previous -> previous ...)
|
||||
# and not inheritted
|
||||
def location(self):
|
||||
location = None
|
||||
if self.previous:
|
||||
location = self.previous.location()
|
||||
|
||||
return location
|
||||
|
||||
# returns the event history ordered from newest to oldest
|
||||
@property
|
||||
def history(self):
|
||||
@@ -85,7 +96,10 @@ class NewHostEvent(Event):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.host)
|
||||
|
||||
|
||||
# Event's logical location to be used mainly for reports.
|
||||
def location(self):
|
||||
return str(self.host)
|
||||
|
||||
class OpenPortEvent(Event):
|
||||
def __init__(self, port):
|
||||
@@ -93,7 +107,14 @@ class OpenPortEvent(Event):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.port)
|
||||
|
||||
|
||||
# Event's logical location to be used mainly for reports.
|
||||
def location(self):
|
||||
if self.host:
|
||||
location = str(self.host) + ":" + str(self.port)
|
||||
else:
|
||||
location = str(self.port)
|
||||
return location
|
||||
|
||||
class HuntFinished(Event):
|
||||
pass
|
||||
|
||||
+3
-1
@@ -48,5 +48,7 @@ class AccessRisk(object):
|
||||
class PrivilegeEscalation(KubernetesCluster):
|
||||
name = "Privilege Escalation"
|
||||
|
||||
class DenialOfService(object):
|
||||
name = "Denial of Service"
|
||||
|
||||
from events import handler # import is in the bottom to break import loops
|
||||
from .events import handler # import is in the bottom to break import loops
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import report
|
||||
import discovery
|
||||
import hunting
|
||||
from . import report
|
||||
from . import discovery
|
||||
from . import hunting
|
||||
|
||||
@@ -4,4 +4,5 @@ import glob
|
||||
# dynamically importing all modules in folder
|
||||
files = glob.glob(dirname(__file__)+"/*.py")
|
||||
for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')):
|
||||
exec('from {} import *'.format(module_name))
|
||||
if not module_name.startswith('test_'):
|
||||
exec('from .{} import *'.format(module_name))
|
||||
|
||||
@@ -23,6 +23,14 @@ class RunningAsPodEvent(Event):
|
||||
self.client_cert = self.get_service_account_file("ca.crt")
|
||||
self.namespace = self.get_service_account_file("namespace")
|
||||
|
||||
# Event's logical location to be used mainly for reports.
|
||||
def location(self):
|
||||
location = "Local to Pod"
|
||||
if 'HOSTNAME' in os.environ:
|
||||
location += "(" + os.environ['HOSTNAME'] + ")"
|
||||
|
||||
return location
|
||||
|
||||
def get_service_account_file(self, file):
|
||||
try:
|
||||
with open("/var/run/secrets/kubernetes.io/serviceaccount/{file}".format(file=file)) as f:
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import logging
|
||||
import json
|
||||
import requests
|
||||
import uuid
|
||||
import ast
|
||||
|
||||
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
|
||||
|
||||
""" Vulnerabilities """
|
||||
class ServerApiVersionEndPointAccess(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
|
||||
|
||||
# Passive Hunter
|
||||
@handler.subscribe(ApiServer)
|
||||
class IsVulnerableToCVEAttack(Hunter):
|
||||
""" Node is running a Kubernetes version vulnerable to critical CVE-2018-1002105 """
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.headers = dict()
|
||||
self.path = "https://{}:{}".format(self.event.host, self.event.port)
|
||||
self.service_account_token_evidence = ''
|
||||
self.api_server_evidence = ''
|
||||
self.k8sVersion = ''
|
||||
|
||||
def access_api_server_version_end_point(self):
|
||||
logging.debug(self.event.host)
|
||||
logging.debug('Passive Hunter is attempting to access the API server /version end point using the pod\'s service account token')
|
||||
try:
|
||||
res = requests.get("{path}/version".format(path=self.path),
|
||||
headers=self.headers, verify=False)
|
||||
self.api_server_evidence = res.content
|
||||
resDict = ast.literal_eval(res.content)
|
||||
version = resDict["gitVersion"].split('.')
|
||||
first_two_minor_digists = eval(version[1])
|
||||
last_two_minor_digists = eval(version[2])
|
||||
|
||||
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
|
||||
except (requests.exceptions.ConnectionError, KeyError):
|
||||
return False
|
||||
|
||||
def get_service_account_token(self):
|
||||
logging.debug(self.event.host)
|
||||
logging.debug('Passive Hunter is attempting to access pod\'s service account token')
|
||||
try:
|
||||
with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r') as token:
|
||||
data = token.read()
|
||||
self.service_account_token_evidence = data
|
||||
self.headers = {'Authorization': 'Bearer ' + self.service_account_token_evidence}
|
||||
return True
|
||||
except IOError: # Couldn't read file
|
||||
return False
|
||||
|
||||
def execute(self):
|
||||
self.get_service_account_token() # From within a Pod we may have extra credentials
|
||||
if self.access_api_server_version_end_point():
|
||||
self.publish_event(ServerApiVersionEndPointAccess(self.api_server_evidence))
|
||||
|
||||
@@ -4,4 +4,4 @@ import glob
|
||||
# dynamically importing all modules in folder
|
||||
files = glob.glob(dirname(__file__)+"/*.py")
|
||||
for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')):
|
||||
exec('from {} import *'.format(module_name))
|
||||
exec('from .{} import *'.format(module_name))
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
import requests
|
||||
|
||||
from kubelet import ExposedRunHandler
|
||||
from .kubelet import ExposedRunHandler
|
||||
|
||||
from ...core.events import handler
|
||||
from ...core.events.types import Event, Vulnerability
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import logging
|
||||
import json
|
||||
import requests
|
||||
import uuid
|
||||
import ast
|
||||
|
||||
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):
|
||||
""" Node is running a Kubernetes version vulnerable to critical CVE-2018-1002105 """
|
||||
|
||||
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 = "https://{}:{}".format(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.content
|
||||
resDict = ast.literal_eval(res.content)
|
||||
version = resDict["gitVersion"].split('.')
|
||||
first_two_minor_digits = eval(version[1])
|
||||
last_two_minor_digits = eval(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))
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ import glob
|
||||
# dynamically importing all modules in folder
|
||||
files = glob.glob(dirname(__file__)+"/*.py")
|
||||
for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')):
|
||||
exec('from {} import *'.format(module_name))
|
||||
exec('from .{} import *'.format(module_name))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from collector import services, vulnerabilities, services_lock, vulnerabilities_lock
|
||||
from .collector import services, vulnerabilities, services_lock, vulnerabilities_lock
|
||||
|
||||
class BaseReporter(object):
|
||||
def get_nodes(self):
|
||||
@@ -24,7 +24,7 @@ class BaseReporter(object):
|
||||
|
||||
def get_vulnerabilities(self):
|
||||
vulnerabilities_lock.acquire()
|
||||
vulnerabilities_data = [{"location": "{}:{}".format(vuln.host, vuln.port) if vuln.host else "",
|
||||
vulnerabilities_data = [{"location": vuln.location(),
|
||||
"category": vuln.category.name,
|
||||
"vulnerability": vuln.get_name(),
|
||||
"description": vuln.explain(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from base import BaseReporter
|
||||
from .base import BaseReporter
|
||||
|
||||
class JSONReporter(BaseReporter):
|
||||
def get_report(self):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import print_function
|
||||
from prettytable import ALL, PrettyTable
|
||||
|
||||
from __main__ import config
|
||||
from collector import services, vulnerabilities, services_lock, vulnerabilities_lock
|
||||
from .collector import services, vulnerabilities, services_lock, vulnerabilities_lock
|
||||
|
||||
EVIDENCE_PREVIEW = 40
|
||||
MAX_TABLE_WIDTH = 20
|
||||
@@ -82,7 +82,7 @@ class PlainReporter(object):
|
||||
|
||||
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()]
|
||||
row = [vuln.location(), 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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import StringIO
|
||||
|
||||
from io import StringIO
|
||||
from ruamel.yaml import YAML
|
||||
from base import BaseReporter
|
||||
from .base import BaseReporter
|
||||
|
||||
class YAMLReporter(BaseReporter):
|
||||
def get_report(self):
|
||||
@@ -13,4 +12,4 @@ class YAMLReporter(BaseReporter):
|
||||
}
|
||||
output = StringIO.StringIO()
|
||||
yaml.dump(report, output)
|
||||
return output.getvalue()
|
||||
return output.getvalue()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import requests_mock
|
||||
import time
|
||||
from Queue import Empty
|
||||
from queue import Empty
|
||||
|
||||
from src.modules.discovery.hosts import FromPodHostDiscovery, RunningAsPodEvent, HostScanEvent, AzureMetadataApi
|
||||
from src.core.events.types import Event, NewHostEvent
|
||||
@@ -59,4 +59,4 @@ class testHostDiscoveryEvent(object):
|
||||
@handler.subscribe(AzureMetadataApi)
|
||||
class testAzureMetadataApi(object):
|
||||
def __init__(self, event):
|
||||
assert config.azure
|
||||
assert config.azure
|
||||
|
||||
Reference in New Issue
Block a user