mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-08-23 22:26:23 +00:00
Merge pull request #117 from aquasecurity/insecure_port
Insecure port for api server
This commit is contained in:
@@ -8,24 +8,34 @@ from ...core.events.types import OpenPortEvent, Service, Event
|
||||
|
||||
class ApiServer(Service, Event):
|
||||
"""The API server is in charge of all operations on the cluster."""
|
||||
def __init__(self):
|
||||
def __init__(self, protocol="https"):
|
||||
Service.__init__(self, name="API Server")
|
||||
self.protocol=protocol
|
||||
|
||||
|
||||
# Other devices could have this port open, but we can check to see if it looks like a Kubernetes node
|
||||
# A Kubernetes API server will respond with a JSON message that includes a "code" field for the HTTP status code
|
||||
@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port==443 or x.port==6443)
|
||||
@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port==443 or x.port==6443 or x.port==8080)
|
||||
class ApiServerDiscovery(Discovery):
|
||||
"""Api Server Discovery
|
||||
"""API Server Discovery
|
||||
Checks for the existence of a an API Server
|
||||
"""
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
def execute(self):
|
||||
logging.debug("Attempting to discover an API server")
|
||||
main_request = requests.get("https://{}:{}".format(self.event.host, self.event.port), verify=False).text
|
||||
if '"code"' in main_request:
|
||||
self.event.role = "Master"
|
||||
self.publish_event(ApiServer())
|
||||
logging.debug("Attempting to discover an API server on {}:{}".format(self.event.host, self.event.port))
|
||||
self.make_request(protocol="https")
|
||||
self.make_request(protocol="http")
|
||||
|
||||
def make_request(self, protocol):
|
||||
try:
|
||||
r = requests.get("{}://{}:{}".format(protocol, self.event.host, self.event.port), verify=False)
|
||||
if ('k8s' in r.text) or ('"code"' in r.text and r.status_code is not 200):
|
||||
self.event.role = "Master"
|
||||
self.publish_event(ApiServer(protocol=protocol))
|
||||
except requests.exceptions.SSLError:
|
||||
logging.debug("{} protocol not accepted on {}:{}".format(protocol, self.event.host, self.event.port))
|
||||
except Exception as e:
|
||||
logging.debug("{} on {}:{}".format(e, self.event.host, self.event.port))
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ class KubeletDiscovery(Discovery):
|
||||
self.event = event
|
||||
|
||||
def get_read_only_access(self):
|
||||
logging.debug(self.event.host)
|
||||
logging.debug("Passive hunter is attempting to get kubelet read access")
|
||||
logging.debug("Passive hunter is attempting to get kubelet read access at {}:{}".format(self.event.host, self.event.port))
|
||||
r = requests.get("http://{host}:{port}/pods".format(host=self.event.host, port=self.event.port))
|
||||
if r.status_code == 200:
|
||||
self.publish_event(ReadOnlyKubeletEvent())
|
||||
|
||||
@@ -7,7 +7,7 @@ from ...core.events import handler
|
||||
from ...core.events.types import NewHostEvent, OpenPortEvent
|
||||
|
||||
|
||||
default_ports = [8001, 10250, 10255, 30000, 443, 6443, 2379]
|
||||
default_ports = [8001, 8080, 10250, 10255, 30000, 443, 6443, 2379]
|
||||
|
||||
@handler.subscribe(NewHostEvent)
|
||||
class PortDiscovery(Discovery):
|
||||
|
||||
@@ -27,6 +27,15 @@ class ServerApiAccess(Vulnerability, Event):
|
||||
Vulnerability.__init__(self, KubernetesCluster, name=name, category=category)
|
||||
self.evidence = evidence
|
||||
|
||||
class ServerApiHTTPAccess(Vulnerability, Event):
|
||||
""" The API Server port is accessible over HTTP, and therefore unencrypted. Depending on your RBAC settings this could expose access to or control of your cluster. """
|
||||
|
||||
def __init__(self, evidence):
|
||||
name = "Insecure (HTTP) access to API"
|
||||
category = UnauthenticatedAccess
|
||||
Vulnerability.__init__(self, KubernetesCluster, name=name, category=category)
|
||||
self.evidence = evidence
|
||||
|
||||
class ApiInfoDisclosure(Vulnerability, Event):
|
||||
def __init__(self, evidence, using_token, name):
|
||||
if using_token:
|
||||
@@ -196,13 +205,12 @@ class AccessApiServer(Hunter):
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.path = "https://{}:{}".format(self.event.host, self.event.port)
|
||||
self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port)
|
||||
self.headers = {}
|
||||
self.with_token = False
|
||||
|
||||
def access_api_server(self):
|
||||
logging.debug('Passive Hunter is attempting to access the API at {host}:{port}'.format(host=self.event.host,
|
||||
port=self.event.port))
|
||||
logging.debug('Passive Hunter is attempting to access the API at {}'.format(self.path))
|
||||
try:
|
||||
r = requests.get("{path}/api".format(path=self.path), headers=self.headers, verify=False)
|
||||
if r.status_code == 200 and r.content != '':
|
||||
@@ -249,7 +257,10 @@ class AccessApiServer(Hunter):
|
||||
def execute(self):
|
||||
api = self.access_api_server()
|
||||
if api:
|
||||
self.publish_event(ServerApiAccess(api, self.with_token))
|
||||
if self.event.protocol == "http":
|
||||
self.publish_event(ServerApiHTTPAccess(api))
|
||||
else:
|
||||
self.publish_event(ServerApiAccess(api, self.with_token))
|
||||
|
||||
namespaces = self.get_items("{path}/api/v1/namespaces".format(path=self.path))
|
||||
if namespaces:
|
||||
@@ -294,7 +305,7 @@ class AccessApiServerActive(ActiveHunter):
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.path = "https://{}:{}".format(self.event.host, self.event.port)
|
||||
self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port)
|
||||
|
||||
def create_item(self, path, name, data):
|
||||
headers = {
|
||||
|
||||
@@ -42,7 +42,7 @@ class IsVulnerableToCVEAttack(Hunter):
|
||||
# From within a Pod we may have extra credentials
|
||||
if self.event.auth_token:
|
||||
self.headers = {'Authorization': 'Bearer ' + self.event.auth_token}
|
||||
self.path = "https://{}:{}".format(self.event.host, self.event.port)
|
||||
self.path = "{}://{}:{}".format(self.event.protocol, self.event.host, self.event.port)
|
||||
self.api_server_evidence = ''
|
||||
self.k8sVersion = ''
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ def test_ApiServer():
|
||||
counter = 0
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get('https://mockOther:443', text='elephant')
|
||||
m.get('https://mockKubernetes:443', text='{"code":403}')
|
||||
m.get('https://mockKubernetes:443', text='{"code":403}', status_code=403)
|
||||
|
||||
e = Event()
|
||||
e.protocol = "https"
|
||||
e.port = 443
|
||||
e.host = 'mockOther'
|
||||
|
||||
@@ -33,10 +34,11 @@ def test_ApiServerWithServiceAccountToken():
|
||||
counter = 0
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get('https://mockKubernetes:443', request_headers={'Authorization':'Bearer very_secret'}, text='{"code":200}')
|
||||
m.get('https://mockKubernetes:443', text='{"code":403}')
|
||||
m.get('https://mockKubernetes:443', text='{"code":403}', status_code=403)
|
||||
m.get('https://mockOther:443', text='elephant')
|
||||
|
||||
e = Event()
|
||||
e.protocol = "https"
|
||||
e.port = 443
|
||||
|
||||
# We should discover an API Server regardless of whether we have a token
|
||||
@@ -60,10 +62,44 @@ def test_ApiServerWithServiceAccountToken():
|
||||
assert counter == 2
|
||||
|
||||
|
||||
def test_InsecureApiServer():
|
||||
global counter
|
||||
counter = 0
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get('http://mockOther:8080', text='elephant')
|
||||
m.get('http://mockKubernetes:8080', text="""{
|
||||
"paths": [
|
||||
"/api",
|
||||
"/api/v1",
|
||||
"/apis",
|
||||
"/apis/",
|
||||
"/apis/admissionregistration.k8s.io",
|
||||
"/apis/admissionregistration.k8s.io/v1beta1",
|
||||
"/apis/apiextensions.k8s.io"
|
||||
]}""")
|
||||
|
||||
e = Event()
|
||||
e.protocol = "http"
|
||||
e.port = 8080
|
||||
e.host = 'mockOther'
|
||||
|
||||
a = ApiServerDiscovery(e)
|
||||
a.execute()
|
||||
|
||||
e.host = 'mockKubernetes'
|
||||
a.execute()
|
||||
|
||||
# Allow the events to be processed. Only the one to mockKubernetes should trigger an event
|
||||
time.sleep(0.1)
|
||||
assert counter == 1
|
||||
|
||||
|
||||
|
||||
# We should only generate an ApiServer event for a response that looks like it came from a Kubernetes node
|
||||
@handler.subscribe(ApiServer)
|
||||
class testApiServer(object):
|
||||
def __init__(self, event):
|
||||
print("Event")
|
||||
assert event.host == 'mockKubernetes'
|
||||
global counter
|
||||
counter += 1
|
||||
@@ -35,6 +35,7 @@ def test_AccessApiServer():
|
||||
e = ApiServer()
|
||||
e.host = "mockKubernetes"
|
||||
e.port = 443
|
||||
e.protocol = "https"
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get('https://mockKubernetes:443/api', text='{}')
|
||||
@@ -151,6 +152,7 @@ def test_AccessApiServerActive():
|
||||
e = ApiServerPassiveHunterFinished(namespaces=["hello-namespace"])
|
||||
e.host = "mockKubernetes"
|
||||
e.port = 443
|
||||
e.protocol = "https"
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
# TODO more tests here with real responses
|
||||
|
||||
Reference in New Issue
Block a user