Compare commits

...

23 Commits

Author SHA1 Message Date
Daniel Sagi
40ebaa6259 fixed bug for only one run 2021-06-17 19:42:35 +00:00
Daniel Sagi
c2d0efc6cd fixed kube-hunter logo 2021-06-17 19:34:26 +00:00
Daniel Sagi
348a288411 added auth store selection, and an impersonating: comment on top of the prompt to indicate the current auth data 2021-06-17 19:26:18 +00:00
Daniel Sagi
12f0d17c3a added eveything hunt option under hunt subconsole 2021-06-17 19:06:53 +00:00
Daniel Sagi
23b048c6d2 completed implementation for delete and create methods in the auth subconsole 2021-06-17 17:21:26 +00:00
Daniel Sagi
1aab95085a removed imports from manager 2021-06-17 16:28:20 +00:00
Daniel Sagi
f706802eb9 improved ipdb interactive option 2021-06-17 16:27:42 +00:00
Daniel Sagi
17fd10120e added new base class for general implementation of CMD class 2021-06-17 16:20:41 +00:00
Daniel Sagi
2400ab5bb1 fixed imports 2021-06-17 15:15:36 +00:00
Daniel Sagi
f6e43e2bbb fixed adding __str__ to cloud type 2021-06-17 15:11:19 +00:00
Daniel Sagi
f7e73fe642 changed sub_command to sub_console 2021-06-17 15:10:58 +00:00
Daniel Sagi
6a8568173e removed old env.py file 2021-06-17 15:09:04 +00:00
Daniel Sagi
d5efef45e2 fixed bug in init of cmd classes 2021-06-17 15:08:40 +00:00
Daniel Sagi
46b7f9f5d9 added missing env module, and fixed auth subconsole to use self.env 2021-06-17 15:06:13 +00:00
Daniel Sagi
2f6badd32f changed module hirerarchy, fixed exit problem and changed to using cmd2 2021-06-17 14:40:18 +00:00
Daniel Sagi
6a4e6040f7 merged main into feature 2021-06-17 10:25:05 +00:00
Daniel
6a3c462fde added whereami command and auth subcmd to control auth database. whereami stats the local files and updates to local auth store 2021-06-10 20:36:13 +00:00
Daniel
f1d4defcb6 added pod local discovery 2021-06-10 19:12:38 +00:00
Daniel Sagi
7601692d42 fixed import bug 2021-06-10 19:39:36 +03:00
Daniel Sagi
7fe047e512 Merge branch 'main' into feature/immersed 2021-06-05 16:28:51 +03:00
Daniel Sagi
d6d46527cd started adding nested cmd for discovery 2021-04-30 16:13:29 +03:00
Daniel Sagi
2fac45e42b removed test cmds 2021-04-30 15:06:22 +03:00
Daniel Sagi
5b36c9c06a started adding immersed console feature, implemented environment settings behaviour on prompt cli 2021-04-30 15:05:33 +03:00
16 changed files with 416 additions and 3 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ venv/
# Distribution / packaging
.Python
env/
!kube_hunter/console/env/
build/
develop-eggs/
dist/

View File

@@ -34,6 +34,7 @@ set_config(config)
# Running all other registered plugins before execution
pm.hook.load_plugin(args=args)
from kube_hunter.console.manager import start_console
from kube_hunter.core.events import handler
from kube_hunter.core.events.types import HuntFinished, HuntStarted
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent, HostScanEvent
@@ -92,11 +93,15 @@ def main():
global hunt_started
scan_options = [config.pod, config.cidr, config.remote, config.interface, config.k8s_auto_discover_nodes]
try:
if args.list:
if args.console:
start_console()
return
elif args.list:
list_hunters()
return
if not any(scan_options):
elif not any(scan_options):
if not interactive_set_config():
return

View File

