Skip to content

Commit 1308f94

Browse files
committed
tools: implement hcxdumptool passive pmkid
1 parent 6fad5e2 commit 1308f94

16 files changed

Lines changed: 2550 additions & 4 deletions

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ Features
188188

189189
### Attack Methods
190190
* **[PMKID hash capture](https://hashcat.net/forum/thread-7717.html)** - Fast, clientless WPA/WPA2 attack (enabled by default)
191+
* **Passive PMKID Sniffing** - Continuous, untargeted PMKID capture from all nearby networks (use with: `--pmkid-passive`)
191192
* **WPS Pixie-Dust Attack** - Offline WPS PIN recovery (enabled by default, force with: `--wps-only --pixie`)
192193
* **WPS PIN Attack** - Online WPS brute-force (enabled by default, force with: `--wps-only --no-pixie`)
193194
* **WPA/WPA2 Handshake Capture** - Traditional 4-way handshake attack (enabled by default, force with: `--no-wps`)
@@ -359,6 +360,7 @@ sudo wifite -vv
359360
- Use GPU acceleration (CUDA/OpenCL) for best performance
360361
- Consider using cloud-based cracking services for large wordlists
361362

363+
362364
### Resume Feature
363365

364366
Wifite automatically saves your attack progress and allows you to resume interrupted sessions:
@@ -558,6 +560,11 @@ For Dual Interface specific help:
558560
sudo wifite -h -v | grep -A 20 "DUAL INTERFACE"
559561
```
560562

563+
For Passive PMKID specific help:
564+
```bash
565+
sudo wifite -h -v | grep -A 10 "PMKID"
566+
```
567+
561568

562569
Credits & Acknowledgments
563570
-------------------------

tests/test_hcxdumptool.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
sys.path.insert(0, '..')
99

10-
from wifite.tools.hcxdumptool import HcxDumpTool
10+
from wifite.tools.hcxdumptool import HcxDumpTool, HcxDumpToolPassive
1111
from wifite.config import Configuration
1212

1313

@@ -165,5 +165,153 @@ def test_command_building_triple_interface(self, mock_process, mock_config_init)
165165
self.assertEqual(call_args[i_indices[2] + 1], 'wlan2')
166166

167167

168+
class TestHcxDumpToolPassive(unittest.TestCase):
169+
"""Test suite for HcxDumpToolPassive class"""
170+
171+
def setUp(self):
172+
"""Set up test fixtures"""
173+
Configuration.interface = None
174+
175+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
176+
def test_initialization_with_interface(self, mock_config_init):
177+
"""Test initialization with explicit interface"""
178+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
179+
180+
self.assertEqual(tool.interface, 'wlan0')
181+
self.assertEqual(tool.output_file, '/tmp/passive.pcapng')
182+
self.assertIsNone(tool.pid)
183+
self.assertIsNone(tool.proc)
184+
185+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
186+
@patch('wifite.tools.hcxdumptool.Configuration.temp')
187+
def test_initialization_with_defaults(self, mock_temp, mock_config_init):
188+
"""Test initialization with default output file"""
189+
mock_temp.return_value = '/tmp/wifite_'
190+
Configuration.interface = 'wlan0'
191+
192+
tool = HcxDumpToolPassive()
193+
194+
self.assertEqual(tool.interface, 'wlan0')
195+
self.assertEqual(tool.output_file, '/tmp/wifite_passive_pmkid.pcapng')
196+
197+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
198+
def test_initialization_no_interface_raises_error(self, mock_config_init):
199+
"""Test that missing interface raises exception"""
200+
Configuration.interface = None
201+
202+
with self.assertRaises(Exception) as context:
203+
HcxDumpToolPassive()
204+
205+
self.assertIn('must be defined', str(context.exception))
206+
207+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
208+
@patch('wifite.tools.hcxdumptool.Process')
209+
@patch('wifite.tools.hcxdumptool.time.sleep')
210+
def test_enter_starts_process(self, mock_sleep, mock_process, mock_config_init):
211+
"""Test that __enter__ starts hcxdumptool with correct flags"""
212+
mock_proc_instance = MagicMock()
213+
mock_proc_instance.pid.pid = 12345
214+
mock_proc_instance.poll.return_value = None
215+
mock_process.return_value = mock_proc_instance
216+
217+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
218+
219+
with tool:
220+
# Verify Process was called
221+
mock_process.assert_called_once()
222+
223+
# Get the command that was passed to Process
224+
call_args = mock_process.call_args[0][0]
225+
226+
# Verify command structure
227+
self.assertEqual(call_args[0], 'hcxdumptool')
228+
self.assertIn('-i', call_args)
229+
self.assertIn('wlan0', call_args)
230+
self.assertIn('--rds=3', call_args)
231+
self.assertIn('-w', call_args)
232+
self.assertIn('/tmp/passive.pcapng', call_args)
233+
self.assertIn('--enable_status=15', call_args)
234+
235+
# Verify PID was set
236+
self.assertEqual(tool.pid, 12345)
237+
self.assertIsNotNone(tool.proc)
238+
239+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
240+
@patch('wifite.tools.hcxdumptool.Process')
241+
@patch('wifite.tools.hcxdumptool.time.sleep')
242+
def test_is_running_when_active(self, mock_sleep, mock_process, mock_config_init):
243+
"""Test is_running returns True when process is active"""
244+
mock_proc_instance = MagicMock()
245+
mock_proc_instance.pid.pid = 12345
246+
mock_proc_instance.poll.return_value = None # Process is running
247+
mock_process.return_value = mock_proc_instance
248+
249+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
250+
251+
with tool:
252+
self.assertTrue(tool.is_running())
253+
254+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
255+
@patch('wifite.tools.hcxdumptool.Process')
256+
@patch('wifite.tools.hcxdumptool.time.sleep')
257+
def test_is_running_when_stopped(self, mock_sleep, mock_process, mock_config_init):
258+
"""Test is_running returns False when process has stopped"""
259+
mock_proc_instance = MagicMock()
260+
mock_proc_instance.pid.pid = 12345
261+
mock_proc_instance.poll.return_value = 0 # Process has exited
262+
mock_process.return_value = mock_proc_instance
263+
264+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
265+
266+
with tool:
267+
self.assertFalse(tool.is_running())
268+
269+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
270+
@patch('wifite.tools.hcxdumptool.os.path.exists')
271+
@patch('wifite.tools.hcxdumptool.os.path.getsize')
272+
def test_get_capture_size_file_exists(self, mock_getsize, mock_exists, mock_config_init):
273+
"""Test get_capture_size returns file size when file exists"""
274+
mock_exists.return_value = True
275+
mock_getsize.return_value = 1024000 # 1MB
276+
277+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
278+
279+
size = tool.get_capture_size()
280+
self.assertEqual(size, 1024000)
281+
mock_exists.assert_called_once_with('/tmp/passive.pcapng')
282+
mock_getsize.assert_called_once_with('/tmp/passive.pcapng')
283+
284+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
285+
@patch('wifite.tools.hcxdumptool.os.path.exists')
286+
def test_get_capture_size_file_not_exists(self, mock_exists, mock_config_init):
287+
"""Test get_capture_size returns 0 when file doesn't exist"""
288+
mock_exists.return_value = False
289+
290+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
291+
292+
size = tool.get_capture_size()
293+
self.assertEqual(size, 0)
294+
mock_exists.assert_called_once_with('/tmp/passive.pcapng')
295+
296+
@patch('wifite.tools.hcxdumptool.Configuration.initialize')
297+
@patch('wifite.tools.hcxdumptool.Process')
298+
@patch('wifite.tools.hcxdumptool.time.sleep')
299+
@patch('wifite.tools.hcxdumptool.os.kill')
300+
def test_exit_stops_process(self, mock_kill, mock_sleep, mock_process, mock_config_init):
301+
"""Test that __exit__ stops the process gracefully"""
302+
mock_proc_instance = MagicMock()
303+
mock_proc_instance.pid.pid = 12345
304+
mock_proc_instance.poll.return_value = None # Process is running
305+
mock_process.return_value = mock_proc_instance
306+
307+
tool = HcxDumpToolPassive(interface='wlan0', output_file='/tmp/passive.pcapng')
308+
309+
with tool:
310+
pass # Exit the context
311+
312+
# Verify interrupt was called
313+
mock_proc_instance.interrupt.assert_called_once()
314+
315+
168316
if __name__ == '__main__':
169317
unittest.main()

wifite/args.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ def get_arguments(self):
239239
self._add_wps_args(parser.add_argument_group(Color.s('{C}WPS{W}')))
240240
self._add_pmkid_args(parser.add_argument_group(Color.s('{C}PMKID{W}')))
241241
self._add_eviltwin_args(parser.add_argument_group(Color.s('{C}EVIL TWIN{W}')))
242+
self._add_wpasec_args(parser.add_argument_group(Color.s('{C}WPA-SEC UPLOAD{W}')))
242243
self._add_command_args(parser.add_argument_group(Color.s('{C}COMMANDS{W}')))
243244

244245
return parser.parse_args()
@@ -830,6 +831,113 @@ def _add_pmkid_args(self, pmkid):
830831
type=int,
831832
help=Color.s('Time to wait for PMKID capture (default: {G}%d{W} seconds)'
832833
% self.config.pmkid_timeout))
834+
835+
# Passive PMKID capture arguments
836+
pmkid.add_argument('--pmkid-passive',
837+
action='store_true',
838+
dest='pmkid_passive',
839+
help=Color.s('Passive PMKID capture mode: Sniff all networks '
840+
'without deauth. {R}WARNING:{W} Requires authorization. '
841+
'(default: {G}off{W})'))
842+
843+
pmkid.add_argument('--pmkid-sniff',
844+
action='store_true',
845+
dest='pmkid_passive',
846+
help=argparse.SUPPRESS) # Alias for --pmkid-passive
847+
848+
pmkid.add_argument('--pmkid-passive-duration',
849+
action='store',
850+
dest='pmkid_passive_duration',
851+
metavar='[seconds]',
852+
type=int,
853+
help=self._verbose('Duration for passive capture in seconds '
854+
'(default: {G}infinite{W})'))
855+
856+
pmkid.add_argument('--pmkid-passive-interval',
857+
action='store',
858+
dest='pmkid_passive_interval',
859+
metavar='[seconds]',
860+
type=int,
861+
help=self._verbose('Interval between hash extractions '
862+
'(default: {G}%d{W} seconds)'
863+
% self.config.pmkid_passive_interval))
864+
865+
def _add_wpasec_args(self, wpasec):
866+
"""
867+
Add wpa-sec upload command-line arguments to argument parser.
868+
869+
Defines all wpa-sec related arguments including:
870+
- --wpasec: Enable upload functionality
871+
- --wpasec-key: API key for authentication
872+
- --wpasec-auto: Automatic upload mode
873+
- --wpasec-url: Custom server URL
874+
- --wpasec-timeout: Connection timeout
875+
- --wpasec-email: Notification email
876+
- --wpasec-remove: Remove files after upload
877+
878+
Args:
879+
wpasec: Argument group for wpa-sec options
880+
881+
Example:
882+
>>> wpasec_group = parser.add_argument_group('WPA-SEC UPLOAD')
883+
>>> self._add_wpasec_args(wpasec_group)
884+
"""
885+
wpasec.add_argument('--wpasec',
886+
action='store_true',
887+
dest='wpasec_enabled',
888+
help=Color.s('Enable {C}wpa-sec.stanev.org{W} upload functionality. '
889+
'Uploads captured handshakes to online cracking service. '
890+
'Requires API key from wpa-sec.stanev.org (default: {G}off{W})'))
891+
892+
wpasec.add_argument('--wpasec-key',
893+
action='store',
894+
dest='wpasec_api_key',
895+
metavar='[key]',
896+
type=str,
897+
help=Color.s('API key for {C}wpa-sec.stanev.org{W}. '
898+
'Register at wpa-sec.stanev.org to obtain your key. '
899+
'Required for uploading captures.'))
900+
901+
wpasec.add_argument('--wpasec-auto',
902+
action='store_true',
903+
dest='wpasec_auto_upload',
904+
help=Color.s('Automatically upload all captured handshakes without prompting. '
905+
'When disabled, you will be asked before each upload. '
906+
'(default: {G}off{W})'))
907+
908+
wpasec.add_argument('--wpasec-url',
909+
action='store',
910+
dest='wpasec_url',
911+
metavar='[url]',
912+
type=str,
913+
help=self._verbose('Custom wpa-sec server URL. '
914+
'Use this to upload to alternative wpa-sec instances. '
915+
'(default: {G}https://wpa-sec.stanev.org{W})'))
916+
917+
wpasec.add_argument('--wpasec-timeout',
918+
action='store',
919+
dest='wpasec_timeout',
920+
metavar='[seconds]',
921+
type=int,
922+
help=self._verbose('Connection timeout for uploads in seconds. '
923+
'Increase if you have a slow connection. '
924+
'(default: {G}30{W} seconds)'))
925+
926+
wpasec.add_argument('--wpasec-email',
927+
action='store',
928+
dest='wpasec_email',
929+
metavar='[email]',
930+
type=str,
931+
help=self._verbose('Email address for wpa-sec notifications. '
932+
'Receive alerts when passwords are cracked. '
933+
'(default: {G}none{W})'))
934+
935+
wpasec.add_argument('--wpasec-remove',
936+
action='store_true',
937+
dest='wpasec_remove_after_upload',
938+
help=self._verbose('Remove capture files after successful upload. '
939+
'Saves disk space but prevents local cracking. '
940+
'(default: {G}off{W})'))
833941

834942
@staticmethod
835943
def _add_command_args(commands):

wifite/attack/pmkid.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ..util.output import OutputManager
1010
from ..model.pmkid_result import CrackResultPMKID
1111
from ..tools.airodump import Airodump
12+
from ..util.wpasec_uploader import WpaSecUploader
1213
from threading import Thread, active_count
1314
import os
1415
import time
@@ -162,6 +163,27 @@ def run_hashcat(self):
162163
self.view.add_log(f"PMKID hash ready: {os.path.basename(pmkid_file)}")
163164
self.view.add_log("Proceeding to crack phase...")
164165

166+
# Upload to wpa-sec if configured
167+
# Note: wpa-sec only accepts pcap/pcapng files, not .22000 hash files
168+
# Upload the original pcapng capture file instead of the hash file
169+
if WpaSecUploader.should_upload():
170+
if self.view:
171+
self.view.add_log("Checking wpa-sec upload configuration...")
172+
173+
# Use the pcapng file if it exists, otherwise skip upload
174+
if os.path.exists(self.pcapng_file):
175+
WpaSecUploader.upload_capture(
176+
self.pcapng_file,
177+
self.target.bssid,
178+
self.target.essid,
179+
capture_type='pmkid',
180+
view=self.view
181+
)
182+
else:
183+
Color.pl('{!} {O}wpa-sec upload skipped: pcapng file not found{W}')
184+
if self.view:
185+
self.view.add_log("wpa-sec upload skipped: pcapng file not found")
186+
165187
# Check for the --skip-crack flag
166188
if Configuration.skip_crack:
167189
if self.view:

0 commit comments

Comments
 (0)