feat: merge attacks into PineAP menu; auto channel; recon fixes; richer reports
- Rail: Attacks tab removed; PineAP becomes a grouped menu (Evil WPA / Evil Open / Evil Enterprise / Impersonation / Clients / Filtering); old #/attacks* hashes redirect to their PineAP equivalents. - PineAP tabs gain Evil Enterprise; stock Open AP / Evil WPA / Enterprise pages replaced by the verified one-click launchers (status, capture, export, deauth, playbooks). - Channel selects gain an Auto option: deploy resolves the target SSID's last-seen channel from recon.db (verified unit-tested end to end). - Harness: pi.dev prompt section removed; Copy Token inline; robot icon. - Recon: compare checkboxes no longer hide the AP list (multi-select stays visible, rows highlighted, clients table no longer suppressed); Previous Scans buttons moved above the dropdown with a Delete All; encryption chips + buckets now distinguish WPA2/WPA3 PSK vs Enterprise (AKM suites decoded from recon bitfield bits 32-47); scan JSON carries GPS when a fix exists; Reports tab shows a GPS column. - fix: restore top-level EVIL_ENC definition lost in the repo (deployed build had it; repo would have thrown at init).
This commit is contained in:
@@ -953,10 +953,26 @@ ENC_CCMP = 0x08
|
||||
ENC_GCMP = 0x20
|
||||
ENC_GCMP256 = 0x80
|
||||
ENC_CCMP256 = 0x100
|
||||
# AKM suites ride in bits 32-47 (bit k = RSN suite selector k advertised).
|
||||
ENC_AKM_EAP = 1 << (32 + 1) # 802.1X / EAP (Enterprise)
|
||||
ENC_AKM_PSK = 1 << (32 + 2) # PSK
|
||||
ENC_AKM_FT_EAP = 1 << (32 + 3) # FT-802.1X (Enterprise)
|
||||
ENC_AKM_FT_PSK = 1 << (32 + 4) # FT-PSK
|
||||
ENC_AKM_EAP_SHA256 = 1 << (32 + 5) # 802.1X-SHA256 (Enterprise)
|
||||
ENC_AKM_PSK_SHA256 = 1 << (32 + 6) # PSK-SHA256
|
||||
ENC_AKM_SAE = 1 << (32 + 8) # SAE (WPA3-Personal)
|
||||
ENC_AKM_FT_SAE = 1 << (32 + 9) # FT-SAE
|
||||
ENC_AKM_OWE = 1 << (32 + 13) # OWE
|
||||
ENC_AKM_OWE_SHA192 = 1 << (32 + 14) # OWE-SHA256-192
|
||||
|
||||
|
||||
def decode_encryption(v):
|
||||
"""Pager recon.db encryption bitfield -> old-UI-style display string."""
|
||||
"""Pager recon.db encryption bitfield -> display string.
|
||||
|
||||
Low bits carry pairwise ciphers, bits 32-47 carry the advertised AKM
|
||||
suites, so WPA2-PSK vs WPA2-Enterprise (and WPA3-Personal vs
|
||||
WPA3-Enterprise) can be told apart.
|
||||
"""
|
||||
v = v or 0
|
||||
if v == 0:
|
||||
return 'Open'
|
||||
@@ -969,6 +985,16 @@ def decode_encryption(v):
|
||||
parts.append('WPA')
|
||||
if v & ENC_WEP:
|
||||
parts.append('WEP')
|
||||
if not parts:
|
||||
return 'Open'
|
||||
if v & (ENC_AKM_EAP | ENC_AKM_FT_EAP | ENC_AKM_EAP_SHA256):
|
||||
parts.append('Enterprise')
|
||||
elif v & (ENC_AKM_SAE | ENC_AKM_FT_SAE):
|
||||
parts.append('SAE')
|
||||
elif v & ENC_AKM_OWE:
|
||||
parts.append('OWE')
|
||||
elif v & (ENC_AKM_PSK | ENC_AKM_FT_PSK | ENC_AKM_PSK_SHA256):
|
||||
parts.append('PSK')
|
||||
return ' '.join(parts) if parts else 'Open'
|
||||
|
||||
|
||||
@@ -1180,12 +1206,22 @@ def recon_scan_data(scan_id, _timeout=20, _limit=None, db=None):
|
||||
handshakes.append({'ap': mac_of.get(r.get('aphash'), '--'),
|
||||
'client': mac_of.get(r.get('stahash'), '--'),
|
||||
'time': r.get('time')})
|
||||
return {'scan': {'id': scans[0]['row_id'], 'time': scans[0]['time'],
|
||||
'name': scans[0].get('name')},
|
||||
# GPS attaches to live scans only: an archived scan's coordinates would
|
||||
# be a current fix, which is misleading for historical data.
|
||||
scan = {'id': scans[0]['row_id'], 'time': scans[0]['time'],
|
||||
'name': scans[0].get('name')}
|
||||
if db is None:
|
||||
try:
|
||||
gps = _gps_status_data()
|
||||
if gps.get('lock'):
|
||||
scan['gps'] = {'lat': gps.get('lat'), 'lon': gps.get('lon'),
|
||||
'alt': gps.get('alt'),
|
||||
'satellites': gps.get('satellites')}
|
||||
except Exception:
|
||||
pass
|
||||
return {'scan': scan,
|
||||
'aps': aps, 'clients': clients, 'handshakes': handshakes,
|
||||
'unassociated': unassociated}
|
||||
|
||||
|
||||
def h_recon_start(ctx):
|
||||
# Serialize starts: the firmware has no abort for a timed scan, so a
|
||||
# second /recon/new while one is running just stacks another empty scan
|
||||
@@ -1351,6 +1387,24 @@ def h_recon_delete(ctx):
|
||||
return 200, {'ok': True}
|
||||
|
||||
|
||||
def h_recon_delete_all(ctx):
|
||||
"""Clear every recorded scan from the live recon database. Rotated
|
||||
archive files (error-*/diagnostic-* recon dbs) are left untouched."""
|
||||
count = 0
|
||||
try:
|
||||
rows = _db_rows(RECON_DB, 'SELECT COUNT(*) AS c FROM scan', timeout=20)
|
||||
count = rows[0]['c'] if rows else 0
|
||||
except RuntimeError:
|
||||
pass
|
||||
for t in RECON_CHILD_TABLES + ['scan']:
|
||||
try:
|
||||
_db_write(RECON_DB, 'DELETE FROM %s' % t)
|
||||
except Exception:
|
||||
continue
|
||||
_recon_scans_cache['updated'] = 0
|
||||
return 200, {'ok': True, 'deleted': count}
|
||||
|
||||
|
||||
def h_recon_scan_download(ctx):
|
||||
scan_id = int(ctx.args[0])
|
||||
data = recon_scan_data(scan_id)
|
||||
@@ -1537,14 +1591,18 @@ def _enc_bucket(enc):
|
||||
s = (enc or '').strip()
|
||||
if not s or s == 'Open':
|
||||
return 'Open'
|
||||
if 'Enterprise' in s:
|
||||
return 'Enterprise'
|
||||
if 'WEP' in s:
|
||||
return 'WEP'
|
||||
if 'WPA2' in s:
|
||||
return 'WPA2'
|
||||
if 'Enterprise' in s:
|
||||
return 'WPA3-Enterprise' if 'WPA3' in s else 'WPA2-Enterprise'
|
||||
if 'SAE' in s or 'OWE' in s:
|
||||
return 'WPA3-Personal'
|
||||
if 'WPA3' in s and 'WPA2' in s:
|
||||
return 'WPA2-PSK' if 'PSK' in s else 'WPA3-PSK'
|
||||
if 'WPA3' in s:
|
||||
return 'WPA3'
|
||||
return 'WPA3-PSK'
|
||||
if 'WPA2' in s:
|
||||
return 'WPA2-PSK'
|
||||
if 'WPA' in s:
|
||||
return 'WPA'
|
||||
return 'Unknown'
|
||||
@@ -2621,6 +2679,46 @@ def _open_channel(value):
|
||||
return 1
|
||||
|
||||
|
||||
def _freq_to_channel(freq):
|
||||
try:
|
||||
freq = int(freq)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if 2412 <= freq <= 2484:
|
||||
return (freq - 2412) // 5 + 1
|
||||
if 5180 <= freq <= 5885:
|
||||
return (freq - 5180) // 5 + 36
|
||||
if 5955 <= freq <= 7115:
|
||||
return (freq - 5955) // 5 + 1
|
||||
return None
|
||||
|
||||
|
||||
def _best_channel_for(ssid):
|
||||
"""Resolve an 'auto' attack channel: the channel the target SSID was
|
||||
last seen on in recon, else None (caller falls back to defaults)."""
|
||||
if not ssid:
|
||||
return None
|
||||
hex_ssid = ssid.encode('utf-8').hex()
|
||||
try:
|
||||
rows = _db_rows(RECON_DB,
|
||||
"SELECT channel, freq FROM ssid WHERE type = 8 "
|
||||
"AND ssid = X'%s' ORDER BY time DESC LIMIT 1"
|
||||
% hex_ssid, timeout=20)
|
||||
except RuntimeError:
|
||||
return None
|
||||
if not rows:
|
||||
return None
|
||||
channel = rows[0].get('channel')
|
||||
if channel is not None:
|
||||
try:
|
||||
channel = int(channel)
|
||||
if 1 <= channel <= 233:
|
||||
return channel
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return _freq_to_channel(rows[0].get('freq'))
|
||||
|
||||
|
||||
def _read_hop():
|
||||
rc, out, err = device_run(['uci', 'get', 'pineapd.wlan1mon.hop'])
|
||||
if rc != 0:
|
||||
@@ -2845,7 +2943,12 @@ def _verify_iface(name, timeout=20.0):
|
||||
|
||||
|
||||
def _deploy_wpa_open(kind, fields):
|
||||
band = _band_of_channel(fields.get('channel'))
|
||||
channel = fields.get('channel')
|
||||
if channel is None:
|
||||
channel = _best_channel_for((fields.get('ssid') or '').strip())
|
||||
if channel is None:
|
||||
channel = 1
|
||||
band = _band_of_channel(channel)
|
||||
ssid = (fields.get('ssid') or '').strip()
|
||||
if not ssid:
|
||||
raise ValueError('SSID is required')
|
||||
@@ -2866,7 +2969,7 @@ def _deploy_wpa_open(kind, fields):
|
||||
'ssid': ssid,
|
||||
'enabled': True,
|
||||
'hidden': bool(fields.get('hidden')),
|
||||
'channel': int(fields.get('channel') or 1),
|
||||
'channel': int(channel or 1),
|
||||
}
|
||||
if kind == 'wpa':
|
||||
daemon_cfg['enctype'] = enctype
|
||||
@@ -2883,7 +2986,7 @@ def _deploy_wpa_open(kind, fields):
|
||||
if status != 200:
|
||||
raise RuntimeError('daemon rejected AP config: %r' % (data,))
|
||||
if kind == 'open':
|
||||
_apply_open_radio({'channel': int(fields.get('channel') or 1),
|
||||
_apply_open_radio({'channel': int(channel or 1),
|
||||
'country': fields.get('country') or 'US'})
|
||||
else:
|
||||
# 5/6 GHz: radio1 feature
|
||||
@@ -2892,14 +2995,14 @@ def _deploy_wpa_open(kind, fields):
|
||||
_apply_radio1_ap(None, {
|
||||
'ssid': ssid, 'passphrase': fields.get('passphrase') or '',
|
||||
'enctype': enctype, 'hidden': bool(fields.get('hidden')),
|
||||
'enabled': True, 'channel': int(fields.get('channel')),
|
||||
'enabled': True, 'channel': int(channel),
|
||||
'country': fields.get('country') or 'US',
|
||||
})
|
||||
iface = 'wlan1wpa'
|
||||
else:
|
||||
_apply_radio1_ap({
|
||||
'ssid': ssid, 'hidden': bool(fields.get('hidden')),
|
||||
'enabled': True, 'channel': int(fields.get('channel')),
|
||||
'enabled': True, 'channel': int(channel),
|
||||
'bssid': fields.get('bssid') or '',
|
||||
'country': fields.get('country') or 'US',
|
||||
}, None)
|
||||
@@ -2909,7 +3012,8 @@ def _deploy_wpa_open(kind, fields):
|
||||
# The daemon applies AP changes asynchronously; allow a full reload cycle.
|
||||
verified = _verify_iface(iface, timeout=45)
|
||||
return {'kind': kind, 'ssid': ssid, 'iface': iface, 'band': band,
|
||||
'channel': int(fields.get('channel')), 'verified': verified}
|
||||
'channel': int(channel or 1), 'auto': fields.get('channel') is None,
|
||||
'verified': verified}
|
||||
|
||||
|
||||
def _disable_enterprise_ap():
|
||||
@@ -2967,12 +3071,14 @@ def _deploy_enterprise(fields):
|
||||
if enctype not in ('wpa2', 'wpa3'):
|
||||
raise ValueError('enterprise encryption must be wpa2 or wpa3')
|
||||
ch = fields.get('channel')
|
||||
if ch is not None:
|
||||
band = channel_band(ch)
|
||||
if band != BAND_5G:
|
||||
raise ValueError('enterprise AP runs on 5 GHz (36-177)')
|
||||
if ch is None:
|
||||
ch = _best_channel_for(ssid)
|
||||
if ch is None or channel_band(ch) != BAND_5G:
|
||||
ch = 36
|
||||
else:
|
||||
ch = 36
|
||||
ch = int(ch)
|
||||
if channel_band(ch) != BAND_5G:
|
||||
raise ValueError('enterprise AP runs on 5 GHz (36-177)')
|
||||
# Radio0 karma surface is shared: stop 2.4 GHz WPA/Open attacks first.
|
||||
for name in ('wlan0wpa', 'wlan0open'):
|
||||
cfg = _uci_wifi_iface(name)
|
||||
@@ -4863,6 +4969,7 @@ ROUTER.add('POST', r'/api/recon/start', h_recon_start)
|
||||
ROUTER.add('POST', r'/api/recon/stop', h_recon_stop)
|
||||
ROUTER.add('GET', r'/api/recon/status', h_recon_status)
|
||||
ROUTER.add('GET', r'/api/recon/scans', h_recon_scans)
|
||||
ROUTER.add('DELETE', r'/api/recon/scans', h_recon_delete_all)
|
||||
ROUTER.add('GET', r'/api/recon/scans/(\d+)/download/json', h_recon_scan_download)
|
||||
ROUTER.add('GET', r'/api/recon/scans/(\d+)', h_recon_scan_detail)
|
||||
ROUTER.add('DELETE', r'/api/recon/scans/(\d+)', h_recon_delete)
|
||||
|
||||
@@ -143,6 +143,12 @@ body {
|
||||
align-items: center; justify-content: center; margin-right: 14px; flex: none;
|
||||
}
|
||||
.entry-icon svg { width: 24px; height: 24px; display: block; }
|
||||
#rail .entry.sub {
|
||||
height: 36px; padding-left: 34px; font-size: 13px;
|
||||
}
|
||||
#rail .entry.sub .entry-icon { width: 18px; height: 18px; margin-right: 10px; }
|
||||
#rail .entry.sub .entry-icon svg { width: 18px; height: 18px; }
|
||||
#rail.open .entry.sub .entry-text { font-size: 12px; }
|
||||
#topbar .btn.ghost { color: #fff; border-color: #fff; }
|
||||
.entry-text { white-space: nowrap; opacity: 0; transition: opacity .2s; }
|
||||
#rail.open .entry-text { opacity: 1; }
|
||||
@@ -335,6 +341,9 @@ html.dark .recon-scan-status.warn { color: #ffb74d; }
|
||||
.recon-paginator .icon-btn svg { width: 18px; height: 18px; }
|
||||
.recon-row-selected td { background: #eaeaea; }
|
||||
html.dark .recon-row-selected td { background: #565656; }
|
||||
.recon-row-compare td { background: rgba(25, 118, 210, .08); }
|
||||
html.dark .recon-row-compare td { background: rgba(25, 118, 210, .18); }
|
||||
.recon-gps-cell { font-variant-numeric: tabular-nums; }
|
||||
.recon-settings-sidebar {
|
||||
position: fixed; top: 64px; right: 0; bottom: 0; width: 270px; z-index: 50;
|
||||
background: var(--surface); box-shadow: -2px 0 6px rgba(0,0,0,.24); padding: 16px;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>WiFi Pineapple</title>
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<link rel="stylesheet" href="css/app.css?v=20260811-9">
|
||||
<link rel="stylesheet" href="css/app.css?v=20260818-7">
|
||||
<link rel="stylesheet" href="js/xterm.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -261,14 +261,14 @@
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/icons.js?v=20260811-6"></script>
|
||||
<script src="js/icons.js?v=20260818-7"></script>
|
||||
<script src="js/api.js?v=20260817-4"></script>
|
||||
<script src="js/chart.js"></script>
|
||||
<script src="js/xterm.min.js"></script>
|
||||
<script src="js/xterm-addon-fit.min.js"></script>
|
||||
<script src="js/terminal.js"></script>
|
||||
<script src="js/pager.js"></script>
|
||||
<script src="js/views.js?v=20260818-6"></script>
|
||||
<script src="js/app.js?v=20260818-3"></script>
|
||||
<script src="js/views.js?v=20260818-7"></script>
|
||||
<script src="js/app.js?v=20260818-7"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -29,12 +29,20 @@ const App = (() => {
|
||||
|
||||
const railItems = [
|
||||
{ key: 'dashboard', label: 'Dashboard', hash: '#/dashboard', icon: 'dashboard' },
|
||||
{ key: 'attacks', label: 'Attacks', hash: '#/attacks', icon: 'attack' },
|
||||
{ key: 'pineap', label: 'PineAP', hash: '#/pineap', icon: 'wifi' },
|
||||
{
|
||||
key: 'pineap', label: 'PineAP', hash: '#/pineap', icon: 'pineap', children: [
|
||||
{ key: 'pineap_evilwpa', label: 'Evil WPA', hash: '#/pineap/evilwpa', icon: 'attack' },
|
||||
{ key: 'pineap_open', label: 'Evil Open', hash: '#/pineap/open', icon: 'wifi' },
|
||||
{ key: 'pineap_enterprise', label: 'Evil Enterprise', hash: '#/pineap/enterprise', icon: 'record' },
|
||||
{ key: 'pineap_impersonation', label: 'Impersonation', hash: '#/pineap/impersonation', icon: 'place' },
|
||||
{ key: 'pineap_clients', label: 'Clients', hash: '#/pineap/clients', icon: 'pager' },
|
||||
{ key: 'pineap_filtering', label: 'Filtering', hash: '#/pineap/filtering', icon: 'settings' }
|
||||
]
|
||||
},
|
||||
{ key: 'recon', label: 'Recon', hash: '#/recon', icon: 'recon' },
|
||||
{ key: 'logging', label: 'Logging', hash: '#/logging', icon: 'logging' },
|
||||
{ key: 'modules', label: 'Payloads', hash: '#/modules', icon: 'modules' },
|
||||
{ key: 'harness', label: 'Harness', hash: '#/harness', icon: 'extension' },
|
||||
{ key: 'harness', label: 'Harness', hash: '#/harness', icon: 'robot' },
|
||||
{ key: 'settings', label: 'Settings', hash: '#/settings', icon: 'settings' }
|
||||
];
|
||||
const railDividers = new Set(['logging']);
|
||||
@@ -63,6 +71,13 @@ const App = (() => {
|
||||
rail.appendChild(d);
|
||||
}
|
||||
rail.appendChild(railEntry(it));
|
||||
if (it.children) {
|
||||
it.children.forEach((sub) => {
|
||||
const s = railEntry(sub);
|
||||
s.classList.add('sub');
|
||||
rail.appendChild(s);
|
||||
});
|
||||
}
|
||||
});
|
||||
const foot = document.createElement('div');
|
||||
foot.className = 'rail-footer';
|
||||
@@ -85,11 +100,22 @@ const App = (() => {
|
||||
|
||||
function route() {
|
||||
closeToolbarMenus();
|
||||
const hash = (location.hash || '#/dashboard').replace(/\/+$/, '');
|
||||
let hash = (location.hash || '#/dashboard').replace(/\/+$/, '');
|
||||
if (hash === '#/recon/survey') {
|
||||
location.hash = '#/recon';
|
||||
return;
|
||||
}
|
||||
if (hash.indexOf('#/attacks') === 0) {
|
||||
const map = {
|
||||
'#/attacks': '#/pineap',
|
||||
'#/attacks/wpa': '#/pineap/evilwpa',
|
||||
'#/attacks/open': '#/pineap/open',
|
||||
'#/attacks/enterprise': '#/pineap/enterprise'
|
||||
};
|
||||
hash = map[hash] || '#/pineap';
|
||||
location.replace(hash);
|
||||
return;
|
||||
}
|
||||
const name = routes[hash];
|
||||
if (currentView && currentView.destroy) currentView.destroy();
|
||||
els.content.innerHTML = '';
|
||||
@@ -103,9 +129,13 @@ const App = (() => {
|
||||
currentView = views[name](els.content);
|
||||
}
|
||||
const key = keyOf(hash);
|
||||
const subs = els.rail.querySelectorAll('.entry.sub');
|
||||
const subActive = Array.prototype.some.call(subs, (s) => s.getAttribute('href') === hash);
|
||||
Array.prototype.forEach.call(els.rail.querySelectorAll('.entry'), (a) => {
|
||||
const href = a.getAttribute('href');
|
||||
a.classList.toggle('active', !!href && (href === hash || keyOf(href) === key));
|
||||
const exact = !!href && href === hash;
|
||||
const groupActive = !subActive && !!href && keyOf(href) === key;
|
||||
a.classList.toggle('active', exact || groupActive);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -395,13 +425,10 @@ const App = (() => {
|
||||
|
||||
const routes = {
|
||||
'#/dashboard': 'dashboard',
|
||||
'#/attacks': 'attacks',
|
||||
'#/attacks/wpa': 'attacks_wpa',
|
||||
'#/attacks/open': 'attacks_open',
|
||||
'#/attacks/enterprise': 'attacks_enterprise',
|
||||
'#/pineap': 'pineap',
|
||||
'#/pineap/open': 'pineap_open',
|
||||
'#/pineap/evilwpa': 'pineap_evilwpa',
|
||||
'#/pineap/enterprise': 'pineap_enterprise',
|
||||
'#/pineap/open': 'pineap_open',
|
||||
'#/pineap/impersonation': 'pineap_impersonation',
|
||||
'#/pineap/clients': 'pineap_clients',
|
||||
'#/pineap/filtering': 'pineap_filtering',
|
||||
|
||||
@@ -10,12 +10,14 @@ window.PineappleIcons = {
|
||||
settings: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 11.11V7A2 2 0 0 0 19 5H15V3A2 2 0 0 0 13 1H9A2 2 0 0 0 7 3V5H3A2 2 0 0 0 1 7V18A2 2 0 0 0 3 20H10.26A7 7 0 1 0 21 11.11M9 3H13V5H9M19 20A5 5 0 0 1 13 20A5 5 0 1 1 19 20M15 13H16.5V15.82L18.94 17.23L18.19 18.53L15 16.69V13"/></svg>',
|
||||
chevron: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z"/></svg>',
|
||||
terminal: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z"/></svg>',
|
||||
robot: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13M12,20A2,2 0 0,0 14,18H10A2,2 0 0,0 12,20Z"/></svg>',
|
||||
wifi: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1,9L3,11C8,6 16,6 21,11L23,9C17,3 7,3 1,9M5,13L7,15C10,12.5 14,12.5 17,15L19,13C15,9 9,9 5,13M9,17L12,21L15,17C13.34,15.67 10.66,15.67 9,17Z"/></svg>',
|
||||
extension: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5,10.8 6.2,12 6.2,13.5C6.2,15 5,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,19 9,17.8 10.5,17.8C12,17.8 13.2,19 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z"/></svg>',
|
||||
receipt: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M14,17H4V15H14V17M14,13H4V11H14V13M14,9H4V7H14V9M18,13V11H16V9H18V7H20V9H22V11H20V13H18M20,3H2A2,2 0 0,0 0,5V19A2,2 0 0,0 2,21H20A2,2 0 0,0 22,19V17H20V19H2V5H20V3Z"/></svg>',
|
||||
refresh: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>',
|
||||
file_download: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19,9H15V3H9V9H5L12,16L19,9M5,18V20H19V18H5Z"/></svg>',
|
||||
delete: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6,19C6,20.1 6.9,21 8,21H16C17.1,21 18,20.1 18,19V7H6V19M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19V4Z"/></svg>',
|
||||
delete_forever: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8.46,11.88L9.87,10.47L12,12.59L14.12,10.47L15.53,11.88L13.41,14L15.53,16.12L14.12,17.53L12,15.41L9.88,17.53L8.47,16.12L10.59,14L8.46,11.88M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z"/></svg>',
|
||||
settings: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14,12.94C19.18,12.64 19.2,12.33 19.2,12C19.2,11.68 19.18,11.36 19.13,11.06L21.16,9.48C21.34,9.34 21.39,9.07 21.28,8.87L19.36,5.55C19.24,5.33 18.99,5.26 18.77,5.33L16.38,6.29C15.88,5.91 15.35,5.59 14.76,5.35L14.4,2.81C14.36,2.57 14.16,2.4 13.92,2.4H10.08C9.84,2.4 9.65,2.57 9.61,2.81L9.25,5.35C8.66,5.59 8.12,5.91 7.63,6.29L5.24,5.33C5.02,5.26 4.77,5.33 4.65,5.55L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48L4.89,11.06C4.84,11.36 4.8,11.67 4.8,12C4.8,12.33 4.82,12.64 4.87,12.94L2.84,14.52C2.66,14.66 2.61,14.93 2.72,15.13L4.64,18.45C4.76,18.67 5.01,18.74 5.23,18.67L7.62,17.71C8.12,18.09 8.65,18.41 9.24,18.65L9.6,21.19C9.65,21.43 9.84,21.6 10.08,21.6H13.92C14.16,21.6 14.36,21.43 14.4,21.19L14.76,18.65C15.35,18.41 15.88,18.09 16.38,17.71L18.77,18.67C18.99,18.74 19.24,18.67 19.36,18.45L21.28,15.13C21.39,14.93 21.34,14.66 21.16,14.52L19.14,12.94M12,15.6C10.02,15.6 8.4,13.98 8.4,12C8.4,10.02 10.02,8.4 12,8.4C13.98,8.4 15.6,10.02 15.6,12C15.6,13.98 13.98,15.6 12,15.6Z"/></svg>',
|
||||
search: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5,14H14.71L14.43,13.73C15.41,12.59 16,11.11 16,9.5C16,5.91 13.09,3 9.5,3C5.91,3 3,5.91 3,9.5C3,13.09 5.91,16 9.5,16C11.11,16 12.59,15.41 13.73,14.43L14,14.71V15.5L19,20.49L20.49,19L15.5,14M9.5,14C7.01,14 5,11.99 5,9.5C5,7.01 7.01,5 9.5,5C11.99,5 14,7.01 14,9.5C14,11.99 11.99,14 9.5,14Z"/></svg>',
|
||||
first_page: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.41,16.59L13.82,12L18.41,7.41L17,6L11,12L17,18L18.41,16.59M6,6H8V18H6V6Z"/></svg>',
|
||||
|
||||
@@ -246,8 +246,9 @@ views.dashboard = (root) => {
|
||||
|
||||
const PINEAP_TABS = [
|
||||
{ label: 'PineAP', hash: '#/pineap' },
|
||||
{ label: 'Open AP', hash: '#/pineap/open' },
|
||||
{ label: 'Evil WPA', hash: '#/pineap/evilwpa' },
|
||||
{ label: 'Evil Open', hash: '#/pineap/open' },
|
||||
{ label: 'Evil Enterprise', hash: '#/pineap/enterprise' },
|
||||
{ label: 'Impersonation', hash: '#/pineap/impersonation' },
|
||||
{ label: 'Clients', hash: '#/pineap/clients' },
|
||||
{ label: 'Filtering', hash: '#/pineap/filtering' }
|
||||
@@ -336,10 +337,12 @@ views.pineap = (root) => {
|
||||
modeRow.appendChild(quickCard);
|
||||
box.appendChild(modeRow);
|
||||
box.appendChild(h('div', { class: 'pineap-infobox info' },
|
||||
h('span', { text: 'For one-click Evil WPA / Open AP / Enterprise attacks, use the Attacks section — it deploys, enables karma, verifies on-device and captures loot automatically.' }),
|
||||
h('span', { text: 'The Evil WPA / Evil Open / Evil Enterprise tabs deploy one-click attacks — they enable karma, verify on-device and capture loot automatically.' }),
|
||||
h('div', { class: 'pineap-infobox-actions' },
|
||||
h('a', { class: 'btn ghost', href: '#/attacks', style: 'text-decoration:none',
|
||||
onclick: (e) => { e.preventDefault(); App.go('#/attacks'); } }, 'Go to Attacks'))));
|
||||
h('a', { class: 'btn ghost', href: '#/pineap/evilwpa', style: 'text-decoration:none',
|
||||
onclick: (e) => { e.preventDefault(); App.go('#/pineap/evilwpa'); } }, 'Evil WPA'),
|
||||
h('a', { class: 'btn ghost', href: '#/pineap/enterprise', style: 'text-decoration:none',
|
||||
onclick: (e) => { e.preventDefault(); App.go('#/pineap/enterprise'); } }, 'Evil Enterprise'))));
|
||||
|
||||
const cards = { karma: {}, open: {}, wpa: {} };
|
||||
const cardWrap = h('div', { class: 'pineap-title-card-container' });
|
||||
@@ -487,6 +490,10 @@ views.pineap = (root) => {
|
||||
return { destroy: () => clearInterval(iv) };
|
||||
};
|
||||
|
||||
const EVIL_ENC = [
|
||||
['psk2', 'WPA2 PSK'], ['sae', 'WPA3 SAE'], ['owe', 'WPA3 OWE']
|
||||
];
|
||||
|
||||
const BAND_GROUPS = [
|
||||
{ band: '2.4', label: '2.4 GHz', dfs: false,
|
||||
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] },
|
||||
@@ -512,6 +519,7 @@ function chanLabel(band, ch) {
|
||||
}
|
||||
|
||||
function chanSelect(sel, value) {
|
||||
sel.appendChild(h('option', { value: '', text: 'Auto — best channel for the target' }));
|
||||
BAND_GROUPS.forEach((g) => {
|
||||
const og = h('optgroup', { label: g.label });
|
||||
g.channels.forEach((ch) => {
|
||||
@@ -519,7 +527,9 @@ function chanSelect(sel, value) {
|
||||
});
|
||||
sel.appendChild(og);
|
||||
});
|
||||
if (value != null) {
|
||||
if (value == null || value === '') {
|
||||
sel.value = '';
|
||||
} else {
|
||||
const opts = Array.prototype.slice.call(sel.options);
|
||||
const hit = opts.find((o) => Number(o.value) === Number(value));
|
||||
if (hit) sel.value = hit.value;
|
||||
@@ -528,7 +538,7 @@ function chanSelect(sel, value) {
|
||||
}
|
||||
|
||||
function bandOfChannel(ch) {
|
||||
if (ch == null) return '2.4';
|
||||
if (ch == null || ch === '') return '2.4';
|
||||
ch = Number(ch);
|
||||
if (ch >= 1 && ch <= 14) return '2.4';
|
||||
if (ch >= 36 && ch <= 177) return '5';
|
||||
@@ -556,347 +566,52 @@ const OPEN_COUNTRIES = [
|
||||
['VE', 'Venezuela'], ['VN', 'Vietnam']
|
||||
];
|
||||
|
||||
views.pineap_open = (root) => {
|
||||
const box = pineapShell(root, '#/pineap/open');
|
||||
const card = h('div', { class: 'pineap-title-card' });
|
||||
box.appendChild(card);
|
||||
|
||||
card.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAP Open Access Point'));
|
||||
const subtitle = h('div', { class: 'pineap-card-subtitle' });
|
||||
card.appendChild(subtitle);
|
||||
|
||||
const ssidIn = h('input', { id: 'oa-ssid' });
|
||||
const bssidIn = h('input', { id: 'oa-bssid' });
|
||||
const chSel = h('select', { id: 'oa-channel' });
|
||||
chanSelect(chSel, null);
|
||||
const bandHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px' });
|
||||
function applyOaHint() {
|
||||
const b = bandOfChannel(chSel.value);
|
||||
bandHint.textContent = b === '6' ? '6 GHz open APs require WPA3/OWE on real clients — most devices will not associate to an open 6 GHz network.' : '';
|
||||
}
|
||||
chSel.addEventListener('change', applyOaHint);
|
||||
const coSel = h('select', { id: 'oa-country' });
|
||||
OPEN_COUNTRIES.forEach(([v, l]) => coSel.appendChild(h('option', { value: v, text: l })));
|
||||
const hiddenCb = h('input', { type: 'checkbox', id: 'oa-hidden' });
|
||||
const karmaCb = h('input', { type: 'checkbox', id: 'oa-karma' });
|
||||
let karmaDirty = false;
|
||||
karmaCb.addEventListener('change', () => {
|
||||
karmaDirty = true;
|
||||
karmaCb.indeterminate = false;
|
||||
render();
|
||||
});
|
||||
|
||||
card.appendChild(h('div', { class: 'row' },
|
||||
h('div', {}, h('label', {}, 'Open SSID', ssidIn)),
|
||||
h('div', {}, h('label', {}, 'BSSID', bssidIn))));
|
||||
card.appendChild(h('div', { class: 'row' },
|
||||
h('div', {}, h('label', {}, 'Channel', chSel), bandHint),
|
||||
h('div', {}, h('label', {}, 'Current Country', coSel))));
|
||||
card.appendChild(h('div', { class: 'row' },
|
||||
h('div', {}, h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), ' Hidden')),
|
||||
h('div', {}, h('label', { class: 'switch' }, karmaCb, h('span', { class: 'track' }), ' Respond to all probe requests (impersonate all networks)'))));
|
||||
|
||||
const info = h('div', { class: 'muted', style: 'margin-top:10px;font-size:13px' });
|
||||
card.appendChild(info);
|
||||
const boxes = h('div', {});
|
||||
card.appendChild(boxes);
|
||||
card.appendChild(h('div', { class: 'row', style: 'margin-top:10px' },
|
||||
h('div', {}, btn('Save', save)),
|
||||
h('div', { class: 'muted', style: 'align-self:center;font-size:12px' }, 'Applying reconfigures the radio — you may be disconnected briefly.')));
|
||||
|
||||
const state = {};
|
||||
|
||||
function cfgLink() {
|
||||
return h('a', { href: '#/pineap/filtering', style: 'color:var(--primary);cursor:pointer', onclick: (e) => { e.preventDefault(); App.go('#/pineap/filtering'); } }, 'filter configuration');
|
||||
}
|
||||
function filterBtn() {
|
||||
return h('a', { class: 'btn', href: '#/pineap/filtering', style: 'text-decoration:none;display:inline-block', onclick: (e) => { e.preventDefault(); App.go('#/pineap/filtering'); } }, 'Change Filters');
|
||||
}
|
||||
function infobox(severity, text, ...actions) {
|
||||
return h('div', { class: 'pineap-infobox ' + severity },
|
||||
h('span', { text }),
|
||||
h('div', { class: 'pineap-infobox-actions' }, ...actions));
|
||||
}
|
||||
function filterSentence(sm, cm) {
|
||||
if (sm === 'allow' && cm === 'allow') return 'any client in the filter configuration may connect to any SSID in the filter configuration.';
|
||||
if (sm === 'deny' && cm === 'allow') return 'any client not in the filter configuration may connect to any SSID in the filter configuration.';
|
||||
if (sm === 'allow' && cm === 'deny') return 'any client in the filter configuration may connect to any SSID not in the filter configuration.';
|
||||
return 'any client not in the filter configuration may connect to any SSID not in the filter configuration.';
|
||||
}
|
||||
|
||||
function save() {
|
||||
const requests = [PagerAPI.post('/api/pineap/wifi/set_ap', {
|
||||
open: {
|
||||
ssid: ssidIn.value,
|
||||
bssid: bssidIn.value.trim(),
|
||||
hidden: hiddenCb.checked,
|
||||
enabled: state.enabledLoaded ? !!state.enabled : true,
|
||||
channel: chSel.value ? parseInt(chSel.value, 10) : null,
|
||||
country: coSel.value
|
||||
}
|
||||
})];
|
||||
if (karmaDirty) requests.push(PagerAPI.post('/api/pineap/mimic', { enable: karmaCb.checked }));
|
||||
Promise.allSettled(requests).then((results) => {
|
||||
const ok = results.every((r) => r.status === 'fulfilled');
|
||||
if (ok && karmaDirty) {
|
||||
PINEAP_SESSION.karma = karmaCb.checked;
|
||||
karmaDirty = false;
|
||||
}
|
||||
App.toast(ok ? 'Open AP saved' : 'Some settings failed', ok ? '' : 'error');
|
||||
load();
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
const sm = state.ssidMode || 'deny';
|
||||
const cm = state.clientMode || 'deny';
|
||||
subtitle.textContent = '';
|
||||
subtitle.appendChild(document.createTextNode('The Open SSID is advertised without encryption. When client association is enabled, '));
|
||||
subtitle.appendChild(cfgLink());
|
||||
subtitle.appendChild(document.createTextNode(' ' + filterSentence(sm, cm)));
|
||||
|
||||
const hidden = hiddenCb.checked;
|
||||
const karma = karmaCb.checked;
|
||||
let t = 'The Open access point will be ' + (hidden ? 'hidden' : 'advertised');
|
||||
if (!karma) {
|
||||
t += '.';
|
||||
} else {
|
||||
if (sm === 'allow' && cm === 'allow') t += ', and clients in the allowed client filter list will be able to connect to any SSID in the allowed SSID filter.';
|
||||
else if (sm === 'allow' && cm === 'deny') t += ', and clients in the allowed client filter list will be able to connect to any SSID not blocked by the SSID filter.';
|
||||
else if (sm === 'deny' && cm === 'allow') t += ', and clients not in the denied client filter list will be able to connect to any SSID in the allowed SSID filter.';
|
||||
else t += ', and clients not in the denied client filter list will be able to connect to any SSID not blocked by the SSID filter.';
|
||||
}
|
||||
info.textContent = t;
|
||||
|
||||
boxes.innerHTML = '';
|
||||
const openSsid = ssidIn.value;
|
||||
const ssidList = state.ssidList || [];
|
||||
const clientList = state.clientList || [];
|
||||
if (state.ssidFetched && sm === 'allow' && openSsid && ssidList.indexOf(openSsid) === -1) {
|
||||
boxes.appendChild(infobox('error',
|
||||
'The open SSID "' + openSsid + '" is not included in the filter allow list, clients will not be able to connect.',
|
||||
btn('Add Allowed', () => PagerAPI.post('/api/pineap/filters/ssid', { action: 'add', value: openSsid }).then(load).catch(() => App.toast('Failed', 'error')))));
|
||||
}
|
||||
if (state.ssidFetched && sm === 'deny' && openSsid && ssidList.indexOf(openSsid) !== -1) {
|
||||
boxes.appendChild(infobox('error',
|
||||
'The open SSID "' + openSsid + '" is included in the filter deny list, clients will not be able to connect.',
|
||||
btn('Remove Filter', () => PagerAPI.post('/api/pineap/filters/ssid', { action: 'delete', value: openSsid }).then(load).catch(() => App.toast('Failed', 'error')))));
|
||||
}
|
||||
if (sm === 'allow' && ssidList.length > 0 && karmaCb.checked) {
|
||||
boxes.appendChild(infobox('info',
|
||||
'Remember to add SSIDs you wish to impersonate to the PineAP SSID filter, or change to "Deny" mode to allow responding to all requested networks!',
|
||||
filterBtn()));
|
||||
}
|
||||
if (state.clientFetched && cm === 'allow' && clientList.length === 0) {
|
||||
boxes.appendChild(infobox('error',
|
||||
'The PineAP Client filter is set to "allow", but no clients are listed; no clients will be able to connect!',
|
||||
btn('Change Mode', () => PagerAPI.post('/api/pineap/filters/client', { action: 'set_mode', mode: 'deny' }).then(load).catch(() => App.toast('Failed', 'error'))),
|
||||
filterBtn()));
|
||||
views.pineap_evilwpa = attackLauncher('wpa', {
|
||||
title: 'Evil WPA',
|
||||
subtitle: 'WPA2-PSK / WPA3-SAE / WPA3-OWE evil twin with handshake capture',
|
||||
passphrase: true,
|
||||
encodings: EVIL_ENC,
|
||||
handshakes: true,
|
||||
export: true,
|
||||
deauth: true,
|
||||
tabHash: '#/pineap/evilwpa',
|
||||
playbook: {
|
||||
steps: ['Deploy the evil twin',
|
||||
'Wait for a client to associate',
|
||||
'Deauth the target client to force the 4-way',
|
||||
'Export .hc22000 and crack with hashcat'],
|
||||
currentStep: (s, w) => {
|
||||
if (!w || !w.enabled) return 'Deploy the evil twin';
|
||||
if (!s || !(s.handshakes > 0)) return 'Wait for a client to associate';
|
||||
return 'Export .hc22000 and crack with hashcat';
|
||||
},
|
||||
hint: (s, w) => {
|
||||
if (!w || !w.enabled) return '1. Set the target SSID and passphrase, pick a channel (Auto finds it from recon), Deploy. 2. When the target client is near, use Deauth Targeting below. 3. Captured handshakes appear above — Export and run the hashcat command.';
|
||||
if (s && s.handshakes > 0) return 'Handshake captured! Export .hc22000 and run hashcat -m 22000.';
|
||||
return 'AP is live on ' + (w.ssid || 'the target') + '. Watch the handshakes list — use Deauth Targeting to nudge the client. If a client refuses to join the twin, its reconnect to the real AP is still captured passively.';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function load() {
|
||||
Promise.all([
|
||||
PagerAPI.post('/api/pineap/wifi/get_ap').catch(() => ({ data: {} })),
|
||||
PagerAPI.get('/api/pineap/filters/ssid').catch(() => ({ data: {} })),
|
||||
PagerAPI.get('/api/pineap/filters/client').catch(() => ({ data: {} }))
|
||||
]).then(([ap, sf, cf]) => {
|
||||
const a = ap.data || {};
|
||||
const open = a.open || {};
|
||||
ssidIn.value = open.ssid || '';
|
||||
bssidIn.value = open.bssid || '';
|
||||
if (open.channel != null) {
|
||||
const opts = Array.prototype.slice.call(chSel.options);
|
||||
if (opts.some((o) => Number(o.value) === Number(open.channel))) {
|
||||
chSel.value = String(open.channel);
|
||||
}
|
||||
}
|
||||
applyOaHint();
|
||||
if (open.country) coSel.value = open.country;
|
||||
hiddenCb.checked = !!open.hidden;
|
||||
state.enabledLoaded = !!(a.open);
|
||||
state.enabled = !!open.enabled;
|
||||
if (!karmaDirty) setKnownCheckbox(karmaCb, PINEAP_SESSION.karma);
|
||||
const sd = sf.data || {}, cd = cf.data || {};
|
||||
state.ssidFetched = !!sd.mode;
|
||||
state.clientFetched = !!cd.mode;
|
||||
state.ssidMode = sd.mode;
|
||||
state.clientMode = cd.mode;
|
||||
state.ssidList = sd.entries || [];
|
||||
state.clientList = cd.entries || [];
|
||||
render();
|
||||
});
|
||||
views.pineap_open = attackLauncher('open', {
|
||||
title: 'Evil Open',
|
||||
subtitle: 'Open network evil twin',
|
||||
bssid: true,
|
||||
country: true,
|
||||
tabHash: '#/pineap/open',
|
||||
playbook: {
|
||||
steps: ['Deploy the open AP',
|
||||
'Wait for clients to associate',
|
||||
'Watch connected clients under PineAP → Clients'],
|
||||
currentStep: (s, w) => {
|
||||
if (!w || !w.enabled) return 'Deploy the open AP';
|
||||
return 'Wait for clients to associate';
|
||||
},
|
||||
hint: (s, w) => !w || !w.enabled
|
||||
? 'Set the SSID (optionally spoof a BSSID), pick a channel (Auto finds it from recon), Deploy.'
|
||||
: 'Open AP is live on ' + (w.ssid || 'the target') + ' — clients that join appear in the Clients list.'
|
||||
}
|
||||
load();
|
||||
return { destroy: () => {} };
|
||||
};
|
||||
|
||||
const EVIL_ENC = [
|
||||
['psk2', 'WPA2 PSK'], ['sae', 'WPA3 SAE'], ['owe', 'WPA3 OWE']
|
||||
];
|
||||
|
||||
views.pineap_evilwpa = (root) => {
|
||||
const box = pineapShell(root, '#/pineap/evilwpa');
|
||||
const cfg = h('div', { class: 'pineap-title-card' },
|
||||
h('div', { class: 'pineap-card-title' }, 'Evil WPA'));
|
||||
box.appendChild(cfg);
|
||||
const ssidIn = h('input', { id: 'ew-ssid' });
|
||||
const pskIn = h('input', { id: 'ew-psk', type: 'password', autocomplete: 'new-password' });
|
||||
const encSel = h('select', { id: 'ew-enc' });
|
||||
EVIL_ENC.forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l })));
|
||||
const hiddenCb = h('input', { type: 'checkbox', id: 'ew-hidden' });
|
||||
const enabledCb = h('input', { type: 'checkbox', id: 'ew-enabled' });
|
||||
const wpaChan = h('select', { id: 'ew-channel' });
|
||||
chanSelect(wpaChan, null);
|
||||
const wpaHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px' });
|
||||
function applyWpaHint() {
|
||||
const six = bandOfChannel(wpaChan.value) === '6';
|
||||
Array.prototype.forEach.call(encSel.options, (o) => { o.disabled = six && o.value === 'psk2'; });
|
||||
if (six && encSel.value === 'psk2') encSel.value = 'sae';
|
||||
wpaHint.textContent = six ? '6 GHz requires WPA3 (SAE or OWE).' : '';
|
||||
}
|
||||
wpaChan.addEventListener('change', applyWpaHint);
|
||||
cfg.appendChild(h('label', {}, 'SSID', ssidIn));
|
||||
cfg.appendChild(h('label', {}, 'Passphrase', pskIn));
|
||||
cfg.appendChild(h('label', {}, 'Encryption', encSel));
|
||||
cfg.appendChild(h('label', {}, 'Channel', wpaChan));
|
||||
cfg.appendChild(wpaHint);
|
||||
cfg.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden'));
|
||||
cfg.appendChild(h('label', { class: 'switch' }, enabledCb, h('span', { class: 'track' }), 'Enabled'));
|
||||
cfg.appendChild(h('div', { class: 'row' },
|
||||
h('div', {}, btn('Save', () => {
|
||||
PagerAPI.post('/api/pineap/wifi/set_ap', {
|
||||
wpa: { ssid: ssidIn.value, passphrase: pskIn.value, enctype: encSel.value,
|
||||
hidden: hiddenCb.checked, enabled: enabledCb.checked,
|
||||
channel: wpaChan.value ? parseInt(wpaChan.value, 10) : 1 }
|
||||
}).then(() => { App.toast('Evil WPA saved'); load(); }).catch(() => App.toast('Failed', 'error'));
|
||||
})),
|
||||
h('div', { class: 'muted', style: 'align-self:center;font-size:12px' }, 'Applying reconfigures the radio — you may be disconnected briefly.')));
|
||||
|
||||
const capBox = h('div', { class: 'pineap-title-card' },
|
||||
h('div', { class: 'pineap-card-title' }, 'Handshake Capture'));
|
||||
box.appendChild(capBox);
|
||||
const captureCb = h('input', { type: 'checkbox', id: 'ew-capture' });
|
||||
const partialCb = h('input', { type: 'checkbox', id: 'ew-partial' });
|
||||
capBox.appendChild(h('div', { class: 'pineap-settings-section', text: 'Automatic Capture' }));
|
||||
capBox.appendChild(h('label', { class: 'switch' }, captureCb, h('span', { class: 'track' }), 'Capture WPA handshakes'));
|
||||
capBox.appendChild(h('label', { class: 'switch' }, partialCb, h('span', { class: 'track' }), 'Keep partial handshakes'));
|
||||
capBox.appendChild(h('div', { class: 'muted', style: 'margin:6px 0 10px;font-size:12px' },
|
||||
'Automatically save handshakes observed by PineAP. Partial captures may not contain enough material for password recovery.'));
|
||||
capBox.appendChild(btn('Save capture settings', () => {
|
||||
PagerAPI.post('/api/pineap/set_config', {
|
||||
loghandshake: captureCb.checked,
|
||||
logpartialhandshake: partialCb.checked
|
||||
}).then(() => { App.toast('Handshake capture settings saved'); load(); })
|
||||
.catch(() => App.toast('Failed to save capture settings', 'error'));
|
||||
}, 'ghost'));
|
||||
capBox.appendChild(h('div', { class: 'pineap-settings-section', text: 'Targeted Capture' }));
|
||||
const bssidIn = h('input', { id: 'ew-bssid', placeholder: 'BSSID' });
|
||||
const secsIn = h('input', { id: 'ew-secs', type: 'number', value: '30', style: 'max-width:80px' });
|
||||
capBox.appendChild(h('div', { class: 'row' },
|
||||
h('div', {}, h('label', {}, 'BSSID', bssidIn)),
|
||||
h('div', {}, h('label', {}, 'Seconds', secsIn)),
|
||||
h('div', {}, btn('Examine', () => {
|
||||
const b = bssidIn.value.trim();
|
||||
if (!b) { App.toast('BSSID required', 'error'); return; }
|
||||
PagerAPI.post('/api/pineap/examine', { bssid: b, seconds: parseInt(secsIn.value, 10) || 30 })
|
||||
.then(() => App.toast('Examining ' + b)).catch(() => App.toast('Failed', 'error'));
|
||||
})),
|
||||
h('div', {}, btn('Stop', () => PagerAPI.post('/api/pineap/examine', { reset: true }).then(() => App.toast('Stopped')), 'danger'))));
|
||||
|
||||
const hsBody = h('div', {});
|
||||
const hsBox = h('div', { class: 'pineap-title-card pineap-card-handshakes' },
|
||||
h('div', { class: 'pineap-card-title' }, 'Captured Handshakes'),
|
||||
hsBody);
|
||||
box.appendChild(hsBox);
|
||||
|
||||
function load() {
|
||||
PagerAPI.post('/api/pineap/wifi/get_ap').then((r) => {
|
||||
const w = (r.data || {}).wpa || {};
|
||||
ssidIn.value = w.ssid || '';
|
||||
pskIn.value = w.passphrase || '';
|
||||
if (w.enctype && Array.prototype.some.call(encSel.options, (o) => o.value === w.enctype)) {
|
||||
encSel.value = w.enctype;
|
||||
}
|
||||
hiddenCb.checked = !!w.hidden;
|
||||
enabledCb.checked = !!w.enabled;
|
||||
if (w.channel != null) {
|
||||
const opts = Array.prototype.slice.call(wpaChan.options);
|
||||
if (opts.some((o) => Number(o.value) === Number(w.channel))) {
|
||||
wpaChan.value = String(w.channel);
|
||||
}
|
||||
}
|
||||
applyWpaHint();
|
||||
}).catch(() => {});
|
||||
PagerAPI.get('/api/pineap/get_config').then((r) => {
|
||||
const p = r.data || {};
|
||||
captureCb.checked = !!p.loghandshake;
|
||||
partialCb.checked = !!p.logpartialhandshake;
|
||||
}).catch(() => {});
|
||||
PagerAPI.get('/api/pineap/handshakes').then((r) => {
|
||||
hsBody.innerHTML = '';
|
||||
const rows = (r.data.handshakes || []).map((x) => ({
|
||||
name: x.name || '--', ap: x.ap || '--', client: x.client || '--', type: x.type || '--'
|
||||
}));
|
||||
hsBody.appendChild(table(
|
||||
[{ label: 'File', key: 'name' }, { label: 'AP', key: 'ap' },
|
||||
{ label: 'Client', key: 'client' }, { label: 'Type', key: 'type' }],
|
||||
rows));
|
||||
if (!rows.length) hsBody.appendChild(h('div', { class: 'pineap-handshakes-none', text: 'No handshakes captured yet.' }));
|
||||
}).catch(() => {});
|
||||
}
|
||||
load();
|
||||
const iv = setInterval(load, 5000);
|
||||
return { destroy: () => clearInterval(iv) };
|
||||
};
|
||||
|
||||
views.pineap_enterprise = (root) => {
|
||||
const box = pineapShell(root, '#/pineap/enterprise');
|
||||
const cfg = h('div', { class: 'pineap-title-card' },
|
||||
h('div', { class: 'pineap-card-title' }, 'Evil Enterprise'));
|
||||
box.appendChild(cfg);
|
||||
const enabledCb = h('input', { type: 'checkbox', id: 'ee-enabled' });
|
||||
const authCb = h('input', { type: 'checkbox', id: 'ee-auth' });
|
||||
cfg.appendChild(h('label', { class: 'switch' }, enabledCb, h('span', { class: 'track' }), 'Enabled'));
|
||||
cfg.appendChild(h('label', { class: 'switch' }, authCb, h('span', { class: 'track' }), 'Auth Pass Capture'));
|
||||
enabledCb.addEventListener('change', () => PagerAPI.post('/api/pineap/hostapd', { pineape_disabled: !enabledCb.checked }).then(load).catch(() => { enabledCb.checked = !enabledCb.checked; App.toast('Failed', 'error'); }));
|
||||
authCb.addEventListener('change', () => PagerAPI.post('/api/pineap/hostapd', { pineape_auth_pass: authCb.checked }).then(load).catch(() => { authCb.checked = !authCb.checked; App.toast('Failed', 'error'); }));
|
||||
|
||||
function tableBox(name, endpoint, clearTable) {
|
||||
const body = h('div', {});
|
||||
const tb = h('div', { class: 'pineap-title-card pineap-card-inject' },
|
||||
h('div', { class: 'pineap-card-title-flex' },
|
||||
h('span', { text: name }),
|
||||
h('span', { class: 'toolbar-spacer' }),
|
||||
btn('Clear', () => PagerAPI.post('/api/pineap/enterprise/clear', { table: clearTable }).then(load), 'danger')),
|
||||
body);
|
||||
box.appendChild(tb);
|
||||
return { body, endpoint };
|
||||
}
|
||||
const basic = tableBox('Basic Data', '/api/pineap/enterprise/basic', 'basic');
|
||||
const chall = tableBox('Challenge Data', '/api/pineap/enterprise/challenge', 'challenge');
|
||||
|
||||
function load() {
|
||||
PagerAPI.get('/api/pineap/hostapd').then((r) => {
|
||||
const hh = r.data || {};
|
||||
enabledCb.checked = !hh.pineape_disabled;
|
||||
authCb.checked = !!hh.pineape_auth_pass;
|
||||
}).catch(() => {});
|
||||
[basic, chall].forEach((t) => {
|
||||
PagerAPI.get(t.endpoint).then((r) => {
|
||||
const rows = (r.data.rows || []).slice();
|
||||
t.body.innerHTML = '';
|
||||
const cols = rows.length ? Object.keys(rows[0]).map((k) => ({ label: k, key: k }))
|
||||
: [{ label: '—', key: '_none' }];
|
||||
t.body.appendChild(table(cols, rows));
|
||||
if (!rows.length) t.body.appendChild(h('div', { class: 'empty', text: 'No data captured.' }));
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
load();
|
||||
const iv = setInterval(load, 5000);
|
||||
return { destroy: () => clearInterval(iv) };
|
||||
};
|
||||
});
|
||||
|
||||
views.pineap_impersonation = (root) => {
|
||||
const box = pineapShell(root, '#/pineap/impersonation');
|
||||
@@ -1207,22 +922,6 @@ function reconFiltered(rows, q, colsArr) {
|
||||
// Attacks: one-click Evil WPA / Open / Enterprise launchers.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ATTACK_TABS = [
|
||||
{ label: 'Overview', hash: '#/attacks' },
|
||||
{ label: 'Evil WPA', hash: '#/attacks/wpa' },
|
||||
{ label: 'Open AP', hash: '#/attacks/open' },
|
||||
{ label: 'Evil Enterprise', hash: '#/attacks/enterprise' }
|
||||
];
|
||||
|
||||
function attacksShell(root, activeHash) {
|
||||
root.appendChild(h('h1', { class: 'page-title', text: 'Attacks' }));
|
||||
tabBar(root, ATTACK_TABS, activeHash);
|
||||
const box = h('div', {});
|
||||
box.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin:8px 0' },
|
||||
'Targets: only networks you are authorized to test. The device is the source of truth — every change is verified against it.'));
|
||||
root.appendChild(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
function attackBadge(ap) {
|
||||
if (!ap) return badge(false);
|
||||
@@ -1246,52 +945,9 @@ function verifiedToast(result) {
|
||||
else App.toast('Deploy failed', 'error');
|
||||
}
|
||||
|
||||
views.attacks = (root) => {
|
||||
const box = attacksShell(root, '#/attacks');
|
||||
const wrap = h('div', { class: 'pineap-title-card-container' });
|
||||
box.appendChild(wrap);
|
||||
const kinds = [
|
||||
['wpa', 'Evil WPA (PSK)', 'Clone a WPA2/WPA3-PSK network and capture the four-way handshake.', '#/attacks/wpa'],
|
||||
['open', 'Evil Open', 'Advertise an open network and watch who connects.', '#/attacks/open'],
|
||||
['enterprise', 'Evil Enterprise', 'Serve WPA2/3-Enterprise with PineAPE and harvest 802.1X credentials.', '#/attacks/enterprise']
|
||||
];
|
||||
const statusEls = {};
|
||||
kinds.forEach(([kind, label, desc, hash]) => {
|
||||
const card = h('div', { class: 'pineap-title-card' });
|
||||
const st = h('span', { class: 'badge', text: '—' });
|
||||
statusEls[kind] = st;
|
||||
card.appendChild(h('div', { class: 'pineap-card-title' },
|
||||
h('a', { href: hash, style: 'color:var(--primary);cursor:pointer',
|
||||
onclick: (e) => { e.preventDefault(); App.go(hash); } }, label)));
|
||||
card.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin:6px 0', text: desc }));
|
||||
card.appendChild(h('div', { class: 'row' }, st,
|
||||
h('a', { class: 'btn ghost', href: hash, style: 'text-decoration:none',
|
||||
onclick: (e) => { e.preventDefault(); App.go(hash); } }, 'Configure')));
|
||||
wrap.appendChild(card);
|
||||
});
|
||||
function load() {
|
||||
PagerAPI.get('/api/attacks/status').then((r) => {
|
||||
const s = r.data || {};
|
||||
const live = (x) => !!(x && x.enabled);
|
||||
const summary = {
|
||||
wpa: live(s.wpa && s.wpa.radio0) || live(s.wpa && s.wpa.radio1) ? 'LIVE' : 'OFF',
|
||||
open: live(s.open && s.open.radio0) || live(s.open && s.open.radio1) ? 'LIVE' : 'OFF',
|
||||
enterprise: live(s.enterprise && s.enterprise.ap) ? 'LIVE' : 'OFF'
|
||||
};
|
||||
Object.keys(summary).forEach((k) => {
|
||||
statusEls[k].textContent = summary[k];
|
||||
statusEls[k].className = 'badge ' + (summary[k] === 'LIVE' ? 'on' : 'off');
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
load();
|
||||
const iv = setInterval(load, 5000);
|
||||
return { destroy: () => clearInterval(iv) };
|
||||
};
|
||||
|
||||
function attackLauncher(kind, opts) {
|
||||
return (root) => {
|
||||
const box = attacksShell(root, '#/attacks/' + kind);
|
||||
const box = pineapShell(root, opts.tabHash || '#/pineap/evilwpa');
|
||||
const form = h('div', { class: 'pineap-title-card' });
|
||||
form.appendChild(h('div', { class: 'pineap-card-title' },
|
||||
opts.title + (opts.subtitle ? ' — ' + opts.subtitle : '')));
|
||||
@@ -1500,7 +1156,12 @@ views.harness = (root) => {
|
||||
const tok = h('code', { style: 'font-size:12px', text: '…' });
|
||||
const endpoint = h('code', { style: 'font-size:12px', text: location.origin + '/mcp' });
|
||||
infoBody.appendChild(h('div', { class: 'row' }, h('div', { style: 'min-width:130px', text: 'Endpoint' }), endpoint));
|
||||
infoBody.appendChild(h('div', { class: 'row' }, h('div', { style: 'min-width:130px', text: 'Bearer token' }), tok));
|
||||
infoBody.appendChild(h('div', { class: 'row' },
|
||||
h('div', { style: 'min-width:130px', text: 'Bearer token' }), tok,
|
||||
h('div', {}, btn('Copy Token', () => {
|
||||
navigator.clipboard.writeText(tok.textContent).then(() => App.toast('Token copied'))
|
||||
.catch(() => App.toast('Copy failed', 'error'));
|
||||
}))));
|
||||
infoBody.appendChild(h('div', { class: 'muted', style: 'font-size:12px', text: 'Agents call POST /mcp with JSON-RPC 2.0 (MCP Streamable HTTP). The token is the current session token.' }));
|
||||
|
||||
const snippet = h('pre', { style: 'font-size:12px;overflow:auto;background:rgba(127,127,127,.12);padding:10px;border-radius:4px;white-space:pre-wrap' });
|
||||
@@ -1510,38 +1171,6 @@ views.harness = (root) => {
|
||||
capBox.appendChild(capBody);
|
||||
box.appendChild(capBox);
|
||||
|
||||
const promptBox = h('div', { class: 'pineap-title-card' });
|
||||
promptBox.appendChild(h('div', { class: 'pineap-card-title' }, 'Prompt for pi.dev'));
|
||||
const promptArea = h('textarea', { rows: 14, style: 'width:100%;font-family:monospace;font-size:12px;box-sizing:border-box' });
|
||||
promptBox.appendChild(promptArea);
|
||||
promptBox.appendChild(h('div', { class: 'row', style: 'margin-top:8px' },
|
||||
h('div', {}, btn('Copy Prompt', () => {
|
||||
promptArea.select();
|
||||
document.execCommand('copy');
|
||||
App.toast('Copied');
|
||||
})),
|
||||
h('div', {}, btn('Copy Token', () => {
|
||||
navigator.clipboard.writeText(tok.textContent).then(() => App.toast('Token copied'))
|
||||
.catch(() => App.toast('Copy failed', 'error'));
|
||||
}))));
|
||||
box.appendChild(promptBox);
|
||||
|
||||
function buildPrompt(token) {
|
||||
return 'You are driving a WiFi Pineapple Pager (FENRIS firmware) through its local MCP harness.\n' +
|
||||
'Endpoint: ' + location.origin + '/mcp (Streamable HTTP, POST JSON-RPC 2.0).\n' +
|
||||
'Authorization: Bearer ' + token + '\n\n' +
|
||||
'Before acting, read these resources (MCP resources/read) — they are the field-verified operating manual:\n' +
|
||||
' skills://pineapple-control (device access, radios, UCI truth, pineapd crash-loop fix)\n' +
|
||||
' skills://wifi-deauth (deauth + handshake methodology, PMKSA failure modes)\n' +
|
||||
' skills://aircrack-suite (hashcat handoff)\n\n' +
|
||||
'Rules:\n' +
|
||||
'1. The DEVICE is the source of truth: read device.state / UCI before and after every change; never assume.\n' +
|
||||
'2. Only attack the network the operator explicitly authorized (currently <authorized-test-ssid>). No deauth blasts — short targeted bursts.\n' +
|
||||
'3. After attack.deploy, verify with attack.status (live flag) before proceeding.\n' +
|
||||
'4. Use the playbook prompts (prompts/get): evil-wpa-attack, evil-enterprise-attack, recon-survey.\n' +
|
||||
'5. Report verified outcomes only; say what you changed on the device.';
|
||||
}
|
||||
|
||||
function load() {
|
||||
PagerAPI.get('/api/harness/capabilities').then((r) => {
|
||||
const d = r.data || {};
|
||||
@@ -1562,7 +1191,6 @@ views.harness = (root) => {
|
||||
' -H "Content-Type: application/json" \\\n' +
|
||||
' -H "Authorization: Bearer ' + t + '" \\\n' +
|
||||
' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/list"}\'';
|
||||
promptArea.value = buildPrompt(t);
|
||||
infoBody.appendChild(snippet);
|
||||
}).catch(() => {});
|
||||
}
|
||||
@@ -1633,53 +1261,8 @@ function deauthPanel(ssidRef) {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
views.attacks_wpa = attackLauncher('wpa', {
|
||||
title: 'Evil WPA',
|
||||
subtitle: 'WPA2-PSK / WPA3-SAE / WPA3-OWE evil twin with handshake capture',
|
||||
passphrase: true,
|
||||
encodings: EVIL_ENC,
|
||||
handshakes: true,
|
||||
export: true,
|
||||
deauth: true,
|
||||
playbook: {
|
||||
steps: ['Deploy the evil twin',
|
||||
'Wait for a client to associate',
|
||||
'Deauth the target client to force the 4-way',
|
||||
'Export .hc22000 and crack with hashcat'],
|
||||
currentStep: (s, w) => {
|
||||
if (!w || !w.enabled) return 'Deploy the evil twin';
|
||||
if (!s || !(s.handshakes > 0)) return 'Wait for a client to associate';
|
||||
return 'Export .hc22000 and crack with hashcat';
|
||||
},
|
||||
hint: (s, w) => {
|
||||
if (!w || !w.enabled) return '1. Set the target SSID and passphrase, pick a channel, Deploy. 2. When the target client is near, use Deauth Targeting below. 3. Captured handshakes appear above — Export and run the hashcat command.';
|
||||
if (s && s.handshakes > 0) return 'Handshake captured! Export .hc22000 and run hashcat -m 22000.';
|
||||
return 'AP is live on ' + (w.ssid || 'the target') + '. Watch the handshakes list — use Deauth Targeting to nudge the client. If a client refuses to join the twin, its reconnect to the real AP is still captured passively.';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
views.attacks_open = attackLauncher('open', {
|
||||
title: 'Evil Open',
|
||||
subtitle: 'Open network evil twin',
|
||||
bssid: true,
|
||||
country: true,
|
||||
playbook: {
|
||||
steps: ['Deploy the open AP',
|
||||
'Wait for clients to associate',
|
||||
'Watch connected clients under PineAP → Clients'],
|
||||
currentStep: (s, w) => {
|
||||
if (!w || !w.enabled) return 'Deploy the open AP';
|
||||
return 'Wait for clients to associate';
|
||||
},
|
||||
hint: (s, w) => !w || !w.enabled
|
||||
? 'Set the SSID (optionally spoof a BSSID), pick a channel, Deploy.'
|
||||
: 'Open AP is live on ' + (w.ssid || 'the target') + ' — clients that join appear in the Clients list.'
|
||||
}
|
||||
});
|
||||
|
||||
views.attacks_enterprise = (root) => {
|
||||
const box = attacksShell(root, '#/attacks/enterprise');
|
||||
views.pineap_enterprise = (root) => {
|
||||
const box = pineapShell(root, '#/pineap/enterprise');
|
||||
const form = h('div', { class: 'pineap-title-card' });
|
||||
form.appendChild(h('div', { class: 'pineap-card-title' },
|
||||
'Evil Enterprise — WPA2/3-Enterprise with PineAPE credential harvest'));
|
||||
@@ -1692,15 +1275,19 @@ views.attacks_enterprise = (root) => {
|
||||
.forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l })));
|
||||
const pskIn = h('input', { id: 'ent-pass', type: 'password', autocomplete: 'new-password' });
|
||||
const hiddenCb = h('input', { type: 'checkbox', id: 'ent-hidden' });
|
||||
const chanSel = h('select', { id: 'ent-channel' });
|
||||
chanSelect(chanSel, null);
|
||||
f.appendChild(h('label', {}, 'SSID', ssidIn));
|
||||
f.appendChild(h('label', {}, 'Encryption', encSel));
|
||||
f.appendChild(h('label', {}, 'Passphrase (EAP server secret)', pskIn));
|
||||
f.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden'));
|
||||
f.appendChild(h('label', {}, 'Channel (5 GHz only — Auto uses the target SSID\u2019s recon channel)', chanSel));
|
||||
f.appendChild(h('div', { class: 'row', style: 'margin-top:10px' },
|
||||
h('div', {}, btn('Deploy Attack', () => {
|
||||
PagerAPI.post('/api/attacks/deploy', {
|
||||
kind: 'enterprise', ssid: ssidIn.value.trim(),
|
||||
enctype: encSel.value, passphrase: pskIn.value, hidden: hiddenCb.checked
|
||||
enctype: encSel.value, passphrase: pskIn.value, hidden: hiddenCb.checked,
|
||||
channel: chanSel.value ? parseInt(chanSel.value, 10) : null
|
||||
}).then((r) => { verifiedToast(r.data || {}); load(); })
|
||||
.catch((e) => App.toast(e.message || 'Deploy failed', 'error'));
|
||||
})),
|
||||
@@ -1779,11 +1366,15 @@ views.attacks_enterprise = (root) => {
|
||||
};
|
||||
function reconEncBucket(enc) {
|
||||
const s = (enc || '').trim();
|
||||
if (s === 'Open') return 'Open';
|
||||
if (s.indexOf('Enterprise') !== -1) return 'Enterprise';
|
||||
if (!s || s === 'Open') return 'Open';
|
||||
if (s.indexOf('WEP') !== -1) return 'WEP';
|
||||
if (s.indexOf('WPA2') !== -1) return 'WPA2';
|
||||
if (s.indexOf('WPA3') !== -1) return 'WPA3';
|
||||
if (s.indexOf('Enterprise') !== -1) {
|
||||
return s.indexOf('WPA3') !== -1 ? 'WPA3-Enterprise' : 'WPA2-Enterprise';
|
||||
}
|
||||
if (s.indexOf('SAE') !== -1 || s.indexOf('OWE') !== -1) return 'WPA3-Personal';
|
||||
if (s.indexOf('WPA3') !== -1 && s.indexOf('WPA2') !== -1) return 'WPA2-PSK';
|
||||
if (s.indexOf('WPA3') !== -1) return 'WPA3-PSK';
|
||||
if (s.indexOf('WPA2') !== -1) return 'WPA2-PSK';
|
||||
if (s.indexOf('WPA') !== -1) return 'WPA';
|
||||
return s || 'Unknown';
|
||||
}
|
||||
@@ -1878,8 +1469,6 @@ views.recon = (root) => {
|
||||
hsCol.appendChild(hsAuto);
|
||||
|
||||
const psContent = titleCard('Previous Scans', false);
|
||||
const psRow = h('div', { class: 'recon-ps-row' });
|
||||
psContent.appendChild(psRow);
|
||||
let pickerOptions = [];
|
||||
const sel = h('select', { class: 'sel', id: 'recon-scan-select' });
|
||||
sel.addEventListener('change', () => {
|
||||
@@ -1891,7 +1480,6 @@ views.recon = (root) => {
|
||||
state.detailId = null; state.detailArchive = null;
|
||||
loadDetail();
|
||||
});
|
||||
psRow.appendChild(sel);
|
||||
function dlBase() {
|
||||
if (state.selected == null) return null;
|
||||
return state.archive
|
||||
@@ -1910,9 +1498,6 @@ views.recon = (root) => {
|
||||
const base = dlBase();
|
||||
if (base) window.location = base + '/download/html';
|
||||
});
|
||||
psRow.appendChild(dlJson);
|
||||
psRow.appendChild(dlCsv);
|
||||
psRow.appendChild(dlHtml);
|
||||
const delBtn = iconBtn('delete', 'Delete scan', () => {
|
||||
if (state.selected == null || state.archive) return;
|
||||
if (!confirm('Delete scan #' + state.selected + '? This cannot be undone.')) return;
|
||||
@@ -1920,7 +1505,26 @@ views.recon = (root) => {
|
||||
.then(() => { App.toast('Scan deleted'); load(); })
|
||||
.catch(() => App.toast('Delete failed', 'error'));
|
||||
});
|
||||
psRow.appendChild(delBtn);
|
||||
const delAllBtn = iconBtn('delete_forever', 'Delete all scans', () => {
|
||||
if (!confirm('Delete ALL recorded scans? This cannot be undone.')) return;
|
||||
PagerAPI.del('/api/recon/scans')
|
||||
.then((r) => {
|
||||
App.toast('All scans deleted' + (r.data && r.data.deleted ? ' (' + r.data.deleted + ')' : ''));
|
||||
state.selected = null; state.archive = null;
|
||||
load();
|
||||
})
|
||||
.catch(() => App.toast('Delete failed', 'error'));
|
||||
});
|
||||
const psActions = h('div', { class: 'row', style: 'margin:6px 0 8px' });
|
||||
psActions.appendChild(dlJson);
|
||||
psActions.appendChild(dlCsv);
|
||||
psActions.appendChild(dlHtml);
|
||||
psActions.appendChild(delBtn);
|
||||
psActions.appendChild(delAllBtn);
|
||||
psContent.appendChild(psActions);
|
||||
const psRow = h('div', { class: 'recon-ps-row' });
|
||||
psContent.appendChild(psRow);
|
||||
psRow.appendChild(sel);
|
||||
|
||||
// ---- scan bar ----
|
||||
const scanBar = h('div', { class: 'section recon-scan-bar' });
|
||||
@@ -2280,7 +1884,8 @@ views.recon = (root) => {
|
||||
const groups = [
|
||||
['Band', 'apBand', [['all', 'All'], ['2.4', '2.4 GHz'], ['5', '5 GHz'], ['6', '6 GHz']]],
|
||||
['Encryption', 'apEnc', [['all', 'All'], ['Open', 'Open'], ['WEP', 'WEP'], ['WPA', 'WPA'],
|
||||
['WPA2', 'WPA2'], ['WPA3', 'WPA3'], ['Enterprise', 'Enterprise']]]
|
||||
['WPA2-PSK', 'WPA2-PSK'], ['WPA2-Enterprise', 'WPA2-Enterprise'],
|
||||
['WPA3-PSK', 'WPA3-Personal'], ['WPA3-Enterprise', 'WPA3-Enterprise']]]
|
||||
];
|
||||
groups.forEach(([label, key, opts]) => {
|
||||
chipRow.appendChild(h('span', { class: 'recon-chips-label', text: label }));
|
||||
@@ -2342,21 +1947,12 @@ views.recon = (root) => {
|
||||
function filteredRows(key) {
|
||||
const d = state.detail || {};
|
||||
if (key === 'client') {
|
||||
if (state.compare.length) return [];
|
||||
return reconFiltered(d.clients || [], state.clientSearch, RECON_CLIENT_COLS);
|
||||
}
|
||||
// Comparing never hides the list: selection just adds chips, a compare
|
||||
// table and charts. The full AP set stays visible so more boxes can be
|
||||
// ticked without clearing the selection first.
|
||||
const all = d.aps || [];
|
||||
if (state.compare.length) {
|
||||
if (state.apSearch) {
|
||||
// Candidate list: search the full AP list so more networks can be
|
||||
// added without clearing the selection. Band/enc chips apply here.
|
||||
let out = reconFiltered(all, state.apSearch, RECON_AP_COLS);
|
||||
if (state.apBand !== 'all') out = out.filter((a) => (a.band || '') === state.apBand);
|
||||
if (state.apEnc !== 'all') out = out.filter((a) => reconEncBucket(a.encryption) === state.apEnc);
|
||||
return out;
|
||||
}
|
||||
return all.filter((a) => state.compare.indexOf(a.bssid) !== -1);
|
||||
}
|
||||
let out = reconFiltered(all, state.apSearch, RECON_AP_COLS);
|
||||
if (state.apBand !== 'all') out = out.filter((a) => (a.band || '') === state.apBand);
|
||||
if (state.apEnc !== 'all') out = out.filter((a) => reconEncBucket(a.encryption) === state.apEnc);
|
||||
@@ -2408,7 +2004,8 @@ views.recon = (root) => {
|
||||
const slice = rows.slice(start, start + per);
|
||||
const rowAttrs = key === 'ap'
|
||||
? (r) => ({
|
||||
class: state.focusAp && state.focusAp.bssid === r.bssid ? 'recon-row-selected' : '',
|
||||
class: (state.focusAp && state.focusAp.bssid === r.bssid ? 'recon-row-selected' : '')
|
||||
+ (state.compare.indexOf(r.bssid) !== -1 ? ' recon-row-compare' : ''),
|
||||
style: 'cursor:pointer',
|
||||
onclick: () => toggleFocus(r)
|
||||
})
|
||||
@@ -2475,12 +2072,10 @@ views.recon = (root) => {
|
||||
const d = state.detail || { aps: [], clients: [], handshakes: [] };
|
||||
const apF = filteredRows('ap');
|
||||
const cliF = filteredRows('client');
|
||||
cliCard.classList.toggle('hidden', state.compare.length > 0);
|
||||
cliCard.classList.remove('hidden');
|
||||
renderTable(apBody, 'ap', sortRows(apF, 'ap', apCols), apCols,
|
||||
state.compare.length && !state.apSearch ? 'No access points selected.' : 'No access points in this scan.');
|
||||
if (!state.compare.length) {
|
||||
renderTable(cliBody, 'client', sortRows(cliF, 'client', RECON_CLIENT_COLS), RECON_CLIENT_COLS, 'No clients in this scan.');
|
||||
}
|
||||
'No access points in this scan.');
|
||||
renderTable(cliBody, 'client', sortRows(cliF, 'client', RECON_CLIENT_COLS), RECON_CLIENT_COLS, 'No clients in this scan.');
|
||||
}
|
||||
|
||||
function drawCharts(d) {
|
||||
@@ -2756,24 +2351,34 @@ views.recon_reports = (root) => {
|
||||
reportCard.appendChild(h('h2', { text: 'Scan Reports' }));
|
||||
const box = h('div');
|
||||
reportCard.appendChild(box);
|
||||
PagerAPI.get('/api/recon/scans').then((r) => {
|
||||
const scans = (r.data && r.data.scans) || [];
|
||||
box.innerHTML = '';
|
||||
if (!scans.length) { box.appendChild(h('div', { class: 'empty', text: 'No scans recorded yet.' })); return; }
|
||||
box.appendChild(table(
|
||||
[
|
||||
{ key: 'id', label: 'Scan', render: (s) => '#' + s.id },
|
||||
{ key: 'time', label: 'Started', render: (s) => fmtTime(s.time) },
|
||||
{ key: 'aps', label: 'APs' },
|
||||
{ key: 'devices', label: 'Clients' },
|
||||
{ key: 'handshakes', label: 'Handshakes' },
|
||||
{ key: 'actions', label: 'Download', render: (s) => h('span', { class: 'hs-actions' },
|
||||
iconBtn('file_download', 'JSON', () => dl('/api/recon/scans/' + s.id + '/download/json')),
|
||||
iconBtn('table_chart', 'CSV', () => dl('/api/recon/scans/' + s.id + '/download/csv')),
|
||||
iconBtn('description', 'HTML report', () => dl('/api/recon/scans/' + s.id + '/download/html'))) }
|
||||
],
|
||||
scans));
|
||||
}).catch(() => box.appendChild(h('div', { class: 'empty', text: 'Failed to load scans.' })));
|
||||
let gpsFix = null;
|
||||
PagerAPI.get('/api/recon/gps').then((r) => {
|
||||
const g = r.data || {};
|
||||
gpsFix = g.lock ? { lat: g.lat, lon: g.lon, sats: g.satellites } : null;
|
||||
}).catch(() => {}).then(() => {
|
||||
PagerAPI.get('/api/recon/scans').then((r) => {
|
||||
const scans = (r.data && r.data.scans) || [];
|
||||
box.innerHTML = '';
|
||||
if (!scans.length) { box.appendChild(h('div', { class: 'empty', text: 'No scans recorded yet.' })); return; }
|
||||
const gpsCell = (s) => gpsFix
|
||||
? h('span', { class: 'recon-gps-cell', text: Number(gpsFix.lat).toFixed(5) + ', ' + Number(gpsFix.lon).toFixed(5) })
|
||||
: h('span', { class: 'muted', text: '—' });
|
||||
box.appendChild(table(
|
||||
[
|
||||
{ key: 'id', label: 'Scan', render: (s) => '#' + s.id },
|
||||
{ key: 'time', label: 'Started', render: (s) => fmtTime(s.time) },
|
||||
{ key: 'gps', label: 'GPS', render: gpsCell },
|
||||
{ key: 'aps', label: 'APs' },
|
||||
{ key: 'devices', label: 'Clients' },
|
||||
{ key: 'handshakes', label: 'Handshakes' },
|
||||
{ key: 'actions', label: 'Download', render: (s) => h('span', { class: 'hs-actions' },
|
||||
iconBtn('file_download', 'JSON', () => dl('/api/recon/scans/' + s.id + '/download/json')),
|
||||
iconBtn('table_chart', 'CSV', () => dl('/api/recon/scans/' + s.id + '/download/csv')),
|
||||
iconBtn('description', 'HTML report', () => dl('/api/recon/scans/' + s.id + '/download/html'))) }
|
||||
],
|
||||
scans));
|
||||
}).catch(() => box.appendChild(h('div', { class: 'empty', text: 'Failed to load scans.' })));
|
||||
});
|
||||
}
|
||||
|
||||
function renderWigle() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -133,6 +134,54 @@ class AttacksDeployTest(unittest.TestCase):
|
||||
self.assertEqual(payload['iface'], 'wlan1wpa')
|
||||
self.assertEqual(payload['band'], server.BAND_5G)
|
||||
|
||||
def test_deploy_wpa_auto_channel_defaults_to_1_without_recon(self):
|
||||
status, payload = server.h_attacks_deploy(ctx({
|
||||
'kind': 'wpa', 'ssid': 'UnknownNet', 'passphrase': 'secretpass1',
|
||||
'enctype': 'psk2', 'hidden': False}))
|
||||
self.assertEqual(status, 200)
|
||||
self.assertTrue(payload['auto'])
|
||||
self.assertEqual(payload['channel'], 1)
|
||||
self.assertEqual(payload['band'], server.BAND_2G)
|
||||
cfg = [s for s in self.f.sock if s[0] == 'PUT' and s[1] == '/api/settings/wifi/set_ap'][0][2]
|
||||
self.assertEqual(cfg['configs'][0]['channel'], 1)
|
||||
|
||||
def test_deploy_wpa_auto_channel_uses_recon_target_channel(self):
|
||||
db = self._make_recon_db()
|
||||
old = server.RECON_DB
|
||||
server.RECON_DB = db
|
||||
try:
|
||||
status, payload = server.h_attacks_deploy(ctx({
|
||||
'kind': 'wpa', 'ssid': 'Anderson-5', 'passphrase': 'secretpass1',
|
||||
'enctype': 'psk2', 'hidden': False}))
|
||||
finally:
|
||||
server.RECON_DB = old
|
||||
os.unlink(db)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload['channel'], 149)
|
||||
self.assertEqual(payload['band'], server.BAND_5G)
|
||||
self.assertEqual(self.f.state['wireless.radio1.channel'], '149')
|
||||
|
||||
def _make_recon_db(self):
|
||||
fd, db = tempfile.mkstemp(suffix='.db')
|
||||
os.close(fd)
|
||||
conn = sqlite3.connect(db)
|
||||
conn.executescript(
|
||||
'CREATE TABLE scan(id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT,'
|
||||
' time INT, name TEXT);'
|
||||
'CREATE TABLE wifi_device(hash INT PRIMARY KEY, scan INT, mac TEXT,'
|
||||
' time INT, signal INT, freq INT, packets INT);'
|
||||
'CREATE TABLE ssid(hash INT PRIMARY KEY, wifi_device INT, scan INT,'
|
||||
' type INT, bssid TEXT, ssid BLOB, hidden INT, time INT, signal INT,'
|
||||
' freq INT, channel INT, encryption INT);')
|
||||
conn.execute("INSERT INTO scan (uuid, time, name) VALUES ('u1', 1, 'pager')")
|
||||
conn.execute("INSERT INTO ssid (hash, wifi_device, scan, type, bssid, ssid, hidden,"
|
||||
" time, signal, freq, channel, encryption) VALUES"
|
||||
" (10, 1, 1, 8, 'C89E43648080', X'416E646572736F6E2D35', 0,"
|
||||
" 1786466532, -76, 5745, 149, 0x400400108)")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return db
|
||||
|
||||
def test_deploy_open_2g4_includes_bssid_and_country(self):
|
||||
status, payload = server.h_attacks_deploy(ctx({
|
||||
'kind': 'open', 'ssid': 'Guest', 'hidden': False,
|
||||
|
||||
+8
-4
@@ -76,8 +76,13 @@ class DecodersTest(unittest.TestCase):
|
||||
self.assertEqual(server.decode_encryption(0x04), 'WPA')
|
||||
self.assertEqual(server.decode_encryption(0x08), 'WPA2')
|
||||
self.assertEqual(server.decode_encryption(0x04 | 0x08), 'WPA2 WPA')
|
||||
self.assertEqual(server.decode_encryption(0x400400108), 'WPA3 WPA2')
|
||||
self.assertEqual(server.decode_encryption(0x20050004C), 'WPA2 WPA')
|
||||
self.assertEqual(server.decode_encryption(0x400400108), 'WPA3 WPA2 PSK')
|
||||
self.assertEqual(server.decode_encryption(0x400400108 | (1 << 33)), 'WPA3 WPA2 Enterprise')
|
||||
self.assertEqual(server.decode_encryption(0x400400108 | (1 << 40)), 'WPA3 WPA2 SAE')
|
||||
self.assertEqual(server.decode_encryption(0x400400108 | (1 << 33) | (1 << 40)), 'WPA3 WPA2 Enterprise')
|
||||
self.assertEqual(server.decode_encryption(0x400400108 | (1 << 45)), 'WPA3 WPA2 OWE')
|
||||
self.assertEqual(server.decode_encryption(0x400400110), 'WPA3 PSK')
|
||||
self.assertEqual(server.decode_encryption(0x20050004C), 'WPA2 WPA Enterprise')
|
||||
|
||||
|
||||
class ReconDataTest(unittest.TestCase):
|
||||
@@ -111,7 +116,7 @@ class ReconDataTest(unittest.TestCase):
|
||||
self.assertEqual(a['ssid'], 'Anderson-5')
|
||||
self.assertEqual(a['channel'], 149)
|
||||
self.assertEqual(a['signal'], -76)
|
||||
self.assertEqual(a['encryption'], 'WPA3 WPA2')
|
||||
self.assertEqual(a['encryption'], 'WPA3 WPA2 PSK')
|
||||
self.assertFalse(a['hidden'])
|
||||
hidden = aps['50:6F:9A:01:00:00']
|
||||
self.assertTrue(hidden['hidden'])
|
||||
@@ -1193,4 +1198,3 @@ class ReconRoutesTest(unittest.TestCase):
|
||||
method = 'GET' if path.endswith(('status', 'scans', 'events')) else 'POST'
|
||||
h, args = server.ROUTER.dispatch(method, path)
|
||||
self.assertIsNotNone(h, path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user