From d6efc0b70d215907f3a54ddc7a8a2c58b7efc9bf Mon Sep 17 00:00:00 2001 From: Simone Margaritelli Date: Mon, 7 Oct 2019 13:06:29 +0200 Subject: [PATCH] new: api plugin will report pwned access points --- pwnagotchi/defaults.yml | 1 + pwnagotchi/plugins/default/api.py | 168 +++++++++++++++++++++++------- pwnagotchi/utils.py | 25 ++++- 3 files changed, 154 insertions(+), 40 deletions(-) diff --git a/pwnagotchi/defaults.yml b/pwnagotchi/defaults.yml index dff6b3bf..e9b45b64 100644 --- a/pwnagotchi/defaults.yml +++ b/pwnagotchi/defaults.yml @@ -8,6 +8,7 @@ main: plugins: api: enabled: false + report: true # report pwned networks auto-update: enabled: false interval: 1 # every day diff --git a/pwnagotchi/plugins/default/api.py b/pwnagotchi/plugins/default/api.py index 60792af4..eae93432 100644 --- a/pwnagotchi/plugins/default/api.py +++ b/pwnagotchi/plugins/default/api.py @@ -4,63 +4,155 @@ __name__ = 'api' __license__ = 'GPL3' __description__ = 'This plugin signals the unit cryptographic identity to api.pwnagotchi.ai' +import os import logging -import json import requests +import glob import subprocess import pwnagotchi -from pwnagotchi.utils import StatusFile +import pwnagotchi.utils as utils OPTIONS = dict() -READY = False -STATUS = StatusFile('/root/.api-enrollment.json') +AUTH = utils.StatusFile('/root/.api-enrollment.json', data_format='json') +REPORT = utils.StatusFile('/root/.api-report.json', data_format='json') def on_loaded(): logging.info("api plugin loaded.") -def on_internet_available(ui, keypair, config, log): - global STATUS +def get_api_token(log, keys): + global AUTH - if STATUS.newer_then_minutes(25): - return + if AUTH.newer_then_minutes(25) and AUTH.data is not None and 'token' in AUTH.data: + return AUTH.data['token'] + + if AUTH.data is None: + logging.info("api: enrolling unit ...") + else: + logging.info("api: refreshing token ...") + + identity = "%s@%s" % (pwnagotchi.name(), keys.fingerprint) + # sign the identity string to prove we own both keys + _, signature_b64 = keys.sign(identity) + + api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll' + enrollment = { + 'identity': identity, + 'public_key': keys.pub_key_pem_b64, + 'signature': signature_b64, + 'data': { + 'duration': log.duration, + 'epochs': log.epochs, + 'train_epochs': log.train_epochs, + 'avg_reward': log.avg_reward, + 'min_reward': log.min_reward, + 'max_reward': log.max_reward, + 'deauthed': log.deauthed, + 'associated': log.associated, + 'handshakes': log.handshakes, + 'peers': log.peers, + 'uname': subprocess.getoutput("uname -a") + } + } + + r = requests.post(api_address, json=enrollment) + if r.status_code != 200: + raise Exception("(status %d) %s" % (r.status_code, r.json())) + + AUTH.update(data=r.json()) + + logging.info("api: done") + + return AUTH.data["token"] + + +def parse_packet(packet, info): + from scapy.all import Dot11Elt, Dot11Beacon, Dot11, Dot11ProbeResp, Dot11AssoReq, Dot11ReassoReq + + if packet.haslayer(Dot11Beacon): + if packet.haslayer(Dot11Beacon) \ + or packet.haslayer(Dot11ProbeResp) \ + or packet.haslayer(Dot11AssoReq) \ + or packet.haslayer(Dot11ReassoReq): + if 'bssid' not in info and hasattr(packet[Dot11], 'addr3'): + info['bssid'] = packet[Dot11].addr3 + if 'essid' not in info and hasattr(packet[Dot11Elt], 'info'): + info['essid'] = packet[Dot11Elt].info.decode('utf-8') + + return info + + +def parse_pcap(filename): + logging.info("api: parsing %s ..." % filename) + + essid = bssid = None try: - logging.info("api: signign enrollment request ...") + from scapy.all import rdpcap - identity = "%s@%s" % (pwnagotchi.name(), keypair.fingerprint) - # sign the identity string to prove we own both keys - _, signature_b64 = keypair.sign(identity) + info = {} - api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll' - enroll = { - 'identity': identity, - 'public_key': keypair.pub_key_pem_b64, - 'signature': signature_b64, - 'data': { - 'duration': log.duration, - 'epochs': log.epochs, - 'train_epochs': log.train_epochs, - 'avg_reward': log.avg_reward, - 'min_reward': log.min_reward, - 'max_reward': log.max_reward, - 'deauthed': log.deauthed, - 'associated': log.associated, - 'handshakes': log.handshakes, - 'peers': log.peers, - 'uname': subprocess.getoutput("uname -a") - } + for pkt in rdpcap(filename): + info = parse_packet(pkt, info) + + bssid = info['bssid'] if 'bssid' in info else None + essid = info['essid'] if 'essid' in info else None + + except Exception as e: + bssid = None + logging.error("api: %s" % e) + + return essid, bssid + + +def api_report_ap(token, essid, bssid): + logging.info("api: reporting %s (%s)" % (essid, bssid)) + + try: + api_address = 'https://api.pwnagotchi.ai/api/v1/unit/report/ap' + headers = {'Authorization': 'access_token %s' % token} + report = { + 'essid': essid, + 'bssid': bssid, } + r = requests.post(api_address, headers=headers, json=report) + if r.status_code != 200: + raise Exception("(status %d) %s" % (r.status_code, r.text)) + except Exception as e: + logging.error("api: %s" % e) + return False - logging.info("api: enrolling unit to %s ..." % api_address) - r = requests.post(api_address, json=enroll) - if r.status_code == 200: - token = r.json() - logging.info("api: enrolled") - STATUS.update(data=json.dumps(token)) - else: - logging.error("error %d: %s" % (r.status_code, r.json())) + return True + + +def on_internet_available(ui, keys, config, log): + global REPORT + + try: + + pcap_files = glob.glob(os.path.join(config['bettercap']['handshakes'], "*.pcap")) + num_networks = len(pcap_files) + reported = REPORT.data_field_or('reported', default=[]) + num_reported = len(reported) + num_new = num_networks - num_reported + + if num_new > 0: + logging.info("api: %d new networks to report" % num_new) + token = get_api_token(log, keys) + + if OPTIONS['report']: + for pcap_file in pcap_files: + net_id = os.path.basename(pcap_file).replace('.pcap', '') + if net_id not in reported: + essid, bssid = parse_pcap(pcap_file) + if bssid: + if api_report_ap(token, essid, bssid): + reported.append(net_id) + + REPORT.update(data={'reported': reported}) + else: + logging.info("api: reporting disabled") except Exception as e: logging.exception("error while enrolling the unit") diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 28346641..e6e15f53 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -5,6 +5,7 @@ import os import time import subprocess import yaml +import json # https://stackoverflow.com/questions/823196/yaml-merge-in-python @@ -82,12 +83,24 @@ def blink(times=1, delay=0.3): class StatusFile(object): - def __init__(self, path): + def __init__(self, path, data_format='raw'): self._path = path self._updated = None + self._format = data_format + self.data = None if os.path.exists(path): self._updated = datetime.fromtimestamp(os.path.getmtime(path)) + with open(path) as fp: + if data_format == 'json': + self.data = json.load(fp) + else: + self.data = fp.read() + + def data_field_or(self, name, default=""): + if self.data is not None and name in self.data: + return self.data[name] + return default def newer_then_minutes(self, minutes): return self._updated is not None and ((datetime.now() - self._updated).seconds / 60) < minutes @@ -97,5 +110,13 @@ class StatusFile(object): def update(self, data=None): self._updated = datetime.now() + self.data = data with open(self._path, 'w') as fp: - fp.write(str(self._updated) if data is None else data) + if data is None: + fp.write(str(self._updated)) + + elif self._format == 'json': + json.dump(self.data, fp) + + else: + fp.write(data)