469 lines
17 KiB
Python
Raw Normal View History

2019-09-19 15:15:46 +02:00
import time
import json
import os
import re
import logging
2019-09-19 15:15:46 +02:00
import _thread
import pwnagotchi
2019-10-04 00:26:00 +02:00
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.ui.web.server import Server
from pwnagotchi.automata import Automata
from pwnagotchi.log import LastSession
from pwnagotchi.bettercap import Client
2019-09-30 21:22:01 +02:00
from pwnagotchi.mesh.utils import AsyncAdvertiser
2019-09-19 15:15:46 +02:00
from pwnagotchi.ai.train import AsyncTrainer
RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def __init__(self, view, config, keypair):
2019-09-19 15:15:46 +02:00
Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'],
config['bettercap']['port'],
config['bettercap']['username'],
config['bettercap']['password'])
Automata.__init__(self, config, view)
AsyncAdvertiser.__init__(self, config, view, keypair)
2019-09-19 15:15:46 +02:00
AsyncTrainer.__init__(self, config)
self._started_at = time.time()
2019-12-18 18:58:28 +01:00
self._filter = None if not config['main']['filter'] else re.compile(config['main']['filter'])
2019-09-19 15:15:46 +02:00
self._current_channel = 0
self._tot_aps = 0
self._aps_on_channel = 0
2019-10-04 00:26:00 +02:00
self._supported_channels = utils.iface_channels(config['main']['iface'])
2019-09-19 15:15:46 +02:00
self._view = view
self._view.set_agent(self)
self._web_ui = Server(self, config['ui'])
2019-09-19 15:15:46 +02:00
self._access_points = []
self._last_pwnd = None
self._history = {}
self._handshakes = {}
self.last_session = LastSession(self._config)
self.mode = 'auto'
2019-09-19 15:15:46 +02:00
if not os.path.exists(config['bettercap']['handshakes']):
os.makedirs(config['bettercap']['handshakes'])
logging.info("%s@%s (v%s)", pwnagotchi.name(), self.fingerprint(), pwnagotchi.version)
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s", plugin.__class__.__name__, plugin.__version__)
def config(self):
return self._config
def view(self):
return self._view
2019-09-21 18:52:00 +02:00
def supported_channels(self):
return self._supported_channels
2019-09-19 15:15:46 +02:00
def setup_events(self):
logging.info("connecting to %s ...", self.url)
2019-09-19 15:15:46 +02:00
for tag in self._config['bettercap']['silence']:
try:
self.run('events.ignore %s' % tag, verbose_errors=False)
2019-09-19 15:15:46 +02:00
except Exception as e:
pass
def _reset_wifi_settings(self):
mon_iface = self._config['main']['iface']
self.run('set wifi.interface %s' % mon_iface)
self.run('set wifi.ap.ttl %d' % self._config['personality']['ap_ttl'])
self.run('set wifi.sta.ttl %d' % self._config['personality']['sta_ttl'])
self.run('set wifi.rssi.min %d' % self._config['personality']['min_rssi'])
self.run('set wifi.handshakes.file %s' % self._config['bettercap']['handshakes'])
self.run('set wifi.handshakes.aggregate false')
def start_monitor_mode(self):
mon_iface = self._config['main']['iface']
mon_start_cmd = self._config['main']['mon_start_cmd']
restart = not self._config['main']['no_restart']
has_mon = False
while has_mon is False:
s = self.session()
for iface in s['interfaces']:
if iface['name'] == mon_iface:
logging.info("found monitor interface: %s", iface['name'])
2019-09-19 15:15:46 +02:00
has_mon = True
break
if has_mon is False:
if mon_start_cmd is not None and mon_start_cmd != '':
logging.info("starting monitor interface ...")
2019-09-19 15:15:46 +02:00
self.run('!%s' % mon_start_cmd)
else:
logging.info("waiting for monitor interface %s ...", mon_iface)
2019-09-19 15:15:46 +02:00
time.sleep(1)
logging.info("supported channels: %s", self._supported_channels)
logging.info("handshakes will be collected inside %s", self._config['bettercap']['handshakes'])
2019-09-19 15:15:46 +02:00
self._reset_wifi_settings()
wifi_running = self.is_module_running('wifi')
if wifi_running and restart:
logging.debug("restarting wifi module ...")
2019-10-04 12:22:16 +02:00
self.restart_module('wifi.recon')
2019-09-19 15:15:46 +02:00
self.run('wifi.clear')
elif not wifi_running:
logging.debug("starting wifi module ...")
2019-10-04 12:22:16 +02:00
self.start_module('wifi.recon')
2019-09-19 15:15:46 +02:00
self.start_advertising()
def _wait_bettercap(self):
while True:
try:
s = self.session()
return
except:
logging.info("waiting for bettercap API to be available ...")
time.sleep(1)
2019-10-04 00:11:31 +02:00
def start(self):
self.start_ai()
self._wait_bettercap()
2019-10-04 00:11:31 +02:00
self.setup_events()
self.set_starting()
self.start_monitor_mode()
self.start_event_polling()
# print initial stats
self.next_epoch()
self.set_ready()
2019-09-19 15:15:46 +02:00
def recon(self):
recon_time = self._config['personality']['recon_time']
max_inactive = self._config['personality']['max_inactive_scale']
recon_mul = self._config['personality']['recon_inactive_multiplier']
channels = self._config['personality']['channels']
if self._epoch.inactive_for >= max_inactive:
recon_time *= recon_mul
self._view.set('channel', '*')
if not channels:
self._current_channel = 0
logging.debug("RECON %ds", recon_time)
2019-09-19 15:15:46 +02:00
self.run('wifi.recon.channel clear')
else:
logging.debug("RECON %ds ON CHANNELS %s", recon_time, ','.join(map(str, channels)))
2019-09-19 15:15:46 +02:00
try:
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
2019-09-19 15:15:46 +02:00
except Exception as e:
logging.exception("error")
2019-09-19 15:15:46 +02:00
self.wait_for(recon_time, sleeping=False)
def _filter_included(self, ap):
return self._filter is None or \
self._filter.match(ap['hostname']) is not None or \
self._filter.match(ap['mac']) is not None
def set_access_points(self, aps):
self._access_points = aps
plugins.on('wifi_update', self, aps)
self._epoch.observe(aps, list(self._peers.values()))
2019-09-19 15:15:46 +02:00
return self._access_points
def get_access_points(self):
whitelist = self._config['main']['whitelist']
aps = []
try:
s = self.session()
plugins.on("unfiltered_ap_list", self, s['wifi']['aps'])
2019-09-19 15:15:46 +02:00
for ap in s['wifi']['aps']:
if ap['encryption'] == '' or ap['encryption'] == 'OPEN':
continue
elif ap['hostname'] not in whitelist \
and ap['mac'].lower() not in whitelist \
and ap['mac'][:8].lower() not in whitelist:
2019-09-19 15:15:46 +02:00
if self._filter_included(ap):
aps.append(ap)
except Exception as e:
logging.exception("error")
2019-09-19 15:15:46 +02:00
aps.sort(key=lambda ap: ap['channel'])
return self.set_access_points(aps)
def get_total_aps(self):
return self._tot_aps
def get_aps_on_channel(self):
return self._aps_on_channel
def get_current_channel(self):
return self._current_channel
2019-09-19 15:15:46 +02:00
def get_access_points_by_channel(self):
aps = self.get_access_points()
channels = self._config['personality']['channels']
grouped = {}
2019-09-22 12:08:11 +02:00
# group by channel
2019-09-19 15:15:46 +02:00
for ap in aps:
ch = ap['channel']
# if we're sticking to a channel, skip anything
# which is not on that channel
2019-12-13 19:29:44 +01:00
if channels and ch not in channels:
2019-09-19 15:15:46 +02:00
continue
if ch not in grouped:
grouped[ch] = [ap]
else:
grouped[ch].append(ap)
# sort by more populated channels
return sorted(grouped.items(), key=lambda kv: len(kv[1]), reverse=True)
def _find_ap_sta_in(self, station_mac, ap_mac, session):
for ap in session['wifi']['aps']:
if ap['mac'] == ap_mac:
for sta in ap['clients']:
if sta['mac'] == station_mac:
return (ap, sta)
return (ap, {'mac': station_mac, 'vendor': ''})
return None
def _update_uptime(self, s):
secs = pwnagotchi.uptime()
2019-10-04 00:26:00 +02:00
self._view.set('uptime', utils.secs_to_hhmmss(secs))
# self._view.set('epoch', '%04d' % self._epoch.epoch)
2019-09-19 15:15:46 +02:00
def _update_counters(self):
self._tot_aps = len(self._access_points)
2019-09-19 15:15:46 +02:00
tot_stas = sum(len(ap['clients']) for ap in self._access_points)
if self._current_channel == 0:
self._view.set('aps', '%d' % self._tot_aps)
2019-09-19 15:15:46 +02:00
self._view.set('sta', '%d' % tot_stas)
else:
self._aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
2019-09-19 15:15:46 +02:00
stas_on_channel = sum(
[len(ap['clients']) for ap in self._access_points if ap['channel'] == self._current_channel])
self._view.set('aps', '%d (%d)' % (self._aps_on_channel, self._tot_aps))
2019-09-19 15:15:46 +02:00
self._view.set('sta', '%d (%d)' % (stas_on_channel, tot_stas))
def _update_handshakes(self, new_shakes=0):
if new_shakes > 0:
self._epoch.track(handshake=True, inc=new_shakes)
2019-10-04 00:26:00 +02:00
tot = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
2019-09-19 15:15:46 +02:00
txt = '%d (%d)' % (len(self._handshakes), tot)
if self._last_pwnd is not None:
2019-09-22 12:08:11 +02:00
txt += ' [%s]' % self._last_pwnd[:20]
2019-09-19 15:15:46 +02:00
self._view.set('shakes', txt)
if new_shakes > 0:
self._view.on_handshakes(new_shakes)
def _update_peers(self):
self._view.set_closest_peer(self._closest_peer, len(self._peers))
2019-09-19 15:15:46 +02:00
def _reboot(self):
self.set_rebooting()
self._save_recovery_data()
pwnagotchi.reboot()
2019-09-19 15:15:46 +02:00
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
2019-09-19 15:15:46 +02:00
with open(RECOVERY_DATA_FILE, 'w') as fp:
data = {
'started_at': self._started_at,
'epoch': self._epoch.epoch,
'history': self._history,
'handshakes': self._handshakes,
'last_pwnd': self._last_pwnd
}
json.dump(data, fp)
def _load_recovery_data(self, delete=True, no_exceptions=True):
try:
with open(RECOVERY_DATA_FILE, 'rt') as fp:
data = json.load(fp)
logging.info("found recovery data: %s", data)
2019-09-19 15:15:46 +02:00
self._started_at = data['started_at']
self._epoch.epoch = data['epoch']
self._handshakes = data['handshakes']
self._history = data['history']
self._last_pwnd = data['last_pwnd']
if delete:
logging.info("deleting %s", RECOVERY_DATA_FILE)
2019-09-19 15:15:46 +02:00
os.unlink(RECOVERY_DATA_FILE)
except:
if not no_exceptions:
raise
def _event_poller(self):
self._load_recovery_data()
self.run('events.clear')
while True:
time.sleep(1)
new_shakes = 0
logging.debug("polling events ...")
2019-09-19 15:15:46 +02:00
try:
s = self.session()
self._update_uptime(s)
self._update_advertisement(s)
self._update_peers()
self._update_counters()
2019-09-19 15:15:46 +02:00
for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
filename = h['data']['file']
2019-09-19 15:15:46 +02:00
sta_mac = h['data']['station']
ap_mac = h['data']['ap']
key = "%s -> %s" % (sta_mac, ap_mac)
if key not in self._handshakes:
self._handshakes[key] = h
new_shakes += 1
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
if ap_and_station is None:
logging.warning("!!! captured new handshake: %s !!!", key)
2019-09-19 15:15:46 +02:00
self._last_pwnd = ap_mac
plugins.on('handshake', self, filename, ap_mac, sta_mac)
2019-09-19 15:15:46 +02:00
else:
(ap, sta) = ap_and_station
2019-09-19 15:15:46 +02:00
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
'hostname'] != '<hidden>' else ap_mac
logging.warning(
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
ap['channel'],
ap['rssi'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor'])
plugins.on('handshake', self, filename, ap, sta)
2019-09-19 15:15:46 +02:00
except Exception as e:
logging.error("error: %s", e)
2019-09-19 15:15:46 +02:00
finally:
self._update_handshakes(new_shakes)
def start_event_polling(self):
_thread.start_new_thread(self._event_poller, ())
def is_module_running(self, module):
s = self.session()
for m in s['modules']:
if m['name'] == module:
return m['running']
return False
2019-10-04 12:22:16 +02:00
def start_module(self, module):
2019-09-19 15:15:46 +02:00
self.run('%s on' % module)
2019-10-04 12:22:16 +02:00
def restart_module(self, module):
2019-09-19 15:15:46 +02:00
self.run('%s off; %s on' % (module, module))
def _has_handshake(self, bssid):
for key in self._handshakes:
if bssid.lower() in key:
return True
return False
def _should_interact(self, who):
if self._has_handshake(who):
return False
elif who not in self._history:
self._history[who] = 1
return True
else:
self._history[who] += 1
return self._history[who] < self._config['personality']['max_interactions']
def associate(self, ap, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping assoc(%s)", ap['mac'])
2019-09-19 15:15:46 +02:00
return
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
self._view.on_assoc(ap)
try:
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm...",
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi'])
2019-09-19 15:15:46 +02:00
self.run('wifi.assoc %s' % ap['mac'])
self._epoch.track(assoc=True)
except Exception as e:
self._on_error(ap['mac'], e)
plugins.on('association', self, ap)
2019-09-19 15:15:46 +02:00
if throttle > 0:
time.sleep(throttle)
self._view.on_normal()
def deauth(self, ap, sta, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping deauth(%s)", sta['mac'])
2019-09-19 15:15:46 +02:00
return
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
self._view.on_deauth(sta)
try:
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ...",
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi'])
2019-09-19 15:15:46 +02:00
self.run('wifi.deauth %s' % sta['mac'])
self._epoch.track(deauth=True)
except Exception as e:
self._on_error(sta['mac'], e)
plugins.on('deauthentication', self, ap, sta)
2019-09-19 15:15:46 +02:00
if throttle > 0:
time.sleep(throttle)
self._view.on_normal()
def set_channel(self, channel, verbose=True):
if self.is_stale():
logging.debug("recon is stale, skipping set_channel(%d)", channel)
2019-09-19 15:15:46 +02:00
return
# if in the previous loop no client stations has been deauthenticated
2019-09-22 12:08:11 +02:00
# and only association frames have been sent, we don't need to wait
2019-09-19 15:15:46 +02:00
# very long before switching channel as we don't have to wait for
# such client stations to reconnect in order to sniff the handshake.
wait = 0
if self._epoch.did_deauth:
wait = self._config['personality']['hop_recon_time']
elif self._epoch.did_associate:
wait = self._config['personality']['min_recon_time']
if channel != self._current_channel:
if self._current_channel != 0 and wait > 0:
if verbose:
logging.info("waiting for %ds on channel %d ...", wait, self._current_channel)
else:
logging.debug("waiting for %ds on channel %d ...", wait, self._current_channel)
2019-09-19 15:15:46 +02:00
self.wait_for(wait)
if verbose and self._epoch.any_activity:
logging.info("CHANNEL %d", channel)
2019-09-19 15:15:46 +02:00
try:
self.run('wifi.recon.channel %d' % channel)
self._current_channel = channel
self._epoch.track(hop=True)
self._view.set('channel', '%d' % channel)
plugins.on('channel_hop', self, channel)
2019-09-19 15:15:46 +02:00
except Exception as e:
logging.error("error: %s", e)