feat: attacks workflow — playbook steps + auto-targeting deauth panel
Evil WPA/Open pages now show a state-aware playbook (current step highlighted, contextual hint) and the Deauth Targeting panel auto-fills the live attack SSID and refreshes clients automatically. Enterprise page gains a playbook hint card.
This commit is contained in:
@@ -1394,11 +1394,41 @@ function attackLauncher(kind, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let deauthCard = null;
|
let deauthCard = null;
|
||||||
|
const ssidRef = { current: '' };
|
||||||
if (opts.deauth) {
|
if (opts.deauth) {
|
||||||
deauthCard = deauthPanel();
|
deauthCard = deauthPanel(ssidRef);
|
||||||
box.appendChild(deauthCard);
|
box.appendChild(deauthCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.playbook) {
|
||||||
|
const pb = h('div', { class: 'pineap-title-card' });
|
||||||
|
pb.appendChild(h('div', { class: 'pineap-card-title' }, 'Playbook'));
|
||||||
|
const pbBody = h('div', { style: 'font-size:13px;line-height:1.8' });
|
||||||
|
pb.appendChild(pbBody);
|
||||||
|
box.appendChild(pb);
|
||||||
|
const steps = opts.playbook.steps;
|
||||||
|
const stepEls = {};
|
||||||
|
const stepList = h('ol', { style: 'margin:0;padding-left:20px' });
|
||||||
|
steps.forEach((s) => {
|
||||||
|
const li = h('li', { style: 'margin-top:4px' }, h('span', { text: s }));
|
||||||
|
stepEls[s] = li;
|
||||||
|
stepList.appendChild(li);
|
||||||
|
});
|
||||||
|
pbBody.appendChild(stepList);
|
||||||
|
const pbHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:6px' });
|
||||||
|
pbBody.appendChild(pbHint);
|
||||||
|
const pbTick = (status, which) => {
|
||||||
|
const activeStep = opts.playbook.currentStep(status, which);
|
||||||
|
steps.forEach((s) => {
|
||||||
|
stepEls[s].style.color = s === activeStep ? 'var(--primary)' : '';
|
||||||
|
stepEls[s].style.fontWeight = s === activeStep ? '600' : '';
|
||||||
|
});
|
||||||
|
pbHint.textContent = opts.playbook.hint(status, which) || '';
|
||||||
|
};
|
||||||
|
pbTick(null, null);
|
||||||
|
box.__pbTick = pbTick;
|
||||||
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
PagerAPI.get('/api/attacks/status').then((r) => {
|
PagerAPI.get('/api/attacks/status').then((r) => {
|
||||||
const s = r.data || {};
|
const s = r.data || {};
|
||||||
@@ -1416,6 +1446,11 @@ function attackLauncher(kind, opts) {
|
|||||||
text: 'Configured but interface not live yet — radio reload may still be in progress.' }));
|
text: 'Configured but interface not live yet — radio reload may still be in progress.' }));
|
||||||
}
|
}
|
||||||
capIface = (which && (which.band === '5' || which.band === '6')) ? 'wlan1mon' : 'wlan0mon';
|
capIface = (which && (which.band === '5' || which.band === '6')) ? 'wlan1mon' : 'wlan0mon';
|
||||||
|
if (ssidRef) {
|
||||||
|
ssidRef.current = which && which.ssid ? which.ssid : '';
|
||||||
|
if (ssidRef.tick) ssidRef.tick();
|
||||||
|
}
|
||||||
|
if (box.__pbTick) box.__pbTick(s, which);
|
||||||
if (opts.handshakes) {
|
if (opts.handshakes) {
|
||||||
hsBox.innerHTML = '';
|
hsBox.innerHTML = '';
|
||||||
hsBox.appendChild(h('div', { class: 'pineap-card-title', text: 'Handshakes Captured (' + (s.handshakes || 0) + ')' }));
|
hsBox.appendChild(h('div', { class: 'pineap-card-title', text: 'Handshakes Captured (' + (s.handshakes || 0) + ')' }));
|
||||||
@@ -1525,7 +1560,7 @@ views.harness = (root) => {
|
|||||||
return { destroy: () => {} };
|
return { destroy: () => {} };
|
||||||
};
|
};
|
||||||
|
|
||||||
function deauthPanel() {
|
function deauthPanel(ssidRef) {
|
||||||
const wrap = h('div', { class: 'pineap-title-card' });
|
const wrap = h('div', { class: 'pineap-title-card' });
|
||||||
wrap.appendChild(h('div', { class: 'pineap-card-title' }, 'Deauth Targeting'));
|
wrap.appendChild(h('div', { class: 'pineap-card-title' }, 'Deauth Targeting'));
|
||||||
const body = h('div', { style: 'font-size:13px' });
|
const body = h('div', { style: 'font-size:13px' });
|
||||||
@@ -1533,46 +1568,58 @@ function deauthPanel() {
|
|||||||
const ssidBox = h('input', { placeholder: 'SSID to find clients for' });
|
const ssidBox = h('input', { placeholder: 'SSID to find clients for' });
|
||||||
const apSel = h('select', {});
|
const apSel = h('select', {});
|
||||||
const clTable = h('div', {});
|
const clTable = h('div', {});
|
||||||
body.appendChild(h('div', { class: 'row' }, ssidBox,
|
let lastLookup = '';
|
||||||
h('div', {}, btn('Find', () => {
|
function lookup(q) {
|
||||||
const q = ssidBox.value.trim();
|
if (!q || q === lastLookup) return;
|
||||||
if (!q) return;
|
lastLookup = q;
|
||||||
PagerAPI.get('/api/attacks/clients?ssid=' + encodeURIComponent(q)).then((r) => {
|
PagerAPI.get('/api/attacks/clients?ssid=' + encodeURIComponent(q)).then((r) => {
|
||||||
const d = r.data || {};
|
const d = r.data || {};
|
||||||
apSel.innerHTML = '';
|
apSel.innerHTML = '';
|
||||||
(d.aps || []).forEach((a) => apSel.appendChild(h('option', {
|
(d.aps || []).forEach((a) => apSel.appendChild(h('option', {
|
||||||
value: (a.bssid || '') + '|' + (a.channel || 1),
|
value: (a.bssid || '') + '|' + (a.channel || 1),
|
||||||
text: (a.ssid || q) + ' — ' + (a.bssid || '?') + ' ch' + (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') };
|
||||||
})));
|
})));
|
||||||
if (!(d.aps || []).length) apSel.appendChild(h('option', { value: '|1', text: 'No APs found — check SSID' }));
|
}).catch(() => App.toast('Lookup failed', 'error'));
|
||||||
clTable.innerHTML = '';
|
}
|
||||||
const cl = (d.clients || []).slice(0, 30);
|
body.appendChild(h('div', { class: 'row' }, ssidBox,
|
||||||
if (!cl.length) {
|
h('div', {}, btn('Find', () => lookup(ssidBox.value.trim())))));
|
||||||
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(apSel);
|
||||||
body.appendChild(clTable);
|
body.appendChild(clTable);
|
||||||
body.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px',
|
body.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px',
|
||||||
text: 'Only deauth targets you are authorized to test.' }));
|
text: 'Only deauth targets you are authorized to test.' }));
|
||||||
|
if (ssidRef) {
|
||||||
|
ssidRef.tick = () => {
|
||||||
|
const liveSsid = ssidRef.current && ssidRef.current.trim();
|
||||||
|
if (liveSsid && ssidBox.value.trim() !== liveSsid) {
|
||||||
|
ssidBox.value = liveSsid;
|
||||||
|
lastLookup = '';
|
||||||
|
lookup(liveSsid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1583,14 +1630,42 @@ views.attacks_wpa = attackLauncher('wpa', {
|
|||||||
encodings: EVIL_ENC,
|
encodings: EVIL_ENC,
|
||||||
handshakes: true,
|
handshakes: true,
|
||||||
export: true,
|
export: true,
|
||||||
deauth: 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.';
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
views.attacks_open = attackLauncher('open', {
|
views.attacks_open = attackLauncher('open', {
|
||||||
title: 'Evil Open',
|
title: 'Evil Open',
|
||||||
subtitle: 'Open network evil twin',
|
subtitle: 'Open network evil twin',
|
||||||
bssid: true,
|
bssid: true,
|
||||||
country: 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) => {
|
views.attacks_enterprise = (root) => {
|
||||||
@@ -1628,6 +1703,13 @@ views.attacks_enterprise = (root) => {
|
|||||||
const status = attackStatusCard();
|
const status = attackStatusCard();
|
||||||
box.appendChild(status.card);
|
box.appendChild(status.card);
|
||||||
|
|
||||||
|
const pb = h('div', { class: 'pineap-title-card' });
|
||||||
|
pb.appendChild(h('div', { class: 'pineap-card-title' }, 'Playbook'));
|
||||||
|
const pbHint = h('div', { style: 'font-size:13px;line-height:1.8', text:
|
||||||
|
'1. Deploy the enterprise twin (5 GHz). 2. When a client attempts 802.1X, its identity and MSCHAPv2 challenge/response land in the tables below. 3. Crack offline: hashcat -m 5500 on the challenge data.' });
|
||||||
|
pb.appendChild(pbHint);
|
||||||
|
box.appendChild(pb);
|
||||||
|
|
||||||
const pApe = h('div', { class: 'pineap-title-card' });
|
const pApe = h('div', { class: 'pineap-title-card' });
|
||||||
pApe.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAPE'));
|
pApe.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAPE'));
|
||||||
const pApeBody = h('div', {});
|
const pApeBody = h('div', {});
|
||||||
|
|||||||
Reference in New Issue
Block a user