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

View File

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

View File

@ -4,39 +4,42 @@ __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']
try:
logging.info("api: signign enrollment request ...")
if AUTH.data is None:
logging.info("api: enrolling unit ...")
else:
logging.info("api: refreshing token ...")
identity = "%s@%s" % (pwnagotchi.name(), keypair.fingerprint)
identity = "%s@%s" % (pwnagotchi.name(), keys.fingerprint)
# sign the identity string to prove we own both keys
_, signature_b64 = keypair.sign(identity)
_, signature_b64 = keys.sign(identity)
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll'
enroll = {
enrollment = {
'identity': identity,
'public_key': keypair.pub_key_pem_b64,
'public_key': keys.pub_key_pem_b64,
'signature': signature_b64,
'data': {
'duration': log.duration,
@ -53,14 +56,103 @@ def on_internet_available(ui, keypair, config, log):
}
}
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))
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:
from scapy.all import rdpcap
info = {}
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
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.error("error %d: %s" % (r.status_code, r.json()))
logging.info("api: reporting disabled")
except Exception as e:
logging.exception("error while enrolling the unit")

View File

@ -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)