Full security platform with web dashboard, 16 Flask blueprints, 26 modules, autonomous AI agent, WebUSB hardware support, and Archon Android companion app. Includes Hash Toolkit, debug console, anti-stalkerware shield, Metasploit/RouterSploit integration, WireGuard VPN, OSINT reconnaissance, and multi-backend LLM support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
494 lines
17 KiB
Python
494 lines
17 KiB
Python
"""
|
|
AUTARCH Reverse Shell Listener
|
|
Accepts incoming reverse shell connections from the Archon Android companion app.
|
|
|
|
Protocol: JSON over TCP, newline-delimited. Matches ArchonShell.java.
|
|
|
|
Auth handshake:
|
|
Client → Server: {"type":"auth","token":"xxx","device":"model","android":"14","uid":2000}
|
|
Server → Client: {"type":"auth_ok"} or {"type":"auth_fail","reason":"..."}
|
|
|
|
Command flow:
|
|
Server → Client: {"type":"cmd","cmd":"ls","timeout":30,"id":"abc"}
|
|
Client → Server: {"type":"result","id":"abc","stdout":"...","stderr":"...","exit_code":0}
|
|
|
|
Special commands: __sysinfo__, __packages__, __screenshot__, __download__, __upload__,
|
|
__processes__, __netstat__, __dumplog__, __disconnect__
|
|
"""
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
import os
|
|
import socket
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, List, Any, Tuple
|
|
|
|
from core.paths import get_data_dir
|
|
|
|
logger = logging.getLogger('autarch.revshell')
|
|
|
|
|
|
class RevShellSession:
|
|
"""Active reverse shell session with an Archon device."""
|
|
|
|
def __init__(self, sock: socket.socket, device_info: dict, session_id: str):
|
|
self.socket = sock
|
|
self.device_info = device_info
|
|
self.session_id = session_id
|
|
self.connected_at = datetime.now()
|
|
self.command_log: List[dict] = []
|
|
self._lock = threading.Lock()
|
|
self._reader = sock.makefile('r', encoding='utf-8', errors='replace')
|
|
self._writer = sock.makefile('w', encoding='utf-8', errors='replace')
|
|
self._alive = True
|
|
self._cmd_counter = 0
|
|
|
|
@property
|
|
def alive(self) -> bool:
|
|
return self._alive
|
|
|
|
@property
|
|
def device_name(self) -> str:
|
|
return self.device_info.get('device', 'unknown')
|
|
|
|
@property
|
|
def android_version(self) -> str:
|
|
return self.device_info.get('android', '?')
|
|
|
|
@property
|
|
def uid(self) -> int:
|
|
return self.device_info.get('uid', -1)
|
|
|
|
@property
|
|
def uptime(self) -> float:
|
|
return (datetime.now() - self.connected_at).total_seconds()
|
|
|
|
def execute(self, command: str, timeout: int = 30) -> dict:
|
|
"""Send a command and wait for result. Returns {stdout, stderr, exit_code}."""
|
|
with self._lock:
|
|
if not self._alive:
|
|
return {'stdout': '', 'stderr': 'Session disconnected', 'exit_code': -1}
|
|
|
|
self._cmd_counter += 1
|
|
cmd_id = f"cmd_{self._cmd_counter}"
|
|
|
|
msg = json.dumps({
|
|
'type': 'cmd',
|
|
'cmd': command,
|
|
'timeout': timeout,
|
|
'id': cmd_id
|
|
})
|
|
|
|
try:
|
|
self._writer.write(msg + '\n')
|
|
self._writer.flush()
|
|
|
|
# Read response (with extended timeout for command execution)
|
|
self.socket.settimeout(timeout + 10)
|
|
response_line = self._reader.readline()
|
|
if not response_line:
|
|
self._alive = False
|
|
return {'stdout': '', 'stderr': 'Connection closed', 'exit_code': -1}
|
|
|
|
result = json.loads(response_line)
|
|
|
|
# Log command
|
|
self.command_log.append({
|
|
'time': datetime.now().isoformat(),
|
|
'cmd': command,
|
|
'exit_code': result.get('exit_code', -1)
|
|
})
|
|
|
|
return {
|
|
'stdout': result.get('stdout', ''),
|
|
'stderr': result.get('stderr', ''),
|
|
'exit_code': result.get('exit_code', -1)
|
|
}
|
|
|
|
except (socket.timeout, OSError, json.JSONDecodeError) as e:
|
|
logger.error(f"Session {self.session_id}: execute error: {e}")
|
|
self._alive = False
|
|
return {'stdout': '', 'stderr': f'Communication error: {e}', 'exit_code': -1}
|
|
|
|
def execute_special(self, command: str, **kwargs) -> dict:
|
|
"""Execute a special command with extra parameters."""
|
|
with self._lock:
|
|
if not self._alive:
|
|
return {'stdout': '', 'stderr': 'Session disconnected', 'exit_code': -1}
|
|
|
|
self._cmd_counter += 1
|
|
cmd_id = f"cmd_{self._cmd_counter}"
|
|
|
|
msg = {'type': 'cmd', 'cmd': command, 'id': cmd_id, 'timeout': 60}
|
|
msg.update(kwargs)
|
|
|
|
try:
|
|
self._writer.write(json.dumps(msg) + '\n')
|
|
self._writer.flush()
|
|
|
|
self.socket.settimeout(70)
|
|
response_line = self._reader.readline()
|
|
if not response_line:
|
|
self._alive = False
|
|
return {'stdout': '', 'stderr': 'Connection closed', 'exit_code': -1}
|
|
|
|
return json.loads(response_line)
|
|
|
|
except (socket.timeout, OSError, json.JSONDecodeError) as e:
|
|
logger.error(f"Session {self.session_id}: special cmd error: {e}")
|
|
self._alive = False
|
|
return {'stdout': '', 'stderr': f'Communication error: {e}', 'exit_code': -1}
|
|
|
|
def sysinfo(self) -> dict:
|
|
"""Get device system information."""
|
|
return self.execute('__sysinfo__')
|
|
|
|
def packages(self) -> dict:
|
|
"""List installed packages."""
|
|
return self.execute('__packages__', timeout=30)
|
|
|
|
def screenshot(self) -> Optional[bytes]:
|
|
"""Capture screenshot. Returns PNG bytes or None."""
|
|
result = self.execute('__screenshot__', timeout=30)
|
|
if result['exit_code'] != 0:
|
|
return None
|
|
try:
|
|
return base64.b64decode(result['stdout'])
|
|
except Exception:
|
|
return None
|
|
|
|
def download(self, remote_path: str) -> Optional[Tuple[bytes, str]]:
|
|
"""Download file from device. Returns (data, filename) or None."""
|
|
result = self.execute_special('__download__', path=remote_path)
|
|
if result.get('exit_code', -1) != 0:
|
|
return None
|
|
try:
|
|
data = base64.b64decode(result.get('stdout', ''))
|
|
filename = result.get('filename', os.path.basename(remote_path))
|
|
return (data, filename)
|
|
except Exception:
|
|
return None
|
|
|
|
def upload(self, local_path: str, remote_path: str) -> dict:
|
|
"""Upload file to device."""
|
|
try:
|
|
with open(local_path, 'rb') as f:
|
|
data = base64.b64encode(f.read()).decode('ascii')
|
|
except IOError as e:
|
|
return {'stdout': '', 'stderr': f'Failed to read local file: {e}', 'exit_code': -1}
|
|
|
|
return self.execute_special('__upload__', path=remote_path, data=data)
|
|
|
|
def processes(self) -> dict:
|
|
"""List running processes."""
|
|
return self.execute('__processes__', timeout=10)
|
|
|
|
def netstat(self) -> dict:
|
|
"""Get network connections."""
|
|
return self.execute('__netstat__', timeout=10)
|
|
|
|
def dumplog(self, lines: int = 100) -> dict:
|
|
"""Get logcat output."""
|
|
return self.execute_special('__dumplog__', lines=min(lines, 5000))
|
|
|
|
def ping(self) -> bool:
|
|
"""Send keepalive ping."""
|
|
with self._lock:
|
|
if not self._alive:
|
|
return False
|
|
try:
|
|
self._writer.write('{"type":"ping"}\n')
|
|
self._writer.flush()
|
|
self.socket.settimeout(10)
|
|
response = self._reader.readline()
|
|
if not response:
|
|
self._alive = False
|
|
return False
|
|
result = json.loads(response)
|
|
return result.get('type') == 'pong'
|
|
except Exception:
|
|
self._alive = False
|
|
return False
|
|
|
|
def disconnect(self):
|
|
"""Gracefully disconnect the session."""
|
|
with self._lock:
|
|
if not self._alive:
|
|
return
|
|
try:
|
|
self._writer.write('{"type":"disconnect"}\n')
|
|
self._writer.flush()
|
|
except Exception:
|
|
pass
|
|
self._alive = False
|
|
try:
|
|
self.socket.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Serialize session info for API responses."""
|
|
return {
|
|
'session_id': self.session_id,
|
|
'device': self.device_name,
|
|
'android': self.android_version,
|
|
'uid': self.uid,
|
|
'connected_at': self.connected_at.isoformat(),
|
|
'uptime': int(self.uptime),
|
|
'commands_executed': len(self.command_log),
|
|
'alive': self._alive,
|
|
}
|
|
|
|
|
|
class RevShellListener:
|
|
"""TCP listener for incoming Archon reverse shell connections."""
|
|
|
|
def __init__(self, host: str = '0.0.0.0', port: int = 17322, auth_token: str = None):
|
|
self.host = host
|
|
self.port = port
|
|
self.auth_token = auth_token or uuid.uuid4().hex[:32]
|
|
self.sessions: Dict[str, RevShellSession] = {}
|
|
self._server_socket: Optional[socket.socket] = None
|
|
self._accept_thread: Optional[threading.Thread] = None
|
|
self._keepalive_thread: Optional[threading.Thread] = None
|
|
self._running = False
|
|
self._lock = threading.Lock()
|
|
|
|
# Data directory for screenshots, downloads, etc.
|
|
self._data_dir = get_data_dir() / 'revshell'
|
|
self._data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
@property
|
|
def running(self) -> bool:
|
|
return self._running
|
|
|
|
@property
|
|
def active_sessions(self) -> List[RevShellSession]:
|
|
return [s for s in self.sessions.values() if s.alive]
|
|
|
|
def start(self) -> Tuple[bool, str]:
|
|
"""Start listening for incoming reverse shell connections."""
|
|
if self._running:
|
|
return (False, 'Listener already running')
|
|
|
|
try:
|
|
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self._server_socket.settimeout(2.0) # Accept timeout for clean shutdown
|
|
self._server_socket.bind((self.host, self.port))
|
|
self._server_socket.listen(5)
|
|
except OSError as e:
|
|
return (False, f'Failed to bind {self.host}:{self.port}: {e}')
|
|
|
|
self._running = True
|
|
|
|
self._accept_thread = threading.Thread(target=self._accept_loop, daemon=True)
|
|
self._accept_thread.start()
|
|
|
|
self._keepalive_thread = threading.Thread(target=self._keepalive_loop, daemon=True)
|
|
self._keepalive_thread.start()
|
|
|
|
logger.info(f"RevShell listener started on {self.host}:{self.port}")
|
|
logger.info(f"Auth token: {self.auth_token}")
|
|
return (True, f'Listening on {self.host}:{self.port}')
|
|
|
|
def stop(self):
|
|
"""Stop listener and disconnect all sessions."""
|
|
self._running = False
|
|
|
|
# Disconnect all sessions
|
|
for session in list(self.sessions.values()):
|
|
try:
|
|
session.disconnect()
|
|
except Exception:
|
|
pass
|
|
|
|
# Close server socket
|
|
if self._server_socket:
|
|
try:
|
|
self._server_socket.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# Wait for threads
|
|
if self._accept_thread:
|
|
self._accept_thread.join(timeout=5)
|
|
if self._keepalive_thread:
|
|
self._keepalive_thread.join(timeout=5)
|
|
|
|
logger.info("RevShell listener stopped")
|
|
|
|
def get_session(self, session_id: str) -> Optional[RevShellSession]:
|
|
"""Get session by ID."""
|
|
return self.sessions.get(session_id)
|
|
|
|
def list_sessions(self) -> List[dict]:
|
|
"""List all sessions with their info."""
|
|
return [s.to_dict() for s in self.sessions.values()]
|
|
|
|
def remove_session(self, session_id: str):
|
|
"""Disconnect and remove a session."""
|
|
session = self.sessions.pop(session_id, None)
|
|
if session:
|
|
session.disconnect()
|
|
|
|
def save_screenshot(self, session_id: str) -> Optional[str]:
|
|
"""Capture and save screenshot. Returns file path or None."""
|
|
session = self.get_session(session_id)
|
|
if not session or not session.alive:
|
|
return None
|
|
|
|
png_data = session.screenshot()
|
|
if not png_data:
|
|
return None
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
filename = f'screenshot_{session.device_name}_{timestamp}.png'
|
|
filepath = self._data_dir / filename
|
|
filepath.write_bytes(png_data)
|
|
return str(filepath)
|
|
|
|
def save_download(self, session_id: str, remote_path: str) -> Optional[str]:
|
|
"""Download file from device and save locally. Returns local path or None."""
|
|
session = self.get_session(session_id)
|
|
if not session or not session.alive:
|
|
return None
|
|
|
|
result = session.download(remote_path)
|
|
if not result:
|
|
return None
|
|
|
|
data, filename = result
|
|
filepath = self._data_dir / filename
|
|
filepath.write_bytes(data)
|
|
return str(filepath)
|
|
|
|
# ── Internal ────────────────────────────────────────────────────
|
|
|
|
def _accept_loop(self):
|
|
"""Accept incoming connections in background thread."""
|
|
while self._running:
|
|
try:
|
|
client_sock, addr = self._server_socket.accept()
|
|
client_sock.settimeout(30)
|
|
logger.info(f"Connection from {addr[0]}:{addr[1]}")
|
|
|
|
# Handle auth in a separate thread to not block accept
|
|
threading.Thread(
|
|
target=self._handle_new_connection,
|
|
args=(client_sock, addr),
|
|
daemon=True
|
|
).start()
|
|
|
|
except socket.timeout:
|
|
continue
|
|
except OSError:
|
|
if self._running:
|
|
logger.error("Accept error")
|
|
break
|
|
|
|
def _handle_new_connection(self, sock: socket.socket, addr: tuple):
|
|
"""Authenticate a new connection."""
|
|
try:
|
|
reader = sock.makefile('r', encoding='utf-8', errors='replace')
|
|
writer = sock.makefile('w', encoding='utf-8', errors='replace')
|
|
|
|
# Read auth message
|
|
auth_line = reader.readline()
|
|
if not auth_line:
|
|
sock.close()
|
|
return
|
|
|
|
auth_msg = json.loads(auth_line)
|
|
|
|
if auth_msg.get('type') != 'auth':
|
|
writer.write('{"type":"auth_fail","reason":"Expected auth message"}\n')
|
|
writer.flush()
|
|
sock.close()
|
|
return
|
|
|
|
# Verify token
|
|
if auth_msg.get('token') != self.auth_token:
|
|
logger.warning(f"Auth failed from {addr[0]}:{addr[1]}")
|
|
writer.write('{"type":"auth_fail","reason":"Invalid token"}\n')
|
|
writer.flush()
|
|
sock.close()
|
|
return
|
|
|
|
# Auth OK — create session
|
|
writer.write('{"type":"auth_ok"}\n')
|
|
writer.flush()
|
|
|
|
session_id = uuid.uuid4().hex[:12]
|
|
device_info = {
|
|
'device': auth_msg.get('device', 'unknown'),
|
|
'android': auth_msg.get('android', '?'),
|
|
'uid': auth_msg.get('uid', -1),
|
|
'remote_addr': f"{addr[0]}:{addr[1]}"
|
|
}
|
|
|
|
session = RevShellSession(sock, device_info, session_id)
|
|
with self._lock:
|
|
self.sessions[session_id] = session
|
|
|
|
logger.info(f"Session {session_id}: {device_info['device']} "
|
|
f"(Android {device_info['android']}, UID {device_info['uid']})")
|
|
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
logger.error(f"Auth error from {addr[0]}:{addr[1]}: {e}")
|
|
try:
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def _keepalive_loop(self):
|
|
"""Periodically ping sessions and remove dead ones."""
|
|
while self._running:
|
|
time.sleep(30)
|
|
dead = []
|
|
for sid, session in list(self.sessions.items()):
|
|
if not session.alive:
|
|
dead.append(sid)
|
|
continue
|
|
# Ping to check liveness
|
|
if not session.ping():
|
|
dead.append(sid)
|
|
logger.info(f"Session {sid} lost (keepalive failed)")
|
|
|
|
for sid in dead:
|
|
self.sessions.pop(sid, None)
|
|
|
|
|
|
# ── Singleton ───────────────────────────────────────────────────────
|
|
|
|
_listener: Optional[RevShellListener] = None
|
|
|
|
|
|
def get_listener() -> RevShellListener:
|
|
"""Get or create the global RevShellListener singleton."""
|
|
global _listener
|
|
if _listener is None:
|
|
_listener = RevShellListener()
|
|
return _listener
|
|
|
|
|
|
def start_listener(host: str = '0.0.0.0', port: int = 17322,
|
|
token: str = None) -> Tuple[bool, str]:
|
|
"""Start the global listener."""
|
|
global _listener
|
|
_listener = RevShellListener(host=host, port=port, auth_token=token)
|
|
return _listener.start()
|
|
|
|
|
|
def stop_listener():
|
|
"""Stop the global listener."""
|
|
global _listener
|
|
if _listener:
|
|
_listener.stop()
|
|
_listener = None
|