Files
2026-06-19 17:06:25 +02:00

751 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DarkMesh</title>
<style>
:root {
--bg-color: #121212;
--panel-color: #1e1e1e;
--panel-color-alt: #242424;
--text-color: #e0e0e0;
--primary-color: #00ff7f;
--border-color: #333;
--danger-color: #ff4136;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Courier New', Courier, monospace;
background-color: var(--bg-color);
color: var(--text-color);
padding: 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
}
header {
grid-column: 1 / -1;
text-align: center;
margin-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 1rem;
}
header h1 {
color: var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
}
.panel {
background-color: var(--panel-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.panel.full-width {
grid-column: 1 / -1;
}
h2 {
color: var(--primary-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
}
button {
font-family: inherit;
background-color: var(--primary-color);
color: var(--bg-color);
border: none;
padding: 0.75rem 1rem;
cursor: pointer;
font-weight: bold;
border-radius: 5px;
transition: all 0.2s ease;
margin-top: auto;
}
button:hover {
box-shadow: 0 0 10px var(--primary-color);
}
button#stop-attack-btn {
background-color: var(--danger-color);
}
button#stop-attack-btn:hover {
box-shadow: 0 0 10px var(--danger-color);
}
input,
textarea,
select {
font-family: inherit;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.5rem;
border-radius: 5px;
width: 100%;
}
textarea {
resize: vertical;
min-height: 60px;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.input-group input {
flex-grow: 1;
}
.btn-all {
padding: 0 1rem;
margin-top: 0;
background-color: var(--border-color);
color: var(--text-color);
font-weight: normal;
}
.param-group {
padding: 1rem;
border: 1px dashed var(--border-color);
border-radius: 5px;
margin-top: 0.5rem;
display: none;
flex-direction: column;
gap: 1rem;
}
.param-group.active {
display: flex;
}
.param-group p {
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #aaa;
}
.coord-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
}
#status-value {
font-weight: bold;
color: var(--primary-color);
}
.note {
font-size: 0.8rem;
color: #aaa;
font-style: italic;
}
.note code {
background-color: #000;
padding: 2px 5px;
border-radius: 3px;
font-style: normal;
}
.table-wrapper {
max-height: 400px;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 0.5rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
#node-list-body tr:nth-child(even) {
background-color: var(--panel-color-alt);
}
#node-list-body tr td:first-child {
cursor: pointer;
color: var(--primary-color);
}
th {
position: sticky;
top: 0;
background-color: var(--panel-color);
}
#debug-log {
background-color: #000;
height: 300px;
overflow-y: auto;
padding: 1rem;
border-radius: 5px;
white-space: pre-wrap;
word-break: break-all;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>💀 DarkMesh 💀</h1>
<p>Meshtastic Pen-Testing Suite</p>
</header>
<div class="panel">
<h2>Radio Configuration</h2>
<label for="radio-freq">Frequency (MHz)</label>
<input type="number" id="radio-freq" value="869.525" step="0.01">
<label for="lora-preset-selector">LoRa Presets</label>
<select id="lora-preset-selector">
<option value="long_fast">LongFast (Default)</option>
<option value="long_moderate">LongModerate</option>
<option value="long_slow">LongSlow</option>
<option value="medium_slow">MediumSlow</option>
<option value="medium_fast">MediumFast</option>
<option value="short_slow">ShortSlow</option>
<option value="short_fast">ShortFast</option>
<option value="custom">Custom</option>
</select>
<div class="config-grid">
<div><label for="lora-bw">Bandwidth</label><input type="number" id="lora-bw" value="250.0" step="0.1">
</div>
<div><label for="lora-sf">Spread Factor</label><input type="number" id="lora-sf" value="11" min="7"
max="12"></div>
<div><label for="lora-cr">Coding Rate</label><input type="number" id="lora-cr" value="5" min="5"
max="8"></div>
</div>
<label for="radio-power">Output Power (dBm)</label>
<input type="number" id="radio-power" value="22" min="2" max="22">
<label for="radio-chanhash">Channel Hash</label>
<input type="number" id="radio-chanhash" value="8" min="0" max="255">
<button id="apply-config-btn">Apply Configuration</button>
</div>
<div class="panel">
<h2>WiFi STA Configuration</h2>
<label for="wifi-ssid">SSID</label>
<input type="text" id="wifi-ssid" placeholder="Enter WiFi SSID">
<label for="wifi-password">Password</label>
<input type="password" id="wifi-password" placeholder="Enter WiFi password">
<button id="save-wifi-btn">Save WiFi Configuration</button>
</div>
<div class="panel">
<h2>Continuous Attacks</h2>
<p>Status: <span id="status-value">Idle</span></p>
<label for="attack-interval">Attack Interval (s)</label>
<input type="number" id="attack-interval" min="10" max="7200" value="10" title="The delay between attacks.">
<label for="attack-mqtt">Ok to MQTT</label>
<input type="checkbox" id="attack-mqtt" title="Allow attacks to be sent via MQTT.">
<select id="attack-selector">
<option value="none">-- Select Attack --</option>
<option value="node_flood">Flood with Nodes</option>
<option value="name_change">Namechange - Emoji</option>
<option value="pos_poison">Position Poison</option>
<option value="pki_poison">PKI Poison</option>
<option value="pki_dupe">PKI Duplication</option>
<option value="ddos">DDoS</option>
<option value="waypoint_flood">Waypoint Flood</option>
</select>
<p class="note">Note: A Target Node ID of <code>!ffffffff</code> means the attack will cycle through all
seen nodes.</p>
<div id="params-node_flood" class="param-group">
<p>Spams new Meshtastic nodes with coordinates too, filling nodedb-s.</p>
<div class="coord-grid">
<div><label for="flood-min-lat">Min Latitude</label><input type="number" id="flood-min-lat"
value="47.0"></div>
<div><label for="flood-max-lat">Max Latitude</label><input type="number" id="flood-max-lat"
value="48.0"></div>
<div><label for="flood-min-lon">Min Longitude</label><input type="number" id="flood-min-lon"
value="18.0"></div>
<div><label for="flood-max-lon">Max Longitude</label><input type="number" id="flood-max-lon"
value="20.0"></div>
</div>
<!-- <div> NOT WORKING WITH NEW CLIENT
<label for="flood-clientcrash">Crashclient on click</label>
<input type="checkbox" id="flood-clientcrash">
</div> -->
</div>
<div id="params-name_change" class="param-group">
<p>This appends the given string to longname of the selected node / random node. It is not saved.</p>
<label for="namechange-target-id">Target Node ID</label>
<div class="input-group">
<input type="text" id="namechange-target-id" value="!ffffffff">
<button type="button" class="btn-all">All</button>
</div>
<label for="emoji-input">Emoji/Text to Append</label>
<input type="text" id="emoji-input" value="😈" maxlength="30">
</div>
<div id="params-pos_poison" class="param-group">
<p>Randomizes a selected (or randomly selected) node's position.</p>
<label for="pospoison-target-id">Target Node ID</label>
<div class="input-group">
<input type="text" id="pospoison-target-id" value="!ffffffff">
<button type="button" class="btn-all">All</button>
</div>
<div class="coord-grid">
<div><label for="min-lat">Min Latitude</label><input type="number" id="min-lat" value="47.0"></div>
<div><label for="max-lat">Max Latitude</label><input type="number" id="max-lat" value="48.0"></div>
<div><label for="min-lon">Min Longitude</label><input type="number" id="min-lon" value="18.0"></div>
<div><label for="max-lon">Max Longitude</label><input type="number" id="max-lon" value="20.0"></div>
</div>
</div>
<div id="params-pki_poison" class="param-group">
<p>Randomizes a selected (or randomly selected) node's PKI, marking it red in other nodes' list.
</p>
<label for="pkipoison-target-id">Target Node ID</label>
<div class="input-group">
<input type="text" id="pkipoison-target-id" value="!ffffffff">
<button type="button" class="btn-all">All</button>
</div>
</div>
<div id="params-pki_dupe" class="param-group">
<p>Copied PKI of a selected (or randomly selected) node, alerting it the key has exposed.
</p>
<label for="pkidupe-target-id">Target Node ID</label>
<div class="input-group">
<input type="text" id="pkidupe-target-id" value="!ffffffff">
<button type="button" class="btn-all">All</button>
</div>
</div>
<div id="params-waypoint_flood" class="param-group">
<p>Floods the network with waypoints from a selected (or randomly selected) node.</p>
<label for="waypointflood-target-id">Source Node ID</label>
<div class="input-group">
<input type="text" id="waypoint_flood-target-id" value="!ffffffff">
<button type="button" class="btn-all">All</button>
</div>
<div class="coord-grid">
<div><label for="min-lat">Min Latitude</label><input type="number" id="min-lat" value="47.0"></div>
<div><label for="max-lat">Max Latitude</label><input type="number" id="max-lat" value="48.0"></div>
<div><label for="min-lon">Min Longitude</label><input type="number" id="min-lon" value="18.0"></div>
<div><label for="max-lon">Max Longitude</label><input type="number" id="max-lon" value="20.0"></div>
</div>
<div class="input-group">
<label for="flood-clientcrash">Crash client on map view</label>
<input type="checkbox" id="flood-clientcrash">
</div>
</div>
<label for="autostart-attack">Autostart Attack on boot</label>
<input type="checkbox" id="autostart-attack" title="Autostart the last selected attack on boot.">
<button id="start-attack-btn">Start Selected Attack</button>
<button id="stop-attack-btn">Stop Current Attack</button>
</div>
<div class="panel">
<h2>Send Message As (One-Time)</h2>
<label for="source-node-id">Source Node ID</label>
<input type="text" id="source-node-id" placeholder="e.g., !a1b2c3d4">
<label for="message-text">Message</label>
<textarea id="message-text" placeholder="Enter your message here..."></textarea>
<button id="send-message-btn">Send Message</button>
</div>
<div class="panel full-width">
<h2>Nodes Detected</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Node ID</th>
<th>Name</th>
<th>Position</th>
<th>Last Heard</th>
</tr>
</thead>
<tbody id="node-list-body">
<tr>
<td colspan="4">Waiting for nodes to show up...</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel full-width">
<h2>Debug Console</h2>
<div id="debug-log"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const allNodes = new Map();
const loraPresets = {
'long_fast': { bw: 250.0, sf: 11, cr: 5, chanhash: 8 },
'long_slow': { bw: 125.0, sf: 12, cr: 8, chanhash: 8 },
'long_moderate': { bw: 125.0, sf: 11, cr: 8, chanhash: 8 },
'medium_slow': { bw: 250.0, sf: 10, cr: 5, chanhash: 8 },
'medium_fast': { bw: 250.0, sf: 9, cr: 5, chanhash: 31 },
'short_slow': { bw: 250.0, sf: 8, cr: 5, chanhash: 8 },
'short_fast': { bw: 250.0, sf: 7, cr: 5, chanhash: 8 },
};
const statusValue = document.getElementById('status-value');
const nodeListBody = document.getElementById('node-list-body');
const debugLog = document.getElementById('debug-log');
const attackSelector = document.getElementById('attack-selector');
const startAttackBtn = document.getElementById('start-attack-btn');
const stopAttackBtn = document.getElementById('stop-attack-btn');
const autostartAttack = document.getElementById('autostart-attack');
const sendMessageBtn = document.getElementById('send-message-btn');
const applyConfigBtn = document.getElementById('apply-config-btn');
const saveWifiBtn = document.getElementById('save-wifi-btn');
const loraPresetSelector = document.getElementById('lora-preset-selector');
const loraBw = document.getElementById('lora-bw');
const loraSf = document.getElementById('lora-sf');
const loraCr = document.getElementById('lora-cr');
let socket;
function initWebSocket() {
const gateway = `ws://${window.location.hostname}/ws`;
socket = new WebSocket(gateway);
socket.onopen = () => {
logToDebug("🔗 Connected to DarkMesh ESP32.");
sendWebSocketMessage({ action: 'initme' });
};
socket.onclose = () => {
logToDebug("⛔ Connection lost. Retrying in 3 seconds...");
setTimeout(initWebSocket, 3000);
};
socket.onerror = (error) => {
logToDebug(`❌ WebSocket Error`);
};
socket.onmessage = async (event) => {
let str = "";
try {
str = await event.data.text();
const data = JSON.parse(str);
handleWebSocketMessage(data);
} catch (e) {
logToDebug(`Received non-JSON message: ${str}`);
}
};
}
function handleWebSocketMessage(data) {
switch (data.type) {
case 'status_update':
const attackName = data.current_attack || 'none';
statusValue.textContent = data.current_attack || 'Idle';
autostartAttack.checked = data.autostart_attack || false;
if (attackSelector.value !== attackName) {
attackSelector.value = attackName;
attackSelector.dispatchEvent(new Event('change'));
}
break;
case 'node_update':
if (data.nodes && Array.isArray(data.nodes)) {
data.nodes.forEach(node => {
if (node.id) {
node.js_last_seen = Date.now();
allNodes.set(node.id, node);
}
});
updateNodeList();
}
break;
case 'debug':
logToDebug(data.message);
break;
case 'radio_config':
if (data.config) {
loraPresetSelector.value = 'custom';
document.getElementById('radio-freq').value = data.config.frequency || 869.525;
loraBw.value = data.config.bandwidth || 250.0;
loraSf.value = data.config.spreading_factor || 11;
loraCr.value = data.config.coding_rate || 5;
document.getElementById('radio-chanhash').value = data.config.chanhash || 8;
document.getElementById('radio-power').value = data.config.power || 20;
break;
}
case 'wifi_sta_config':
if (data.config) {
document.getElementById('wifi-ssid').value = data.config.ssid || '';
document.getElementById('wifi-password').value = data.config.password || '';
}
break;
default:
logToDebug(`Unknown message type: ${data.type}`);
}
}
function timeAgo(timestamp) {
const now = Date.now();
const seconds = Math.floor((now - timestamp) / 1000);
if (seconds < 10) return "Just now";
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
return `${hours}h ago`;
}
function updateNodeList() {
const nodesArray = Array.from(allNodes.values());
nodesArray.sort((a, b) => b.js_last_seen - a.js_last_seen);
nodeListBody.innerHTML = '';
if (nodesArray.length > 0) {
nodesArray.forEach(node => {
const row = document.createElement('tr');
const lat = node.pos ? (node.pos.lat / 1e7).toFixed(4) : 'N/A';
const lon = node.pos ? (node.pos.lon / 1e7).toFixed(4) : 'N/A';
row.innerHTML = `
<td>${node.id || 'N/A'}</td>
<td>${node.name || 'N/A'}</td>
<td>${node.pos ? `${lat}, ${lon}` : 'N/A'}</td>
<td>${timeAgo(node.js_last_seen)}</td>
`;
nodeListBody.appendChild(row);
});
} else {
if (nodeListBody.innerHTML === '') {
nodeListBody.innerHTML = '<tr><td colspan="4">Waiting for nodes to show up...</td></tr>';
}
}
}
function logToDebug(message) {
const timestamp = new Date().toLocaleTimeString();
debugLog.innerHTML += `[${timestamp}] ${message}\n`;
debugLog.scrollTop = debugLog.scrollHeight;
}
attackSelector.addEventListener('change', () => {
document.querySelectorAll('.param-group').forEach(group => group.classList.remove('active'));
const selectedAttack = attackSelector.value;
const paramGroup = document.getElementById(`params-${selectedAttack}`);
if (paramGroup) {
paramGroup.classList.add('active');
}
});
loraPresetSelector.addEventListener('change', () => {
const preset = loraPresetSelector.value;
if (preset in loraPresets) {
const values = loraPresets[preset];
loraBw.value = values.bw;
loraSf.value = values.sf;
loraCr.value = values.cr;
document.getElementById('radio-chanhash').value = values.chanhash;
}
});
[loraBw, loraSf, loraCr].forEach(input => {
input.addEventListener('input', () => {
loraPresetSelector.value = 'custom';
});
});
function sendWebSocketMessage(data) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
} else {
logToDebug("❌ WebSocket not connected. Cannot send command.");
}
}
startAttackBtn.addEventListener('click', () => {
const attack = attackSelector.value;
if (attack === 'none') {
alert('Please select an attack first.');
return;
}
const command = { action: 'start_attack', attack_type: attack, params: {} };
command.params.interval = parseInt(document.getElementById('attack-interval').value, 10);
command.params.mqtt = document.getElementById('attack-mqtt').checked ? 1 : 0;
command.params.autostart = document.getElementById('autostart-attack').checked ? 1 : 0;
if (attack === 'node_flood') {
command.params.min_lat = parseFloat(document.getElementById('flood-min-lat').value);
command.params.max_lat = parseFloat(document.getElementById('flood-max-lat').value);
command.params.min_lon = parseFloat(document.getElementById('flood-min-lon').value);
command.params.max_lon = parseFloat(document.getElementById('flood-max-lon').value);
command.params.crashclient = 0; //document.getElementById('flood-clientcrash').checked ? 1 : 0; //not working with new clients
} else if (attack === 'name_change') {
command.params.target_id = document.getElementById('namechange-target-id').value;
command.params.emoji = document.getElementById('emoji-input').value;
} else if (attack === 'pos_poison') {
command.params.target_id = document.getElementById('pospoison-target-id').value;
command.params.min_lat = parseFloat(document.getElementById('min-lat').value);
command.params.max_lat = parseFloat(document.getElementById('max-lat').value);
command.params.min_lon = parseFloat(document.getElementById('min-lon').value);
command.params.max_lon = parseFloat(document.getElementById('max-lon').value);
} else if (attack === 'pki_poison') {
command.params.target_id = document.getElementById('pkipoison-target-id').value;
} else if (attack === 'pki_dupe') {
command.params.target_id = document.getElementById('pkidupe-target-id').value;
} else if (attack === 'waypoint_flood') {
command.params.target_id = document.getElementById('waypoint_flood-target-id').value;
command.params.min_lat = parseFloat(document.getElementById('min-lat').value);
command.params.max_lat = parseFloat(document.getElementById('max-lat').value);
command.params.min_lon = parseFloat(document.getElementById('min-lon').value);
command.params.max_lon = parseFloat(document.getElementById('max-lon').value);
command.params.crashclient = document.getElementById('flood-clientcrash').checked ? 1 : 0;
}
logToDebug(`▶️ Starting attack: ${attack}`);
sendWebSocketMessage(command);
});
stopAttackBtn.addEventListener('click', () => {
logToDebug("⏹️ Stopping current attack.");
sendWebSocketMessage({ action: 'stop_attack' });
});
sendMessageBtn.addEventListener('click', () => {
const sourceId = document.getElementById('source-node-id').value;
const message = document.getElementById('message-text').value;
if (!sourceId || !message) {
alert('Source Node ID and message cannot be empty.');
return;
}
logToDebug(`✉️ Sending message as ${sourceId}`);
sendWebSocketMessage({ action: 'send_message', source_id: sourceId, message: message });
});
applyConfigBtn.addEventListener('click', () => {
const config = {
action: 'set_config',
params: {
frequency: parseFloat(document.getElementById('radio-freq').value),
bandwidth: parseFloat(loraBw.value),
spreading_factor: parseInt(loraSf.value),
coding_rate: parseInt(loraCr.value),
power: parseInt(document.getElementById('radio-power').value),
chanhash: parseInt(document.getElementById('radio-chanhash').value)
}
};
logToDebug(`⚙️ Applying new radio configuration...`);
sendWebSocketMessage(config);
});
saveWifiBtn.addEventListener('click', () => {
const ssid = document.getElementById('wifi-ssid').value.trim();
const password = document.getElementById('wifi-password').value;
if (!ssid) {
alert('SSID cannot be empty.');
return;
}
logToDebug(`📶 Saving WiFi STA configuration for "${ssid}"...`);
sendWebSocketMessage({
action: 'set_wifi_sta',
params: {
ssid,
password
}
});
});
nodeListBody.addEventListener('click', (e) => {
if (e.target.tagName === 'TD' && e.target.cellIndex === 0) {
const nodeId = e.target.textContent;
if (nodeId && nodeId !== 'N/A') {
document.getElementById('namechange-target-id').value = nodeId;
document.getElementById('pospoison-target-id').value = nodeId;
document.getElementById('pkipoison-target-id').value = nodeId;
document.getElementById('pkidupe-target-id').value = nodeId;
document.getElementById('source-node-id').value = nodeId;
logToDebug(`📋 Copied Node ID to all fields: ${nodeId}`);
}
}
});
document.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-all')) {
const inputField = e.target.previousElementSibling;
if (inputField && inputField.tagName === 'INPUT') {
inputField.value = '!ffffffff';
}
}
});
initWebSocket();
setInterval(() => {
if (allNodes.size > 0) {
updateNodeList();
}
}, 30000);
});
</script>
</body>
</html>