Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHYXPc5u9FfVNS8auAUgC4
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""airodump-ng CSV parsing + wireless-interface discovery for the wifi deauth tool."""
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
def run(cmd, timeout=30):
|
|
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
|
|
|
|
def list_wifi_interfaces():
|
|
"""[{iface, phy, type, addr, monitor_capable}] from `iw dev`."""
|
|
try:
|
|
out = run(["iw", "dev"]).stdout
|
|
except Exception:
|
|
return []
|
|
ifaces, cur_phy, cur = [], None, None
|
|
for line in out.splitlines():
|
|
m = re.match(r"phy#(\d+)", line)
|
|
if m:
|
|
cur_phy = "phy" + m.group(1)
|
|
m = re.match(r"\s+Interface (\S+)", line)
|
|
if m:
|
|
if cur:
|
|
ifaces.append(cur)
|
|
cur = {"iface": m.group(1), "phy": cur_phy, "type": "?", "addr": "?"}
|
|
m = re.match(r"\s+addr (\S+)", line)
|
|
if m and cur:
|
|
cur["addr"] = m.group(1)
|
|
m = re.match(r"\s+type (\S+)", line)
|
|
if m and cur:
|
|
cur["type"] = m.group(1)
|
|
if cur:
|
|
ifaces.append(cur)
|
|
for it in ifaces:
|
|
it["monitor_capable"] = _phy_has_monitor(it["phy"])
|
|
return ifaces
|
|
|
|
|
|
def _phy_has_monitor(phy):
|
|
if not phy:
|
|
return False
|
|
out = run(["iw", phy, "info"]).stdout
|
|
grab = False
|
|
for line in out.splitlines():
|
|
if "Supported interface modes:" in line:
|
|
grab = True
|
|
continue
|
|
if grab:
|
|
if re.match(r"\s+\*", line):
|
|
if "monitor" in line:
|
|
return True
|
|
else:
|
|
break
|
|
return False
|
|
|
|
|
|
def parse_airodump_csv(path):
|
|
try:
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
text = f.read()
|
|
except OSError:
|
|
return [], []
|
|
aps, stations, section = [], [], None
|
|
for line in text.splitlines():
|
|
s = line.strip()
|
|
if not s:
|
|
continue
|
|
if s.startswith("BSSID,"):
|
|
section = "ap"
|
|
continue
|
|
if s.startswith("Station MAC,"):
|
|
section = "sta"
|
|
continue
|
|
# naive comma split; an ESSID containing a comma will skew fields.
|
|
parts = [c.strip() for c in line.split(",")]
|
|
if section == "ap" and len(parts) >= 14:
|
|
aps.append({
|
|
"bssid": parts[0], "channel": parts[3], "power": parts[8],
|
|
"privacy": parts[5], "essid": parts[13],
|
|
})
|
|
elif section == "sta" and len(parts) >= 6:
|
|
stations.append({
|
|
"station": parts[0], "power": parts[3],
|
|
"packets": parts[4], "bssid": parts[5],
|
|
})
|
|
return aps, stations
|
|
|
|
|
|
def _selftest():
|
|
sample = (
|
|
"BSSID, First time seen, Last time seen, channel, Speed, Privacy, Cipher, "
|
|
"Authentication, Power, # beacons, # IV, LAN IP, ID-length, ESSID, Key\r\n"
|
|
"\r\n"
|
|
"AA:BB:CC:DD:EE:FF, 2024-01-01 00:00:00, 2024-01-01 00:01:00, 6, 130, "
|
|
"WPA2, CCMP, PSK, -40, 10, 0, 0.0.0.0, 6, MyWiFi, \r\n"
|
|
"\r\n"
|
|
"Station MAC, First time seen, Last time seen, Power, # packets, BSSID, "
|
|
"Probed ESSIDs\r\n"
|
|
"\r\n"
|
|
"11:22:33:44:55:66, 2024-01-01 00:00:10, 2024-01-01 00:01:00, -50, 25, "
|
|
"AA:BB:CC:DD:EE:FF, MyWiFi\r\n"
|
|
"99:88:77:66:55:44, 2024-01-01 00:00:10, 2024-01-01 00:01:00, -60, 5, "
|
|
"(not associated), Other\r\n"
|
|
)
|
|
p = os.path.join(tempfile.gettempdir(), "wd_selftest.csv")
|
|
with open(p, "w") as f:
|
|
f.write(sample)
|
|
aps, sts = parse_airodump_csv(p)
|
|
os.remove(p)
|
|
assert len(aps) == 1, aps
|
|
assert aps[0]["essid"] == "MyWiFi" and aps[0]["channel"] == "6", aps
|
|
assert len(sts) == 2, sts
|
|
assoc = [s for s in sts if s["bssid"].lower() == "aa:bb:cc:dd:ee:ff"]
|
|
assert len(assoc) == 1 and assoc[0]["station"] == "11:22:33:44:55:66", sts
|
|
print("selftest ok")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if "--selftest" in sys.argv:
|
|
_selftest()
|
|
sys.exit(0)
|