diff --git a/intercept.py b/intercept.py index 8402853..b83c23f 100755 --- a/intercept.py +++ b/intercept.py @@ -65,6 +65,39 @@ AIRTAG_PREFIXES = ['4C:00'] # Apple continuity TILE_PREFIXES = ['C4:E7', 'DC:54', 'E4:B0', 'F8:8A'] SAMSUNG_TRACKER = ['58:4D', 'A0:75'] +# Drone detection patterns +DRONE_SSID_PATTERNS = [ + # DJI + 'DJI-', 'DJI_', 'Mavic', 'Phantom', 'Spark-', 'Mini-', 'Air-', 'Inspire', + 'Matrice', 'Avata', 'FPV-', 'Osmo', 'RoboMaster', 'Tello', + # Parrot + 'Parrot', 'Bebop', 'Anafi', 'Disco-', 'Mambo', 'Swing', + # Autel + 'Autel', 'EVO-', 'Dragonfish', 'Lite+', 'Nano', + # Skydio + 'Skydio', + # Other brands + 'Holy Stone', 'Potensic', 'SYMA', 'Hubsan', 'Eachine', 'FIMI', + 'Xiaomi_FIMI', 'Yuneec', 'Typhoon', 'PowerVision', 'PowerEgg', + # Generic drone patterns + 'Drone', 'UAV-', 'Quadcopter', 'FPV_', 'RC-Drone' +] + +# Drone OUI prefixes (MAC address prefixes for drone manufacturers) +DRONE_OUI_PREFIXES = { + # DJI + '60:60:1F': 'DJI', '48:1C:B9': 'DJI', '34:D2:62': 'DJI', 'E0:DB:55': 'DJI', + 'C8:6C:87': 'DJI', 'A0:14:3D': 'DJI', '70:D7:11': 'DJI', '98:3A:56': 'DJI', + # Parrot + '90:03:B7': 'Parrot', 'A0:14:3D': 'Parrot', '00:12:1C': 'Parrot', '00:26:7E': 'Parrot', + # Autel + '8C:F5:A3': 'Autel', 'D8:E0:E1': 'Autel', + # Yuneec + '60:60:1F': 'Yuneec', + # Skydio + 'F8:0F:6F': 'Skydio', +} + # OUI Database for manufacturer lookup (expanded) OUI_DATABASE = { # Apple (extensive list) @@ -1658,6 +1691,12 @@ HTML_TEMPLATE = ''' Disable Monitor +
+ +
Monitor mode: Inactive
@@ -1715,6 +1754,37 @@ HTML_TEMPLATE = ''' + + + @@ -1773,23 +1843,9 @@ HTML_TEMPLATE = ''' -
- - -
- - -
-

Attack Options

