new: api plugin will report pwned access points
This commit is contained in:
parent
da52bcd705
commit
d6efc0b70d
@ -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,39 +4,42 @@ __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']
|
||||||
|
|
||||||
try:
|
if AUTH.data is None:
|
||||||
logging.info("api: signign enrollment request ...")
|
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
|
# 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'
|
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll'
|
||||||
enroll = {
|
enrollment = {
|
||||||
'identity': identity,
|
'identity': identity,
|
||||||
'public_key': keypair.pub_key_pem_b64,
|
'public_key': keys.pub_key_pem_b64,
|
||||||
'signature': signature_b64,
|
'signature': signature_b64,
|
||||||
'data': {
|
'data': {
|
||||||
'duration': log.duration,
|
'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=enrollment)
|
||||||
r = requests.post(api_address, json=enroll)
|
if r.status_code != 200:
|
||||||
if r.status_code == 200:
|
raise Exception("(status %d) %s" % (r.status_code, r.json()))
|
||||||
token = r.json()
|
|
||||||
logging.info("api: enrolled")
|
AUTH.update(data=r.json())
|
||||||
STATUS.update(data=json.dumps(token))
|
|
||||||
|
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:
|
else:
|
||||||
logging.error("error %d: %s" % (r.status_code, r.json()))
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user