Skip to content

Commit 8e22303

Browse files
authored
Merge pull request #525 from kimocoder/audit-handshake-and-packaging-fixes
Improve handshake analysis, fix packaging, and harden silent exceptio…
2 parents b085733 + ba1143d commit 8e22303

10 files changed

Lines changed: 161 additions & 81 deletions

File tree

requirements.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Core dependencies for wifite2
22
# These should match pyproject.toml [project.dependencies]
33
chardet>=7.4.3
4-
requests>=2.33.1
4+
requests>=2.34.2
55
rich>=15.0.0
66
scapy>=2.7.1rc1; python_version < '4'
77
setuptools>=82.0.1
88

9-
# Development dependencies (optional)
10-
# For development, install with: pip install -e ".[dev]"
11-
# (see [project.optional-dependencies] in pyproject.toml for dev pins)
12-
urllib3>=2.6.3 # not directly required, pinned by Snyk to avoid a vulnerability
9+
urllib3>=2.7.0 # not directly required, pinned by Snyk to avoid a vulnerability
10+
11+
# Development / test dependencies are defined once in pyproject.toml
12+
# ([project.optional-dependencies].dev). Install them with: pip install -e ".[dev]"

setup.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,25 @@
1010
with open('README.md', 'r', encoding='utf-8') as fh:
1111
long_description = fh.read()
1212

13+
14+
def _read_requirements():
15+
"""Parse runtime dependencies from requirements.txt.
16+
17+
Keeps a single source of truth for runtime deps (shared with the
18+
`pip install -r requirements.txt` path) instead of re-listing them here.
19+
Strips blank lines and comments while preserving PEP 508 environment
20+
markers (e.g. "scapy>=...; python_version < '4'").
21+
"""
22+
requirements = []
23+
with open('requirements.txt', 'r', encoding='utf-8') as fh:
24+
for line in fh:
25+
line = line.split('#', 1)[0].strip()
26+
if line:
27+
requirements.append(line)
28+
return requirements
29+
1330
setup(
14-
name='wifite',
31+
name='wifite2',
1532
version=Configuration.version,
1633
author='kimocoder',
1734
author_email='christian@aircrack-ng.org',
@@ -22,6 +39,11 @@
2239
},
2340
license='GNU GPLv2',
2441
scripts=['bin/wifite'],
42+
python_requires='>=3.10',
43+
install_requires=_read_requirements(),
44+
# Dev/test extras are defined once in pyproject.toml
45+
# ([project.optional-dependencies].dev), which poetry-core uses as the build
46+
# backend. Install them with: pip install -e ".[dev]"
2547
description='Wireless Network Auditor for Linux & Android',
2648
long_description=long_description,
2749
long_description_content_type='text/markdown',

tests/test_Handshake.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ def testHandshakeTshark(self):
6363
def testHandshakeCowpatty(self):
6464
print("\nTesting handshake with cowpatty...")
6565
hs_file = self.getFile('handshake_exists.cap')
66-
hs = Handshake(hs_file, bssid='A4:2B:8C:16:6B:3A')
66+
# cowpatty -c needs an ESSID, so pass it explicitly. (tshark/aircrack
67+
# tests don't need this — only cowpatty enforces it.)
68+
hs = Handshake(hs_file, bssid='A4:2B:8C:16:6B:3A',
69+
essid='Test Router Please Ignore')
6770
assert (len(hs.cowpatty_handshakes()) > 0), f'Expected len>0 but got len({len(hs.cowpatty_handshakes())})'
6871

6972
@pytest.mark.timeout(15)

wifite/attack/owe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ def _capture_owe_exchange(self):
237237
if client.station not in clients_seen:
238238
clients_seen.add(client.station)
239239
Color.pl('{+} {G}New client:{W} %s' % client.station)
240-
except Exception:
241-
pass
240+
except Exception as e:
241+
log_debug('AttackOWE', 'Client tracking error: %s' % e)
242242

243243
# Deauth to trigger re-association
244244
if deauth_timer.ended() and len(clients_seen) > 0:

wifite/model/handshake.py

Lines changed: 38 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -147,74 +147,54 @@ def aircrack_handshakes(self):
147147
else:
148148
return []
149149

