new: api plugin will report pwned access points

This commit is contained in:
Simone Margaritelli 2019-10-07 13:06:29 +02:00
parent da52bcd705
commit d6efc0b70d
3 changed files with 154 additions and 40 deletions
pwnagotchi

@ -8,6 +8,7 @@ main:
plugins: plugins:
api: api:
enabled: false enabled: false
report: true # report pwned networks
auto-update: auto-update:
enabled: false enabled: false
interval: 1 # every day interval: 1 # every day

@ -4,63 +4,155 @@ __name__ = 'api'
__license__ = 'GPL3' __license__ = 'GPL3'
__description__ = 'This plugin signals the unit cryptographic identity to api.pwnagotchi.ai' __description__ = 'This plugin signals the unit cryptographic identity to api.pwnagotchi.ai'
import os
import logging import logging
import json
import requests import requests
import glob
import subprocess import subprocess
import pwnagotchi import pwnagotchi
from pwnagotchi.utils import StatusFile import pwnagotchi.utils as utils
OPTIONS = dict() OPTIONS = dict()
READY = False AUTH = utils.StatusFile('/root/.api-enrollment.json', data_format='json')
STATUS = StatusFile('/root/.api-enrollment.json') REPORT = utils.StatusFile('/root/.api-report.json', data_format='json')
def on_loaded(): def on_loaded():
logging.info("api plugin loaded.") logging.info("api plugin loaded.")
def on_internet_available(ui, keypair, config, log): def get_api_token(log, keys):
global STATUS global AUTH
if STATUS.newer_then_minutes(25): if AUTH.newer_then_minutes(25) and AUTH.data is not None and 'token' in AUTH.data:
return 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: try:
logging.info("api: signign enrollment request ...") from scapy.all import rdpcap
identity = "%s@%s" % (pwnagotchi.name(), keypair.fingerprint) info = {}
# sign the identity string to prove we own both keys
_, signature_b64 = keypair.sign(identity)
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll' for pkt in rdpcap(filename):
enroll = { info = parse_packet(pkt, info)
'identity': identity,
'public_key': keypair.pub_key_pem_b64, bssid = info['bssid'] if 'bssid' in info else None
'signature': signature_b64, essid = info['essid'] if 'essid' in info else None
'data': {
'duration': log.duration, except Exception as e:
'epochs': log.epochs, bssid = None
'train_epochs': log.train_epochs, logging.error("api: %s" % e)
'avg_reward': log.avg_reward,
'min_reward': log.min_reward, return essid, bssid
'max_reward': log.max_reward,
'deauthed': log.deauthed,
'associated': log.associated, def api_report_ap(token, essid, bssid):
'handshakes': log.handshakes, logging.info("api: reporting %s (%s)" % (essid, bssid))
'peers': log.peers,
'uname': subprocess.getoutput("uname -a") 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) return True
r = requests.post(api_address, json=enroll)
if r.status_code == 200:
token = r.json() def on_internet_available(ui, keys, config, log):
logging.info("api: enrolled") global REPORT
STATUS.update(data=json.dumps(token))
else: try:
logging.error("error %d: %s" % (r.status_code, r.json()))
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: except Exception as e:
logging.exception("error while enrolling the unit") logging.exception("error while enrolling the unit")

@ -5,6 +5,7 @@ import os
import time import time
import subprocess import subprocess
import yaml import yaml
import json
# https://stackoverflow.com/questions/823196/yaml-merge-in-python # https://stackoverflow.com/questions/823196/yaml-merge-in-python
@ -82,12 +83,24 @@ def blink(times=1, delay=0.3):
class StatusFile(object): class StatusFile(object):
def __init__(self, path): def __init__(self, path, data_format='raw'):
self._path = path self._path = path
self._updated = None self._updated = None
self._format = data_format
self.data = None
if os.path.exists(path): if os.path.exists(path):
self._updated = datetime.fromtimestamp(os.path.getmtime(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): def newer_then_minutes(self, minutes):
return self._updated is not None and ((datetime.now() - self._updated).seconds / 60) < 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): def update(self, data=None):
self._updated = datetime.now() self._updated = datetime.now()
self.data = data
with open(self._path, 'w') as fp: 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)