Skip to content

Commit 09121e9

Browse files
committed
improve wps + improve sessions
1 parent 03a147d commit 09121e9

File tree

7 files changed

+125
-44
lines changed

7 files changed

+125
-44
lines changed

wifite/attack/attack_monitor.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,17 @@ def log_attack_event(self, event):
337337
timestamp_str = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(event['timestamp']))
338338

339339
# Format log entry as CSV
340+
# Convert channel to string if it's an integer
341+
channel_str = str(event.get('channel', '')) if event.get('channel') is not None else ''
342+
340343
log_entry = '%s,%s,%s,%s,%s,%s,%s\n' % (
341344
timestamp_str,
342345
event['type'],
343346
event['source_mac'],
344347
event['dest_mac'],
345348
event['bssid'],
346349
event.get('essid', ''),
347-
event.get('channel', '')
350+
channel_str
348351
)
349352

350353
# Add to buffer
@@ -543,6 +546,9 @@ def run(self):
543546
self.tui_view.add_log(traceback.format_exc())
544547
else:
545548
Color.pl('{!} {R}%s{W}' % traceback.format_exc())
549+
550+
# Ensure cleanup happens even on exception
551+
self.cleanup()
546552
return False
547553
finally:
548554
# Stop TUI view if it was started

wifite/model/handshake.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,12 @@ def cowpatty_handshakes(self):
100100
command.append('-2')
101101
command.extend([
102102
'-r', self.capfile,
103-
'-c' # Check for handshake
103+
'-c' # Check for handshake (requires ESSID)
104104
])
105+
106+
# Add ESSID if available (required for -c option)
107+
if self.essid:
108+
command.append(self.essid)
105109

106110
proc = Process(command, devnull=False)
107111
return next(

wifite/tools/bully.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ def __init__(self, target2, target3=None, pixie_dust=True):
6060
self.cmd.extend([
6161
'bully',
6262
'--bssid', self.target.bssid,
63-
'--channel', self.target.channel,
63+
'--channel', str(self.target.channel),
6464
'--force',
6565
'-v', '4',
66+
'--nofcs',
6667
Configuration.interface
6768
])
6869

@@ -463,7 +464,7 @@ def get_psk_from_pin(target3, pin):
463464
"""
464465
cmd = [
465466
'bully',
466-
'--channel', target3.channel,
467+
'--channel', str(target3.channel),
467468
'--bssid', target3.bssid,
468469
'--pin', pin,
469470
'--bruteforce',

wifite/tools/reaver.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(self, target, pixie_dust=True, null_pin=False):
5151
'reaver',
5252
'--interface', Configuration.interface,
5353
'--bssid', self.target.bssid,
54-
'--channel', self.target.channel,
54+
'--channel', str(self.target.channel),
5555
'-vv',
5656
'-N',
5757
]
@@ -86,15 +86,21 @@ def run(self):
8686
except Exception as e:
8787
# Unexpected errors
8888
self.pattack('{R}Failed:{O} Unexpected error: %s' % str(e), newline=True)
89-
return self.crack_result is not None
90-
91-
# Stop reaver if it's still running
92-
if self.reaver_proc.poll() is None:
93-
self.reaver_proc.interrupt()
89+
finally:
90+
# Always clean up resources, even on exception
91+
# Stop reaver if it's still running
92+
if self.reaver_proc and self.reaver_proc.poll() is None:
93+
try:
94+
self.reaver_proc.interrupt()
95+
except Exception:
96+
pass # Ignore errors during cleanup
9497

95-
# Clean up open file handle
96-
if self.output_write:
97-
self.output_write.close()
98+
# Clean up open file handle
99+
if self.output_write:
100+
try:
101+
self.output_write.close()
102+
except Exception:
103+
pass # Ignore errors during cleanup
98104

99105
return self.crack_result is not None
100106

wifite/util/color.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ def pattack(attack_type, target, attack_name, progress):
9292
e.g.: Router2G (23db) WEP replay attack: 102 IVs
9393
"""
9494
essid = '{C}%s{W}' % target.essid if target.essid_known else '{O}unknown{W}'
95+
# Convert power to string to avoid type errors in string formatting
96+
power_str = str(target.power) if target.power is not None else '??'
9597
Color.p('\r{+} {G}%s{W} ({C}%sdb{W}) {G}%s {C}%s{W}: %s ' % (
96-
essid, target.power, attack_type, attack_name, progress))
98+
essid, power_str, attack_type, attack_name, progress))
9799

98100
@staticmethod
99101
def pexception(exception):

wifite/util/crack.py

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,67 @@
2020

2121
# TODO: Bring back the 'print' option, for easy copy/pasting. Just one-liners people can paste into terminal.
2222

