Files
Mark-VIII/tests/test_attacks.py
T
c4ch3c4d3 88d7141d45 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
2026-08-23 19:50:41 -06:00

737 lines
31 KiB
Python

import os
import shutil
import sqlite3
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui'))
import server
def setUpModule():
__import__('importlib').reload(server)
def ctx(body=None, query=None):
return type('C', (), {'body': body, 'args': (), 'query': query or {}})()
class FakeUciDevice:
"""In-memory uci + device_run fake: 'uci set wireless.X=Y' state."""
def __init__(self):
self.state = {}
self.runs = []
self.sock = []
self._verify = True
def device_run(self, args, timeout=20, input_data=None):
self.runs.append((list(args), input_data))
a = list(args)
if a[:2] == ['uci', 'set']:
k, _, v = a[2].partition('=')
self.state[k] = v
elif a[:2] == ['uci', 'get']:
return (0, self.state.get(a[2], '') + '\n', '')
elif a[:2] == ['uci', 'delete']:
for k in list(self.state):
if k == a[2] or k.startswith(a[2] + '.'):
del self.state[k]
elif a[:2] == ['uci', 'commit']:
pass
elif a[0] == 'uci' and a[1] == 'show':
sec = a[2]
return (0, ''.join("%s=%s\n" % (k, v) for k, v in self.state.items()
if k == sec or k.startswith(sec + '.')), '')
elif a[0] == 'hostapd_cli' and a[-1] == 'status':
return (0, 'state=ENABLED\nssid[0]=test\n', '')
return (0, '', '')
def uci_iface(self, name):
cfg = {}
prefix = 'wireless.%s.' % name
for k, v in self.state.items():
if k.startswith(prefix):
cfg[k[len(prefix):]] = v
if not cfg:
return {}
return cfg
def daemon_sock_call(self, method, path, body=None, timeout=10):
self.sock.append((method, path, body))
if path == '/api/pineap/hostapd/get_config':
return 200, {'pineape_disabled': False, 'pineape_auth_pass': True}
if path == '/api/pineap/get_config':
return 200, {'autossidpool': True}
return 200, {'success': True}
class AttacksDeployTest(unittest.TestCase):
def setUp(self):
self.f = FakeUciDevice()
server.device_run = self.f.device_run
server.daemon_sock_call = self.f.daemon_sock_call
server._uci_wifi_iface = self.f.uci_iface
server._uci_section = self.f.uci_iface
server._verify_iface = lambda name, timeout=20: self.f._verify
server._allow_all_ssids = lambda: True
self.tmp = tempfile.mkdtemp(prefix='pager-attacks-')
self.old_state = server.PINEAP_STATE_FILE
server.PINEAP_STATE_FILE = os.path.join(self.tmp, 'state.json')
self.old_ent = {k: getattr(server, k) for k in
('ENT_CONF', 'ENT_PIDFILE', 'ENT_EAP_USERS', 'ENT_STATE',
'ENT_DIR', 'ENT_CA_CERT', 'ENT_SERVER_CERT', 'ENT_SERVER_KEY',
'ENT_LOG', 'ENT_CAPTURES', 'ENT_DH_FILE')}
server.ENT_CONF = os.path.join(self.tmp, 'enterprise.conf')
server.ENT_PIDFILE = os.path.join(self.tmp, 'mk8.pid')
server.ENT_EAP_USERS = os.path.join(self.tmp, 'eap_users')
server.ENT_STATE = os.path.join(self.tmp, 'state.json')
server.ENT_DIR = os.path.join(self.tmp, 'ent')
server.ENT_CA_CERT = os.path.join(server.ENT_DIR, 'ca.pem')
server.ENT_SERVER_CERT = os.path.join(server.ENT_DIR, 'server.pem')
server.ENT_SERVER_KEY = os.path.join(server.ENT_DIR, 'server.key')
server.ENT_LOG = os.path.join(server.ENT_DIR, 'hostapd.log')
server.ENT_CAPTURES = os.path.join(server.ENT_DIR, 'captures.json')
server.ENT_DH_FILE = os.path.join(server.ENT_DIR, 'dh.pem')
# The fake device_run cannot execute real openssl; pre-seed the cert
# files so _ensure_ent_certs() short-circuits to True.
os.makedirs(server.ENT_DIR, exist_ok=True)
for p in (server.ENT_CA_CERT, server.ENT_SERVER_CERT,
server.ENT_SERVER_KEY, server.ENT_DH_FILE):
with open(p, 'w') as f:
f.write('stub\n')
self.old_ent_running = server._ent_running
self.old_ent_state = server._ent_state_loaded
server._ent_running = lambda: True
server._ent_state_loaded = lambda: {'ssid': 'CorpAP', 'channel': 36}
def tearDown(self):
server.PINEAP_STATE_FILE = self.old_state
server._ent_running = self.old_ent_running
server._ent_state_loaded = self.old_ent_state
for k, v in self.old_ent.items():
setattr(server, k, v)
shutil.rmtree(self.tmp)
def test_deploy_wpa_2g4_calls_daemon_and_enables_engine(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'TargetNet', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'hidden': False, 'channel': 6}))
self.assertEqual(status, 200)
self.assertTrue(payload['ok'])
self.assertTrue(payload['verified'])
cfg = [s for s in self.f.sock if s[0] == 'PUT' and s[1] == '/api/settings/wifi/set_ap'][0][2]
self.assertEqual(cfg['configs'][0]['interface'], 'wlan0wpa')
self.assertEqual(cfg['configs'][0]['ssid'], 'TargetNet')
self.assertEqual(cfg['configs'][0]['key'], 'secretpass1')
self.assertEqual(cfg['configs'][0]['enctype'], 'psk2')
engines = [s for s in self.f.sock
if s[1] in ('/api/pineap/hostapd/enable_pineap',
'/api/pineap/mimic/enable')]
self.assertEqual(len(engines), 2)
def test_deploy_wpa_2g4_stops_enterprise_ap(self):
server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'TargetNet', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'hidden': False, 'channel': 6}))
cmds = [r[0] for r in self.f.runs]
self.assertIn(['iw', 'dev', 'wlan1ent', 'del'], cmds)
def test_deploy_wpa_5g_writes_radio1(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'Corp', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'hidden': False, 'channel': 36}))
self.assertEqual(status, 200)
self.assertEqual(self.f.state['wireless.wlan1wpa.ssid'], 'Corp')
self.assertEqual(self.f.state['wireless.wlan1wpa.encryption'], 'psk2')
self.assertEqual(self.f.state['wireless.radio1.channel'], '36')
self.assertEqual(self.f.state['pineapd.wlan1mon.hop'], '0')
self.assertEqual(payload['iface'], 'wlan1wpa')
self.assertEqual(payload['band'], server.BAND_5G)
def test_deploy_wpa_auto_channel_defaults_to_1_without_recon(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'UnknownNet', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'hidden': False}))
self.assertEqual(status, 200)
self.assertTrue(payload['auto'])
self.assertEqual(payload['channel'], 1)
self.assertEqual(payload['band'], server.BAND_2G)
cfg = [s for s in self.f.sock if s[0] == 'PUT' and s[1] == '/api/settings/wifi/set_ap'][0][2]
self.assertEqual(cfg['configs'][0]['channel'], 1)
def test_deploy_wpa_auto_channel_uses_recon_target_channel(self):
db = self._make_recon_db()
old = server.RECON_DB
server.RECON_DB = db
try:
status, payload = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'Anderson-5', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'hidden': False}))
finally:
server.RECON_DB = old
os.unlink(db)
self.assertEqual(status, 200)
self.assertEqual(payload['channel'], 149)
self.assertEqual(payload['band'], server.BAND_5G)
self.assertEqual(self.f.state['wireless.radio1.channel'], '149')
def _make_recon_db(self):
fd, db = tempfile.mkstemp(suffix='.db')
os.close(fd)
conn = sqlite3.connect(db)
conn.executescript(
'CREATE TABLE scan(id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT,'
' time INT, name TEXT);'
'CREATE TABLE wifi_device(hash INT PRIMARY KEY, scan INT, mac TEXT,'
' time INT, signal INT, freq INT, packets INT);'
'CREATE TABLE ssid(hash INT PRIMARY KEY, wifi_device INT, scan INT,'
' type INT, bssid TEXT, ssid BLOB, hidden INT, time INT, signal INT,'
' freq INT, channel INT, encryption INT);')
conn.execute("INSERT INTO scan (uuid, time, name) VALUES ('u1', 1, 'pager')")
conn.execute("INSERT INTO ssid (hash, wifi_device, scan, type, bssid, ssid, hidden,"
" time, signal, freq, channel, encryption) VALUES"
" (10, 1, 1, 8, 'C89E43648080', X'416E646572736F6E2D35', 0,"
" 1786466532, -76, 5745, 149, 0x400400108)")
conn.commit()
conn.close()
return db
def test_deploy_open_2g4_includes_bssid_and_country(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'open', 'ssid': 'Guest', 'hidden': False,
'channel': 1, 'country': 'US',
'bssid': 'DE:AD:BE:EF:00:01'}))
self.assertEqual(status, 200)
self.assertEqual(payload['iface'], 'wlan0open')
cfg = [s for s in self.f.sock if s[1] == '/api/settings/wifi/set_ap'][0][2]
self.assertEqual(cfg['configs'][0]['bssid'], 'DE:AD:BE:EF:00:01')
def test_deploy_enterprise_uses_standalone_phy1_engine(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'enterprise', 'ssid': 'CorpAP', 'passphrase': 'anypass',
'enctype': 'wpa2', 'hidden': False, 'channel': 36}))
self.assertEqual(status, 200)
self.assertEqual(payload['iface'], 'wlan1ent')
self.assertEqual(payload['band'], server.BAND_5G)
cmds = [r[0] for r in self.f.runs]
self.assertIn(['iw', 'phy', 'phy1', 'interface', 'add', 'wlan1ent',
'type', 'managed'], cmds)
self.assertIn(['iw', 'dev', 'wlan1ent', 'set', 'type', 'ap'], cmds)
self.assertTrue(any(c[:4] == ['/usr/sbin/hostapd', '-B', '-P', server.ENT_PIDFILE]
for c in cmds))
self.assertEqual(self.f.state['pineapd.@hostapd[0].mgmtiface'], 'wlan1ent')
self.assertEqual(self.f.state['pineapd.@hostapd[0].pineape_auth_pass'], '0')
self.assertEqual(self.f.state['pineapd.wlan1mon.hop'], '0')
with open(server.ENT_CONF) as f:
conf = f.read()
self.assertIn('ca_cert=', conf)
self.assertIn('server_cert=', conf)
self.assertIn('private_key=', conf)
self.assertIn('ieee8021x=1', conf)
self.assertIn('eap_server=1', conf)
self.assertNotIn('eap_server_identity', conf)
self.assertNotIn('eap_server_erp', conf)
self.assertNotIn('dh_file=', conf)
self.assertIn('ieee80211w=0', conf)
with open(server.ENT_EAP_USERS) as f:
users = f.read()
self.assertIn('PEAP,TTLS', users)
self.assertIn('[2]', users)
# Phase-2 entries must use a quoted empty prefix: hostapd never
# wildcard-matches a bare `*` identity for phase2 lookups.
self.assertIn('""*', users)
self.assertNotRegex(users, r'(?m)^\*\t.*\[2\]$')
def test_eap_users_text_grammar_matches_pineapple_wpad(self):
text = server._eap_users_text('secret123', 'any')
lines = text.splitlines()
self.assertEqual(lines[0], '*\tPEAP,TTLS')
self.assertTrue(lines[1].startswith('""*\t'))
self.assertIn('"secret123"', lines[1])
self.assertTrue(lines[1].endswith('[2]'))
# A bare-wildcard phase-2 line is the firmware-broken grammar.
for line in lines:
if line.endswith('[2]'):
self.assertFalse(line.startswith('*\t'))
def test_deploy_enterprise_rejects_non_5g_channel(self):
status, _ = server.h_attacks_deploy(ctx({
'kind': 'enterprise', 'ssid': 'CorpAP', 'enctype': 'wpa2',
'channel': 6}))
self.assertEqual(status, 400)
def test_stop_enterprise_tears_down_engine(self):
server._ent_running = lambda: True
server._ent_state_loaded = lambda: {'ssid': 'CorpAP', 'channel': 36}
self.f.state['pineapd.@hostapd[0].pineape_auth_pass'] = '0'
status, payload = server.h_attacks_stop(ctx({'kind': 'enterprise'}))
self.assertEqual(status, 200)
self.assertIn('wlan1ent', payload['stopped'])
cmds = [r[0] for r in self.f.runs]
self.assertIn(['iw', 'dev', 'wlan1ent', 'del'], cmds)
# Deploy disabled PineAPE auth passthrough; stop must restore it.
self.assertEqual(self.f.state['pineapd.@hostapd[0].pineape_auth_pass'],
'1')
def test_deploy_enterprise_writes_passphrase_and_hidden(self):
status, payload = server.h_attacks_deploy(ctx({
'kind': 'enterprise', 'ssid': 'CorpAP', 'passphrase': 'labsecret',
'enctype': 'wpa2', 'hidden': True, 'channel': 36}))
self.assertEqual(status, 200)
with open(server.ENT_EAP_USERS) as f:
users = f.read()
self.assertIn('labsecret', users)
self.assertIn('PEAP,TTLS', users)
self.assertIn('[2]', users)
with open(server.ENT_CONF) as f:
conf = f.read()
self.assertIn('ignore_broadcast_ssid=1', conf)
self.assertIn('ca_cert=', conf)
def test_eap_secret_sanitizes_quotes(self):
self.assertEqual(server._eap_secret(''), 'dummy')
self.assertEqual(server._eap_secret('ab"c\ndef'), 'abcdef')
def test_hostapd_unknown_items_parse_pager_error(self):
err = ("Line 14: unknown configuration item 'eap_server_identity'\n"
"1 errors found in configuration file '/root/loot/enterprise.conf'\n"
"Failed to set up interface with /root/loot/enterprise.conf\n"
"Failed to initialize interface\n")
self.assertEqual(server._hostapd_unknown_items(err), ['eap_server_identity'])
def test_drop_hostapd_keys_removes_only_named_lines(self):
conf = ('interface=wlan1ent\n'
'eap_server=1\n'
'eap_server_identity=hostapd\n'
'dh_file=/tmp/dh.pem\n')
new, changed = server._drop_hostapd_keys(
conf, ['eap_server_identity', 'dh_file'])
self.assertTrue(changed)
self.assertIn('eap_server=1', new)
self.assertIn('interface=wlan1ent', new)
self.assertNotIn('eap_server_identity', new)
self.assertNotIn('dh_file=', new)
def test_start_ent_hostapd_strips_unknown_keys_and_retries(self):
with open(server.ENT_CONF, 'w') as f:
f.write('interface=wlan1ent\neap_server=1\neap_server_identity=hostapd\n')
orig = server.device_run
seen = []
def wrapped(args, timeout=20, input_data=None):
a = list(args)
if a and a[0] == '/usr/sbin/hostapd':
with open(server.ENT_CONF) as fh:
text = fh.read()
seen.append(text)
if 'eap_server_identity' in text:
return (1, '',
"Line 3: unknown configuration item 'eap_server_identity'\n"
"1 errors found in configuration file '%s'\n"
"Failed to initialize interface\n" % server.ENT_CONF)
return (0, '', '')
return orig(args, timeout=timeout, input_data=input_data)
server.device_run = wrapped
try:
rc, _out, _err = server._start_ent_hostapd()
finally:
server.device_run = orig
self.assertEqual(rc, 0)
self.assertGreaterEqual(len(seen), 2)
with open(server.ENT_CONF) as f:
conf = f.read()
self.assertNotIn('eap_server_identity', conf)
self.assertIn('eap_server=1', conf)
def test_deploy_validation(self):
status, _ = server.h_attacks_deploy(ctx({'kind': 'wpa', 'ssid': ''}))
self.assertEqual(status, 400)
status, _ = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'X', 'passphrase': 'short',
'enctype': 'psk2', 'channel': 1}))
self.assertEqual(status, 400)
status, _ = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'X', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'channel': 200}))
self.assertEqual(status, 400)
status, _ = server.h_attacks_deploy(ctx({'kind': 'bogus'}))
self.assertEqual(status, 400)
def test_stop_wpa_disables_both_bands(self):
self.f.state['wireless.wlan0wpa.disabled'] = '0'
self.f.state['wireless.wlan1wpa.disabled'] = '0'
self.f.state['pineapd.wlan1mon.hop'] = '0'
status, payload = server.h_attacks_stop(ctx({'kind': 'wpa'}))
self.assertEqual(status, 200)
self.assertEqual(self.f.state['wireless.wlan0wpa.disabled'], '1')
self.assertEqual(self.f.state['wireless.wlan1wpa.disabled'], '1')
self.assertIn('wlan0wpa', payload['stopped'])
self.assertEqual(self.f.state['pineapd.wlan1mon.hop'], '1')
class AttacksDeauthTest(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_deauth_2g4_uses_wlan0mon_inject(self):
status, payload = server.h_attacks_deauth(ctx({
'bssid': 'AA:BB:CC:DD:EE:FF', 'client': '11:22:33:44:55:66',
'channel': 6}))
self.assertEqual(status, 200)
self.assertEqual(payload['inject'], 'wlan0mon')
calls = [r[0] for r in self.f.runs]
self.assertIn(['_pineap', 'INTERFACE', 'INJECT', 'wlan0mon'], calls)
self.assertIn(['/usr/bin/hak5cmd', 'PINEAPPLE_DEAUTH_CLIENT', 'AA:BB:CC:DD:EE:FF',
'11:22:33:44:55:66', '6'], calls)
def test_deauth_5g_keeps_wlan1mon_inject(self):
status, payload = server.h_attacks_deauth(ctx({
'bssid': 'AA:BB:CC:DD:EE:FF', 'client': '11:22:33:44:55:66',
'channel': 36}))
self.assertEqual(status, 200)
self.assertEqual(payload['inject'], 'wlan1mon')
def test_deauth_bad_macs_rejected(self):
status, _ = server.h_attacks_deauth(ctx({
'bssid': 'nope', 'client': '11:22:33:44:55:66', 'channel': 6}))
self.assertEqual(status, 400)
class AttackFilterModeTest(unittest.TestCase):
def test_allow_all_ssids_uses_full_network_filter_app_name(self):
calls = []
old_run = server.device_run
server.device_run = lambda args, timeout=20, input_data=None: (
calls.append(list(args)) or (0, '', ''))
try:
self.assertTrue(server._allow_all_ssids())
finally:
server.device_run = old_run
self.assertEqual(calls, [[server.HAK5CMD,
'PINEAPPLE_NETWORK_FILTER_MODE', 'deny']])
class AttacksExportTest(unittest.TestCase):
def setUp(self):
self.f = FakeUciDevice()
self.f._verify = True
server.device_run = self.f.device_run
def fake_run(args, timeout=20, input_data=None):
self.f.runs.append((list(args), input_data))
a = list(args)
if a[0] == 'ls':
return (0, 'a.pcap\nb.cap\n', '')
if a[0] == 'hcxpcapngtool':
return (0, '', '')
return (0, '', '')
server.device_run = fake_run
server.daemon_sock_call = lambda method, path, body=None, timeout=10: (200, {})
self._real_exists = os.path.exists
self._real_getsize = os.path.getsize
server.os.path.exists = lambda p: p.endswith('.hc22000') or p.startswith('/sys')
server.os.path.getsize = lambda p: 12
def tearDown(self):
server.os.path.exists = self._real_exists
server.os.path.getsize = self._real_getsize
try:
os.unlink('/tmp/mk8test.hc22000')
except OSError:
pass
def test_export_converts_captures(self):
status, payload = server.h_attacks_export_hc22000(ctx())
self.assertEqual(status, 200)
self.assertEqual(payload['size'], 12)
self.assertIn('hashcat -m 22000', payload['hashcat'])
hc = [r[0] for r in self.f.runs if r[0][0] == 'hcxpcapngtool'][0]
self.assertEqual(hc[1], '-o')
self.assertTrue(hc[2].startswith('/root/loot/hc22000/'))
self.assertTrue(hc[2].endswith('.hc22000'))
self.assertIn('/root/loot/handshakes/a.pcap', hc)
self.assertIn('/root/loot/pcap/b.cap', hc)
class AttacksStatusTest(unittest.TestCase):
def setUp(self):
self.saved = {
'_count_table': server._count_table,
'_ent_summary': server._ent_summary,
'daemon_sock_call': server.daemon_sock_call,
'_uci_ap_summary': server._uci_ap_summary,
'_read_hop': server._read_hop,
}
def tearDown(self):
for name, fn in self.saved.items():
setattr(server, name, fn)
def test_status_exposes_enterprise_ap_for_ui(self):
server._count_table = lambda t: {
'hostap_handshake': 2, 'hostap_basic': 3, 'hostap_chalresp': 1
}.get(t, 0)
server._ent_summary = lambda detail=True: {
'enabled': True, 'live': True, 'ssid': 'CorpLab',
'iface': 'wlan1ent', 'stations': ['AA:BB:CC:DD:EE:FF'],
'auth_method': 'mschapv2', 'certs': True, 'captures': 4,
'ctrl_linked': True,
}
server.daemon_sock_call = lambda *a, **k: (200, {'pineape_disabled': False})
server._uci_ap_summary = lambda *a, **k: None
server._read_hop = lambda: '1'
status, payload = server.h_attacks_status(ctx())
self.assertEqual(status, 200)
self.assertTrue(payload['enterprise']['ap']['live'])
self.assertEqual(payload['enterprise']['ap']['ssid'], 'CorpLab')
self.assertEqual(payload['enterprise']['identities'], 3)
self.assertEqual(payload['enterprise']['mschapv2'], 1)
self.assertEqual(payload['enterprise']['creds'], 4)
self.assertEqual(payload['handshakes'], 2)
self.assertTrue(payload['enterprise']['pineape']['enabled'])
class AttacksCaptureTest(unittest.TestCase):
def setUp(self):
self.f = FakeUciDevice()
server.device_run = self.f.device_run
server.daemon_sock_call = lambda *a, **k: (200, {})
server._uci_wifi_iface = self.f.uci_iface
server._uci_section = self.f.uci_iface
server._verify_iface = lambda name, timeout=20: self.f._verify
self.pidfile = tempfile.mktemp(prefix='mk8-cap-test-')
self.real_exists = os.path.exists
def tearDown(self):
os.path.exists = self.real_exists
try:
os.unlink(self.pidfile)
except OSError:
pass
def test_status_reports_dead_pid_as_stopped_stale(self):
with open(self.pidfile, 'w') as f:
f.write('999999\n')
running, pid, stale = server._capture_state(self.pidfile, 'wlan0mon')
self.assertFalse(running)
self.assertTrue(stale)
self.assertFalse(os.path.exists(self.pidfile),
'stale pidfile must be cleaned up')
def test_status_kills_capture_when_iface_dropped(self):
with open(self.pidfile, 'w') as f:
f.write('4242\n')
real_exists = os.path.exists
killed = []
old_run = server.device_run
def fake_run(args, timeout=20, input_data=None):
if args[0] == 'kill':
killed.append(args[1])
return (0, '', '')
server.device_run = fake_run
try:
os.path.exists = lambda p: p.startswith('/proc/4242')
running, pid, stale = server._capture_state(
self.pidfile, 'wlan0mon')
finally:
os.path.exists = real_exists
server.device_run = old_run
self.assertFalse(running)
self.assertTrue(stale)
self.assertEqual(killed, ['4242'])
def test_status_live_capture_running(self):
with open(self.pidfile, 'w') as f:
f.write(str(os.getpid()))
real_exists = os.path.exists
try:
# /proc/<pid> exists for our own process; iface path faked up.
os.path.exists = lambda p: (
not p.startswith('/sys/class/net') or p.endswith('wlan0mon'))
running, pid, stale = server._capture_state(
self.pidfile, 'wlan0mon')
finally:
os.path.exists = real_exists
self.assertTrue(running)
self.assertFalse(stale)
def test_deploy_retries_radio0_set_ap_when_iface_never_verifies(self):
calls = {'set_ap': 0}
def sock(method, path, body=None, timeout=10):
if path == '/api/settings/wifi/set_ap':
calls['set_ap'] += 1
return (200, {})
server.daemon_sock_call = sock
self.f._verify = False
status, payload = server.h_attacks_deploy(ctx({
'kind': 'open', 'ssid': 'Guest', 'channel': 1}))
self.assertEqual(status, 200)
self.assertFalse(payload['verified'])
self.assertEqual(calls['set_ap'], 2,
'radio0 deploy must retry set_ap once')
def test_deploy_5g_does_not_retry_set_ap(self):
calls = {'set_ap': 0}
def sock(method, path, body=None, timeout=10):
if path == '/api/settings/wifi/set_ap':
calls['set_ap'] += 1
return (200, {})
server.daemon_sock_call = sock
self.f._verify = False
status, payload = server.h_attacks_deploy(ctx({
'kind': 'wpa', 'ssid': 'Corp', 'passphrase': 'secretpass1',
'enctype': 'psk2', 'channel': 36}))
self.assertEqual(status, 200)
self.assertEqual(calls['set_ap'], 0,
'5 GHz path writes UCI directly, no set_ap')
self.assertEqual(self.f.state['pineapd.@hostapd[0].mgmtiface'],
'wlan1wpa')
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