150-
def hcxpcapngtool_handshakes(self):
151-
"""
152-
Returns tuple (BSSID,None) if hcxpcapngtool can extract valid handshake data.
153-
Supports both .cap and .pcapng capture files.
154-
"""
155-
if not Process.exists('hcxpcapngtool'):
156-
return []
157-
158-
import tempfile
159-
160-
# Create a temporary hash file to test if hcxpcapngtool can extract data
161-
hash_file = None
162-
try:
163-
with tempfile.NamedTemporaryFile(mode='w', suffix='.22000', delete=False) as tmp:
164-
hash_file = tmp.name
165-
166-
command = [
167-
'hcxpcapngtool',
168-
'-o', hash_file,
169-
self.capfile
170-
]
171-
172-
proc = Process(command, devnull=False)
173-
174-
# Check if hash file was created and has content
175-
if os.path.exists(hash_file) and os.path.getsize(hash_file) > 0:
176-
# Successfully extracted handshake data
177-
result = [(self.bssid, None)] if self.bssid else [(None, self.essid)]
178-
179-
# Clean up temp file
180-
try:
181-
os.remove(hash_file)
182-
except OSError:
183-
pass
184-
185-
return result
186-
else:
187-
# No valid handshake data found
188-
try:
189-
if os.path.exists(hash_file):
190-
os.remove(hash_file)
191-
except OSError:
192-
pass
193-
return []
194-
195-
except Exception:
196-
# If anything goes wrong, clean up and return empty
197-
try:
198-
if hash_file and os.path.exists(hash_file):
199-
os.remove(hash_file)
200-
except (OSError, NameError):
201-
pass
202-
return []
203-
204150
def analyze(self):
205151
"""Prints analysis of handshake capfile"""
206152
self.divine_bssid_and_essid()
207153

208154
if Tshark.exists():
209-
Handshake.print_pairs(self.tshark_handshakes(), 'tshark')
155+
self._print_tshark_analysis()
210156

211157
if Process.exists('cowpatty'):
212158
Handshake.print_pairs(self.cowpatty_handshakes(), 'cowpatty')
213159

214160
Handshake.print_pairs(self.aircrack_handshakes(), 'aircrack')
215-
216-
if Process.exists('hcxpcapngtool'):
217-
Handshake.print_pairs(self.hcxpcapngtool_handshakes(), 'hcxpcapng')
161+
162+
def _print_tshark_analysis(self):
163+
"""
164+
Print tshark verdict for this capfile.
165+
166+
On a full 4-way handshake, emits the standard 'contains a valid handshake'
167+
line. Otherwise inspects the EAPOL frames tshark *did* see and reports
168+
which messages are present and which are missing, so the user can tell
169+
the difference between "no EAPOL at all" and "M1+M2+M3 but no M4
170+
(still crackable by hashcat/cowpatty)".
171+
"""
172+
# Single tshark parse yields both the full-handshake verdict and, when
173+
# absent, the partial-4-way breakdown — no need to read the file twice.
174+
bssids, summary = Tshark.eapol_analysis(self.capfile, bssid=self.bssid)
175+
if bssids:
176+
Handshake.print_pairs([(bssid, None) for bssid in bssids], 'tshark')
177+
return
178+
179+
tool_str = '{C}%s{W}: ' % 'tshark'.rjust(8)
180+
181+
if not summary:
182+
Color.pl('{!} %s.cap file contains {O}no EAPOL frames{W} '
183+
'({R}no handshake to crack{W})' % tool_str)
184+
return
185+
186+
for bssid, msgs in summary.items():
187+
found = sorted(msgs)
188+
missing = [m for m in (1, 2, 3, 4) if m not in msgs]
189+
found_str = ', '.join('M%d' % m for m in found)
190+
missing_str = ', '.join('M%d' % m for m in missing)
191+
crackable = 2 in msgs and (1 in msgs or 3 in msgs)
192+
verdict = ('{G}crackable partial{W}'
193+
if crackable else
194+
'{R}not crackable{W}')
195+
Color.pl('{!} %s.cap file has {O}partial 4-way{W} for ({O}%s{W}): '
196+
'found {C}%s{W}, missing {R}%s{W} — %s'
197+
% (tool_str, bssid.upper(), found_str, missing_str, verdict))
218198

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

wifite/native/deauth.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,13 @@ def run(self):
351351
self.packets_sent += sent
352352
self.bursts_sent += 1
353353
self.last_burst_time = time.time()
354-
355-
except Exception:
356-
pass
357-
354+
355+
except Exception as e:
356+
# Best-effort burst loop: keep running, but leave a trace so a
357+
# persistent failure isn't completely invisible.
358+
from ..util.logger import log_debug
359+
log_debug('ScapyDeauth', 'Deauth burst failed: %s' % e)
360+
358361
# Wait for next burst
359362
self._stop_event.wait(timeout=self.interval)
360363

wifite/tools/john.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ def _read_stderr(self):
7070
# Progress line: "0g 0:00:00:03 13% (ETA: ...) 0g/s 500p/s ..."
7171
if re.match(r'\d+g\s+\d+:\d+:\d+:\d+', line):
7272
self._parse_progress(line)
73-
except Exception:
74-
pass
73+
except Exception as e:
74+
# Reader thread is best-effort (progress display only); log so a
75+
# crash here doesn't silently stop progress updates without a trace.
76+
from ..util.logger import log_debug
77+
log_debug('John', 'Progress reader thread stopped: %s' % e)
7578

7679
def _parse_progress(self, line):
7780
"""Update internal status from a john stderr progress line."""

