feat(deauth,evilportal,capture): bulk deauth UX, Hak5-compatible Evil Portal, monitor capture fixes
- Recon AP focus sidebar: 'Deauth All Clients' with engagement-scope confirm - Deauth Targeting card: 'Deauth All' behind the same scope confirmation - New POST /api/attacks/deauth/bulk (max 32 targets, per-target results) - Evil Portal tab: import EvilPortalNano-format portal zips into /mmc/mk8/portals, serve active portal on port 80 to unauthenticated clients via a minimal PHP shim, capture all form POSTs (.logs in stock MyPortal.php format + captures.jsonl), dnsmasq address=/#/ DNS hijack - OpenAP: Evil Portal template dropdown (greyed when none), activated with the attack and stopped with it - Monitor Capture fix: iface-less status now reports whichever monitor is actually capturing; pcap dir mkdir'd; tcpdump stderr surfaced instead of discarded
This commit is contained in:
@@ -604,3 +604,133 @@ class AttacksCaptureTest(unittest.TestCase):
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
class AttacksDeauthBulkTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.f = FakeUciDevice()
|
||||
self.f._verify = True
|
||||
server.device_run = self.f.device_run
|
||||
server.daemon_sock_call = self.f.daemon_sock_call
|
||||
server._uci_wifi_iface = self.f.uci_iface
|
||||
|
||||
def test_bulk_deauth_all_targets(self):
|
||||
targets = [
|
||||
{'bssid': 'AA:BB:CC:DD:EE:FF', 'client': '11:22:33:44:55:66',
|
||||
'channel': 6},
|
||||
{'bssid': 'AA:BB:CC:DD:EE:FF', 'client': '22:22:33:44:55:66',
|
||||
'channel': 36},
|
||||
]
|
||||
status, payload = server.h_attacks_deauth_bulk(ctx({'targets': targets}))
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload['sent'], 2)
|
||||
self.assertEqual(payload['failed'], 0)
|
||||
calls = [r[0] for r in self.f.runs]
|
||||
self.assertIn(['/usr/bin/hak5cmd', 'PINEAPPLE_DEAUTH_CLIENT',
|
||||
'AA:BB:CC:DD:EE:FF', '11:22:33:44:55:66', '6'], calls)
|
||||
self.assertIn(['/usr/bin/hak5cmd', 'PINEAPPLE_DEAUTH_CLIENT',
|
||||
'AA:BB:CC:DD:EE:FF', '22:22:33:44:55:66', '36'], calls)
|
||||
|
||||
def test_bulk_deauth_mixed_validity_reports_per_target(self):
|
||||
targets = [
|
||||
{'bssid': 'AA:BB:CC:DD:EE:FF', 'client': '11:22:33:44:55:66',
|
||||
'channel': 6},
|
||||
{'bssid': 'nope', 'client': '22:22:33:44:55:66', 'channel': 6},
|
||||
]
|
||||
status, payload = server.h_attacks_deauth_bulk(ctx({'targets': targets}))
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload['sent'], 1)
|
||||
self.assertEqual(payload['failed'], 1)
|
||||
self.assertFalse(payload['results'][1]['ok'])
|
||||
self.assertEqual(payload['results'][1]['error'], 'invalid AP MAC')
|
||||
|
||||
def test_bulk_deauth_rejects_empty_and_oversized(self):
|
||||
status, _ = server.h_attacks_deauth_bulk(ctx({'targets': []}))
|
||||
self.assertEqual(status, 400)
|
||||
status, _ = server.h_attacks_deauth_bulk(ctx({}))
|
||||
self.assertEqual(status, 400)
|
||||
big = [{'bssid': 'AA:BB:CC:DD:EE:FF', 'client': '11:22:33:44:55:%02d' % (i % 256),
|
||||
'channel': 6} for i in range(33)]
|
||||
status, payload = server.h_attacks_deauth_bulk(ctx({'targets': big}))
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
|
||||
class AttacksCaptureStatusBothIfacesTest(unittest.TestCase):
|
||||
"""The UI polls status without an iface; the handler must report the
|
||||
monitor that actually has a live capture (regression: wlan1mon captures
|
||||
flipped back to 'Not capturing' within one 5s poll)."""
|
||||
|
||||
def setUp(self):
|
||||
self.f = FakeUciDevice()
|
||||
server.device_run = self.f.device_run
|
||||
self.pidfiles = ['/tmp/mk8_capture_wlan0mon.pid',
|
||||
'/tmp/mk8_capture_wlan1mon.pid']
|
||||
self.real_exists = os.path.exists
|
||||
for p in self.pidfiles:
|
||||
try:
|
||||
os.unlink(p)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
os.path.exists = self.real_exists
|
||||
for p in self.pidfiles:
|
||||
try:
|
||||
os.unlink(p)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _live_pidfile(self, iface):
|
||||
with open('/tmp/mk8_capture_%s.pid' % iface, 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
def test_status_without_iface_finds_running_wlan1mon(self):
|
||||
self._live_pidfile('wlan1mon')
|
||||
# Own pid always exists in /proc; pretend the wlan1mon netdev exists.
|
||||
os.path.exists = lambda p: (
|
||||
not p.startswith('/sys/class/net') or p.endswith('wlan1mon'))
|
||||
status, payload = server.h_attacks_capture(ctx({'action': 'status'}))
|
||||
self.assertEqual(status, 200)
|
||||
self.assertTrue(payload['running'])
|
||||
self.assertEqual(payload['iface'], 'wlan1mon')
|
||||
|
||||
def test_status_without_iface_defaults_when_none_running(self):
|
||||
status, payload = server.h_attacks_capture(ctx({'action': 'status'}))
|
||||
self.assertEqual(status, 200)
|
||||
self.assertFalse(payload['running'])
|
||||
self.assertIn(payload['iface'], ('wlan0mon', 'wlan1mon'))
|
||||
|
||||
def test_start_mkdirs_pcap_dir_and_logs_stderr(self):
|
||||
calls = []
|
||||
|
||||
def fake_run(args, timeout=20, input_data=None):
|
||||
calls.append(list(args))
|
||||
if args[:2] == ['sh', '-c'] and 'echo $!' in args[2]:
|
||||
with open('/tmp/mk8_capture_wlan1mon.pid', 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
return (0, '', '')
|
||||
|
||||
old_exists = os.path.exists
|
||||
server.device_run = fake_run
|
||||
# /proc does not exist on dev hosts; fake liveness for our own pid.
|
||||
os.path.exists = lambda p: (
|
||||
p.startswith('/proc/') or
|
||||
not p.startswith('/sys/class/net') or p.endswith('wlan1mon'))
|
||||
try:
|
||||
status, payload = server.h_attacks_capture(ctx({
|
||||
'action': 'start', 'iface': 'wlan1mon'}))
|
||||
finally:
|
||||
server.device_run = self.f.device_run
|
||||
os.path.exists = old_exists
|
||||
self.assertEqual(status, 200)
|
||||
self.assertTrue(payload['running'])
|
||||
self.assertTrue(any(a[:3] == ['mkdir', '-p', '/root/loot/pcap']
|
||||
for a in calls),
|
||||
'capture dir must be created before starting tcpdump')
|
||||
sh_cmd = next(a[2] for a in calls if a[:2] == ['sh', '-c'])
|
||||
self.assertNotIn('/dev/null', sh_cmd)
|
||||
self.assertIn('mk8_capture_wlan1mon.log', sh_cmd)
|
||||
try:
|
||||
os.unlink('/tmp/mk8_capture_wlan1mon.pid')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user