23+
def decode_hex_essid_if_needed(essid):
24+
"""
25+
Decode hex-encoded ESSID with strict validation to prevent false positives.
26+
27+
Only decodes if ALL of the following criteria are met:
28+
1. Length >= 16 characters (minimum for 8-char ESSID encoded as hex)
29+
2. Length is even (valid hex pairs)
30+
3. All characters are valid hexadecimal digits
31+
4. Decoded result is shorter than original (hex encoding expands length)
32+
5. Decoded result contains only printable characters (ASCII 32-126 or UTF-8 >= 128)
33+
34+
This prevents false positives for legitimate network names like "CAFE", "DEAD", "BEEF", etc.
35+
36+
Args:
37+
essid: ESSID string to potentially decode
38+
39+
Returns:
40+
Decoded ESSID if valid hex-encoded, otherwise original ESSID
41+
"""
42+
# Validation check 1: Minimum length (too short to be hex-encoded)
43+
if len(essid) < 16:
44+
return essid
45+
46+
# Validation check 2: Even length (valid hex pairs)
47+
if len(essid) % 2 != 0:
48+
return essid
49+
50+
# Validation check 3: All characters are hex digits
51+
if not all(c in '0123456789ABCDEFabcdef' for c in essid):
52+
return essid
53+
54+
try:
55+
# Try to decode from hex with strict error handling
56+
decoded_essid = bytes.fromhex(essid).decode('utf-8', errors='strict')
57+
58+
# Validation check 4: Decoded result must not be empty
59+
if not decoded_essid or len(decoded_essid) == 0:
60+
return essid
61+
62+
# Validation check 5: Hex encoding should make string longer, not shorter
63+
if len(decoded_essid) >= len(essid):
64+
return essid
65+
66+
# Validation check 6: Check for printable characters (ASCII 32-126 or extended UTF-8)
67+
if not all(32 <= ord(c) <= 126 or ord(c) >= 128 for c in decoded_essid):
68+
return essid
69+
70+
# All checks passed, use decoded version
71+
if Configuration.verbose > 1:
72+
Color.pl('{+} {C}Decoded hex ESSID: {O}%s{W} -> {G}%s{W}' % (essid, decoded_essid))
73+
74+
return decoded_essid
75+
76+
except (ValueError, UnicodeDecodeError) as e:
77+
# Decoding failed, keep original
78+
# This is expected for legitimate network names like "CAFE", "DEAD", etc.
79+
if Configuration.verbose > 2:
80+
Color.pl('{!} {O}Hex ESSID decode failed for %s: %s{W}' % (essid, str(e)))
81+
return essid
82+
83+
2384
class CrackHelper:
2485
"""Manages handshake retrieval, selection, and running the cracking commands."""
2586

@@ -197,30 +258,8 @@ def get_handshakes(cls):
197258
essid = essid if essid_discovery is None else essid_discovery
198259
elif hs_type == 'PMKID':
199260
# Decode hex-encoded ESSID from passive PMKID capture
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)):
210-
try:
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
219-
essid = decoded_essid
220-
except (ValueError, UnicodeDecodeError):
221-
# If decoding fails, keep the original hex string
222-
# This is expected for legitimate network names like "CAFE", "DEAD", etc.
223-
pass
261+
# Use dedicated function with strict validation to prevent false positives
262+
essid = decode_hex_essid_if_needed(essid)
224263

225264
handshake = {
226265
'filename': os.path.join(hs_dir, hs_file),

wifite/util/session.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,28 @@ def create_session(self, targets: List, config) -> SessionState:
323323

324324
def save_session(self, session: SessionState) -> None:
325325
"""
326-
Persist session state to disk.
326+
Persist session state to disk with atomic file operations.
327+
328+
This method implements atomic session saving to prevent corruption:
329+
1. Creates unique temporary file with secure permissions (0600)
330+
2. Writes session data to temporary file
331+
3. Atomically renames temp file to final location
332+
4. Cleans up on error
333+
334+
The atomic rename ensures that:
335+
- Multiple instances never corrupt each other's session files
336+
- Session files are never partially written
337+
- No race conditions occur during concurrent saves
327338
328339
Args:
329340
session: SessionState to save
341+
342+
Raises:
343+
IOError: If save operation fails (with detailed error message)
330344
"""
331345
import tempfile
346+
from ..config import Configuration
347+
from ..util.color import Color
332348

333349
session.updated_at = time.time()
334350
session_path = self._get_session_path(session.session_id)
@@ -341,23 +357,29 @@ def save_session(self, session: SessionState) -> None:
341357
try:
342358
# Create secure temporary file in same directory as target
343359
# This ensures atomic rename works (same filesystem)
360+
# Prefix with session ID for uniqueness across multiple instances
344361
temp_fd, temp_path = tempfile.mkstemp(
345362
suffix='.tmp',
346363
prefix=f'.{session.session_id}_',
347364
dir=self.session_dir
348365
)
349366

350367
# Write session data using file descriptor
368+
# fdopen takes ownership of the file descriptor
351369
with os.fdopen(temp_fd, 'w') as f:
352-
temp_fd = None # fdopen takes ownership
370+
temp_fd = None # Prevent double-close
353371
json.dump(session.to_dict(), f, indent=2)
354372

355373
# Permissions already secure (0600 by default from mkstemp)
356-
# Atomic rename to final location
374+
# Atomic rename to final location (replaces existing file atomically)
357375
os.rename(temp_path, session_path)
358-
temp_path = None # Successfully renamed
376+
temp_path = None # Successfully renamed, prevent cleanup
377+
378+
# Log successful save in verbose mode
379+
if Configuration.verbose > 1:
380+
Color.pl('{+} {G}Session saved: %s{W}' % session.session_id)
359381

360-
except Exception:
382+
except Exception as e:
361383
# Clean up temp file on error
362384
if temp_fd is not None:
363385
try:
@@ -371,7 +393,8 @@ def save_session(self, session: SessionState) -> None:
371393
except OSError:
372394
pass
373395

374-
raise # Re-raise the original exception
396+
# Re-raise with detailed context
397+
raise IOError(f'Failed to save session {session.session_id}: {str(e)}') from e
375398

376399
def load_session(self, session_id: str = None) -> SessionState:
377400
"""

0 commit comments

Comments
 (0)