wifite/tools/tshark.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,8 @@ def bssids_with_handshakes(cls, capfile, bssid=None):
116116
'-n',
117117
'-Y', 'eapol'
118118
]
119-
tshark = Process(command, devnull=False)
120-
121-
target_client_msg_nums = Tshark._build_target_client_handshake_map(tshark.stdout(), bssid=bssid)
119+
with Process(command, devnull=False) as tshark:
120+
target_client_msg_nums = Tshark._build_target_client_handshake_map(tshark.stdout(), bssid=bssid)
122121

123122
bssids = set()
124123
for (target_client, num) in list(target_client_msg_nums.items()):
@@ -128,6 +127,74 @@ def bssids_with_handshakes(cls, capfile, bssid=None):
128127

129128
return list(bssids)
130129

130+
@staticmethod
131+
def _build_eapol_summary(output, bssid=None):
132+
"""Build a per-AP map of which 4-way messages appear in tshark output.
133+
134+
Returns dict mapping bssid (lowercase) -> set of int msg numbers in
135+
{1,2,3,4}.
136+
"""
137+
summary = {}
138+
for line in output.split('\n'):
139+
src, dst, index, total = Tshark._extract_src_dst_index_total(line)
140+
if src is None:
141+
continue
142+
index = int(index)
143+
total = int(total)
144+
if total != 4:
145+
continue
146+
# Odd indexes (1, 3) are AP→client; even (2, 4) are client→AP.
147+
target = src if index % 2 == 1 else dst
148+
if bssid is not None and bssid.lower() != target.lower():
149+
continue
150+
summary.setdefault(target.lower(), set()).add(index)
151+
return summary
152+
153+
@classmethod
154+
def eapol_message_summary(cls, capfile, bssid=None):
155+
"""
156+
Inspect EAPOL traffic and return a per-AP map of which 4-way messages
157+
were observed.
158+
159+
Returns:
160+
dict mapping bssid (lowercase) -> set of int msg numbers in {1,2,3,4}.
161+
Empty dict if tshark isn't available or no EAPOL frames are present.
162+
"""
163+
if not cls.exists():
164+
return {}
165+
166+
command = ['tshark', '-r', capfile, '-n', '-Y', 'eapol']
167+
with Process(command, devnull=False) as tshark:
168+
output = tshark.stdout()
169+
return cls._build_eapol_summary(output, bssid=bssid)
170+
171+
@classmethod
172+
def eapol_analysis(cls, capfile, bssid=None):
173+
"""Run tshark once and derive both EAPOL verdicts from a single parse.
174+
175+
Combines the work of bssids_with_handshakes() and
176+
eapol_message_summary() so the capfile is only read once when the caller
177+
needs both (e.g. handshake analysis that falls back to a partial-4-way
178+
report when no complete handshake is present).
179+
180+
Returns:
181+
(bssids, summary) tuple where `bssids` is the list of APs with a
182+
complete sequential 4-way handshake and `summary` is the per-AP map
183+
of observed message numbers (see eapol_message_summary). Both are
184+
empty if tshark is unavailable or no EAPOL frames are present.
185+
"""
186+
if not cls.exists():
187+
return [], {}
188+
189+
command = ['tshark', '-r', capfile, '-n', '-Y', 'eapol']
190+
with Process(command, devnull=False) as tshark:
191+
output = tshark.stdout()
192+
193+
handshake_map = cls._build_target_client_handshake_map(output, bssid=bssid)
194+
bssids = list({key.split(',')[0] for key, num in handshake_map.items() if num == 4})
195+
summary = cls._build_eapol_summary(output, bssid=bssid)
196+
return bssids, summary
197+
131198
@classmethod
132199
def bssids_with_handshakes_native(cls, capfile, bssid=None):
133200
"""

wifite/ui/tui.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,16 @@ def update(self, renderable):
152152
if self.should_update():
153153
try:
154154
self.live.update(renderable, refresh=True)
155-
except Exception as e:
155+
except Exception:
156156
# If update fails, try to recover or fail gracefully
157157
try:
158158
# Attempt to refresh without update
159159
if self.live is not None:
160160
self.live.refresh()
161-
except (AttributeError, TypeError, ImportError):
162-
# Complete failure - stop TUI
161+
except Exception:
162+
# Complete failure — stop TUI so the caller can fall back
163+
# to classic output instead of hammering a broken Live.
163164
self.is_running = False
164-
pass
165165

166166
def __enter__(self):
167167
"""

wifite/util/tui_logger.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ def initialize(cls, enabled: bool = False, debug_mode: bool = False, log_file: s
2828
Args:
2929
enabled: Whether logging is enabled
3030
debug_mode: Whether debug mode is active
31-
log_file: Path to log file (default: /tmp/wifite_tui.log)
31+
log_file: Path to log file (default: ~/.config/wifite/wifite_tui.log,
32+
in an owner-only 0700 directory; falls back to ~ if that
33+
directory can't be created)
3234
"""
3335
cls._enabled = enabled
3436
cls._debug_mode = debug_mode

0 commit comments

Comments
 (0)