Error-based file extraction of k8s node.
If a pod's container mounts the host's /var/log folder (or any pod log file, usually located in
/var/log/pods/namespace_pod-name_id/container-name/0.log) in write mode, attackers with control of
the container can read any file of the underlying node filesystem by creating a symlink. You can
find more details here.
The extraction used to be quite straightforward by contacting the kubelet or the API, as it seems there was no log format validation at the time of discovery of this misconfiguration abuse. I assume it changed when k8s migrated to cri-containerd and dropped dockershim (I guess dockershim didn't do any validation, but I might be wrong on which component exactly was responsible). The new implementation checks that the log line starts with a timestamp or is of json format: source which seems like it would prevent us from retrieving the line.
If the first line of the "log" file does not match one of the accepted format, the whole line is returned
If you created a symlink 0.log -> /var/lib/kubelet/pki/kubelet-key.pem and you try to retrieve the
pod's logs:
kubectl logs my-vuln-pod
> failed to get parse function: unsupported log format: "-----BEGIN EC PRIVATE KEY-----\n"We can use the tail option of k8s logging to retrieve all the lines: the cri-client first seek the
tail starting points, then reads the first line to pass it to the function trying all the parsers,
so by setting tail to 1, then 2, then 3 etc. we can effectively read the whole file.
git clone https://github.com/Minosity-VR/klogExtract.git
cd klogExtract
go build./klogextract -n (NAMESPACE) -p (POD_NAME) -c [CONTAINER_NAME] -r [REPEAT_LIMIT]The CLI requires two arguments, -n for the target namespace and -p for the target pod. If the
controlled container is not the main pod container but a sidecar, you can switch target with -c.
As we can't know for sure when we reach the top of the file, we keep increasing the tail size until
a line is repeated REPEAT_LIMIT times (then we ignore those repeated lines and keep only one).
Defaults to 5.