Skip to content

Commit 71c9428

Browse files
committed
Add support for WPA3 + OWE
1 parent 48bdc82 commit 71c9428

8 files changed

Lines changed: 263 additions & 82 deletions

File tree

wifite/args.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,22 @@ def _add_wpa_args(self, wpa):
312312
wpa.add_argument('--wpa',
313313
action='store_true',
314314
dest='wpa_filter',
315-
help=Color.s('Show only {C}WPA-encrypted networks{W} (includes {C}WPS{W})'))
315+
help=Color.s('Show only {C}WPA/WPA2-encrypted networks{W} (may include {C}WPS{W})'))
316316
wpa.add_argument('-wpa', help=argparse.SUPPRESS, action='store_true', dest='wpa_filter')
317317

318+
wpa.add_argument('--wpa3',
319+
action='store_true',
320+
dest='wpa3_filter',
321+
help=Color.s('Show only {C}WPA3-encrypted networks{W} (SAE/OWE)'))
322+
wpa.add_argument('-wpa3', help=argparse.SUPPRESS, action='store_true', dest='wpa3_filter')
323+
324+
wpa.add_argument('--owe',
325+
action='store_true',
326+
dest='owe_filter',
327+
help=Color.s('Show only {C}OWE-encrypted networks{W} (Enhanced Open)'))
328+
wpa.add_argument('-owe', help=argparse.SUPPRESS, action='store_true', dest='owe_filter')
329+
330+
318331
wpa.add_argument('--hs-dir',
319332
action='store',
320333
dest='wpa_handshake_dir',

wifite/attack/all.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,34 +65,53 @@ def attack_single(cls, target, targets_remaining):
6565
# TODO: EvilTwin attack
6666
pass
6767

68-
elif 'WEP' in target.encryption:
68+
elif target.primary_encryption == 'WEP':
6969
attacks.append(AttackWEP(target))
7070

71-
elif 'WPA' in target.encryption:
71+
elif target.primary_encryption.startswith('WPA'): # Covers WPA, WPA2, WPA3
7272
# WPA can have multiple attack vectors:
7373

7474
# WPS
75-
if not Configuration.use_pmkid_only and target.wps is WPSState.UNLOCKED and AttackWPS.can_attack_wps():
75+
# For WPA3, WPS is not applicable in the same way.
76+
# WPS is generally being phased out with WPA3, though some transition modes might exist.
77+
# We will only attempt WPS if it's explicitly WPA or WPA2 (not WPA3).
78+
if target.primary_encryption != 'WPA3' and \
79+
not Configuration.use_pmkid_only and \
80+
target.wps is WPSState.UNLOCKED and \
81+
AttackWPS.can_attack_wps():
82+
7683
# Pixie-Dust
7784
if Configuration.wps_pixie:
7885
attacks.append(AttackWPS(target, pixie_dust=True))
7986

8087
# Null PIN zero-day attack
81-
if Configuration.wps_pin:
88+
if Configuration.wps_pin: # This implies not wps_pixie_only
8289
attacks.append(AttackWPS(target, pixie_dust=False, null_pin=True))
8390

8491
# PIN attack
85-
if Configuration.wps_pin:
92+
if Configuration.wps_pin: # This implies not wps_pixie_only
8693
attacks.append(AttackWPS(target, pixie_dust=False))
8794

88-
if not Configuration.wps_only:
95+
# PMKID and Handshake attacks are applicable to WPA, WPA2, and WPA3
96+
if not Configuration.wps_only: # If --wps-only is not set
8997
# PMKID
90-
attacks.append(AttackPMKID(target))
98+
if not Configuration.dont_use_pmkid: # If --no-pmkid is not set
99+
attacks.append(AttackPMKID(target))
91100

92101
# Handshake capture
102+
if not Configuration.use_pmkid_only: # If --pmkid (means pmkid-only) is not set
103+
attacks.append(AttackWPA(target))
104+
elif target.primary_encryption == 'WPA3' and Configuration.wps_only:
105+
# Special case: If it's WPA3 and --wps-only is specified,
106+
# WPS attacks are skipped. We should still allow PMKID/Handshake for WPA3.
107+
Color.pl('{!} {O}Note: --wps-only is active, but target is WPA3. WPS attacks are not applicable.')
108+
Color.pl('{+} {C}Proceeding with PMKID and Handshake attacks for WPA3 target.{W}')
109+
if not Configuration.dont_use_pmkid:
110+
attacks.append(AttackPMKID(target))
93111
if not Configuration.use_pmkid_only:
94112
attacks.append(AttackWPA(target))
95113

114+
96115
if not attacks:
97116
Color.pl('{!} {R}Error: {O}Unable to attack: no attacks available')
98117
return True # Keep attacking other targets (skip)

wifite/attack/wpa.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# -*- coding: utf-8 -*-
33

44
from ..model.attack import Attack
5-
from ..tools.aircrack import Aircrack
5+
from ..tools.aircrack import Aircrack # Still used for other things like WEP
6+
from ..tools.hashcat import Hashcat
67
from ..tools.airodump import Airodump
78
from ..tools.aireplay import Aireplay
89
from ..config import Configuration
@@ -65,17 +66,29 @@ def run(self):
6566
self.success = False
6667
return False
6768

68-
Color.pl('\n{+} {C}Cracking WPA Handshake:{W} Running {C}aircrack-ng{W} with '
69-
'{C}%s{W} wordlist' % os.path.split(Configuration.wordlist)[-1])
69+
# Determine if the target is WPA3-SAE
70+
target_is_wpa3_sae = self.target.primary_authentication == 'SAE'
71+
72+
cracker = "Hashcat" # Default to Hashcat
73+
# TODO: Potentially add a fallback or user choice for aircrack-ng for non-SAE?
74+
# For now, transitioning WPA/WPA2 cracking to Hashcat as well for consistency,
75+
# as Hashcat mode 2500 (hccapx) is generally preferred over aircrack-ng.
76+
# Aircrack.crack_handshake might be removed or kept for WEP only in future.
77+
78+
Color.pl(f'\n{{+}} {{C}}Cracking {"WPA3-SAE" if target_is_wpa3_sae else "WPA/WPA2"} Handshake:{{W}} Running {{C}}{cracker}{{W}} with '
79+
f'{{C}}{os.path.split(Configuration.wordlist)[-1] if Configuration.wordlist else "default wordlist"}{{W}} wordlist')
80+
81+
try:
82+
key = Hashcat.crack_handshake(handshake, target_is_wpa3_sae, show_command=Configuration.verbose > 1)
83+
except ValueError as e: # Catch errors from hash file generation (e.g. bad capture)
84+
Color.pl(f"[!] Error during hash file generation for cracking: {e}")
85+
key = None
7086

71-
# Crack it
72-
key = Aircrack.crack_handshake(handshake, show_command=False)
7387
if key is None:
74-
Color.pl('{!} {R}Failed to crack handshake: {O}%s{R} did not contain password{W}' %
75-
Configuration.wordlist.split(os.sep)[-1])
88+
Color.pl(f"[!] Failed to crack handshake: {Configuration.wordlist.split(os.sep)[-1] if Configuration.wordlist else 'Wordlist'} did not contain password")
7689
self.success = False
7790
else:
78-
Color.pl('{+} {G}Cracked WPA Handshake{W} PSK: {G}%s{W}\n' % key)
91+
Color.pl(f"[+] Cracked {'WPA3-SAE' if target_is_wpa3_sae else 'WPA/WPA2'} Handshake Key: {key}\n")
7992
self.crack_result = CrackResultWPA(handshake.bssid, handshake.essid, handshake.capfile, key)
8093
self.crack_result.dump()
8194
self.success = True

wifite/config.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ def initialize(cls, load_interface=True):
148148
cls.wep_keep_ivs = False # Retain .ivs files across multiple attacks.
149149

150150
# WPA variables
151-
cls.wpa_filter = False # Only attack WPA networks
151+
cls.wpa_filter = False # Only attack WPA/WPA2 networks
152+
cls.wpa3_filter = False # Only attack WPA3 networks
153+
cls.owe_filter = False # Only attack OWE networks
152154
cls.wpa_deauth_timeout = 15 # Wait time between deauths
153155
cls.wpa_attack_timeout = 300 # Wait time before failing
154156
cls.wpa_handshake_dir = 'hs' # Dir to store handshakes
@@ -428,6 +430,12 @@ def parse_wpa_args(cls, args):
428430
if args.wpa_filter:
429431
cls.wpa_filter = args.wpa_filter
430432

433+
if hasattr(args, 'wpa3_filter') and args.wpa3_filter:
434+
cls.wpa3_filter = args.wpa3_filter
435+
436+
if hasattr(args, 'owe_filter') and args.owe_filter:
437+
cls.owe_filter = args.owe_filter
438+
431439
if args.wordlist:
432440
if not os.path.exists(args.wordlist):
433441
cls.wordlist = None
@@ -558,16 +566,23 @@ def parse_encryption(cls):
558566
cls.encryption_filter = []
559567
if cls.wep_filter:
560568
cls.encryption_filter.append('WEP')
561-
if cls.wpa_filter:
562-
cls.encryption_filter.append('WPA')
563-
if cls.wps_filter:
569+
if cls.wpa_filter: # WPA/WPA2
570+
cls.encryption_filter.append('WPA')
571+
if cls.wpa3_filter:
572+
cls.encryption_filter.append('WPA3')
573+
if cls.owe_filter:
574+
cls.encryption_filter.append('OWE')
575+
if cls.wps_filter: # WPS can be on WPA/WPA2
564576
cls.encryption_filter.append('WPS')
565577

566-
if len(cls.encryption_filter) == 3:
567-
Color.pl('{+} {C}option:{W} targeting {G}all encrypted networks{W}')
568-
elif not cls.encryption_filter:
569-
# Default to scan all types
570-
cls.encryption_filter = ['WEP', 'WPA', 'WPS']
578+
cls.encryption_filter = sorted(list(set(cls.encryption_filter))) # Remove duplicates and sort
579+
580+
if not cls.encryption_filter:
581+
# Default to scan all known types if no specific filter is chosen
582+
cls.encryption_filter = ['WEP', 'WPA', 'WPA3', 'OWE', 'WPS']
583+
Color.pl('{+} {C}option:{W} targeting {G}all known encryption types{W} by default')
584+
elif len(cls.encryption_filter) == 5 and 'WPS' in cls.encryption_filter and 'OWE' in cls.encryption_filter: # Approximation for "all"
585+
Color.pl('{+} {C}option:{W} targeting {G}all specified encrypted networks{W}')
571586
else:
572587
Color.pl('{+} {C}option:{W} targeting {G}%s-encrypted{W} networks' % '/'.join(cls.encryption_filter))
573588

wifite/model/target.py

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ def __init__(self, fields):
6161
2 Last time seen (2015-05-27 19:28:46)
6262
3 channel (6)
6363
4 Speed (54)
64-
5 Privacy (WPA2)
64+
5 Privacy (WPA2 OWE)
6565
6 Cipher (CCMP TKIP)
66-
7 Authentication (PSK)
66+
7 Authentication (PSK SAE)
6767
8 Power (-62)
6868
9 beacons (2)
6969
10 # IV (0)
@@ -76,20 +76,37 @@ def __init__(self, fields):
7676
self.wps = WPSState.NONE
7777
self.bssid = fields[0].strip()
7878
self.channel = fields[3].strip()
79-
self.encryption = fields[5].strip()
80-
self.authentication = fields[7].strip()
81-
82-
# airodump sometimes does not report the encryption type for some reason
83-
# In this case (len = 0), defaults to WPA (which is the most common)
84-
if 'WPA' in self.encryption or len(self.encryption) == 0:
85-
self.encryption = 'WPA'
79+
self.encryption = fields[5].strip() # Contains encryption type(s) like "WPA2 WPA3 OWE"
80+
self.authentication = fields[7].strip() # Contains auth type(s) like "PSK SAE MGT"
81+
82+
# Determine primary encryption and auth
83+
if 'WPA3' in self.encryption:
84+
self.primary_encryption = 'WPA3'
85+
elif 'WPA2' in self.encryption:
86+
self.primary_encryption = 'WPA2'
87+
elif 'WPA' in self.encryption: # Handles cases where only "WPA" is present or if no other WPAx is found
88+
self.primary_encryption = 'WPA'
8689
elif 'WEP' in self.encryption:
87-
self.encryption = 'WEP'
88-
elif 'WPS' in self.encryption:
89-
self.encryption = 'WPS'
90+
self.primary_encryption = 'WEP'
91+
elif 'OWE' in self.encryption: # Opportunistic Wireless Encryption
92+
self.primary_encryption = 'OWE'
93+
elif len(self.encryption) == 0: # Default to WPA if not specified, as per old logic
94+
self.primary_encryption = 'WPA'
95+
else: # Fallback for unknown types
96+
self.primary_encryption = self.encryption.split(' ')[0]
97+
98+
99+
if 'SAE' in self.authentication:
100+
self.primary_authentication = 'SAE'
101+
elif 'PSK' in self.authentication:
102+
self.primary_authentication = 'PSK'
103+
elif 'MGT' in self.authentication: # Enterprise
104+
self.primary_authentication = 'MGT'
105+
elif 'OWE' in self.authentication: # OWE uses its own auth mechanism
106+
self.primary_authentication = 'OWE'
107+
else:
108+
self.primary_authentication = self.authentication.split(' ')[0]
90109

91-
if len(self.encryption) > 4:
92-
self.encryption = self.encryption[:4].strip()
93110

94111
self.power = int(fields[8].strip())
95112
if self.power < 0:
@@ -119,6 +136,14 @@ def __init__(self, fields):
119136

120137
self.clients = []
121138

139+
# Store full encryption and authentication strings for detailed info if needed
140+
self.full_encryption_string = self.encryption
141+
self.full_authentication_string = self.authentication
142+
# For compatibility with existing logic that expects a single string:
143+
self.encryption = self.primary_encryption # Overwrite with primary for now
144+
self.authentication = self.primary_authentication # Overwrite with primary for now
145+
146+
122147
self.validate()
123148

124149
def __eq__(self, other):
@@ -142,6 +167,15 @@ def transfer_info(self, other):
142167
other.essid_known = self.essid_known
143168
other.essid_len = self.essid_len
144169

170+
# Transfer new fields as well
171+
if hasattr(self, 'primary_encryption'):
172+
other.primary_encryption = self.primary_encryption
173+
other.full_encryption_string = self.full_encryption_string
174+
if hasattr(self, 'primary_authentication'):
175+
other.primary_authentication = self.primary_authentication
176+
other.full_authentication_string = self.full_authentication_string
177+
178+
145179
def validate(self):
146180
""" Checks that the target is valid. """
147181
if self.channel == '-1':
@@ -203,16 +237,35 @@ def to_str(self, show_bssid=False, show_manufacturer=False):
203237
channel_color = '{C}' if int(self.channel) > 14 else '{G}'
204238
channel = Color.s(f'{channel_color}{str(self.channel).rjust(3)}')
205239

206-
encryption = self.encryption.rjust(3)
207-
if 'WEP' in encryption:
208-
encryption = Color.s('{G}%s' % encryption)
209-
elif 'WPA' in encryption:
210-
if 'PSK' in self.authentication:
211-
encryption = Color.s('{O}%s-P' % encryption)
212-
elif 'MGT' in self.authentication:
213-
encryption = Color.s('{R}%s-E' % encryption)
214-
else:
215-
encryption = Color.s('{O}%s ' % encryption)
240+
# Use primary_encryption and primary_authentication for display
241+
display_encryption = self.primary_encryption.rjust(4) # Adjusted rjust for WPA3
242+
auth_suffix = ''
243+
if self.primary_encryption == 'WPA3':
244+
display_encryption = Color.s('{P}%s' % display_encryption) # Purple for WPA3
245+
if self.primary_authentication == 'SAE':
246+
auth_suffix = Color.s('{P}-S') # Purple for SAE
247+
elif self.primary_authentication == 'MGT':
248+
auth_suffix = Color.s('{R}-E') # Red for Enterprise
249+
elif self.primary_encryption == 'WPA2':
250+
display_encryption = Color.s('{O}%s' % display_encryption) # Orange for WPA2
251+
if self.primary_authentication == 'PSK':
252+
auth_suffix = Color.s('{O}-P')
253+
elif self.primary_authentication == 'MGT':
254+
auth_suffix = Color.s('{R}-E')
255+
elif self.primary_encryption == 'WPA':
256+
display_encryption = Color.s('{O}%s' % display_encryption) # Orange for WPA
257+
if self.primary_authentication == 'PSK':
258+
auth_suffix = Color.s('{O}-P')
259+
elif self.primary_authentication == 'MGT':
260+
auth_suffix = Color.s('{R}-E')
261+
elif self.primary_encryption == 'WEP':
262+
display_encryption = Color.s('{G}%s' % display_encryption) # Green for WEP
263+
elif self.primary_encryption == 'OWE':
264+
display_encryption = Color.s('{B}%s' % display_encryption) # Blue for OWE
265+
else:
266+
display_encryption = Color.s('{W}%s' % display_encryption) # White for others
267+
268+
encryption_display_string = f"{display_encryption}{auth_suffix}".ljust(7) # Ensure consistent length
216269

217270
power = f'{str(self.power).rjust(3)}db'
218271
if self.power > 50:
@@ -238,7 +291,7 @@ def to_str(self, show_bssid=False, show_manufacturer=False):
238291
if len(self.clients) > 0:
239292
clients = Color.s('{G} ' + str(len(self.clients)))
240293

241-
result = f'{essid} {bssid}{manufacturer}{channel} {encryption} {power} {wps} {clients}'
294+
result = f'{essid} {bssid}{manufacturer}{channel} {encryption_display_string} {power} {wps} {clients}'
242295

243296
result += Color.s('{W}')
244297
return result

0 commit comments

Comments
 (0)