Merge branch 'master' into api-server-hunt-improvements

This commit is contained in:
Liz Rice
2019-03-18 17:58:12 +00:00
committed by GitHub
24 changed files with 183 additions and 111 deletions
+2 -5
View File
@@ -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
View File
@@ -1,4 +1,4 @@
FROM python:2.7.15-alpine3.8
FROM python:3.7.2-alpine3.9
RUN apk add --update \
linux-headers \
+4 -3
View File
@@ -1,5 +1,7 @@
![kube-hunter](https://github.com/aquasecurity/kube-hunter/blob/master/kube-hunter.png)
[![Build Status](https://travis-ci.org/aquasecurity/kube-hunter.svg?branch=master)](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`
+1
View File
@@ -7,3 +7,4 @@ PrettyTable
urllib3
ruamel.yaml
requests_mock
future
+2 -2
View File
@@ -1,2 +1,2 @@
import core
import modules
from . import core
from . import modules
+2 -2
View File
@@ -1,2 +1,2 @@
import types
import events
from . import types
from . import events
+2 -2
View File
@@ -1,2 +1,2 @@
from handler import *
import types
from .handler import *
from . import types
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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))
+23 -2
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -1,3 +1,3 @@
import report
import discovery
import hunting
from . import report
from . import discovery
from . import hunting
+2 -1
View File
@@ -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))
+8
View File
@@ -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:
-72
View File
@@ -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))
+1 -1
View File
@@ -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 -1
View File
@@ -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
+114
View File
@@ -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))
+1 -1
View File
@@ -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))
+2 -2
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
import json
from base import BaseReporter
from .base import BaseReporter
class JSONReporter(BaseReporter):
def get_report(self):
+2 -2
View File
@@ -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)
+3 -4
View File
@@ -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()
+2 -2
View File
@@ -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