@@ -1,6 +1,6 @@
from argparse import ArgumentParser
from kube_hunter.plugins import hookimpl
from colorama import init, Fore, Style
@hookimpl
def parser_add_arguments(parser):
@@ -8,6 +8,15 @@ def parser_add_arguments(parser):
This is the default hook implementation for parse_add_argument
Contains initialization for all default arguments
"""
# Initializes colorma
init()
parser.add_argument(
"-c", "--console",
action="store_true",
help=f"Starts kube-hunter's {Fore.GREEN}Immersed Console{Style.RESET_ALL}"
)
parser.add_argument(
"--list",
action="store_true",

View File

@@ -0,0 +1,4 @@
from . import general
from . import manager
from . import env
from . import auth

View File

@@ -0,0 +1 @@
from .auth import AuthSubConsole

View File

@@ -0,0 +1,62 @@
import argparse
from cmd2 import with_argparser
from kube_hunter.console.general import BaseKubeHunterCmd
class AuthSubConsole(BaseKubeHunterCmd):
"""
Manages the underlying AuthStore database
Implementes 3 methods to manage the db
delete - Removes an auth entry by an index
create - creates a new entry in the db,
select - select the auth to use in the environment
"""
def __init__(self, env):
super(AuthSubConsole, self).__init__()
self.env = env
self.sub_console_name = "env/auth"
delete_parser = argparse.ArgumentParser()
delete_parser.add_argument("index", type=int, help="index of the auth entry for deletion")
@with_argparser(delete_parser)
def do_delete(self, opts):
"""Creates a new entry in the auth database based on a given raw jwt token"""
if opts.index:
if self.env.current_auth.get_auths_count() > opts.index:
self.env.current_auth.delete_auth(opts.jwt_token.strip())
else:
self.perror("Index too large")
create_parser = argparse.ArgumentParser()
create_parser.add_argument("jwt_token", help="A raw jwt_token of the new auth entry")
@with_argparser(create_parser)
def do_create(self, opts):
"""Creates a new entry in the auth database based on a given raw jwt token"""
if opts.jwt_token:
self.env.current_auth.new_auth(opts.jwt_token.strip())
show_parser = argparse.ArgumentParser()
show_parser.add_argument("-i", "--index", type=int, help="set index to print raw data for a specific auth entry")
@with_argparser(show_parser)
def do_show(self, opts):
"""Show current collected auths"""
if opts.index is not None:
if self.env.current_auth.get_auths_count() > opts.index:
self.poutput(f"Token:\n{self.env.current_auth.get_auth(opts.index).raw_token}")
else:
self.perror("Index too large")
else:
self.poutput(self.env.current_auth.get_table())
select_parser = argparse.ArgumentParser()
select_parser.add_argument("index", type=int, help="index of auth entry to set for environment")
@with_argparser(select_parser)
def do_select(self, opts):
"""Sets the auth entry for the environment"""
if opts.index is not None:
if self.env.current_auth.get_auths_count() > opts.index:
self.env.current_auth.set_select_auth(opts.index)
else:
self.perror("Index too large")

View File

@@ -0,0 +1,76 @@
import json
import base64
from prettytable import PrettyTable, ALL
""" Auth models"""
class Auth:
def parse_token(self, token):
""" Extracting data from token file """
# adding maximum base64 padding to parse correctly
self.raw_token = token
token_json = base64.b64decode(f"{token.split('.')[1]}==")
token_data = json.loads(token_json)
self.iss = token_data.get("iss")
self.namespace = token_data.get("kubernetes.io/serviceaccount/namespace")
self.name = token_data.get("kubernetes.io/serviceaccount/secret.name")
self.service_account_name = token_data.get("kubernetes.io/serviceaccount/service-account.name")
self.uid = token_data.get("kubernetes.io/serviceaccount/service-account.uid")
self.sub = token_data.get("sub")
def __init__(self, token=None):
if token:
self.parse_token(token)
class AuthStore:
auths = []
selected_auth = None
def new_auth(self, token):
""" Initializes new Auth object and adds it to the auth db """
new_auth = Auth(token)
if not self.is_exists(new_auth):
self.auths.append(new_auth)
# if it's the only auth, selecting it
if not self.selected_auth:
self.selected_auth = 0
def get_auths_count(self):
return len(self.auths)
def delete_auth(self, index):
return self.auths.pop(index)
def is_exists(self, check_auth):
""" Checks for uniques auth in auth_store """
for auth in self.auths:
if auth.sub == check_auth.sub:
return True
return False
def get_current_auth(self):
return self.auths[self.selected_auth]
def get_auth(self, index):
return self.auths[index]
def set_select_auth(self, index):
self.selected_auth = index
def get_table(self):
auth_table = PrettyTable(["index", "Name", "Selected"], hrules=ALL)
auth_table.align = "l"
auth_table.padding_width = 1
auth_table.header_style = "upper"
# building auth token table, showing selected auths
for i, auth in enumerate(self.auths):
selected_mark = ""
if i == self.selected_auth:
selected_mark = "*"
auth_table.add_row([i, auth.sub, selected_mark])
return auth_table

View File

View File

@@ -0,0 +1,67 @@
from kube_hunter.console.auth import AuthSubConsole
from kube_hunter.console.general import BaseKubeHunterCmd
from kube_hunter.conf import Config, set_config
from kube_hunter.conf.logging import setup_logger
from kube_hunter.core.events import handler
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent, HostScanEvent
from kube_hunter.modules.report import get_reporter, get_dispatcher
from kube_hunter.core.events.types import HuntFinished, HuntStarted
import time
from cmd2 import ansi
from progressbar import FormatLabel, RotatingMarker, UnknownLength, ProgressBar, Timer
class HuntSubConsole(BaseKubeHunterCmd):
"""DiscoverSubConsole
In charge of managing and running kube-hunter's discover modules
"""
def __init__(self, env):
super(HuntSubConsole, self).__init__()
self.env = env
self.sub_console_name = "hunt"
@staticmethod
def progress_bar():
"""Displays animated progress bar
Integrates with handler object, to properly block until hunt finish
"""
# Logger
widgets = ['[', Timer(), ']', ': ', FormatLabel(''), ' ', RotatingMarker()]
bar = ProgressBar(max_value=UnknownLength, widgets=widgets)
while handler.unfinished_tasks > 0:
widgets[4] = FormatLabel(f'Tasks Left To Process: {handler.unfinished_tasks}')
bar.update(handler.unfinished_tasks)
time.sleep(0.1)
bar.finish()
def do_everything(self, arg):
"""Wraps running of kube-hunter's hunting
Uses the current environment to specify data to start_event when starting a scan
"""
# TODO: display output
current_auth = self.env.current_auth.get_current_auth()
start_event = None
if self.env.is_inside_pod:
self.pfeedback(ansi.style(f"Hunting Started (as {current_auth.sub})", fg="green"))
start_event = RunningAsPodEvent()
start_event.auth_token = current_auth.raw_token
# setting basic stuff for output methods
setup_logger("none", None)
config = Config()
config.dispatcher = get_dispatcher("stdout")
config.reporter = get_reporter("plain")
set_config(config)
# trigger hunting
handler.publish_event(start_event)
handler.publish_event(HuntStarted())
self.progress_bar()
self.pfeedback(ansi.style(f"Finished hunting. found {0} services and {0} vulnerabilities", fg="green"))
handler.join()
handler.publish_event(HuntFinished())
handler.free()

2
kube_hunter/console/env/__init__.py vendored Normal file
View File

@@ -0,0 +1,2 @@
from .env import EnvSubConsole
from .types import ImmersedEnvironment

17
kube_hunter/console/env/env.py vendored Normal file
View File

@@ -0,0 +1,17 @@
from kube_hunter.console.auth import AuthSubConsole
from kube_hunter.console.general import BaseKubeHunterCmd
class EnvSubConsole(BaseKubeHunterCmd):
"""EnvSubConsole
In charge of managing and viewing the entire current environment state
Includes: Auth database..
"""
def __init__(self, env):
super(EnvSubConsole, self).__init__()
self.env = env
self.sub_console_name = "env"
# self.prompt = self.env.get_prompt(sub_console="env")
def do_auth(self, arg):
AuthSubConsole(self.env).cmdloop()

55
kube_hunter/console/env/types.py vendored Normal file
View File

@@ -0,0 +1,55 @@
from colorama import init, Fore, Style
from kube_hunter.console.general import types as GeneralTypes
from kube_hunter.console.auth import types as AuthTypes
# initializes colorma
init()
class ImmersedEnvironment:
"""
ImmersedEnvironment keeps track of the current console run state.
"""
auths = []
pods = []
is_inside_cloud = False
is_inside_container = False
is_inside_pod = False
current_cloud = GeneralTypes.UnknownCloud()
current_pod = GeneralTypes.Pod()
current_container = GeneralTypes.Container()
current_auth = AuthTypes.AuthStore()
def get_prompt(self, sub_console=""):
"""
Parses current env state to picture a short description of where we are right now
General format is `(cloud) -> (run_unit) kube-hunter $`
"""
arrow = "->"
prompt_prefix = f" kube-hunter{' [' + sub_console + ']' if sub_console else ''} $ "
# add colores unly
cloud = f"({Fore.BLUE}{self.current_cloud}{Style.RESET_ALL})"
pod = f"({Fore.MAGENTA}{self.current_pod}{Style.RESET_ALL})"
container = f"(container: {Fore.CYAN}{self.current_container}{Style.RESET_ALL})"
container_in_pod = f"({Fore.MAGENTA}{self.current_pod}/{{}}{Style.RESET_ALL})"
env_description = ""
if self.current_auth.get_auths_count():
auth = self.current_auth.get_current_auth()
env_description += f" {Fore.LIGHTRED_EX}[Impersonating {auth.sub}]{Style.RESET_ALL}\n"
env_description += cloud
if self.is_inside_pod:
if len(self.current_pod.containers):
env_description += f" {arrow} {container_in_pod.format(self.current_pod.containers[0])}"
else:
env_description += f" {arrow} {pod}"
elif self.is_inside_container:
env_description += f" {arrow} {container}"
return f"{env_description}{prompt_prefix}"

View File

@@ -0,0 +1 @@
from .console import BaseKubeHunterCmd

View File

@@ -0,0 +1,16 @@
import cmd2
class BaseKubeHunterCmd(cmd2.Cmd):
sub_console_name = ""
def postcmd(self, stop, line):
self.prompt = self.env.get_prompt(self.sub_console_name)
if stop:
return True
def do_exit(self, arg):
'exists shell'
return True
def emptyline(self):
pass

View File

@@ -0,0 +1,38 @@
import socket
""" General Types """
class Container:
""" Basic model for Container objects """
name = ""
def __str__(self):
return self.name
class Pod:
""" Basic model for Pod objects """
ip_address = ""
name = ""
namespace = ""
containers = []
def __str__(self):
return f"{self.namespace}/{self.name}"
def incluster_update(self, pod_event):
"""
uses pod_event and other techniques to get full data on the incluster pod env data
"""
self.namespace = pod_event.namespace
# hostname will almost always will be the pod's name
self.name = socket.gethostname()
class Cloud:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
class UnknownCloud(Cloud):
def __init__(self):
super(UnknownCloud, self).__init__("Unknown Cloud")

View File

@@ -0,0 +1,59 @@
from kube_hunter.console.discover.discover import HuntSubConsole
from kube_hunter.console.general import BaseKubeHunterCmd
from kube_hunter.console.env import EnvSubConsole, ImmersedEnvironment
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent
from colorama import (
Back,
Fore,
Style,
)
from cmd2 import ansi
class KubeHunterMainConsole(BaseKubeHunterCmd):
def __init__(self, env):
super(KubeHunterMainConsole, self).__init__()
kube_hunter_logo = r"""
_ __ __ __
/\ \ /\ \ /\ \ /\ \__
\ \ \/'\ __ _\ \ \____ __ \ \ \___ __ __ ___\ \ ,_\ __ _ __
\ \ , < /\ \/\ \ \ '__`\ /'__`\ ______\ \ _ `\/\ \/\ \/' _ `\ \ \/ /'__`/\`'__\
\ \ \\`\\ \ \_\ \ \ \L\ /\ __//\______\ \ \ \ \ \ \_\ /\ \/\ \ \ \_/\ __\ \ \/
\ \_\ \_\ \____/\ \_,__\ \____\/______/\ \_\ \_\ \____\ \_\ \_\ \__\ \____\ \_\
\/_/\/_/\/___/ \/___/ \/____/ \/_/\/_/\/___/ \/_/\/_/\/__/\/____/\/_/
"""
self.intro = f'{kube_hunter_logo}\n\nWelcome to kube-hunter Immeresed Console. Type help or ? to list commands.\n'
self.env = env
def do_hunt(self, arg):
'hunt using specified environment'
HuntSubConsole(self.env).cmdloop()
def do_env(self, arg):
'Show your environment data collected so far'
EnvSubConsole(self.env).cmdloop()
def do_interactive(self, arg):
"""Start interactive ipython session"""
environment = self.env
self.poutput("\n\tStarted an interactive python session. use `environment` to manage the populated environment object")
import ipdb
ipdb.set_trace()
def do_whereami(self, arg):
"""Try to determine you are based on local files and mounts"""
self.pfeedback("Trying to find out where you are...")
pod_event = RunningAsPodEvent()
if pod_event.auth_token:
self.pfeedback(ansi.style("Found running inside a kubernetes pod", fg="green"))
self.env.current_auth.new_auth(pod_event.auth_token)
self.pfeedback(ansi.style("Loaded a new auth entry: (hint: env/auth/show)", fg="green"))
self.env.current_pod.incluster_update(pod_event)
self.env.is_inside_pod = True
self.pfeedback("Updated environment with locally found data")
def start_console():
environment = ImmersedEnvironment()
a = KubeHunterMainConsole(environment)
a.cmdloop()