Autarch/web/templates/revshell.html

451 lines
17 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %}Reverse Shell — AUTARCH{% endblock %}
{% block content %}
<div class="page-header">
<h1>Reverse Shell</h1>
<p>Remote device shell via Archon Companion App</p>
</div>
<!-- Listener Control -->
<div class="card">
<div class="card-header">
<h3>Listener</h3>
<span id="listenerStatus" class="status-badge {% if running %}status-good{% else %}status-bad{% endif %}">
{{ 'Running' if running else 'Stopped' }}
</span>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label>Port</label>
<input type="number" id="listenerPort" value="{{ port }}" class="form-input" style="width:100px">
</div>
<div class="form-group">
<label>Token</label>
<input type="text" id="listenerToken" value="{{ token }}" class="form-input" style="width:300px" readonly>
</div>
<div class="form-group" style="padding-top:24px">
<button onclick="rsListenerStart()" class="btn btn-primary" id="btnStart">Start</button>
<button onclick="rsListenerStop()" class="btn btn-danger" id="btnStop">Stop</button>
<button onclick="rsRefreshSessions()" class="btn btn-secondary">Refresh</button>
</div>
</div>
<p class="help-text">
On the Archon app: Modules tab → Reverse Shell → CONNECT.<br>
The device will connect back to this listener on port <strong id="portDisplay">{{ port }}</strong>.
</p>
</div>
</div>
<!-- Sessions -->
<div class="card">
<div class="card-header">
<h3>Sessions</h3>
<span id="sessionCount" class="status-badge">{{ sessions|length }}</span>
</div>
<div class="card-body">
<div id="sessionList">
{% if sessions %}
{% for s in sessions %}
<div class="session-card" data-sid="{{ s.session_id }}">
<div class="session-info">
<strong>{{ s.device }}</strong>
<span class="dim">Android {{ s.android }} | UID {{ s.uid }}</span>
<span class="dim">Connected {{ s.connected_at }} | {{ s.commands_executed }} cmds</span>
</div>
<div class="session-actions">
<button onclick="rsSelectSession('{{ s.session_id }}')" class="btn btn-primary btn-sm">Select</button>
<button onclick="rsDisconnect('{{ s.session_id }}')" class="btn btn-danger btn-sm">Disconnect</button>
</div>
</div>
{% endfor %}
{% else %}
<p class="dim">No active sessions. Start the listener and connect from the Archon app.</p>
{% endif %}
</div>
</div>
</div>
<!-- Terminal -->
<div class="card" id="terminalCard" style="display:none">
<div class="card-header">
<h3>Terminal — <span id="termDevice">?</span></h3>
<div>
<button onclick="rsSysinfo()" class="btn btn-secondary btn-sm">SysInfo</button>
<button onclick="rsScreenshot()" class="btn btn-secondary btn-sm">Screenshot</button>
<button onclick="rsPackages()" class="btn btn-secondary btn-sm">Packages</button>
<button onclick="rsProcesses()" class="btn btn-secondary btn-sm">Processes</button>
<button onclick="rsNetstat()" class="btn btn-secondary btn-sm">Netstat</button>
<button onclick="rsLogcat()" class="btn btn-secondary btn-sm">Logcat</button>
</div>
</div>
<div class="card-body">
<div id="termOutput" class="terminal-output"></div>
<div class="terminal-input-row">
<span class="terminal-prompt">shell@device:/ $</span>
<input type="text" id="termInput" class="terminal-input"
placeholder="Type command and press Enter"
onkeydown="if(event.key==='Enter')rsExecCmd()">
<button onclick="rsExecCmd()" class="btn btn-primary btn-sm">Run</button>
</div>
</div>
</div>
<!-- File Transfer -->
<div class="card" id="fileCard" style="display:none">
<div class="card-header">
<h3>File Transfer</h3>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label>Download from device</label>
<input type="text" id="downloadPath" class="form-input" placeholder="/sdcard/file.txt">
<button onclick="rsDownload()" class="btn btn-secondary btn-sm" style="margin-top:4px">Download</button>
</div>
<div class="form-group">
<label>Upload to device</label>
<input type="file" id="uploadFile" class="form-input">
<input type="text" id="uploadPath" class="form-input" placeholder="/data/local/tmp/file" style="margin-top:4px">
<button onclick="rsUpload()" class="btn btn-secondary btn-sm" style="margin-top:4px">Upload</button>
</div>
</div>
</div>
</div>
<!-- Screenshot Preview -->
<div class="card" id="screenshotCard" style="display:none">
<div class="card-header">
<h3>Screenshot</h3>
</div>
<div class="card-body" style="text-align:center">
<img id="screenshotImg" style="max-width:100%;max-height:600px;border:1px solid var(--border)">
</div>
</div>
<style>
.session-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background: var(--surface-dark);
border: 1px solid var(--border);
border-radius: 4px;
}
.session-card.selected {
border-color: var(--primary);
}
.session-info { display: flex; flex-direction: column; gap: 2px; }
.session-actions { display: flex; gap: 6px; }
.terminal-output {
background: #0a0a0a;
border: 1px solid var(--border);
border-radius: 4px;
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #00ff41;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.terminal-input-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
background: #0a0a0a;
border: 1px solid var(--border);
border-radius: 4px;
padding: 8px 12px;
}
.terminal-prompt {
color: #00ff41;
font-family: 'Courier New', monospace;
font-size: 13px;
white-space: nowrap;
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: 'Courier New', monospace;
font-size: 13px;
outline: none;
}
.dim { color: var(--text-secondary); font-size: 12px; }
.form-row { display: flex; gap: 16px; flex-wrap: wrap; }
.form-group { display: flex; flex-direction: column; }
.form-group label { font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
.help-text { font-size: 12px; color: var(--text-secondary); margin-top: 8px; }
.status-badge {
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: bold;
}
.status-good { background: #1a3a1a; color: #00ff41; }
.status-bad { background: #3a1a1a; color: #ff4444; }
.term-error { color: #ff4444; }
.term-info { color: #4488ff; }
.term-cmd { color: #888; }
</style>
<script>
let currentSession = null;
const cmdHistory = [];
let historyIdx = -1;
// ── Listener ───────────────────────────────────────────
function rsListenerStart() {
const port = document.getElementById('listenerPort').value;
fetchJSON('/revshell/listener/start', {port: parseInt(port)}, r => {
if (r.success) {
document.getElementById('listenerToken').value = r.token;
document.getElementById('portDisplay').textContent = port;
showStatus('listenerStatus', true, 'Running');
termLog('Listener started on port ' + port, 'info');
} else {
termLog('Failed: ' + r.message, 'error');
}
});
}
function rsListenerStop() {
fetchJSON('/revshell/listener/stop', {}, r => {
showStatus('listenerStatus', false, 'Stopped');
termLog('Listener stopped', 'info');
document.getElementById('sessionList').innerHTML = '<p class="dim">No active sessions.</p>';
});
}
function rsRefreshSessions() {
fetchJSON('/revshell/sessions', {}, r => {
const list = document.getElementById('sessionList');
const count = document.getElementById('sessionCount');
if (!r.sessions || r.sessions.length === 0) {
list.innerHTML = '<p class="dim">No active sessions.</p>';
count.textContent = '0';
return;
}
count.textContent = r.sessions.length;
list.innerHTML = r.sessions.map(s => `
<div class="session-card ${currentSession === s.session_id ? 'selected' : ''}" data-sid="${s.session_id}">
<div class="session-info">
<strong>${esc(s.device)}</strong>
<span class="dim">Android ${esc(s.android)} | UID ${s.uid} | ${s.commands_executed} cmds | ${formatUptime(s.uptime)}</span>
</div>
<div class="session-actions">
<button onclick="rsSelectSession('${s.session_id}')" class="btn btn-primary btn-sm">Select</button>
<button onclick="rsDisconnect('${s.session_id}')" class="btn btn-danger btn-sm">Disconnect</button>
</div>
</div>
`).join('');
});
}
// ── Session ────────────────────────────────────────────
function rsSelectSession(sid) {
currentSession = sid;
document.getElementById('terminalCard').style.display = '';
document.getElementById('fileCard').style.display = '';
document.querySelectorAll('.session-card').forEach(el => {
el.classList.toggle('selected', el.dataset.sid === sid);
});
fetchJSON(`/revshell/session/${sid}/info`, {}, r => {
if (r.success) {
document.getElementById('termDevice').textContent =
r.session.device + ' (Android ' + r.session.android + ')';
}
});
clearTerm();
termLog('Session selected: ' + sid, 'info');
document.getElementById('termInput').focus();
}
function rsDisconnect(sid) {
if (!confirm('Disconnect this session?')) return;
fetchJSON(`/revshell/session/${sid}/disconnect`, {}, r => {
termLog(r.message, r.success ? 'info' : 'error');
if (sid === currentSession) {
currentSession = null;
document.getElementById('terminalCard').style.display = 'none';
document.getElementById('fileCard').style.display = 'none';
}
rsRefreshSessions();
});
}
// ── Terminal ───────────────────────────────────────────
function rsExecCmd() {
if (!currentSession) { termLog('No session selected', 'error'); return; }
const input = document.getElementById('termInput');
const cmd = input.value.trim();
if (!cmd) return;
cmdHistory.unshift(cmd);
historyIdx = -1;
input.value = '';
termLog('$ ' + cmd, 'cmd');
fetchJSON(`/revshell/session/${currentSession}/execute`, {cmd: cmd}, r => {
if (r.stdout) termLog(r.stdout);
if (r.stderr) termLog(r.stderr, 'error');
if (r.exit_code !== 0) termLog('[exit: ' + r.exit_code + ']', 'error');
});
}
document.addEventListener('keydown', e => {
const input = document.getElementById('termInput');
if (document.activeElement !== input) return;
if (e.key === 'ArrowUp' && cmdHistory.length > 0) {
historyIdx = Math.min(historyIdx + 1, cmdHistory.length - 1);
input.value = cmdHistory[historyIdx];
e.preventDefault();
} else if (e.key === 'ArrowDown') {
historyIdx = Math.max(historyIdx - 1, -1);
input.value = historyIdx >= 0 ? cmdHistory[historyIdx] : '';
e.preventDefault();
}
});
// ── Quick Actions ──────────────────────────────────────
function rsSysinfo() {
if (!currentSession) return;
termLog('--- System Info ---', 'info');
fetchJSON(`/revshell/session/${currentSession}/sysinfo`, {}, r => {
termLog(r.stdout || r.stderr || r.message);
});
}
function rsScreenshot() {
if (!currentSession) return;
termLog('Capturing screenshot...', 'info');
const img = document.getElementById('screenshotImg');
img.src = `/revshell/session/${currentSession}/screenshot/view?t=` + Date.now();
document.getElementById('screenshotCard').style.display = '';
img.onerror = () => termLog('Screenshot failed', 'error');
img.onload = () => termLog('Screenshot captured', 'info');
}
function rsPackages() {
if (!currentSession) return;
termLog('--- Installed Packages ---', 'info');
fetchJSON(`/revshell/session/${currentSession}/packages`, {}, r => {
termLog(r.stdout || r.stderr || r.message);
});
}
function rsProcesses() {
if (!currentSession) return;
termLog('--- Processes ---', 'info');
fetchJSON(`/revshell/session/${currentSession}/processes`, {}, r => {
termLog(r.stdout || r.stderr || r.message);
});
}
function rsNetstat() {
if (!currentSession) return;
termLog('--- Network Connections ---', 'info');
fetchJSON(`/revshell/session/${currentSession}/netstat`, {}, r => {
termLog(r.stdout || r.stderr || r.message);
});
}
function rsLogcat() {
if (!currentSession) return;
termLog('--- Logcat (last 50 lines) ---', 'info');
fetchJSON(`/revshell/session/${currentSession}/logcat`, {lines: 50}, r => {
termLog(r.stdout || r.stderr || r.message);
});
}
// ── File Transfer ──────────────────────────────────────
function rsDownload() {
if (!currentSession) return;
const path = document.getElementById('downloadPath').value.trim();
if (!path) { termLog('No path specified', 'error'); return; }
termLog('Downloading: ' + path, 'info');
fetchJSON(`/revshell/session/${currentSession}/download`, {path: path}, r => {
termLog(r.success ? 'Saved to: ' + r.path : 'Failed: ' + r.message, r.success ? 'info' : 'error');
});
}
function rsUpload() {
if (!currentSession) return;
const fileInput = document.getElementById('uploadFile');
const remotePath = document.getElementById('uploadPath').value.trim();
if (!fileInput.files[0] || !remotePath) {
termLog('Select a file and specify remote path', 'error');
return;
}
termLog('Uploading to: ' + remotePath, 'info');
const fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('path', remotePath);
fetch(`/revshell/session/${currentSession}/upload`, {method:'POST', body:fd})
.then(r => r.json())
.then(r => termLog(r.success ? r.stdout : 'Failed: ' + r.stderr, r.success ? 'info' : 'error'));
}
// ── Helpers ────────────────────────────────────────────
function termLog(text, type) {
const out = document.getElementById('termOutput');
if (!out) return;
const span = document.createElement('span');
if (type === 'error') span.className = 'term-error';
else if (type === 'info') span.className = 'term-info';
else if (type === 'cmd') span.className = 'term-cmd';
span.textContent = text + '\n';
out.appendChild(span);
out.scrollTop = out.scrollHeight;
}
function clearTerm() {
const out = document.getElementById('termOutput');
if (out) out.innerHTML = '';
}
function showStatus(id, good, text) {
const el = document.getElementById(id);
el.textContent = text;
el.className = 'status-badge ' + (good ? 'status-good' : 'status-bad');
}
function formatUptime(sec) {
if (sec < 60) return sec + 's';
if (sec < 3600) return Math.floor(sec/60) + 'm';
return Math.floor(sec/3600) + 'h ' + Math.floor((sec%3600)/60) + 'm';
}
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function fetchJSON(url, data, cb) {
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(r => r.json()).then(cb).catch(e => {
if (typeof termLog === 'function') termLog('Request failed: ' + e, 'error');
});
}
// Auto-refresh sessions every 10s
setInterval(rsRefreshSessions, 10000);
</script>
{% endblock %}