mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-11 03:37:52 +00:00
Merge remote-tracking branch 'origin/liz'
This commit is contained in:
11
README.md
11
README.md
@@ -2,7 +2,7 @@
|
||||
---
|
||||
Kube Hunter hunts for security weaknesses in Kubernetes clusters. The tool was developed to increase awareness and visibility for security issues in Kubernetes environments.
|
||||
|
||||
_Developers, please read [Guidelines For Developing Your First Kube Hunter Module](src/README.md)_ *TODO*
|
||||
We welcome contributions, especially new hunter modules that perform additional tests. If you would like to develop your own modules please read [Guidelines For Developing Your First Kube Hunter Module](src/README.md).
|
||||
|
||||
## Hunting
|
||||
|
||||
@@ -26,12 +26,11 @@ To active hunt a cluster, use the `--active` flag. Example:
|
||||
To control logging, you can specify a log level, using the `--log` option. Example:
|
||||
`./kube-hunter.py --active --log WARNING`
|
||||
Available log levels are:
|
||||
#
|
||||
DEBUG
|
||||
INFO (default)
|
||||
WARNING
|
||||
|
||||
* DEBUG
|
||||
* INFO (default)
|
||||
* WARNING
|
||||
|
||||
--
|
||||
To see only a mapping of your nodes network, run with `--mapping` option. Example:
|
||||
`./kube-hunter.py --cidr 192.168.0.0/24 --mapping`
|
||||
This will output all the Kubernetes nodes Kube Hunter has found.
|
||||
|
||||
@@ -18,11 +18,11 @@ kube-hunter/
|
||||
### Design Pattern
|
||||
Kube Hunter is built with the [Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern).
|
||||
With this in mind, every new Service/Vulnerability/Information that has been discovered, will trigger a new event.
|
||||
When you write your module, you can decide on which Event to subscribe to. meaning, when exactly will your module start Hunting.
|
||||
When you write your module, you can decide on which Event to subscribe to, meaning, when exactly will your module start Hunting.
|
||||
|
||||
-----------------------
|
||||
### Hunter Types
|
||||
There are two hunter types which you can implement. a `Hunter` and an `ActiveHunter`.
|
||||
There are two hunter types which you can implement: a `Hunter` and an `ActiveHunter`. Hunters just probe the state of a cluster, whereas ActiveHunter modules can attempt operations that could change the state of the cluster.
|
||||
##### Hunter
|
||||
Example:
|
||||
~~~python
|
||||
@@ -33,36 +33,38 @@ class KubeDashboardDiscovery(Hunter):
|
||||
def execute(self):
|
||||
pass
|
||||
~~~
|
||||
Kube Hunter's core module is taking care of trigerring your hunter by your demands.
|
||||
Kube Hunter's core module triggers your Hunter when the event you have subscribed it to occurs.
|
||||
in this example, we subscribe the Hunter, `KubeDashboardDiscovery`, to an `OpenPortEvent`, with a predicate that checks the open port (of the event) is 30000.
|
||||
|
||||
##### ActiveHunter
|
||||
An active hunter will be subscribed only if an active scanning is in place.
|
||||
Implementing an Active Hunter, is the same as implementing a regular Hunter, you just need to inherint from `ActiveHunter`
|
||||
An ActiveHunter will be subscribed to events (and therefore operate) only if KubeHunter is running in active scanning mode.
|
||||
Implementing an Active Hunter is the same as implementing a regular Hunter, you just need to inherit from `ActiveHunter`
|
||||
Example:
|
||||
```python
|
||||
class ProveSomeVulnerability(ActiveHunter):
|
||||
...
|
||||
```
|
||||
#### **Absolutely important to notice:**
|
||||
* every hunter, needs to implement an `execute` method. the core module will execute this method automatically
|
||||
* every hunter, needs to save its given event from the `__init__` in it's attributes.
|
||||
* when subscribing to an event, if a `predicate` is specified, it will be called with the event itself, pre trigger.
|
||||
|
||||
* Every hunter, needs to implement an `execute` method. the core module will execute this method automatically.
|
||||
* Every hunter, needs to save its given event from the `__init__` in it's attributes.
|
||||
* When subscribing to an event, if a `predicate` is specified, it will be called with the event itself, pre trigger.
|
||||
* When inheriting from `Hunter` or `ActiveHunter` you can use the `self.publish_event(event)`.
|
||||
`event` is an **initialized** event object
|
||||
|
||||
-----------------------
|
||||
## Creating The Module
|
||||
The first step, is to create a new file in the hunting or the discovery folders.
|
||||
|
||||
## Creating The Module
|
||||
The first step is to create a new file in the `hunting` or the `discovery` folder.
|
||||
_The file's (module's) content is imported automatically"_
|
||||
`Convention:` Hunters which discovers a new service should be placed under the discovery/ folder
|
||||
`Convention:` Hunters which discovers a new vulnerability, should be placed under the hunting/ folder
|
||||
`Convention:` Hunters which use vulnerabilities, should be placed under the hunting/ folder and should implement the ActiveHunter base class
|
||||
`Convention:` Hunters which discover a new service should be placed under the `discovery` folder.
|
||||
`Convention:` Hunters which discover a new vulnerability, should be placed under the `hunting` folder.
|
||||
`Convention:` Hunters which use vulnerabilities, should be placed under the `hunting` folder and should implement the ActiveHunter base class.
|
||||
|
||||
The second step, is to determine what events your Hunter will subscribe to, and from where you can get them.
|
||||
`Convention:` Events should be declared in their corresponding module. for example, an KubeDashboardEvent event is declared in the dashboard discovery module.
|
||||
The second step is to determine what events your Hunter will subscribe to, and from where you can get them.
|
||||
`Convention:` Events should be declared in their corresponding module. for example, a KubeDashboardEvent event is declared in the dashboard discovery module.
|
||||
|
||||
Following the above example, lets figure out the imports:
|
||||
Following the above example, let's figure out the imports:
|
||||
```python
|
||||
from ...core.types import Hunter
|
||||
from ...core.events import handler
|
||||
@@ -76,7 +78,7 @@ class KubeDashboardDiscovery(Hunter):
|
||||
def execute(self):
|
||||
pass
|
||||
```
|
||||
As you can see, all of the types here comes from the `core` module. let us list them:
|
||||
As you can see, all of the types here come from the `core` module.
|
||||
|
||||
### Core Imports
|
||||
relative import: `...core.events`
|
||||
@@ -124,22 +126,19 @@ class OpenKubeDns(Service, Event):
|
||||
`Notice:` Every type of event, should have an explanation in exactly the form shown above, that explanation will eventually be used when the report is made.
|
||||
`Notice:` You can add any attribute to the event you create as needed, the examples shown above is the minimum implementation that needs to be made
|
||||
|
||||
-----------------------
|
||||
|
||||
-----------------------
|
||||
## Events Magic
|
||||
`Internals Note:` In Kube Hunter, each event that's getting published, gets all the attributes from the previous event that has been used by it's publisher (Hunter). This process is invisible, and happens on the core module, without worrying the developer.
|
||||
According to this note, we can look at events as individual trees that remembers their past attributes, and gives us access to them.
|
||||
## Events
|
||||
`Internals Note:` In KubeHunter, each published event gets all the attributes from the previous event that has been used by its publisher (Hunter). This process is invisible, and happens on the core module, without worrying the developer.
|
||||
Accordingly, we can look at events as individual trees that remember their past attributes, and gives us access to them.
|
||||
|
||||
#### the event chain
|
||||
#### The event chain
|
||||
Example for an event chain:
|
||||
`NewHostEvent -> OpenPortEvent -> KubeProxyEvent -> KubeDashboard -> K8sVersionDisclosure`
|
||||
*The first node of every event tree is the NewHostEvent*
|
||||
|
||||
Let us assume the following imaginary example:
|
||||
We've defined a Hunter for SSL Certificates, which extracts the CN of the certificate, and is doing some magic with it.
|
||||
imagine this was defined on modules of your creating:
|
||||
--
|
||||
Let us assume the following imaginary example:
|
||||
We've defined a Hunter for SSL Certificates, which extracts the CN of the certificate, and does some magic with it. The example code would be defined in new `discovery` and `hunter` modules for this SSL Magic example:
|
||||
|
||||
Discovery:
|
||||
```python
|
||||
class NewSslCertificate(Event):
|
||||
@@ -162,19 +161,17 @@ class SslHunter(Hunter):
|
||||
def execute(self):
|
||||
do_magic(self.event.certificate)
|
||||
```
|
||||
lets say we now want to do something with the hostname from which we found the certificate from, from the event tree, we can check if the host attribute was assigned to our event previously, by directly accessing `event.host`.
|
||||
if it has not been specified from some reason, the value is `None`.
|
||||
So a simple:
|
||||
Let's say we now want to do something with the hostname from the certificate from. In the event tree, we can check if the host attribute was assigned to our event previously, by directly accessing `event.host`. If it has not been specified from some reason, the value is `None`.
|
||||
So this is sufficient for our example:
|
||||
```python
|
||||
...
|
||||
...
|
||||
def execute(self):
|
||||
do_magic(self.event.certificate)
|
||||
do_something_with_host(self.event.host) # normal access
|
||||
```
|
||||
will do.
|
||||
For the same reasons, the next hunter that will receive some event that this hunter published, could access the `event.certificate`.
|
||||
|
||||
If another Hunter subscribes to the events that this Hunter publishes, if can access the `event.certificate`.
|
||||
|
||||
## Proving Vulnerabilities
|
||||
The process of proving vulnerabilities, is the base concept of the Active Hunting.
|
||||
To prove a vulnerability, create an `ActiveHunter` that is subscribed to the vulnerability, and inside of the `execute`, specify the `evidence` attribute of the event.
|
||||
|
||||
To prove a vulnerability, create an `ActiveHunter` that is subscribed to the vulnerability, and inside of the `execute`, specify the `evidence` attribute of the event.
|
||||
@@ -53,11 +53,12 @@ class HostDiscovery(Hunter):
|
||||
if config.cidr:
|
||||
try:
|
||||
ip, sn = config.cidr.split('/')
|
||||
cloud = self.get_cloud(ip)
|
||||
for ip in self.generate_subnet(ip, sn=sn):
|
||||
self.publish_event(NewHostEvent(host=ip, cloud=cloud))
|
||||
except:
|
||||
logging.error("unable to parse cidr")
|
||||
except ValueError as e:
|
||||
logging.error("unable to parse cidr: {0}".format(e))
|
||||
return
|
||||
cloud = self.get_cloud(ip)
|
||||
for ip in self.generate_subnet(ip, sn=sn):
|
||||
self.publish_event(NewHostEvent(host=ip, cloud=cloud))
|
||||
elif config.internal:
|
||||
self.scan_interfaces()
|
||||
elif len(config.remote) > 0:
|
||||
@@ -70,7 +71,11 @@ class HostDiscovery(Hunter):
|
||||
self.traceroute_discovery()
|
||||
|
||||
def get_cloud(self, host):
|
||||
metadata = requests.get("http://www.azurespeed.com/api/region?ipOrUrl={ip}".format(ip=host)).text
|
||||
try:
|
||||
metadata = requests.get("http://www.azurespeed.com/api/region?ipOrUrl={ip}".format(ip=host)).text
|
||||
except requests.ConnectionError as e:
|
||||
logging.info("unable to check cloud: {0}".format(e))
|
||||
return
|
||||
if "cloud" in metadata:
|
||||
return json.loads(metadata)["cloud"]
|
||||
|
||||
@@ -104,7 +109,12 @@ class HostDiscovery(Hunter):
|
||||
|
||||
# for normal scanning
|
||||
def scan_interfaces(self):
|
||||
external_ip = requests.get("http://canhazip.com").text # getting external ip, to determine if cloud cluster
|
||||
try:
|
||||
external_ip = requests.get("http://canhazip.com").text # getting external ip, to determine if cloud cluster
|
||||
except requests.ConnectionError as e:
|
||||
logging.debug("unable to determine local IP address: {0}".format(e))
|
||||
logging.info("default to 127.0.0.1")
|
||||
external_ip = "127.0.0.1"
|
||||
cloud = self.get_cloud(external_ip)
|
||||
for ip in self.generate_interfaces_subnet():
|
||||
handler.publish_event(NewHostEvent(host=ip, cloud=cloud))
|
||||
|
||||
@@ -12,12 +12,12 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
""" Services """
|
||||
class ReadOnlyKubeletEvent(Service, Event):
|
||||
"""Can expose specific handlers which reveals information about the node/cluster"""
|
||||
"""Could expose endpoints which reveal information about the node/cluster"""
|
||||
def __init__(self):
|
||||
Service.__init__(self, name="Kubelet API (readonly)")
|
||||
|
||||
class SecureKubeletEvent(Service, Event):
|
||||
"""The kubelet ensures that all containers on the node are running and healthy"""
|
||||
"""Could expose endpoints which allow the attacker to access the node"""
|
||||
def __init__(self, cert=False, token=False):
|
||||
self.cert = cert
|
||||
self.token = token
|
||||
|
||||
36
src/modules/hunting/certificates.py
Normal file
36
src/modules/hunting/certificates.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from ...core.types import Hunter, KubernetesCluster
|
||||
from ...core.events import handler
|
||||
from ...core.events.types import Vulnerability, Event, OpenPortEvent
|
||||
|
||||
import ssl
|
||||
import logging
|
||||
import base64
|
||||
import re
|
||||
|
||||
from socket import socket
|
||||
|
||||
email_pattern = re.compile(r"([a-z0-9]+@[a-z0-9]+\.[a-z0-9]+)")
|
||||
|
||||
class CertificateEmail(Vulnerability, Event):
|
||||
"""Certificate includes an email address"""
|
||||
def __init__(self, email):
|
||||
Vulnerability.__init__(self, KubernetesCluster, "Certificate includes email address: {0}".format(email))
|
||||
|
||||
|
||||
@handler.subscribe(OpenPortEvent)
|
||||
class CertificateDiscovery(Hunter):
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
addr = (str(self.event.host), self.event.port)
|
||||
cert = ssl.get_server_certificate(addr)
|
||||
except ssl.SSLError as e:
|
||||
# If the server doesn't offer SSL on this port we won't get a certificate
|
||||
return
|
||||
c = cert.strip(ssl.PEM_HEADER).strip(ssl.PEM_FOOTER)
|
||||
certdata = base64.decodestring(c)
|
||||
emails = re.findall(email_pattern, certdata)
|
||||
for email in emails:
|
||||
self.publish_event( CertificateEmail(email) )
|
||||
Reference in New Issue
Block a user