Skip to content

Commit dfb0be5

Browse files
committed
added hcx tools to validation of captures + various improvements
1 parent f27e223 commit dfb0be5

8 files changed

Lines changed: 293 additions & 63 deletions

File tree

wifite/model/handshake.py

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ def has_handshake(self):
6969
# Check if ANY validator detects a handshake
7070
# Tshark is strict (requires all 4 messages), but cowpatty/aircrack
7171
# can work with just messages 2&3, which is sufficient for cracking
72+
# hcxpcapngtool is best for hcxdumptool captures (pcapng format)
7273
return (len(self.tshark_handshakes()) > 0 or
7374
len(self.cowpatty_handshakes()) > 0 or
74-
len(self.aircrack_handshakes()) > 0)
75+
len(self.aircrack_handshakes()) > 0 or
76+
len(self.hcxpcapngtool_handshakes()) > 0)
7577

7678
def tshark_handshakes(self):
7779
"""Returns list[tuple] of BSSID & ESSID pairs (ESSIDs are always `None`)."""
@@ -83,15 +85,23 @@ def cowpatty_handshakes(self):
8385
if not Process.exists('cowpatty'):
8486
return []
8587

86-
# Needs to check if cowpatty is updated and have the -2 parameter
87-
cowpattycheck = Process('cowpatty', devnull=False)
88-
89-
command = [
90-
'cowpatty',
91-
'-2' if 'frames 1 and 2 or 2 and 3 for key attack' in cowpattycheck.stdout() else '',
92-
'-r', self.capfile,
93-
'-c' # Check for handshake
94-
]
88+
# Check if cowpatty supports the -2 parameter (frames 1&2 or 2&3)
89+
# Run cowpatty with -h to get help output instead of running without args
90+
try:
91+
cowpattycheck = Process(['cowpatty', '-h'], devnull=False)
92+
supports_dash_2 = 'frames 1 and 2 or 2 and 3 for key attack' in cowpattycheck.stdout()
93+
except Exception:
94+
# If help check fails, assume -2 is not supported
95+
supports_dash_2 = False
96+
97+
# Build command - only include -2 if supported
98+
command = ['cowpatty']
99+
if supports_dash_2:
100+
command.append('-2')
101+
command.extend([
102+
'-r', self.capfile,
103+
'-c' # Check for handshake
104+
])
95105

