451 lines
17 KiB
HTML
451 lines
17 KiB
HTML
|
|
{% 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 %}
|