|
|
|
@@ -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() {
|
|
|
|
|