-
- ⚠ Authorized testing only -
-
- - - - -
+
+ `; + alertDiv.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1a1a2e; border: 2px solid var(--accent-orange); padding: 20px; border-radius: 8px; z-index: 10000; text-align: center; box-shadow: 0 0 30px rgba(255,165,0,0.5); min-width: 280px;'; + document.body.appendChild(alertDiv); + setTimeout(() => { if (alertDiv.parentElement) alertDiv.remove(); }, 15000); + } + // Initialize watch list display function initWatchList() { updateWatchListDisplay(); @@ -3216,13 +3376,29 @@ HTML_TEMPLATE = ''' } // Check for rogue APs (same SSID, different BSSID) - function checkRogueAP(ssid, bssid) { + function checkRogueAP(ssid, bssid, channel, signal) { if (!ssid || ssid === 'Hidden' || ssid === '[Hidden]') return false; if (!ssidToBssids[ssid]) { ssidToBssids[ssid] = new Set(); } + // Store details for this BSSID + if (!rogueApDetails[ssid]) { + rogueApDetails[ssid] = []; + } + + // Check if we already have this BSSID stored + const existingEntry = rogueApDetails[ssid].find(e => e.bssid === bssid); + if (!existingEntry) { + rogueApDetails[ssid].push({ + bssid: bssid, + channel: channel || '?', + signal: signal || '?', + firstSeen: new Date().toLocaleTimeString() + }); + } + const isNewBssid = !ssidToBssids[ssid].has(bssid); ssidToBssids[ssid].add(bssid); @@ -3231,12 +3407,88 @@ HTML_TEMPLATE = ''' rogueApCount++; document.getElementById('rogueApCount').textContent = rogueApCount; playAlert(); - showInfo(`⚠ Potential Rogue AP: "${ssid}" seen on multiple BSSIDs (${ssidToBssids[ssid].size} total)`); + + // Get the BSSIDs to show in alert + const bssidList = rogueApDetails[ssid].map(e => e.bssid).join(', '); + showInfo(`⚠ Rogue AP: "${ssid}" has ${ssidToBssids[ssid].size} BSSIDs: ${bssidList}`); return true; } return false; } + // Show rogue AP details popup + function showRogueApDetails() { + const rogueSSIDs = Object.keys(rogueApDetails).filter(ssid => + rogueApDetails[ssid].length > 1 + ); + + if (rogueSSIDs.length === 0) { + showInfo('No rogue APs detected. Rogue AP = same SSID on multiple BSSIDs.'); + return; + } + + // Remove existing popup if any + const existing = document.getElementById('rogueApPopup'); + if (existing) existing.remove(); + + // Build details HTML + let html = '
'; + rogueSSIDs.forEach(ssid => { + const aps = rogueApDetails[ssid]; + html += `
+
+ šŸ“” "${ssid}" (${aps.length} BSSIDs) +
+ + + + + + + `; + aps.forEach((ap, idx) => { + const bgColor = idx % 2 === 0 ? 'rgba(255,255,255,0.05)' : 'transparent'; + html += ` + + + + + `; + }); + html += '
BSSIDCHSignalFirst Seen
${ap.bssid}${ap.channel}${ap.signal} dBm${ap.firstSeen}
'; + }); + html += '
'; + html += '
⚠ Multiple BSSIDs for same SSID may indicate rogue AP or legitimate multi-AP setup
'; + + // Create popup + const popup = document.createElement('div'); + popup.id = 'rogueApPopup'; + popup.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--bg-primary); + border: 1px solid var(--accent-red); + border-radius: 8px; + padding: 16px; + z-index: 10000; + min-width: 400px; + max-width: 600px; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + `; + popup.innerHTML = ` +
+ 🚨 Rogue AP Details + +
+ ${html} + `; + + document.body.appendChild(popup); + } + // Update 5GHz channel graph function updateChannel5gGraph() { const bars = document.querySelectorAll('#channelGraph5g .channel-bar'); @@ -3301,6 +3553,8 @@ HTML_TEMPLATE = ''' return; } + const killProcesses = document.getElementById('killProcesses').checked; + // Show loading state const btn = document.getElementById('monitorStartBtn'); const originalText = btn.textContent; @@ -3310,7 +3564,7 @@ HTML_TEMPLATE = ''' fetch('/wifi/monitor', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({interface: iface, action: 'start'}) + body: JSON.stringify({interface: iface, action: 'start', kill_processes: killProcesses}) }).then(r => r.json()) .then(data => { btn.textContent = originalText; @@ -3464,20 +3718,29 @@ HTML_TEMPLATE = ''' pulseSignal(); // Check for rogue AP (same SSID, different BSSID) - checkRogueAP(net.essid, net.bssid); + checkRogueAP(net.essid, net.bssid, net.channel, net.power); // Check proximity watch list checkWatchList(net.bssid, 'AP'); + + // Check for drone + const droneCheck = isDrone(net.essid, net.bssid); + if (droneCheck.isDrone) { + handleDroneDetection(net, droneCheck); + } } // Update recon display + const droneInfo = isDrone(net.essid, net.bssid); trackDevice({ - protocol: 'WiFi-AP', + protocol: droneInfo.isDrone ? 'DRONE' : 'WiFi-AP', address: net.bssid, message: net.essid || '[Hidden SSID]', model: net.essid, channel: net.channel, - privacy: net.privacy + privacy: net.privacy, + isDrone: droneInfo.isDrone, + droneBrand: droneInfo.brand }); // Add to output @@ -3586,18 +3849,107 @@ HTML_TEMPLATE = ''' }).then(r => r.json()) .then(data => { if (data.status === 'started') { - showInfo('šŸŽÆ Capturing handshakes for ' + bssid + '. File: ' + data.capture_file); + showInfo('šŸŽÆ Capturing handshakes for ' + bssid); setWifiRunning(true); + // Update handshake indicator to show active capture const hsSpan = document.getElementById('handshakeCount'); hsSpan.style.animation = 'pulse 1s infinite'; hsSpan.title = 'Capturing: ' + bssid; + + // Show capture status panel + const panel = document.getElementById('captureStatusPanel'); + panel.style.display = 'block'; + document.getElementById('captureTargetBssid').textContent = bssid; + document.getElementById('captureTargetChannel').textContent = channel; + document.getElementById('captureFilePath').textContent = data.capture_file; + document.getElementById('captureStatus').textContent = 'Waiting for handshake...'; + document.getElementById('captureStatus').style.color = 'var(--accent-orange)'; + + // Store active capture info and start polling + activeCapture = { + bssid: bssid, + channel: channel, + file: data.capture_file, + startTime: Date.now(), + pollInterval: setInterval(checkCaptureStatus, 5000) // Check every 5 seconds + }; } else { alert('Error: ' + data.message); } }); } + // Check handshake capture status + function checkCaptureStatus() { + if (!activeCapture) { + showInfo('No active handshake capture'); + return; + } + + fetch('/wifi/handshake/status', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({file: activeCapture.file, bssid: activeCapture.bssid}) + }).then(r => r.json()) + .then(data => { + const statusSpan = document.getElementById('captureStatus'); + const elapsed = Math.round((Date.now() - activeCapture.startTime) / 1000); + const elapsedStr = elapsed < 60 ? elapsed + 's' : Math.floor(elapsed/60) + 'm ' + (elapsed%60) + 's'; + + if (data.handshake_found) { + // Handshake captured! + statusSpan.textContent = 'āœ“ HANDSHAKE CAPTURED!'; + statusSpan.style.color = 'var(--accent-green)'; + handshakeCount++; + document.getElementById('handshakeCount').textContent = handshakeCount; + playAlert(); + showInfo('šŸŽ‰ Handshake captured for ' + activeCapture.bssid + '! File: ' + data.file); + + // Stop polling + if (activeCapture.pollInterval) { + clearInterval(activeCapture.pollInterval); + } + document.getElementById('handshakeCount').style.animation = ''; + } else if (data.file_exists) { + const sizeKB = (data.file_size / 1024).toFixed(1); + statusSpan.textContent = 'Capturing... (' + sizeKB + ' KB, ' + elapsedStr + ')'; + statusSpan.style.color = 'var(--accent-orange)'; + } else if (data.status === 'stopped') { + statusSpan.textContent = 'Capture stopped'; + statusSpan.style.color = 'var(--text-dim)'; + if (activeCapture.pollInterval) { + clearInterval(activeCapture.pollInterval); + } + } else { + statusSpan.textContent = 'Waiting for data... (' + elapsedStr + ')'; + statusSpan.style.color = 'var(--accent-orange)'; + } + }) + .catch(err => { + console.error('Capture status check failed:', err); + }); + } + + // Stop handshake capture + function stopHandshakeCapture() { + if (activeCapture && activeCapture.pollInterval) { + clearInterval(activeCapture.pollInterval); + } + + // Stop the WiFi scan (which stops airodump-ng) + stopWifiScan(); + + document.getElementById('captureStatus').textContent = 'Stopped'; + document.getElementById('captureStatus').style.color = 'var(--text-dim)'; + document.getElementById('handshakeCount').style.animation = ''; + + // Keep the panel visible so user can see the file path + showInfo('Handshake capture stopped. Check ' + (activeCapture ? activeCapture.file : 'capture file')); + + activeCapture = null; + } + // Send deauth function sendDeauth() { const bssid = document.getElementById('targetBssid').value; @@ -4218,49 +4570,6 @@ HTML_TEMPLATE = ''' }); } - // L2CAP Ping - function btPing() { - const mac = document.getElementById('btTargetMac').value; - if (!mac) { alert('Enter target MAC'); return; } - - fetch('/bt/ping', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({mac: mac, count: 5}) - }).then(r => r.json()) - .then(data => { - if (data.status === 'success') { - showInfo('Ping ' + mac + ': ' + (data.reachable ? 'Reachable' : 'Unreachable')); - } else { - showInfo('Ping error: ' + data.message); - } - }); - } - - // DoS attack - function btDosAttack() { - const mac = document.getElementById('btTargetMac').value; - if (!mac) { alert('Enter target MAC'); return; } - - if (!confirm('Send DoS ping flood to ' + mac + '?\\n\\n⚠ Only test on devices you own!')) return; - - showInfo('Starting DoS test on ' + mac + '...'); - - fetch('/bt/dos', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({mac: mac, count: 100, size: 600}) - }).then(r => r.json()) - .then(data => { - showInfo('DoS test complete: ' + (data.message || 'Done')); - }); - } - - // Stub functions for other attacks - function btReplayAttack() { alert('Replay attack requires captured packets'); } - function btSpoofMac() { alert('MAC spoofing requires root privileges'); } - function btScanVulns() { alert('Vulnerability scanning not yet implemented'); } - // Initialize Bluetooth radar function initBtRadar() { const canvas = document.getElementById('btRadarCanvas'); @@ -5118,8 +5427,13 @@ def toggle_monitor_mode(): interfaces_before = get_wireless_interfaces() print(f"[WiFi] Interfaces before monitor mode: {interfaces_before}", flush=True) - # Kill interfering processes - subprocess.run(['airmon-ng', 'check', 'kill'], capture_output=True, timeout=10) + # Optionally kill interfering processes (can drop other connections) + kill_processes = data.get('kill_processes', False) + if kill_processes: + print("[WiFi] Killing interfering processes...", flush=True) + subprocess.run(['airmon-ng', 'check', 'kill'], capture_output=True, timeout=10) + else: + print("[WiFi] Skipping process kill (other connections preserved)", flush=True) # Start monitor mode result = subprocess.run(['airmon-ng', 'start', interface], @@ -5643,6 +5957,71 @@ def capture_handshake(): return jsonify({'status': 'error', 'message': str(e)}) +@app.route('/wifi/handshake/status', methods=['POST']) +def check_handshake_status(): + """Check if a handshake has been captured in the specified file.""" + import os + + data = request.json + capture_file = data.get('file', '') + target_bssid = data.get('bssid', '') + + # Security: ensure the file path is in /tmp and looks like our capture files + if not capture_file.startswith('/tmp/intercept_handshake_') or '..' in capture_file: + return jsonify({'status': 'error', 'message': 'Invalid capture file path'}) + + # Check if file exists + if not os.path.exists(capture_file): + # Check if capture is still running + with wifi_lock: + if wifi_process and wifi_process.poll() is None: + return jsonify({ + 'status': 'running', + 'file_exists': False, + 'handshake_found': False + }) + else: + return jsonify({ + 'status': 'stopped', + 'file_exists': False, + 'handshake_found': False + }) + + # File exists - get size + file_size = os.path.getsize(capture_file) + + # Use aircrack-ng to check if handshake is present + # aircrack-ng -a 2 -b will show if EAPOL handshake exists + handshake_found = False + try: + if target_bssid and is_valid_mac(target_bssid): + result = subprocess.run( + ['aircrack-ng', '-a', '2', '-b', target_bssid, capture_file], + capture_output=True, + text=True, + timeout=10 + ) + # Check output for handshake indicators + # aircrack-ng shows "1 handshake" if found, or "0 handshake" if not + output = result.stdout + result.stderr + if '1 handshake' in output or 'handshake' in output.lower() and 'wpa' in output.lower(): + # Also check it's not "0 handshake" + if '0 handshake' not in output: + handshake_found = True + except subprocess.TimeoutExpired: + pass # aircrack-ng timed out, assume no handshake yet + except Exception as e: + print(f"[WiFi] Error checking handshake: {e}", flush=True) + + return jsonify({ + 'status': 'running' if wifi_process and wifi_process.poll() is None else 'stopped', + 'file_exists': True, + 'file_size': file_size, + 'file': capture_file, + 'handshake_found': handshake_found + }) + + @app.route('/wifi/networks') def get_wifi_networks(): """Get current list of discovered networks.""" @@ -6333,100 +6712,6 @@ def enum_bt_services(): return jsonify({'status': 'error', 'message': str(e)}) -@app.route('/bt/ping', methods=['POST']) -def ping_bt_device(): - """Ping a Bluetooth device using l2ping.""" - data = request.json - target_mac = data.get('mac') - count = data.get('count', 5) - - if not target_mac: - return jsonify({'status': 'error', 'message': 'Target MAC required'}) - - # Validate MAC address - if not is_valid_mac(target_mac): - return jsonify({'status': 'error', 'message': 'Invalid MAC address format'}) - - # Validate count - try: - count = int(count) - if count < 1 or count > 50: - count = 5 - except (ValueError, TypeError): - count = 5 - - try: - result = subprocess.run( - ['l2ping', '-c', str(count), target_mac], - capture_output=True, text=True, timeout=30 - ) - - return jsonify({ - 'status': 'success', - 'output': result.stdout, - 'reachable': result.returncode == 0 - }) - - except subprocess.TimeoutExpired: - return jsonify({'status': 'error', 'message': 'Ping timed out'}) - except FileNotFoundError: - return jsonify({'status': 'error', 'message': 'l2ping not found'}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}) - - -@app.route('/bt/dos', methods=['POST']) -def dos_bt_device(): - """Flood ping a Bluetooth device (DoS test).""" - data = request.json - target_mac = data.get('mac') - count = data.get('count', 100) - size = data.get('size', 600) - - if not target_mac: - return jsonify({'status': 'error', 'message': 'Target MAC required'}) - - # Validate MAC address - if not is_valid_mac(target_mac): - return jsonify({'status': 'error', 'message': 'Invalid MAC address format'}) - - # Validate count and size to prevent abuse - try: - count = int(count) - if count < 1 or count > 1000: - count = 100 - except (ValueError, TypeError): - count = 100 - - try: - size = int(size) - if size < 1 or size > 1500: - size = 600 - except (ValueError, TypeError): - size = 600 - - try: - # l2ping flood with large packets - result = subprocess.run( - ['l2ping', '-c', str(count), '-s', str(size), '-f', target_mac], - capture_output=True, text=True, timeout=60 - ) - - bt_queue.put({'type': 'info', 'text': f'DoS test complete on {target_mac}'}) - - return jsonify({ - 'status': 'success', - 'output': result.stdout - }) - - except subprocess.TimeoutExpired: - return jsonify({'status': 'success', 'message': 'DoS test timed out (expected)'}) - except FileNotFoundError: - return jsonify({'status': 'error', 'message': 'l2ping not found'}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}) - - @app.route('/bt/devices') def get_bt_devices(): """Get current list of discovered Bluetooth devices."""