hak5cmd on this firmware has no CLIENT_KICK command and PINEAPPLE_DEAUTH_CLIENT requires (bssid, target, channel) — the UI kick/deauth buttons and the MCP pineap.kick_client tool previously passed only the client MAC, which printed usage and silently did nothing. - _deauth_target/_deauth_client_via_iface resolve the client's association interface via iwinfo (Access Point + Channel), pick the band-aware inject interface, and issue DEAUTH_CLIENT <bssid> <mac> <channel> - h_client_kick: deny-filter (persistent) + immediate deauth with rc checks - h_deauth_client: deauth with the full form; 502 with detail when the client is not associated - MCP pineap.kick_client: resolves the client first (no side effects on failure), then deny-filter + deauth; verified on device (clean error for unassociated clients, filter list restored) - tests updated for the new command chain
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
import os
|
|
import sys
|
|
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)
|
|
|
|
_orig_device_run = server.device_run
|
|
_orig_assoc_clients = server.assoc_clients
|
|
_orig_iface_ap_info = server._iface_ap_info
|
|
_orig_pineap = server._pineap
|
|
|
|
|
|
class NormalizeTest(unittest.TestCase):
|
|
def test_normalize(self):
|
|
self.assertEqual(server.normalize_mac(' 00:11:22:33:44:55 '), '00:11:22:33:44:55')
|
|
self.assertEqual(server.normalize_mac('aa:bb:cc:dd:ee:ff'), 'AA:BB:CC:DD:EE:FF')
|
|
|
|
def test_invalid_returns_none(self):
|
|
self.assertIsNone(server.normalize_mac('nope'))
|
|
|
|
|
|
class ClientsTest(unittest.TestCase):
|
|
def tearDown(self):
|
|
server.device_run = _orig_device_run
|
|
server.assoc_clients = _orig_assoc_clients
|
|
server._iface_ap_info = _orig_iface_ap_info
|
|
server._pineap = _orig_pineap
|
|
|
|
def test_clients_handler(self):
|
|
server.assoc_clients = lambda: [{'mac': 'AA:BB:CC:DD:EE:FF', 'iface': 'wlan0open', 'rssi': -55}]
|
|
class Ctx:
|
|
args = ()
|
|
status, payload = server.h_clients(Ctx())
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload['count'], 1)
|
|
|
|
def _kick_env(self):
|
|
calls = []
|
|
def fake_run(argv, timeout=30):
|
|
calls.append(argv)
|
|
return 0, '', ''
|
|
server.device_run = fake_run
|
|
server.assoc_clients = lambda: [{'mac': '00:11:22:33:44:55', 'iface': 'wlan0open', 'rssi': -55}]
|
|
server._iface_ap_info = lambda iface: ('AA:BB:CC:DD:EE:FF', 6)
|
|
server._pineap = lambda *a, **k: (0, '', '')
|
|
return calls
|
|
|
|
def test_kick_validates_and_deny_adds(self):
|
|
calls = self._kick_env()
|
|
server.h_client_kick(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
|
|
self.assertIn([server.HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_ADD', 'deny', '00:11:22:33:44:55'], calls)
|
|
# The immediate deauth must use the full bssid/target/channel form.
|
|
self.assertTrue(any(c[:4] == [server.HAK5CMD, 'DEAUTH_CLIENT', 'AA:BB:CC:DD:EE:FF',
|
|
'00:11:22:33:44:55'] and c[4] == '6' for c in calls))
|
|
|
|
def test_kick_not_associated_still_filters(self):
|
|
calls = []
|
|
def fake_run(argv, timeout=30):
|
|
calls.append(argv)
|
|
return 0, '', ''
|
|
server.device_run = fake_run
|
|
server.assoc_clients = lambda: []
|
|
status, payload = server.h_client_kick(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
|
|
self.assertEqual(status, 200)
|
|
self.assertIs(payload['deauth'], False)
|
|
self.assertTrue(any(c == [server.HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_ADD', 'deny',
|
|
'00:11:22:33:44:55'] for c in calls))
|
|
|
|
def test_kick_bad_mac_400(self):
|
|
status, payload = server.h_client_kick(type('C', (), {'args': (), 'body': {'mac': 'x'}})())
|
|
self.assertEqual(status, 400)
|
|
|
|
def test_deauth_client(self):
|
|
calls = []
|
|
def fake_run(argv, timeout=30):
|
|
calls.append(argv)
|
|
return 0, '', ''
|
|
server.device_run = fake_run
|
|
server.assoc_clients = lambda: [{'mac': '00:11:22:33:44:55', 'iface': 'wlan1wpa', 'rssi': -60}]
|
|
server._iface_ap_info = lambda iface: ('AA:BB:CC:DD:EE:FF', 149)
|
|
status, payload = server.h_deauth_client(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
|
|
self.assertEqual(status, 200)
|
|
# 5 GHz client -> wlan1mon inject, no _pineap pin needed.
|
|
self.assertTrue(any(c[:4] == [server.HAK5CMD, 'DEAUTH_CLIENT', 'AA:BB:CC:DD:EE:FF',
|
|
'00:11:22:33:44:55'] and c[4] == '149' for c in calls))
|
|
|
|
def test_deauth_client_not_associated_502(self):
|
|
server.assoc_clients = lambda: []
|
|
status, payload = server.h_deauth_client(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
|
|
self.assertEqual(status, 502)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|