Merge pull request #117 from aquasecurity/insecure_port

Insecure port for api server
This commit is contained in:
danielsagi
2019-06-11 17:49:59 +03:00
committed by GitHub
7 changed files with 77 additions and 19 deletions
+18 -8
View File
@@ -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))
+1 -2
View File
@@ -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())
+1 -1
View File
@@ -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):
+16 -5
View File
@@ -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 = {
+1 -1
View File
@@ -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 = ''
+38 -2
View File
@@ -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
+2
View File
@@ -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