DEVICES: 0
@@ -3133,13 +3190,116 @@ HTML_TEMPLATE = '''
let clientCount = 0;
let handshakeCount = 0;
let rogueApCount = 0;
+ let droneCount = 0;
+ let detectedDrones = {}; // Track detected drones by BSSID
let ssidToBssids = {}; // Track SSIDs to their BSSIDs for rogue AP detection
+ let rogueApDetails = {}; // Store details about rogue APs: {ssid: [{bssid, signal, channel, firstSeen}]}
+ let activeCapture = null; // {bssid, channel, file, startTime, pollInterval}
let watchMacs = JSON.parse(localStorage.getItem('watchMacs') || '[]');
let alertedMacs = new Set(); // Prevent duplicate alerts per session
// 5GHz channel mapping for the graph
const channels5g = ['36', '40', '44', '48', '52', '56', '60', '64', '100', '149', '153', '157', '161', '165'];
+ // Drone SSID patterns for detection
+ const dronePatterns = [
+ /^DJI[-_]/i, /Mavic/i, /Phantom/i, /^Spark[-_]/i, /^Mini[-_]/i, /^Air[-_]/i,
+ /Inspire/i, /Matrice/i, /Avata/i, /^FPV[-_]/i, /Osmo/i, /RoboMaster/i, /Tello/i,
+ /Parrot/i, /Bebop/i, /Anafi/i, /^Disco[-_]/i, /Mambo/i, /Swing/i,
+ /Autel/i, /^EVO[-_]/i, /Dragonfish/i, /Skydio/i,
+ /Holy.?Stone/i, /Potensic/i, /SYMA/i, /Hubsan/i, /Eachine/i, /FIMI/i,
+ /Yuneec/i, /Typhoon/i, /PowerVision/i, /PowerEgg/i,
+ /Drone/i, /^UAV[-_]/i, /Quadcopter/i, /^RC[-_]Drone/i
+ ];
+
+ // Drone OUI prefixes
+ const droneOuiPrefixes = {
+ '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',
+ '90:03:B7': 'Parrot', '00:12:1C': 'Parrot', '00:26:7E': 'Parrot',
+ '8C:F5:A3': 'Autel', 'D8:E0:E1': 'Autel', 'F8:0F:6F': 'Skydio'
+ };
+
+ // Check if network is a drone
+ function isDrone(ssid, bssid) {
+ // Check SSID patterns
+ if (ssid) {
+ for (const pattern of dronePatterns) {
+ if (pattern.test(ssid)) {
+ return { isDrone: true, method: 'SSID', brand: ssid.split(/[-_\s]/)[0] };
+ }
+ }
+ }
+ // Check OUI prefix
+ if (bssid) {
+ const prefix = bssid.substring(0, 8).toUpperCase();
+ if (droneOuiPrefixes[prefix]) {
+ return { isDrone: true, method: 'OUI', brand: droneOuiPrefixes[prefix] };
+ }
+ }
+ return { isDrone: false };
+ }
+
+ // Handle drone detection
+ function handleDroneDetection(net, droneInfo) {
+ if (detectedDrones[net.bssid]) return; // Already detected
+
+ detectedDrones[net.bssid] = {
+ ssid: net.essid,
+ bssid: net.bssid,
+ brand: droneInfo.brand,
+ method: droneInfo.method,
+ signal: net.power,
+ channel: net.channel,
+ firstSeen: new Date().toISOString()
+ };
+
+ droneCount++;
+ document.getElementById('droneCount').textContent = droneCount;
+
+ // Calculate approximate distance from signal strength
+ const rssi = parseInt(net.power) || -70;
+ const distance = estimateDroneDistance(rssi);
+
+ // Triple alert for drones
+ playAlert();
+ setTimeout(playAlert, 200);
+ setTimeout(playAlert, 400);
+
+ // Show drone alert
+ showDroneAlert(net.essid, net.bssid, droneInfo.brand, distance, rssi);
+ }
+
+ // Estimate distance from RSSI (rough approximation)
+ function estimateDroneDistance(rssi) {
+ // Using free-space path loss model (very approximate)
+ // Reference: -30 dBm at 1 meter
+ const txPower = -30;
+ const n = 2.5; // Path loss exponent (2-4, higher for obstacles)
+ const distance = Math.pow(10, (txPower - rssi) / (10 * n));
+ return Math.round(distance);
+ }
+
+ // Show drone alert popup
+ function showDroneAlert(ssid, bssid, brand, distance, rssi) {
+ const alertDiv = document.createElement('div');
+ alertDiv.className = 'drone-alert';
+ alertDiv.innerHTML = `
+
š DRONE DETECTED
+
+
SSID: ${escapeHtml(ssid || 'Unknown')}
+
BSSID: ${bssid}
+
Brand: ${brand || 'Unknown'}
+
Signal: ${rssi} dBm
+
Est. Distance: ~${distance}m
+
+
+ `;
+ 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)
+
+
+
+ | BSSID |
+ CH |
+ Signal |
+ First Seen |
+
`;
+ aps.forEach((ap, idx) => {
+ const bgColor = idx % 2 === 0 ? 'rgba(255,255,255,0.05)' : 'transparent';
+ html += `
+ | ${ap.bssid} |
+ ${ap.channel} |
+ ${ap.signal} dBm |
+ ${ap.firstSeen} |
+
`;
+ });
+ html += '
';
+ });
+ 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."""