fix(portals,capture): round-2 validation fixes, live-verified on Pager
- portals: replace zipfile with struct+zlib ZIP writer in portal download (python3-light has no zipfile; GET /api/portals/<name>/download 500ed) - capture: revive watchdog re-arms the 5 GHz deploy auto-capture if the post-deploy radio settle kills it (was: empty pcap, dead tcpdump) - capture: route GET /api/attacks/capture to status (was unrouted -> 404) New tests/test_validation_fixes2.py covers each fix (TDD); full suite (30 modules) green. Live-verified: download CRC-clean via stock zipfile, capture survived settle window and revived automatically (56 MB pcap), GET status returns proper JSON. Round-2 validation report added at docs/validation/ (8/9 attack types PASS against in-scope networks; enterprise PARTIAL per firmware limits).
This commit is contained in:
@@ -3812,6 +3812,8 @@ def _deploy_wpa_open(kind, fields):
|
||||
# loot the 4-ways from a pinned monitor capture instead.
|
||||
try:
|
||||
capture = _ensure_attack_capture('wlan1mon')
|
||||
if capture:
|
||||
_schedule_capture_revive('wlan1mon')
|
||||
except Exception:
|
||||
capture = None
|
||||
return {'kind': kind, 'ssid': ssid, 'iface': iface, 'band': band,
|
||||
@@ -4673,6 +4675,47 @@ def _teardown_attack_capture(iface):
|
||||
return running
|
||||
|
||||
|
||||
CAPTURE_REVIVE_CHECKS = 6
|
||||
CAPTURE_REVIVE_INTERVAL = 10.0
|
||||
|
||||
|
||||
def _capture_revive_once(iface):
|
||||
"""Re-arm the auto-capture if it died (e.g. radios dropped during the
|
||||
post-deploy settle window). No-op while a live capture runs or when the
|
||||
monitor iface itself is gone."""
|
||||
pidfile = '/tmp/mk8_capture_%s.pid' % iface
|
||||
running, _, _ = _capture_state(pidfile, iface)
|
||||
if running:
|
||||
return None
|
||||
if not os.path.exists('/sys/class/net/%s' % iface):
|
||||
return None
|
||||
return _ensure_attack_capture(iface)
|
||||
|
||||
|
||||
def _schedule_capture_revive(iface='wlan1mon',
|
||||
checks=CAPTURE_REVIVE_CHECKS,
|
||||
interval=CAPTURE_REVIVE_INTERVAL):
|
||||
"""Watch a freshly armed capture through the deploy settle window.
|
||||
|
||||
The 5 GHz attack AP bring-up can recreate monitors / reload wifi, which
|
||||
kills the just-started tcpdump; poll a few times and restart it once the
|
||||
radios have converged. Runs in a daemon thread, self-terminates."""
|
||||
def _revive():
|
||||
for _ in range(checks):
|
||||
time.sleep(interval)
|
||||
try:
|
||||
result = _capture_revive_once(iface)
|
||||
except Exception:
|
||||
result = None
|
||||
if result is not None or _capture_state(
|
||||
'/tmp/mk8_capture_%s.pid' % iface, iface)[0]:
|
||||
# Restarted, or the original capture is alive again.
|
||||
return
|
||||
thread = threading.Thread(target=_revive, daemon=True)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
def h_attacks_capture(ctx):
|
||||
body = ctx.body or {}
|
||||
action = body.get('action') or 'status'
|
||||
@@ -4897,6 +4940,41 @@ def _portal_list():
|
||||
|
||||
_EOCD_SIG = b'PK\x05\x06'
|
||||
_CDH_SIG = b'PK\x01\x02'
|
||||
_LFH_SIG = b'PK\x03\x04'
|
||||
|
||||
|
||||
def _zip_create(files):
|
||||
"""Minimal ZIP writer built on struct+zlib only.
|
||||
|
||||
python3-light has no zipfile, so portal download serializes stored
|
||||
(uncompressed) entries by hand: local file headers, central directory,
|
||||
end-of-central-directory. Returns archive bytes; names must be plain
|
||||
relative paths."""
|
||||
import struct
|
||||
try:
|
||||
import zlib
|
||||
except ImportError:
|
||||
zlib = None
|
||||
out = bytearray()
|
||||
central = bytearray()
|
||||
for name in sorted(files):
|
||||
data = files[name]
|
||||
nbuf = name.encode('utf-8')
|
||||
crc = zlib.crc32(data) & 0xFFFFFFFF if zlib is not None else 0
|
||||
offset = len(out)
|
||||
out += struct.pack('<4sHHHHHIIIHH', _LFH_SIG, 20, 0, 0, 0, 0,
|
||||
crc, len(data), len(data), len(nbuf), 0)
|
||||
out += nbuf
|
||||
out += data
|
||||
central += struct.pack('<4sHHHHHHIIIHHHHHII', _CDH_SIG, 20, 20, 0, 0,
|
||||
0, 0, crc, len(data), len(data), len(nbuf),
|
||||
0, 0, 0, 0, 0, offset)
|
||||
central += nbuf
|
||||
cd_off = len(out)
|
||||
out += central
|
||||
out += struct.pack('<4sHHHHIIH', _EOCD_SIG, 0, 0, len(files),
|
||||
len(files), len(central), cd_off, 0)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _zip_entries(data_bytes):
|
||||
@@ -5393,23 +5471,23 @@ def h_portal_logs(ctx):
|
||||
|
||||
|
||||
def h_portal_download(ctx):
|
||||
import zipfile
|
||||
import io as _io
|
||||
name = ctx.args[0] if ctx.args else ''
|
||||
root = _portal_root(name)
|
||||
if not root or not os.path.isdir(root):
|
||||
return 404, {'error': 'portal not found'}
|
||||
buf = _io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for dirpath, _, files in os.walk(root):
|
||||
for fn in files:
|
||||
full = os.path.join(dirpath, fn)
|
||||
rel = os.path.relpath(full, root)
|
||||
try:
|
||||
zf.write(full, os.path.join(name, rel))
|
||||
except OSError:
|
||||
continue
|
||||
return 200, Download(buf.getvalue(), 'application/zip',
|
||||
files = {}
|
||||
for dirpath, _, fnames in os.walk(root):
|
||||
for fn in fnames:
|
||||
full = os.path.join(dirpath, fn)
|
||||
rel = os.path.relpath(full, root)
|
||||
try:
|
||||
with open(full, 'rb') as f:
|
||||
files['%s/%s' % (name, rel.replace(os.sep, '/'))] = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if not files:
|
||||
return 404, {'error': 'portal is empty'}
|
||||
return 200, Download(_zip_create(files), 'application/zip',
|
||||
'%s.zip' % name)
|
||||
|
||||
|
||||
@@ -7909,6 +7987,7 @@ ROUTER.add('POST', r'/api/attacks/deploy', h_attacks_deploy)
|
||||
ROUTER.add('POST', r'/api/attacks/stop', h_attacks_stop)
|
||||
ROUTER.add('GET', r'/api/attacks/status', h_attacks_status)
|
||||
ROUTER.add('POST', r'/api/attacks/capture', h_attacks_capture)
|
||||
ROUTER.add('GET', r'/api/attacks/capture', h_attacks_capture)
|
||||
ROUTER.add('GET', r'/api/attacks/export/hc22000', h_attacks_export_hc22000)
|
||||
ROUTER.add('GET', r'/api/attacks/export/hc22000/([^/]+)', h_attacks_download_hc22000)
|
||||
ROUTER.add('POST', r'/api/attacks/deauth', h_attacks_deauth)
|
||||
|
||||
Reference in New Issue
Block a user