96106
proc = Process(command, devnull=False)
97107
return next(
@@ -122,6 +132,65 @@ def aircrack_handshakes(self):
122132
else:
123133
return []
124134

135+
def hcxpcapngtool_handshakes(self):
136+
"""
137+
Returns tuple (BSSID,None) if hcxpcapngtool can extract valid handshake data.
138+
This is especially useful for pcapng files captured by hcxdumptool.
139+
Only runs on .pcapng files as hcxpcapngtool is optimized for that format.
140+
"""
141+
if not Process.exists('hcxpcapngtool'):
142+
return []
143+
144+
# Only use hcxpcapngtool for pcapng files (hcxdumptool format)
145+
# For older .cap files, use aircrack/cowpatty/tshark instead
146+
if not self.capfile.lower().endswith('.pcapng'):
147+
return []
148+
149+
import tempfile
150+
151+
# Create a temporary hash file to test if hcxpcapngtool can extract data
152+
try:
153+
with tempfile.NamedTemporaryFile(mode='w', suffix='.22000', delete=False) as tmp:
154+
hash_file = tmp.name
155+
156+
command = [
157+
'hcxpcapngtool',
158+
'-o', hash_file,
159+
self.capfile
160+
]
161+
162+
proc = Process(command, devnull=False)
163+
164+
# Check if hash file was created and has content
165+
if os.path.exists(hash_file) and os.path.getsize(hash_file) > 0:
166+
# Successfully extracted handshake data
167+
result = [(self.bssid, None)] if self.bssid else [(None, self.essid)]
168+
169+
# Clean up temp file
170+
try:
171+
os.remove(hash_file)
172+
except OSError:
173+
pass
174+
175+
return result
176+
else:
177+
# No valid handshake data found
178+
try:
179+
if os.path.exists(hash_file):
180+
os.remove(hash_file)
181+
except OSError:
182+
pass
183+
return []
184+
185+
except Exception:
186+
# If anything goes wrong, clean up and return empty
187+
try:
188+
if hash_file and os.path.exists(hash_file):
189+
os.remove(hash_file)
190+
except (OSError, NameError):
191+
pass
192+
return []
193+
125194
def analyze(self):
126195
"""Prints analysis of handshake capfile"""
127196
self.divine_bssid_and_essid()
@@ -133,6 +202,9 @@ def analyze(self):
133202
Handshake.print_pairs(self.cowpatty_handshakes(), 'cowpatty')
134203

135204
Handshake.print_pairs(self.aircrack_handshakes(), 'aircrack')
205+
206+
if Process.exists('hcxpcapngtool'):
207+
Handshake.print_pairs(self.hcxpcapngtool_handshakes(), 'hcxpcapng')
136208

137209
def strip(self, outfile=None):
138210
# XXX: This method might break aircrack-ng, use at own risk.

wifite/model/target.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ def __init__(self, fields):
7272
self.manufacturer = None
7373
self.wps = WPSState.NONE
7474
self.bssid = fields[0].strip()
75-
self.channel = fields[3].strip()
75+
try:
76+
self.channel = int(fields[3].strip())
77+
except (ValueError, IndexError):
78+
self.channel = -1
7679
self.encryption = fields[5].strip() # Contains encryption type(s) like "WPA2 WPA3 OWE"
7780
self.authentication = fields[7].strip() # Contains auth type(s) like "PSK SAE MGT"
7881

@@ -208,7 +211,7 @@ def is_dragonblood_vulnerable(self):
208211

209212
def validate(self):
210213
""" Checks that the target is valid. """
211-
if self.channel == '-1':
214+
if self.channel == -1:
212215
pass
213216

214217
# Filter broadcast/multicast BSSIDs, see https://github.com/derv82/wifite2/issues/32

wifite/tools/hashcat.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,13 @@ def generate_hash_file(handshake_obj, is_wpa3_sae, show_command=False):
195195
# Use mode 22000 format for both WPA2 and WPA3-SAE
196196
# Hashcat mode 22000 supports WPA-PBKDF2-PMKID+EAPOL (includes SAE)
197197
# Mode 22001 is for WPA-PMK-PMKID+EAPOL (pre-computed PMK)
198-
198+
199199
# Create secure temporary file with proper permissions (0600)
200200
# Using NamedTemporaryFile with delete=False to prevent race conditions
201201
log_debug('HcxPcapngTool', 'Creating secure temporary hash file')
202202
with tempfile.NamedTemporaryFile(mode='w', suffix='.22000', delete=False, prefix='wifite_hash_') as tmp:
203203
hash_file = tmp.name
204-
204+
205205
# Verify file permissions are secure (0600)
206206
os.chmod(hash_file, 0o600)
207207
log_debug('HcxPcapngTool', f'Created temporary hash file: {hash_file} (permissions: 0600)')
@@ -219,18 +219,18 @@ def generate_hash_file(handshake_obj, is_wpa3_sae, show_command=False):
219219

220220
process = Process(command)
221221
stdout, stderr = process.get_output()
222-
222+
223223
log_debug('HcxPcapngTool', f'hcxpcapngtool stdout: {stdout[:200]}...' if len(stdout) > 200 else f'hcxpcapngtool stdout: {stdout}')
224224
if stderr:
225225
log_debug('HcxPcapngTool', f'hcxpcapngtool stderr: {stderr[:200]}...' if len(stderr) > 200 else f'hcxpcapngtool stderr: {stderr}')
226-
226+
227227
if not os.path.exists(hash_file) or os.path.getsize(hash_file) == 0:
228228
# Check if this is due to missing frames (common with airodump captures)
229229
if 'no hashes written' in stdout.lower() or 'missing frames' in stdout.lower():
230230
log_warning('HcxPcapngTool', 'Hash generation failed: capture quality issue (missing frames)')
231-
Color.pl('{!} {O}Warning: hcxpcapngtool could not extract hash (capture quality issue){W}')
232-
Color.pl('{!} {O}The capture file is missing required frames or metadata{W}')
233-
Color.pl('{!} {O}This is common with airodump-ng captures - consider using hcxdumptool instead{W}')
231+
#Color.pl('{!} {O}Warning: hcxpcapngtool could not extract hash (capture quality issue){W}')
232+
#Color.pl('{!} {O}The capture file is missing required frames or metadata{W}')
233+
#Color.pl('{!} {O}This is common with airodump-ng captures - consider using hcxdumptool instead{W}')
234234
# Cleanup failed hash file
235235
if os.path.exists(hash_file):
236236
try:
@@ -240,7 +240,7 @@ def generate_hash_file(handshake_obj, is_wpa3_sae, show_command=False):
240240
pass
241241
# Return None to signal fallback to aircrack-ng should be used
242242
return None
243-
243+
244244
# For other errors, provide detailed error message
245245
error_msg = f'Failed to generate {"SAE hash" if is_wpa3_sae else "WPA/WPA2 hash"} file.'
246246
error_msg += f'\nOutput from hcxpcapngtool:\nSTDOUT: {stdout}\nSTDERR: {stderr}'

wifite/tools/tshark.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,51 @@ def check_for_wps_and_update_targets(capfile, targets):
169169
try:
170170
p.wait()
171171
lines = p.stdout()
172+
except KeyboardInterrupt:
173+
raise
174+
except (OSError, IOError) as e:
175+
from ..config import Configuration
176+
if Configuration.verbose > 0:
177+
from ..util.color import Color
178+
Color.pl('{!} {O}Warning: tshark WPS detection failed: %s{W}' % str(e))
179+
return
172180
except Exception as e:
173-
if isinstance(e, KeyboardInterrupt):
174-
raise KeyboardInterrupt from e
181+
from ..config import Configuration
182+
from ..util.color import Color
183+
Color.pl('{!} {R}Unexpected error in WPS detection: %s{W}' % str(e))
184+
if Configuration.verbose > 1:
185+
import traceback
186+
Color.pl('{!} {O}%s{W}' % traceback.format_exc())
175187
return
188+
176189
wps_bssids = set()
177190
locked_bssids = set()
191+
178192
for line in lines.split('\n'):
179-
if ',' not in line:
193+
line = line.strip()
194+
if not line:
180195
continue
181-
bssid, locked = line.split(',')
182-
if '1' not in locked:
183-
wps_bssids.add(bssid.upper())
184-
else:
196+
197+
# Split on first comma only to handle trailing commas
198+
parts = line.split(',', maxsplit=1)
199+
200+
if len(parts) < 1:
201+
continue
202+
203+
bssid = parts[0].strip()
204+
locked = parts[1].strip() if len(parts) > 1 else ''
205+
206+
# Validate BSSID format (basic check: should be 17 chars with colons)
207+
if len(bssid) != 17 or bssid.count(':') != 5:
208+
continue
209+
210+
# Check locked status - be specific!
211+
# 0x01, 0x1, or 1 = locked
212+
# Empty, 0x00, 0, or any other value = unlocked
213+
if locked.lower() in ('0x01', '0x1', '1'):
185214
locked_bssids.add(bssid.upper())
215+
else:
216+
wps_bssids.add(bssid.upper())
186217

187218
for t in targets:
188219
target_bssid = t.bssid.upper()

wifite/util/crack.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,39 @@ def get_handshakes(cls):
154154
else:
155155
continue
156156

157-
name, essid, bssid, date = hs_file.split('_')
158-
date = date.rsplit('.', 1)[0]
159-
days, hours = date.split('T')
160-
hours = hours.replace('-', ':')
161-
date = f'{days} {hours}'
157+
# Parse filename: name_essid_bssid_date.ext
158+
# ESSID can contain underscores, so split from right
159+
# Expected format: handshake_ESSID_AA-BB-CC-DD-EE-FF_20251031T120000.cap
160+
try:
161+
# Remove file extension first
162+
filename_no_ext = hs_file.rsplit('.', 1)[0]
163+
164+
# Split from right: last 3 parts are always bssid, date (and first is name)
165+
parts = filename_no_ext.split('_')
166+
167+
if len(parts) < 4:
168+
# Malformed filename, skip
169+
if Configuration.verbose > 0:
170+
Color.pl('{!} {O}Skipping malformed filename: %s{W}' % hs_file)
171+
continue
172+
173+
# Extract parts: name is first, bssid and date are last two
174+
name = parts[0]
175+
date = parts[-1]
176+
bssid = parts[-2]
177+
# Everything in between is the ESSID (may contain underscores)
178+
essid = '_'.join(parts[1:-2])
179+
180+
# Parse date
181+
days, hours = date.split('T')
182+
hours = hours.replace('-', ':')
183+
date = f'{days} {hours}'
184+
185+
except (ValueError, IndexError) as e:
186+
# Failed to parse filename
187+
if Configuration.verbose > 0:
188+
Color.pl('{!} {O}Error parsing filename %s: %s{W}' % (hs_file, str(e)))
189+
continue
162190

163191
if hs_type == '4-WAY':
164192
# Patch for essid with " " (zero) or dot "." in name
@@ -169,16 +197,29 @@ def get_handshakes(cls):
169197
essid = essid if essid_discovery is None else essid_discovery
170198
elif hs_type == 'PMKID':
171199
# Decode hex-encoded ESSID from passive PMKID capture
172-
# Check if essid looks like hex (all characters are hex digits)
173-
if all(c in '0123456789ABCDEFabcdef' for c in essid):
200+
# Only decode if it looks like hex-encoded UTF-8 (not just valid hex)
201+
# Criteria:
202+
# 1. Length >= 16 (minimum for 8-char ESSID encoded as hex)
203+
# 2. Even length (valid hex pairs)
204+
# 3. All characters are hex digits
205+
# 4. Decoded result is shorter than original (hex encoding expands)
206+
# 5. Decoded result contains only printable characters
207+
if (len(essid) >= 16 and
208+
len(essid) % 2 == 0 and
209+
all(c in '0123456789ABCDEFabcdef' for c in essid)):
174210
try:
175-
# Try to decode from hex
176-
decoded_essid = bytes.fromhex(essid).decode('utf-8', errors='ignore')
177-
# Only use decoded version if it's not empty and looks reasonable
178-
if decoded_essid and len(decoded_essid) > 0:
211+
# Try to decode from hex (strict mode)
212+
decoded_essid = bytes.fromhex(essid).decode('utf-8', errors='strict')
213+
214+
# Validate decoded result
215+
if (decoded_essid and
216+
len(decoded_essid) > 0 and
217+
len(decoded_essid) < len(essid) and # Hex encoding should be longer
218+
all(32 <= ord(c) <= 126 or ord(c) >= 128 for c in decoded_essid)): # Printable chars
179219
essid = decoded_essid
180220
except (ValueError, UnicodeDecodeError):
181221
# If decoding fails, keep the original hex string
222+
# This is expected for legitimate network names like "CAFE", "DEAD", etc.
182223
pass
183224

184225
handshake = {

0 commit comments

Comments
 (0)