|
|
|
@@ -1143,6 +1143,411 @@ function reconFiltered(rows, q, colsArr) {
|
|
|
|
|
return rows.filter((r) => colsArr.some((c) => String(r[c.key] == null ? '' : r[c.key]).toLowerCase().indexOf(ql) !== -1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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);
|
|
|
|
|
return h('div', {}, 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.'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function attackBadge(ap) {
|
|
|
|
|
if (!ap) return badge(false);
|
|
|
|
|
if (!ap.enabled) return h('span', { class: 'badge off', text: 'OFF' });
|
|
|
|
|
return h('span', { class: 'badge' + (ap.live ? ' on' : ' warn'), text: ap.live ? 'LIVE' : 'CONFIGURED' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function attackStatusCard() {
|
|
|
|
|
const card = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
card.appendChild(h('div', { class: 'pineap-card-title' }, 'Status'));
|
|
|
|
|
const rows = h('div', { style: 'font-size:13px;line-height:1.9' });
|
|
|
|
|
card.appendChild(rows);
|
|
|
|
|
const append = (label, node) => rows.appendChild(
|
|
|
|
|
h('div', { class: 'row' }, h('div', { style: 'min-width:130px', text: label }), node));
|
|
|
|
|
return { card, rows, append };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function verifiedToast(result) {
|
|
|
|
|
if (result && result.verified) App.toast('Applied and verified on device');
|
|
|
|
|
else if (result && result.ok) App.toast('Applied (verification pending — radio reloading)', 'error');
|
|
|
|
|
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 form = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
form.appendChild(h('div', { class: 'pineap-card-title' },
|
|
|
|
|
opts.title + (opts.subtitle ? ' — ' + opts.subtitle : '')));
|
|
|
|
|
box.appendChild(form);
|
|
|
|
|
const f = h('div', {});
|
|
|
|
|
form.appendChild(f);
|
|
|
|
|
|
|
|
|
|
const ssidIn = h('input', { id: 'atk-ssid' });
|
|
|
|
|
const hiddenCb = h('input', { type: 'checkbox', id: 'atk-hidden' });
|
|
|
|
|
const chanSel = h('select', { id: 'atk-channel' });
|
|
|
|
|
chanSelect(chanSel, null);
|
|
|
|
|
const bandHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px' });
|
|
|
|
|
let pskIn = null, encSel = null;
|
|
|
|
|
chanSel.addEventListener('change', () => {
|
|
|
|
|
const b = bandOfChannel(chanSel.value);
|
|
|
|
|
bandHint.textContent = b === '6' ? '6 GHz requires WPA3 (SAE/OWE).' : '';
|
|
|
|
|
if (encSel) {
|
|
|
|
|
Array.prototype.forEach.call(encSel.options, (o) => {
|
|
|
|
|
o.disabled = b === '6' && o.value !== 'sae' && o.value !== 'owe';
|
|
|
|
|
});
|
|
|
|
|
if (b === '6' && encSel.value === 'psk2') encSel.value = 'sae';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
f.appendChild(h('label', {}, 'SSID (target network)', ssidIn));
|
|
|
|
|
if (opts.passphrase) {
|
|
|
|
|
pskIn = h('input', { id: 'atk-psk', type: 'password', autocomplete: 'new-password' });
|
|
|
|
|
encSel = h('select', { id: 'atk-enc' });
|
|
|
|
|
(opts.encodings || EVIL_ENC).forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l })));
|
|
|
|
|
f.appendChild(h('label', {}, 'Passphrase', pskIn));
|
|
|
|
|
f.appendChild(h('label', {}, 'Encryption', encSel));
|
|
|
|
|
}
|
|
|
|
|
let bssidIn = null, coSel = null;
|
|
|
|
|
if (opts.bssid) {
|
|
|
|
|
bssidIn = h('input', { id: 'atk-bssid', placeholder: 'AA:BB:CC:DD:EE:FF' });
|
|
|
|
|
f.appendChild(h('label', {}, 'BSSID (spoof, optional)', bssidIn));
|
|
|
|
|
}
|
|
|
|
|
if (opts.country) {
|
|
|
|
|
coSel = h('select', { id: 'atk-country' });
|
|
|
|
|
OPEN_COUNTRIES.forEach(([v, l]) => coSel.appendChild(h('option', { value: v, text: l })));
|
|
|
|
|
f.appendChild(h('label', {}, 'Country', coSel));
|
|
|
|
|
}
|
|
|
|
|
f.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden'));
|
|
|
|
|
f.appendChild(h('label', {}, 'Channel', chanSel));
|
|
|
|
|
f.appendChild(bandHint);
|
|
|
|
|
f.appendChild(h('div', { class: 'row', style: 'margin-top:10px' },
|
|
|
|
|
h('div', {}, btn('Deploy Attack', () => {
|
|
|
|
|
const body = {
|
|
|
|
|
kind, ssid: ssidIn.value.trim(),
|
|
|
|
|
hidden: hiddenCb.checked,
|
|
|
|
|
channel: chanSel.value ? parseInt(chanSel.value, 10) : null
|
|
|
|
|
};
|
|
|
|
|
if (pskIn) { body.passphrase = pskIn.value; body.enctype = encSel.value; }
|
|
|
|
|
if (bssidIn) body.bssid = bssidIn.value.trim();
|
|
|
|
|
if (coSel) body.country = coSel.value;
|
|
|
|
|
PagerAPI.post('/api/attacks/deploy', body)
|
|
|
|
|
.then((r) => { verifiedToast(r.data || {}); load(); })
|
|
|
|
|
.catch((e) => { App.toast(e.message || 'Deploy failed', 'error'); });
|
|
|
|
|
})),
|
|
|
|
|
h('div', {}, btn('Stop Attack', () => {
|
|
|
|
|
PagerAPI.post('/api/attacks/stop', { kind })
|
|
|
|
|
.then(() => { App.toast('Attack stopped'); load(); })
|
|
|
|
|
.catch(() => App.toast('Stop failed', 'error'));
|
|
|
|
|
}, 'danger')),
|
|
|
|
|
h('div', { class: 'muted', style: 'align-self:center;font-size:12px' },
|
|
|
|
|
'Deploy reconfigures the radio — you may be disconnected briefly.')));
|
|
|
|
|
|
|
|
|
|
const status = attackStatusCard();
|
|
|
|
|
box.appendChild(status.card);
|
|
|
|
|
|
|
|
|
|
const hsBox = h('div', {});
|
|
|
|
|
const captureBox = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
captureBox.appendChild(h('div', { class: 'pineap-card-title' }, 'Monitor Capture'));
|
|
|
|
|
const capBody = h('div', { style: 'font-size:13px' });
|
|
|
|
|
captureBox.appendChild(capBody);
|
|
|
|
|
box.appendChild(captureBox);
|
|
|
|
|
|
|
|
|
|
let capIface = 'wlan0mon';
|
|
|
|
|
function capRow(st) {
|
|
|
|
|
capBody.innerHTML = '';
|
|
|
|
|
const run = !!(st && st.running);
|
|
|
|
|
capBody.appendChild(h('div', { class: 'row' },
|
|
|
|
|
h('span', { text: run ? ('Capturing on ' + (st.iface || capIface)) : 'Not capturing' }),
|
|
|
|
|
h('div', {}, btn(run ? 'Stop Capture' : 'Start Capture', () => {
|
|
|
|
|
PagerAPI.post('/api/attacks/capture', {
|
|
|
|
|
iface: capIface, action: run ? 'stop' : 'start'
|
|
|
|
|
}).then((r) => capRow(r.data || {})).catch(() => App.toast('Capture failed', 'error'));
|
|
|
|
|
}))));
|
|
|
|
|
capBody.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px',
|
|
|
|
|
text: run && st.path ? st.path : '' }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opts.export) {
|
|
|
|
|
const exp = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
exp.appendChild(h('div', { class: 'pineap-card-title' }, 'Hashcat Export'));
|
|
|
|
|
const expBody = h('div', { style: 'font-size:13px;line-height:1.8' });
|
|
|
|
|
exp.appendChild(expBody);
|
|
|
|
|
expBody.appendChild(h('div', {}, btn('Export .hc22000', () => {
|
|
|
|
|
PagerAPI.get('/api/attacks/export/hc22000').then((r) => {
|
|
|
|
|
const d = r.data || {};
|
|
|
|
|
if (d.file) {
|
|
|
|
|
expBody.appendChild(h('div', { class: 'row', style: 'margin-top:8px' },
|
|
|
|
|
h('a', { class: 'btn ghost', href: '/api/attacks/export/hc22000/' + d.name,
|
|
|
|
|
style: 'text-decoration:none', download: d.name }, 'Download ' + d.name),
|
|
|
|
|
h('code', { style: 'font-size:12px', text: d.hashcat || '' })));
|
|
|
|
|
App.toast('Export ready');
|
|
|
|
|
} else App.toast('No captures to export', 'error');
|
|
|
|
|
}).catch(() => App.toast('Export failed', 'error'));
|
|
|
|
|
})));
|
|
|
|
|
box.appendChild(exp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let deauthCard = null;
|
|
|
|
|
if (opts.deauth) {
|
|
|
|
|
deauthCard = deauthPanel();
|
|
|
|
|
box.appendChild(deauthCard);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function load() {
|
|
|
|
|
PagerAPI.get('/api/attacks/status').then((r) => {
|
|
|
|
|
const s = r.data || {};
|
|
|
|
|
const ap = s[kind] || {};
|
|
|
|
|
const which = (ap.radio0 && ap.radio0.enabled) ? ap.radio0
|
|
|
|
|
: (ap.radio1 && ap.radio1.enabled) ? ap.radio1 : null;
|
|
|
|
|
status.rows.innerHTML = '';
|
|
|
|
|
status.append('Attack', attackBadge(which));
|
|
|
|
|
status.append('SSID', h('span', { text: which && which.ssid ? which.ssid : '—' }));
|
|
|
|
|
status.append('Interface', h('span', { text: which ? which.iface : '—' }));
|
|
|
|
|
status.append('Band', h('span', { text: which && which.band ? which.band + ' GHz' : '—' }));
|
|
|
|
|
status.append('Channel', h('span', { text: which && which.channel != null ? which.channel : '—' }));
|
|
|
|
|
if (which && !which.live) {
|
|
|
|
|
status.append('Note', h('span', { class: 'pineap-infobox warn',
|
|
|
|
|
text: 'Configured but interface not live yet — radio reload may still be in progress.' }));
|
|
|
|
|
}
|
|
|
|
|
capIface = (which && (which.band === '5' || which.band === '6')) ? 'wlan1mon' : 'wlan0mon';
|
|
|
|
|
if (opts.handshakes) {
|
|
|
|
|
hsBox.innerHTML = '';
|
|
|
|
|
hsBox.appendChild(h('div', { class: 'pineap-card-title', text: 'Handshakes Captured (' + (s.handshakes || 0) + ')' }));
|
|
|
|
|
PagerAPI.get('/api/pineap/handshakes').then((hr) => {
|
|
|
|
|
const hd = (hr.data || {}).parsed || [];
|
|
|
|
|
const rows = hd.slice(-10).reverse().map((x) => ({
|
|
|
|
|
ssid: x.ssid || '—', bssid: x.bssid || '—',
|
|
|
|
|
when: fmtTime(x.ts || 0), type: x.type || ''
|
|
|
|
|
}));
|
|
|
|
|
hsBox.appendChild(rows.length ? table(
|
|
|
|
|
[{ key: 'when', label: 'When' }, { key: 'ssid', label: 'SSID' },
|
|
|
|
|
{ key: 'bssid', label: 'BSSID' }, { key: 'type', label: 'Type' }], rows)
|
|
|
|
|
: h('div', { class: 'empty', text: 'No handshakes yet.' }));
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
box.appendChild(hsBox);
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
PagerAPI.post('/api/attacks/capture', { action: 'status' }).then((r) => capRow(r.data || {})).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
load();
|
|
|
|
|
const iv = setInterval(load, 5000);
|
|
|
|
|
return { destroy: () => clearInterval(iv) };
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deauthPanel() {
|
|
|
|
|
const wrap = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
wrap.appendChild(h('div', { class: 'pineap-card-title' }, 'Deauth Targeting'));
|
|
|
|
|
const body = h('div', { style: 'font-size:13px' });
|
|
|
|
|
wrap.appendChild(body);
|
|
|
|
|
const ssidBox = h('input', { placeholder: 'SSID to find clients for' });
|
|
|
|
|
const apSel = h('select', {});
|
|
|
|
|
const clTable = h('div', {});
|
|
|
|
|
body.appendChild(h('div', { class: 'row' }, ssidBox,
|
|
|
|
|
h('div', {}, btn('Find', () => {
|
|
|
|
|
const q = ssidBox.value.trim();
|
|
|
|
|
if (!q) return;
|
|
|
|
|
PagerAPI.get('/api/attacks/clients?ssid=' + encodeURIComponent(q)).then((r) => {
|
|
|
|
|
const d = r.data || {};
|
|
|
|
|
apSel.innerHTML = '';
|
|
|
|
|
(d.aps || []).forEach((a) => apSel.appendChild(h('option', {
|
|
|
|
|
value: (a.bssid || '') + '|' + (a.channel || 1),
|
|
|
|
|
text: (a.ssid || q) + ' — ' + (a.bssid || '?') + ' ch' + (a.channel || 1)
|
|
|
|
|
})));
|
|
|
|
|
if (!(d.aps || []).length) apSel.appendChild(h('option', { value: '|1', text: 'No APs found — check SSID' }));
|
|
|
|
|
clTable.innerHTML = '';
|
|
|
|
|
const cl = (d.clients || []).slice(0, 30);
|
|
|
|
|
if (!cl.length) {
|
|
|
|
|
clTable.appendChild(h('div', { class: 'empty', text: 'No devices in recon yet.' }));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
clTable.appendChild(table(
|
|
|
|
|
[{ key: 'mac', label: 'MAC' }, { key: 'freq', label: 'Freq (MHz)' },
|
|
|
|
|
{ key: 'signal', label: 'Signal' }, { key: 'packets', label: 'Packets' },
|
|
|
|
|
{ key: '_deauth', label: '' }],
|
|
|
|
|
cl.map((c) => {
|
|
|
|
|
const mac = c.mac || c.client_mac || '';
|
|
|
|
|
return { mac: mac || '—', freq: c.freq || '', signal: c.signal || '',
|
|
|
|
|
packets: c.packets || '', _deauth: btn('Deauth', () => {
|
|
|
|
|
const parts = apSel.value.split('|');
|
|
|
|
|
if (!mac || !parts[0]) { App.toast('Pick an AP and device first', 'error'); return; }
|
|
|
|
|
PagerAPI.post('/api/attacks/deauth', {
|
|
|
|
|
bssid: parts[0], client: mac, channel: parseInt(parts[1], 10)
|
|
|
|
|
}).then(() => App.toast('Deauth frames sent'))
|
|
|
|
|
.catch((e) => App.toast(e.message || 'Deauth failed', 'error'));
|
|
|
|
|
}, 'danger') };
|
|
|
|
|
})));
|
|
|
|
|
}).catch(() => App.toast('Lookup failed', 'error'));
|
|
|
|
|
}))));
|
|
|
|
|
body.appendChild(apSel);
|
|
|
|
|
body.appendChild(clTable);
|
|
|
|
|
body.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px',
|
|
|
|
|
text: 'Only deauth targets you are authorized to test.' }));
|
|
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
views.attacks_open = attackLauncher('open', {
|
|
|
|
|
title: 'Evil Open',
|
|
|
|
|
subtitle: 'Open network evil twin',
|
|
|
|
|
bssid: true,
|
|
|
|
|
country: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
views.attacks_enterprise = (root) => {
|
|
|
|
|
const box = attacksShell(root, '#/attacks/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'));
|
|
|
|
|
box.appendChild(form);
|
|
|
|
|
const f = h('div', {});
|
|
|
|
|
form.appendChild(f);
|
|
|
|
|
const ssidIn = h('input', { id: 'ent-ssid' });
|
|
|
|
|
const encSel = h('select', { id: 'ent-enc' });
|
|
|
|
|
[['wpa2', 'WPA2 Enterprise'], ['wpa3', 'WPA3 Enterprise']]
|
|
|
|
|
.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' });
|
|
|
|
|
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('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
|
|
|
|
|
}).then((r) => { verifiedToast(r.data || {}); load(); })
|
|
|
|
|
.catch((e) => App.toast(e.message || 'Deploy failed', 'error'));
|
|
|
|
|
})),
|
|
|
|
|
h('div', {}, btn('Stop Attack', () => {
|
|
|
|
|
PagerAPI.post('/api/attacks/stop', { kind: 'enterprise' })
|
|
|
|
|
.then(() => { App.toast('Attack stopped'); load(); })
|
|
|
|
|
.catch(() => App.toast('Stop failed', 'error'));
|
|
|
|
|
}, 'danger'))));
|
|
|
|
|
|
|
|
|
|
const status = attackStatusCard();
|
|
|
|
|
box.appendChild(status.card);
|
|
|
|
|
|
|
|
|
|
const pApe = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
pApe.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAPE'));
|
|
|
|
|
const pApeBody = h('div', {});
|
|
|
|
|
pApe.appendChild(pApeBody);
|
|
|
|
|
box.appendChild(pApe);
|
|
|
|
|
const authCb = h('input', { type: 'checkbox', id: 'ent-auth' });
|
|
|
|
|
authCb.addEventListener('change', () => PagerAPI.post('/api/pineap/hostapd', { pineape_auth_pass: authCb.checked })
|
|
|
|
|
.then(load).catch(() => { authCb.checked = !authCb.checked; App.toast('Failed', 'error'); }));
|
|
|
|
|
|
|
|
|
|
const tables = [['Basic Data', '/api/pineap/enterprise/basic', 'basic'],
|
|
|
|
|
['Challenge Data', '/api/pineap/enterprise/challenge', 'challenge']]
|
|
|
|
|
.map(([name, endpoint, tableKey]) => {
|
|
|
|
|
const tb = h('div', { class: 'pineap-title-card' });
|
|
|
|
|
const body = h('div', {});
|
|
|
|
|
tb.appendChild(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: tableKey }).then(load), 'danger')));
|
|
|
|
|
tb.appendChild(body);
|
|
|
|
|
box.appendChild(tb);
|
|
|
|
|
return { body, endpoint };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function load() {
|
|
|
|
|
PagerAPI.get('/api/attacks/status').then((r) => {
|
|
|
|
|
const s = r.data || {};
|
|
|
|
|
const ent = s.enterprise || {};
|
|
|
|
|
const ap = ent.ap || null;
|
|
|
|
|
status.rows.innerHTML = '';
|
|
|
|
|
status.append('Attack', attackBadge(ap));
|
|
|
|
|
status.append('SSID', h('span', { text: ap && ap.ssid ? ap.ssid : '—' }));
|
|
|
|
|
status.append('Interface', h('span', { text: ap ? ap.iface : '—' }));
|
|
|
|
|
status.append('Band', h('span', { text: '2.4 GHz' }));
|
|
|
|
|
status.append('Channel', h('span', { text: ap && ap.channel != null ? ap.channel : '—' }));
|
|
|
|
|
status.append('Credentials', h('span', { text: String(ent.creds || 0) }));
|
|
|
|
|
pApeBody.innerHTML = '';
|
|
|
|
|
pApeBody.appendChild(h('label', { class: 'switch' }, authCb, h('span', { class: 'track' }), 'Auth Pass Capture'));
|
|
|
|
|
if (ent.pineape) {
|
|
|
|
|
pApeBody.appendChild(h('div', { class: 'muted', style: 'font-size:12px',
|
|
|
|
|
text: 'PineAPE ' + (ent.pineape.enabled ? 'enabled' : 'disabled') }));
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
tables.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) };
|
|
|
|
|
};
|
|
|
|
|
function reconEncBucket(enc) {
|
|
|
|
|
const s = (enc || '').trim();
|
|
|
|
|
if (s === 'Open') return 'Open';
|
|
|
|
|