class Device: def __init__(self, name, share_internet, mac, ip, netmask, interval, priority=10, scantime=15, search_order=0, max_tries=0, **kwargs): self.name = name self.status = StatusFile(f'/root/.bt-tether-{name}') self.status.update() self.tries = 0 self.network = None self.max_tries = max_tries self.search_order = search_order self.share_internet = share_internet self.ip = ip self.netmask = netmask self.interval = interval self.mac = mac self.scantime = scantime self.priority = priority def connected(self): """ Checks if device is connected """ return self.network and BTNap.prop_get(self.network, 'Connected') def interface(self): """ Returns the interface name or None """ if not self.connected(): return None return BTNap.prop_get(self.network, 'Interface')
class Watchdog(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '1.0.0' __license__ = 'GPL3' __description__ = 'Restart pwnagotchi when blindbug is detected.' __defaults__ = { 'enabled': False, } def __init__(self): self.options = dict() self.pattern = re.compile( r'brcmf_cfg80211_nexmon_set_channel.*?Set Channel failed') self.status = StatusFile('/root/.pwnagotchi-watchdog') self.status.update() def on_loaded(self): """ Gets called when the plugin gets loaded """ logging.info("Watchdog plugin loaded.") def on_epoch(self, agent, epoch, epoch_data): if self.status.newer_then_minutes(5): return data_keys = ['num_deauths', 'num_associations', 'num_handshakes'] has_interactions = any( [epoch_data[x] for x in data_keys if x in epoch_data]) if has_interactions: return epoch_duration = epoch_data['duration_secs'] # get last 10 lines last_lines = ''.join( list( TextIOWrapper( subprocess.Popen([ 'journalctl', '-n10', '-k', '--since', f"{epoch_duration} seconds ago" ], stdout=subprocess.PIPE).stdout))[-10:]) if len(self.pattern.findall(last_lines)) >= 5: display = agent.view() display.set('status', 'Blind-Bug detected. Restarting.') display.update(force=True) logging.info('[WATCHDOG] Blind-Bug detected. Restarting.') mode = 'MANU' if agent.mode == 'manual' else 'AUTO' import pwnagotchi pwnagotchi.reboot(mode=mode)
class WpaSec(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.1.0' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org' def __init__(self): self.ready = False self.lock = Lock() try: self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json') except JSONDecodeError as json_err: os.remove("/root/.wpa_sec_uploads") self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json') self.options = dict() self.skip = list() def _upload_to_wpasec(self, path, timeout=30): """ Uploads the file to https://wpa-sec.stanev.org, or another endpoint. """ with open(path, 'rb') as file_to_upload: cookie = {'key': self.options['api_key']} payload = {'file': file_to_upload} try: result = requests.post(self.options['api_url'], cookies=cookie, files=payload, timeout=timeout) if ' already submitted' in result.text: logging.warning("%s was already submitted.", path) except requests.exceptions.RequestException as req_e: raise req_e def _download_from_wpasec(self, output, timeout=30): """ Downloads the results from wpasec and safes them to output Output-Format: bssid, station_mac, ssid, password """ api_url = self.options['api_url'] if not api_url.endswith('/'): api_url = f"{api_url}/" api_url = f"{api_url}?api&dl=1" cookie = {'key': self.options['api_key']} try: result = requests.get(api_url, cookies=cookie, timeout=timeout) with open(output, 'wb') as output_file: output_file.write(result.content) except requests.exceptions.RequestException as req_e: raise req_e except OSError as os_e: raise os_e def on_loaded(self): """ Gets called when the plugin gets loaded """ if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None): logging.error( "WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org" ) return if 'api_url' not in self.options or ('api_url' in self.options and self.options['api_url'] is None): logging.error( "WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured." ) return self.ready = True def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ with self.lock: if self.ready: config = agent.config() display = agent.view() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] handshake_new = set(handshake_paths) - set(reported) - set( self.skip) if handshake_new: logging.info( "WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org" ) for idx, handshake in enumerate(handshake_new): display.set( 'status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: self._upload_to_wpasec(handshake) reported.append(handshake) self.report.update(data={'reported': reported}) logging.info("WPA_SEC: Successfully uploaded %s", handshake) except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("WPA_SEC: %s", req_e) continue except OSError as os_e: logging.error("WPA_SEC: %s", os_e) continue if 'download_results' in self.options and self.options[ 'download_results']: cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile') if os.path.exists(cracked_file): last_check = datetime.fromtimestamp( os.path.getmtime(cracked_file)) if last_check is not None and ( (datetime.now() - last_check).seconds / (60 * 60)) < 1: return try: self._download_from_wpasec( os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')) logging.info("WPA_SEC: Downloaded cracked passwords.") except requests.exceptions.RequestException as req_e: logging.debug("WPA_SEC: %s", req_e) except OSError as os_e: logging.debug("WPA_SEC: %s", os_e)
class Grid(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '1.0.1' __license__ = 'GPL3' __description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned ' \ 'networks to api.pwnagotchi.ai ' def __init__(self): self.options = dict() self.report = StatusFile('/root/.api-report.json', data_format='json') self.unread_messages = 0 self.total_messages = 0 def is_excluded(self, what): for skip in self.options['exclude']: skip = skip.lower() what = what.lower() if skip in what or skip.replace(':', '') in what: return True return False def on_loaded(self): logging.info("grid plugin loaded.") def set_reported(self, reported, net_id): if net_id not in reported: reported.append(net_id) self.report.update(data={'reported': reported}) def check_inbox(self, agent): logging.debug("checking mailbox ...") messages = grid.inbox() self.total_messages = len(messages) self.unread_messages = len( [m for m in messages if m['seen_at'] is None]) if self.unread_messages: plugins.on('unread_inbox', self.unread_messages) logging.debug("[grid] unread:%d total:%d" % (self.unread_messages, self.total_messages)) agent.view().on_unread_messages(self.unread_messages, self.total_messages) def check_handshakes(self, agent): logging.debug("checking pcaps") pcap_files = glob.glob( os.path.join(agent.config()['bettercap']['handshakes'], "*.pcap")) num_networks = len(pcap_files) reported = self.report.data_field_or('reported', default=[]) num_reported = len(reported) num_new = num_networks - num_reported if num_new > 0: if self.options['report']: logging.info("grid: %d new networks to report" % num_new) logging.debug("self.options: %s" % self.options) logging.debug(" exclude: %s" % self.options['exclude']) for pcap_file in pcap_files: net_id = os.path.basename(pcap_file).replace('.pcap', '') if net_id not in reported: if self.is_excluded(net_id): logging.debug( "skipping %s due to exclusion filter" % pcap_file) self.set_reported(reported, net_id) continue essid, bssid = parse_pcap(pcap_file) if bssid: if self.is_excluded(essid) or self.is_excluded( bssid): logging.debug( "not reporting %s due to exclusion filter" % pcap_file) self.set_reported(reported, net_id) else: if grid.report_ap(essid, bssid): self.set_reported(reported, net_id) time.sleep(1.5) else: logging.warning("no bssid found?!") else: logging.debug("grid: reporting disabled") def on_internet_available(self, agent): logging.debug("internet available") try: grid.update_data(agent.last_session) except Exception as e: logging.error("error connecting to the pwngrid-peer service: %s" % e) logging.debug(e, exc_info=True) return try: self.check_inbox(agent) except Exception as e: logging.error("[grid] error while checking inbox: %s" % e) logging.debug(e, exc_info=True) try: self.check_handshakes(agent) except Exception as e: logging.error("[grid] error while checking pcaps: %s" % e) logging.debug(e, exc_info=True)
class OnlineHashCrack(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.0.0' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com' def __init__(self): self.ready = False self.report = StatusFile('/root/.ohc_uploads', data_format='json') self.skip = list() def on_loaded(self): """ Gets called when the plugin gets loaded """ if 'email' not in self.options or ('email' in self.options and self.options['email'] is None): logging.error( "OHC: Email isn't set. Can't upload to onlinehashcrack.com") return self.ready = True def _upload_to_ohc(self, path, timeout=30): """ Uploads the file to onlinehashcrack.com """ with open(path, 'rb') as file_to_upload: data = {'email': self.options['email']} payload = {'file': file_to_upload} try: result = requests.post('https://api.onlinehashcrack.com', data=data, files=payload, timeout=timeout) if 'already been sent' in result.text: logging.warning(f"{path} was already uploaded.") except requests.exceptions.RequestException as e: logging.error( f"OHC: Got an exception while uploading {path} -> {e}") raise e def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ if self.ready: display = agent.view() config = agent.config() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] handshake_new = set(handshake_paths) - set(reported) - set( self.skip) if handshake_new: logging.info( "OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com" ) for idx, handshake in enumerate(handshake_new): display.set( 'status', f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: self._upload_to_ohc(handshake) reported.append(handshake) self.report.update(data={'reported': reported}) logging.info(f"OHC: Successfully uploaded {handshake}") except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("OHC: %s", req_e) continue except OSError as os_e: self.skip.append(handshake) logging.error("OHC: %s", os_e) continue
class SessionStats(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '0.1.0' __license__ = 'GPL3' __description__ = 'This plugin displays stats of the current session.' def __init__(self): self.lock = threading.Lock() self.options = dict() self.stats = dict() self.clock = GhettoClock() def on_loaded(self): """ Gets called when the plugin gets loaded """ # this has to happen in "loaded" because the options are not yet # available in the __init__ os.makedirs(self.options['save_directory'], exist_ok=True) self.session_name = "stats_{}.json".format( self.clock.now().strftime("%Y_%m_%d_%H_%M")) self.session = StatusFile(os.path.join(self.options['save_directory'], self.session_name), data_format='json') logging.info("Session-stats plugin loaded.") def on_epoch(self, agent, epoch, epoch_data): """ Save the epoch_data to self.stats """ with self.lock: self.stats[self.clock.now().strftime("%H:%M:%S")] = epoch_data self.session.update(data={'data': self.stats}) @staticmethod def extract_key_values(data, subkeys): result = dict() result['values'] = list() result['labels'] = subkeys for plot_key in subkeys: v = [[ts, d[plot_key]] for ts, d in data.items()] result['values'].append(v) return result def on_webhook(self, path, request): if not path or path == "/": return render_template_string(TEMPLATE) session_param = request.args.get('session') if path == "os": extract_keys = [ 'cpu_load', 'mem_usage', ] elif path == "temp": extract_keys = ['temperature'] elif path == "wifi": extract_keys = [ 'missed_interactions', 'num_hops', 'num_peers', 'tot_bond', 'avg_bond', 'num_deauths', 'num_associations', 'num_handshakes', ] elif path == "duration": extract_keys = [ 'duration_secs', 'slept_for_secs', ] elif path == "reward": extract_keys = [ 'reward', ] elif path == "epoch": extract_keys = [ 'active_for_epochs', ] elif path == "session": return jsonify( {'files': os.listdir(self.options['save_directory'])}) with self.lock: data = self.stats if session_param and session_param != 'Current': file_stats = StatusFile(os.path.join( self.options['save_directory'], session_param), data_format='json') data = file_stats.data_field_or('data', default=dict()) return jsonify(SessionStats.extract_key_values(data, extract_keys))
class OnlineHashCrack(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.0.1' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com' def __init__(self): self.ready = False try: self.report = StatusFile('/root/.ohc_uploads', data_format='json') except JSONDecodeError: os.remove('/root/.ohc_uploads') self.report = StatusFile('/root/.ohc_uploads', data_format='json') self.skip = list() self.lock = Lock() def on_loaded(self): """ Gets called when the plugin gets loaded """ if 'email' not in self.options or ('email' in self.options and not self.options['email']): logging.error( "OHC: Email isn't set. Can't upload to onlinehashcrack.com") return if 'whitelist' not in self.options: self.options['whitelist'] = list() self.ready = True logging.info("OHC: OnlineHashCrack plugin loaded.") def _upload_to_ohc(self, path, timeout=30): """ Uploads the file to onlinehashcrack.com """ with open(path, 'rb') as file_to_upload: data = {'email': self.options['email']} payload = {'file': file_to_upload} try: result = requests.post('https://api.onlinehashcrack.com', data=data, files=payload, timeout=timeout) if 'already been sent' in result.text: logging.warning(f"{path} was already uploaded.") except requests.exceptions.RequestException as e: logging.error( f"OHC: Got an exception while uploading {path} -> {e}") raise e def _download_cracked(self, save_file, timeout=120): """ Downloads the cracked passwords and saves them returns the number of downloaded passwords """ try: s = requests.Session() dashboard = s.get(self.options['dashboard'], timeout=timeout) result = s.get('https://www.onlinehashcrack.com/wpa-exportcsv', timeout=timeout) result.raise_for_status() with open(save_file, 'wb') as output_file: output_file.write(result.content) except requests.exceptions.RequestException as req_e: raise req_e except OSError as os_e: raise os_e def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ if not self.ready or self.lock.locked(): return with self.lock: display = agent.view() config = agent.config() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] # pull out whitelisted APs handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist']) handshake_new = set(handshake_paths) - set(reported) - set( self.skip) if handshake_new: logging.info( "OHC: Internet connectivity detected. Uploading new handshakes to onlinehashcrack.com" ) for idx, handshake in enumerate(handshake_new): display.set( 'status', f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: self._upload_to_ohc(handshake) if handshake not in reported: reported.append(handshake) self.report.update(data={'reported': reported}) logging.info( f"OHC: Successfully uploaded {handshake}") except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("OHC: %s", req_e) continue except OSError as os_e: self.skip.append(handshake) logging.error("OHC: %s", os_e) continue if 'dashboard' in self.options and self.options['dashboard']: cracked_file = os.path.join(handshake_dir, 'onlinehashcrack.cracked') if os.path.exists(cracked_file): last_check = datetime.fromtimestamp( os.path.getmtime(cracked_file)) if last_check is not None and ( (datetime.now() - last_check).seconds / (60 * 60)) < 1: return try: self._download_cracked(cracked_file) logging.info("OHC: Downloaded cracked passwords.") except requests.exceptions.RequestException as req_e: logging.debug("OHC: %s", req_e) except OSError as os_e: logging.debug("OHC: %s", os_e) if 'single_files' in self.options and self.options[ 'single_files']: with open(cracked_file, 'r') as cracked_list: for row in csv.DictReader(cracked_list): if row['password']: filename = re.sub( r'[^a-zA-Z0-9]', '', row['ESSID']) + '_' + row['BSSID'].replace( ':', '') if os.path.exists( os.path.join(handshake_dir, filename + '.pcap')): with open( os.path.join( handshake_dir, filename + '.pcap.cracked'), 'w') as f: f.write(row['password'])
class WpaSec(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.0.1' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org' def __init__(self): self.ready = False self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json') self.options = dict() self.skip = list() def _upload_to_wpasec(self, path, timeout=30): """ Uploads the file to https://wpa-sec.stanev.org, or another endpoint. """ with open(path, 'rb') as file_to_upload: cookie = {'key': self.options['api_key']} payload = {'file': file_to_upload} try: result = requests.post(self.options['api_url'], cookies=cookie, files=payload, timeout=timeout) if ' already submitted' in result.text: logging.warning("%s was already submitted.", path) except requests.exceptions.RequestException as req_e: raise req_e def on_loaded(self): """ Gets called when the plugin gets loaded """ if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None): logging.error( "WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org" ) return if 'api_url' not in self.options or ('api_url' in self.options and self.options['api_url'] is None): logging.error( "WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured." ) return self.ready = True def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ if self.ready: config = agent.config() display = agent.view() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] handshake_new = set(handshake_paths) - set(reported) - set( self.skip) if handshake_new: logging.info( "WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org" ) for idx, handshake in enumerate(handshake_new): display.set( 'status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: self._upload_to_wpasec(handshake) reported.append(handshake) self.report.update(data={'reported': reported}) logging.info("WPA_SEC: Successfully uploaded %s", handshake) except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("WPA_SEC: %s", req_e) continue except OSError as os_e: logging.error("WPA_SEC: %s", os_e) continue
class nextcloud(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '0.0.1' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to a nextcloud webdav endpoint' def __init__(self): self.ready = False self.lock = threading.Lock() try: self.report = StatusFile('/root/.nextcloud_uploads', data_format='json') except JSONDecodeError as json_err: os.remove("/root/.nextcloud_uploads") self.report = StatusFile('/root/.nextcloud_uploads', data_format='json') self.options = dict() self.skip = list() self.session = None self.full_url = None def _make_path(self, dir): return f"{self.options['baseurl']}/remote.php/dav/files/{self.options['user']}/{dir}" def _make_session(self): s = requests.Session() s.auth = (self.options["user"], self.options["pass"]) logging.info("[nextcloud] >> send first req!") # check if creds correct try: r = s.request("PROPFIND", self._make_path("./")) logging.info("[nextcloud] >> send first req 2!") if r.status_code == 401: logging.info("[nextcloud] >> wrong creds!") return False elif r.status_code == 404: logging.info("[nextcloud] >> path does not exist!") return False self.session = s except requests.exceptions.RequestException as e: logging.error("nextcloud: Got an exception checking credentials!") raise e def _make_dir(self, path): try: r = self.session.request("MKCOL", path) if r.status_code == 201: print("created new directory") return True else: return False except requests.exceptions.RequestException as e: logging.error("nextcloud: Got an exception while creating a dir.") raise e def _exists_dir(self, path): try: r = self.session.request("PROPFIND", path) if r.status_code == 404: return False else: return True except requests.exceptions.RequestException as e: logging.error( "nextcloud: Got an exception while checking if a dir exists.") raise e def _upload_to_nextcloud(self, path, timeout=30): head, tail = os.path.split(path) destFile = self.full_url + '/' + tail with open(path, 'rb') as fp: try: r = self.session.put(destFile, data=fp.read()) except requests.exceptions.RequestException as e: logging.error( f"nextcloud: Got an exception while uploading {path} -> {e}" ) raise e def on_loaded(self): for opt in ['baseurl', 'user', 'pass', 'path']: if opt not in self.options or (opt in self.options and self.options[opt] is None): logging.error(f"NEXTCLOUD: Option {opt} is not set.") return self.ready = True logging.info("NEXTCLOUD: Successfully loaded.") def on_internet_available(self, agent): with self.lock: if self.ready: config = agent.config() display = agent.view() reported = self.report.data_field_or('reported', default=dict()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] handshake_new = set(handshake_paths) - set(reported) - set( self.skip) # filter for new files handshake_new = [] for hs in set(handshake_paths) - set(self.skip): if hs not in reported: handshake_new.append(hs) else: if os.path.getmtime(hs) > reported[hs]: handshake_new.append(hs) logging.info( f"[nx]: found {len(handshake_new)} new handshakes") if handshake_new: logging.info( "nextcloud: Internet connectivity detected. Uploading new handshakes" ) logging.info("[nextcloud] create session START") self._make_session() logging.info("[nextcloud] check for dir Half") self.full_url = self._make_path(f"{self.options['path']}/") logging.info("[nextcloud] create session DONE") logging.info("[nextcloud] check for dir START") if not self._exists_dir(self.full_url): if not self._make_dir(self.full_url): logging.info("[nextcloud] check for dir Fail") logging.error( "nextcloud: couldn't create necessary directory" ) return logging.info("[nextcloud] check for dir DONE") for idx, handshake in enumerate(handshake_new): logging.info( f"[nextcloud] uploading hs {handshake}, nr {idx + 1} out of {len(handshake_new)}" ) display.set( 'status', f"Uploading handshake to nextcloud ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: logging.info("[nx] upload 1") self._upload_to_nextcloud(handshake) logging.info("[nx] upload 2") reported[handshake] = os.path.getmtime(handshake) logging.info("[nx] upload 3") self.report.update(data={'reported': reported}) logging.info("nextcloud: Successfully uploaded %s", handshake) except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("nextcloud: %s", req_e) continue except OSError as os_e: logging.error("nextcloud: %s", os_e) continue
class AutoBackup(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '1.0.0' __license__ = 'GPL3' __description__ = 'This plugin backups files when internet is available.' def __init__(self): self.ready = False self.tries = 0 self.status = StatusFile('/root/.auto_backup') def on_loaded(self): for opt in ['files', 'interval', 'commands', 'max_tries']: if opt not in self.options or (opt in self.options and self.options[opt] is None): logging.error(f"[auto_backup] Option {opt} is not set.") return self.ready = True logging.info("[auto_backup] Successfully loaded.") def on_internet_available(self, agent): if not self.ready: return if self.options[ 'max_tries'] and self.tries >= self.options['max_tries']: return if self.status.newer_then_days(self.options['interval']): return # Only backup existing files to prevent errors existing_files = list( filter(lambda f: os.path.exists(f), self.options['files'])) files_to_backup = " ".join(existing_files) try: display = agent.view() logging.info("[auto_backup] Backing up...") display.set('status', 'Backing up...') display.update() for cmd in self.options['commands']: logging.info( f"[auto_backup] Running {cmd.format(files=files_to_backup)}" ) process = subprocess.Popen(cmd.format(files=files_to_backup), shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash") process.wait() if process.returncode > 0: raise OSError(f"Command failed (rc: {process.returncode})") logging.info("[auto_backup] Backup done.") display.set('status', 'Backup done!') display.update() self.status.update() except OSError as os_e: self.tries += 1 logging.info(f"[auto_backup] Error: {os_e}") display.set('status', 'Backup failed!') display.update()
class AutoUpdate(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.0.0' __name__ = 'auto-update' __license__ = 'GPL3' __description__ = 'This plugin checks when updates are available and applies them when internet is available.' __defaults__ = { 'enabled': True, 'install': True, 'interval': 1, } def __init__(self): self.ready = False self.status = StatusFile('/root/.auto-update', data_format='json') self.lock = Lock() self.done_caplets_check = False # only check once def on_loaded(self): self.ready = True logging.info("[auto-update] plugin loaded.") def on_internet_available(self, agent): if self.lock.locked(): return with self.lock: logging.debug("[auto-update] internet connectivity is available (ready %s)" % self.ready) if not self.ready: return if self.status.newer_then_hours(self.options['interval']): logging.debug("[auto-update] last check happened less than %d hours ago" % self.options['interval']) return logging.info("[auto-update] checking for updates ...") config = agent.config() display = agent.view() prev_status = display.get('status') try: display.update(force=True, new_data={'status': 'Checking for updates ...'}) to_install = [] to_check = [ ('dadav/bettercap', parse_version('bettercap -version'), True, 'bettercap'), ('dadav/pwnagotchi', pwnagotchi.__version__, False, 'pwnagotchi') ] for repo, local_version, is_native, svc_name in to_check: info = check(local_version, repo, is_native) if info['url'] is not None: logging.warning( "[auto-update] for %s available (local version is '%s'): %s" % ( repo, info['current'], info['url'])) info['service'] = svc_name to_install.append(info) num_updates = len(to_install) num_installed = 0 if num_updates > 0: if self.options['install']: for update in to_install: plugins.on('updating') if install(display, update): num_installed += 1 else: prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '') if not self.done_caplets_check: prev_commit = self.status.data_field_or('caplets_version', '') try: logging.info('[auto-update] Checking for new caplets.') current_commit = fetch_last_commit('bettercap/caplets') if prev_commit != current_commit: logging.info('[auto-update] Updating caplets.') rc = os.system('bettercap -eval "caplets.update;q"') if rc == 0: self.status.update(data={'caplets_version': current_commit}) except Exception as ex: logging.error("[auto-update] %s", ex) finally: self.done_caplets_check = True # update plugins logging.info("[auto-update] Checking for new plugins") if plugin_update(config) == 0: logging.info("[auto-update] Upgrading plugins") plugin_upgrade(None, config) logging.info("[auto-update] done") self.status.update(data=self.status.data) if num_installed > 0: display.update(force=True, new_data={'status': 'Rebooting ...'}) pwnagotchi.reboot() except Exception as e: logging.error("[auto-update] %s", e) display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
class dropbox(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '0.0.1' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to a dropbox app' def __init__(self): self.ready = False self.lock = threading.Lock() try: self.report = StatusFile('/root/.dropbox_ul_uploads', data_format='json') except JSONDecodeError as json_err: os.remove("/root/.dropbox_ul_uploads") self.report = StatusFile('/root/.dropbox_ul_uploads', data_format='json') self.options = dict() self.skip = list() def _upload_to_dropbox(self, path, timeout=30): """ Uploads the file to dropbox """ head, tail = os.path.split(path) destFile = self.options['path'] + '/' + tail dbOpts = { 'path': destFile, 'mode': 'add', 'autorename': True, 'mute': False, 'strict_conflict': False } headers = { 'Authorization': 'Bearer ' + self.options['app_token'], 'Dropbox-API-Arg': json.dumps(dbOpts), 'Content-Type': 'application/octet-stream', } data = open(path, 'rb').read() try: response = requests.post( 'https://content.dropboxapi.com/2/files/upload', headers=headers, data=data) logging.error(response) except requests.exceptions.RequestException as e: logging.error( f"OHC: Got an exception while uploading {path} -> {e}") raise e def on_loaded(self): """ Gets called when the plugin gets loaded """ if 'app_token' not in self.options or ( 'app_token' in self.options and self.options['app_token'] is None): logging.error("dropbox_ul: APP-TOKEN isn't set.") return logging.info(" [dropbox_ul] plugin loaded") self.ready = True def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ with self.lock: if self.ready: config = agent.config() display = agent.view() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] handshake_new = set(handshake_paths) - set(reported) - set( self.skip) if handshake_new: logging.info( "dropbox_ul: Internet connectivity detected. Uploading new handshakes" ) for idx, handshake in enumerate(handshake_new): display.set( 'status', f"Uploading handshake to dropbox ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: self._upload_to_dropbox(handshake) reported.append(handshake) self.report.update(data={'reported': reported}) logging.info( "dropbox_ul: Successfully uploaded %s", handshake) except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("dropbox_ul: %s", req_e) continue except OSError as os_e: logging.error("dropbox_ul: %s", os_e) continue
class OnlineHashCrack(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.0.1' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com' def __init__(self): self.ready = False try: self.report = StatusFile('/root/.ohc_uploads', data_format='json') except JSONDecodeError as json_err: os.remove('/root/.ohc_uploads') self.report = StatusFile('/root/.ohc_uploads', data_format='json') self.skip = list() self.lock = Lock() def on_loaded(self): """ Gets called when the plugin gets loaded """ if 'email' not in self.options or ('email' in self.options and not self.options['email']): logging.error( "OHC: Email isn't set. Can't upload to onlinehashcrack.com") return if 'whitelist' not in self.options: self.options['whitelist'] = [] # remove special characters from whitelist APs to match on-disk format self.options['whitelist'] = set( map(lambda x: re.sub(r'[^a-zA-Z0-9]', '', x), self.options['whitelist'])) self.ready = True def _filter_handshake_file(self, handshake_filename): try: basename = os.path.basename(handshake_filename) ssid, bssid = basename.split('_') # remove the ".pcap" from the bssid (which is really just the end of the filename) bssid = bssid[:-5] except: # something failed in our parsing of the filename. let the file through return True return ssid not in self.options[ 'whitelist'] and bssid not in self.options['whitelist'] def _upload_to_ohc(self, path, timeout=30): """ Uploads the file to onlinehashcrack.com """ with open(path, 'rb') as file_to_upload: data = {'email': self.options['email']} payload = {'file': file_to_upload} try: result = requests.post('https://api.onlinehashcrack.com', data=data, files=payload, timeout=timeout) if 'already been sent' in result.text: logging.warning(f"{path} was already uploaded.") except requests.exceptions.RequestException as e: logging.error( f"OHC: Got an exception while uploading {path} -> {e}") raise e def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ with self.lock: if self.ready: display = agent.view() config = agent.config() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] handshake_filenames = os.listdir(handshake_dir) handshake_paths = [ os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap') ] # pull out whitelisted APs handshake_paths = filter( lambda path: self._filter_handshake_file(path), handshake_paths) handshake_new = set(handshake_paths) - set(reported) - set( self.skip) if handshake_new: logging.info( "OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com" ) for idx, handshake in enumerate(handshake_new): display.set( 'status', f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})" ) display.update(force=True) try: self._upload_to_ohc(handshake) if handshake not in reported: reported.append(handshake) self.report.update(data={'reported': reported}) logging.info( f"OHC: Successfully uploaded {handshake}") except requests.exceptions.RequestException as req_e: self.skip.append(handshake) logging.error("OHC: %s", req_e) continue except OSError as os_e: self.skip.append(handshake) logging.error("OHC: %s", os_e) continue
class AutoUpdate(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '1.1.1' __name__ = 'auto-update' __license__ = 'GPL3' __description__ = 'This plugin checks when updates are available and applies them when internet is available.' def __init__(self): self.ready = False self.status = StatusFile('/root/.auto-update') self.lock = Lock() def on_loaded(self): if 'interval' not in self.options or ( 'interval' in self.options and self.options['interval'] is None): logging.error( "[update] main.plugins.auto-update.interval is not set") return self.ready = True logging.info("[update] plugin loaded.") def on_internet_available(self, agent): with self.lock: logging.debug( "[update] internet connectivity is available (ready %s)" % self.ready) if not self.ready: return if self.status.newer_then_hours(self.options['interval']): logging.debug( "[update] last check happened less than %d hours ago" % self.options['interval']) return logging.info("[update] checking for updates ...") display = agent.view() prev_status = display.get('status') try: display.update(force=True, new_data={'status': 'Checking for updates ...'}) to_install = [] to_check = [ ('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'), ('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'), ('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi') ] for repo, local_version, is_native, svc_name in to_check: info = check(local_version, repo, is_native) if info['url'] is not None: logging.warning( "update for %s available (local version is '%s'): %s" % (repo, info['current'], info['url'])) info['service'] = svc_name to_install.append(info) num_updates = len(to_install) num_installed = 0 if num_updates > 0: if self.options['install']: for update in to_install: plugins.on('updating') if install(display, update): num_installed += 1 else: prev_status = '%d new update%c available!' % ( num_updates, 's' if num_updates > 1 else '') logging.info("[update] done") self.status.update() if num_installed > 0: display.update(force=True, new_data={'status': 'Rebooting ...'}) pwnagotchi.reboot() except Exception as e: logging.error("[update] %s" % e) display.update(force=True, new_data={ 'status': prev_status if prev_status is not None else '' })
class Wigle(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '2.0.0' __license__ = 'GPL3' __description__ = 'This plugin automatically uploads collected wifis to wigle.net' def __init__(self): self.ready = False self.report = StatusFile('/root/.wigle_uploads', data_format='json') self.skip = list() self.lock = Lock() def on_loaded(self): if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None): logging.debug( "[wigle] api_key isn't set, can't upload to wigle.net.") return if not 'whitelist' in self.options: self.options['whitelist'] = list() self.ready = True logging.info("[wigle] Plugin loaded.") def on_internet_available(self, agent): """ Called in manual mode when there's internet connectivity """ if not self.ready or self.lock.locked(): return from scapy.all import Scapy_Exception config = agent.config() display = agent.view() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] all_files = os.listdir(handshake_dir) all_gps_files = [ os.path.join(handshake_dir, filename) for filename in all_files if filename.endswith('.gps.json') ] all_gps_files = remove_whitelisted(all_gps_files, self.options['whitelist']) new_gps_files = set(all_gps_files) - set(reported) - set(self.skip) if new_gps_files: logging.info( "[wigle] Internet connectivity detected, Uploading new handshakes to wigle.net..." ) csv_entries = list() no_err_entries = list() for gps_file in new_gps_files: pcap_filename = gps_file.replace('.gps.json', '.pcap') if not os.path.exists(pcap_filename): logging.debug(f"[wigle] Can't find pcap for {gps_file}.") self.skip.append(gps_file) continue try: gps_data = _extract_gps_data(gps_file) except OSError as os_err: logging.debug(f"[wigle] {os_err}") self.skip.append(gps_file) continue except json.JSONDecodeError as json_err: logging.debug(f"[wigle]: {json_err}") self.skip.append(gps_file) continue if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0: logging.debug( f"[wigle] Not enough gps-information for {gps_file}. Trying again next time." ) self.skip.append(gps_file) continue try: pcap_data = extract_from_pcap(pcap_filename, [ WifiInfo.BSSID, WifiInfo.ESSID, WifiInfo.ENCRYPTION, WifiInfo.CHANNEL, WifiInfo.RSSI ]) except FieldNotFoundError: logging.debug( f"[wigle] Could not extract all information. Skip {gps_file}." ) self.skip.append(gps_file) continue except Scapy_Exception as sc_e: logging.debug(f"[wigle]: {sc_e}") self.skip.append(gps_file) continue new_entry = _transform_wigle_entry(gps_data, pcap_data) csv_entries.append(new_entry) no_err_entries.append(gps_file) if csv_entries: display.set('status', "Uploading gps-data to wigle.net...") display.update(force=True) try: _send_to_wigle(csv_entries, self.options['api_key']) reported += no_err_entries self.report.update(data={'reported': reported}) logging.info( f"[wigle] Successfully uploaded {len(no_err_entries)} files." ) except requests.exceptions.RequestException as re_e: self.skip += no_err_entries logging.debug( f"[wigle] Got an exception while uploading {re_e}") except OSError as os_e: self.skip += no_err_entries logging.debug(f"[wigle] Got the following error: {os_e}")
class NetPos(plugins.Plugin): __author__ = 'zenzen san' __version__ = '3.0.0' __license__ = 'GPL3' __description__ = """Saves a json file with the access points with more signal whenever a handshake is captured. When internet is available the files are converted in geo locations using Mozilla LocationService """ __defaults__ = { 'enabled': False, 'api_key': 'test', 'api_url': 'https://location.services.mozilla.com/v1/geolocate?key={api}', } def __init__(self): self.report = StatusFile('/root/.net_pos_saved', data_format='json') self.skip = list() self.ready = False self.lock = threading.Lock() self.shutdown = False def on_before_shutdown(self): self.shutdown = True def on_loaded(self): if 'api_key' not in self.options or ('api_key' in self.options and not self.options['api_key']): logging.error( '[net-pos] api_key isn\'t set. Can\'t use mozilla\'s api.') return self.ready = True logging.info('[net-pos] plugin loaded.') logging.debug(f"[net-pos] use api_url: {self.options['api_url']}") def _append_saved(self, path): to_save = list() if isinstance(path, str): to_save.append(path) elif isinstance(path, list): to_save += path else: raise TypeError("Expected list or str, got %s" % type(path)) with open('/root/.net_pos_saved', 'a') as saved_file: for x in to_save: saved_file.write(x + "\n") def on_internet_available(self, agent): if not self.ready or self.lock.locked() or self.shutdown: return with self.lock: config = agent.config() display = agent.view() reported = self.report.data_field_or('reported', default=list()) handshake_dir = config['bettercap']['handshakes'] all_files = os.listdir(handshake_dir) all_np_files = [ os.path.join(handshake_dir, filename) for filename in all_files if filename.endswith('.net-pos.json') ] new_np_files = set(all_np_files) - set(reported) - set(self.skip) if new_np_files: logging.debug( '[net-pos] Found %d new net-pos files. Fetching positions ...', len(new_np_files)) display.set( 'status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ..." ) display.update(force=True) for idx, np_file in enumerate(new_np_files): if self.shutdown: return geo_file = np_file.replace('.net-pos.json', '.geo.json') if os.path.exists(geo_file): # got already the position reported.append(np_file) self.report.update(data={'reported': reported}) continue try: geo_data = self._get_geo_data( np_file) # returns json obj except requests.exceptions.RequestException as req_e: logging.error('[net-pos] %s - RequestException: %s', np_file, req_e) self.skip += np_file continue except json.JSONDecodeError as js_e: logging.error( '[net-pos] %s - JSONDecodeError: %s, removing it...', np_file, js_e) os.remove(np_file) continue except OSError as os_e: logging.error('[net-pos] %s - OSError: %s', np_file, os_e) self.skip += np_file continue with open(geo_file, 'w+t') as sf: json.dump(geo_data, sf) reported.append(np_file) self.report.update(data={'reported': reported}) display.set( 'status', f"Fetching positions ({idx + 1}/{len(new_np_files)})") display.update(force=True) def on_handshake(self, agent, filename, access_point, client_station): netpos = self._get_netpos(agent) if not netpos['wifiAccessPoints']: return netpos["ts"] = int("%.0f" % time.time()) netpos_filename = filename.replace('.pcap', '.net-pos.json') logging.debug('[net-pos] Saving net-location to %s', netpos_filename) try: with open(netpos_filename, 'w+t') as net_pos_file: json.dump(netpos, net_pos_file) except OSError as os_e: logging.error('[net-pos] %s', os_e) def _get_netpos(self, agent): aps = agent.get_access_points() netpos = dict() netpos['wifiAccessPoints'] = list() # 6 seems a good number to save a wifi networks location for access_point in sorted(aps, key=lambda i: i['rssi'], reverse=True)[:6]: netpos['wifiAccessPoints'].append({ 'macAddress': access_point['mac'], 'signalStrength': access_point['rssi'] }) return netpos def _get_geo_data(self, path, timeout=30): geourl = self.options['api_url'].format(api=self.options['api_key']) try: with open(path, "r") as json_file: data = json.load(json_file) except json.JSONDecodeError as js_e: raise js_e except OSError as os_e: raise os_e try: result = requests.post(geourl, json=data, timeout=timeout) return_geo = result.json() if data["ts"]: return_geo["ts"] = data["ts"] return return_geo except requests.exceptions.RequestException as req_e: raise req_e
class GitBackup(plugins.Plugin): __author__ = '*****@*****.**' __version__ = '1.0.0' __license__ = 'GPL3' __description__ = 'git_backup' def __init__(self): self.ready = False self.tries = 0 logging.debug("GIT BACKUP") self.status = StatusFile('/root/.git_backup') # called when http://<host>:<port>/plugins/<plugin>/ is called # must return a html page # IMPORTANT: If you use "POST"s, add a csrf-token (via csrf_token() and render_template_string) def on_webhook(self, path, request): pass # called when the plugin is loaded def on_loaded(self): self.ready = True logging.info("GIT-BACKUP: Successfully loaded.") # called before the plugin is unloaded def on_unload(self, ui): pass # called hen there's internet connectivity def on_internet_available(self, agent): if not self.ready: return if self.tries >= 2: return if self.status.newer_then_days(self.options['interval']): return try: f = open("/home/pi/git_err.txt", "a+") display = agent.view() logging.info("GITBACKUP: Backing up ...") display.set('status', 'Backing up ...') display.update() logging.info(f"GIT-BACKUP: Running") process = subprocess.Popen([ 'sudo cp -r /root/brain.nn /root/brain.json /root/.api-report.json /root/handshakes/ /root/peers/ /etc/pwnagotchi/ /var/log/pwnagotchi.log /root/Pwnagotchi/ && cd /root/Pwnagotchi/ && sudo git pull && sudo git add . && sudo git commit -am "backup" && sudo git push' ], shell=True, stderr=f, stdin=f) process.wait() f.close() logging.info("GIT-BACKUP: backup done") display.set('status', 'GIT-Backup done!') display.update() self.status.update() except OSError as os_e: self.tries += 1 logging.info(f"GIT-BACKUP: Error: {os_e}") display.set('status', 'Backup failed!') display.update() # called to setup the ui elements def on_ui_setup(self, ui): pass # called when the ui is updated def on_ui_update(self, ui): pass # update those elements # called when the hardware display setup is done, display is an hardware specific object def on_display_setup(self, display): pass # called when everything is ready and the main loop is about to start def on_ready(self, agent): logging.info("unit is ready") # you can run custom bettercap commands if you want # agent.run('ble.recon on') # or set a custom state # agent.set_bored() # called when the AI finished loading def on_ai_ready(self, agent): pass # called when the AI finds a new set of parameters def on_ai_policy(self, agent, policy): pass # called when the AI starts training for a given number of epochs def on_ai_training_start(self, agent, epochs): pass # called after the AI completed a training epoch def on_ai_training_step(self, agent, _locals, _globals): pass # called when the AI has done training def on_ai_training_end(self, agent): pass # called when the AI got the best reward so far def on_ai_best_reward(self, agent, reward): pass # called when the AI got the worst reward so far def on_ai_worst_reward(self, agent, reward): pass # called when a non overlapping wifi channel is found to be free def on_free_channel(self, agent, channel): pass # called when the status is set to bored def on_bored(self, agent): pass # called when the status is set to sad def on_sad(self, agent): pass # called when the status is set to excited def on_excited(self, agent): pass # called when the status is set to lonely def on_lonely(self, agent): pass # called when the agent is rebooting the board def on_rebooting(self, agent): pass # called when the agent is waiting for t seconds def on_wait(self, agent, t): pass # called when the agent is sleeping for t seconds def on_sleep(self, agent, t): pass # called when the agent refreshed its access points list def on_wifi_update(self, agent, access_points): pass # called when the agent refreshed an unfiltered access point list # this list contains all access points that were detected BEFORE filtering def on_unfiltered_ap_list(self, agent, access_points): pass # called when the agent is sending an association frame def on_association(self, agent, access_point): pass # called when the agent is deauthenticating a client station from an AP def on_deauthentication(self, agent, access_point, client_station): pass # callend when the agent is tuning on a specific channel def on_channel_hop(self, agent, channel): pass # called when a new handshake is captured, access_point and client_station are json objects # if the agent could match the BSSIDs to the current list, otherwise they are just the strings of the BSSIDs def on_handshake(self, agent, filename, access_point, client_station): pass # called when an epoch is over (where an epoch is a single loop of the main algorithm) def on_epoch(self, agent, epoch, epoch_data): pass # called when a new peer is detected def on_peer_detected(self, agent, peer): pass # called when a known peer is lost def on_peer_lost(self, agent, peer): pass