mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-07 09:26:01 -10:00
The nxcdb export keys command calls self.db.get_keys() which only existed as a method on the SSH protocol database class. When called from any other protocol (smb, mssql, winrm, etc.), it raised an AttributeError because the base database class had no such method. Add a default get_keys() method to BaseDB that returns an empty list. Protocols that support keys (SSH) already override this method with their own implementation, so SSH behavior is unchanged. Also add argument validation and a success message to the keys export handler in nxcdb.py for consistency with other export commands. Fixes: Pennyw0rth/NetExec#1273 Signed-off-by: Yuchen Fan <[email protected]>
215 lines
8.1 KiB
Python
215 lines
8.1 KiB
Python
import configparser
|
|
import ipaddress
|
|
import platform
|
|
import shutil
|
|
import sys
|
|
from os import mkdir
|
|
from os.path import exists
|
|
from os.path import join as path_join
|
|
from pathlib import Path
|
|
from threading import Lock
|
|
|
|
from sqlalchemy import Table, create_engine, MetaData, func
|
|
from sqlalchemy.exc import (
|
|
IllegalStateChangeError,
|
|
NoInspectionAvailable,
|
|
NoSuchTableError,
|
|
)
|
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
from nxc.loaders.protocolloader import ProtocolLoader
|
|
from nxc.logger import nxc_logger
|
|
from nxc.paths import WORKSPACE_DIR
|
|
|
|
|
|
def create_db_engine(db_path):
|
|
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
|
|
|
|
|
def open_config(config_path):
|
|
try:
|
|
config = configparser.ConfigParser()
|
|
config.read(config_path)
|
|
except Exception as e:
|
|
print(f"[-] Error reading nxc.conf: {e}")
|
|
sys.exit(1)
|
|
return config
|
|
|
|
|
|
def get_workspace(config):
|
|
return config.get("nxc", "workspace")
|
|
|
|
|
|
def set_workspace(config_path, workspace_name):
|
|
config = open_config(config_path)
|
|
config.set("nxc", "workspace", workspace_name)
|
|
write_configfile(config, config_path)
|
|
print(f"[*] Workspace set to {workspace_name}")
|
|
|
|
|
|
def get_db(config):
|
|
return config.get("nxc", "last_used_db")
|
|
|
|
|
|
def write_configfile(config, config_path):
|
|
with open(config_path, "w") as configfile:
|
|
config.write(configfile)
|
|
|
|
|
|
def init_protocol_dbs(workspace_name, p_loader=None):
|
|
"""Check for each protocol if the database exists, if not create it."""
|
|
if p_loader is None:
|
|
p_loader = ProtocolLoader()
|
|
protocols = p_loader.get_protocols()
|
|
for protocol in protocols:
|
|
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
|
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
|
|
|
if not exists(proto_db_path):
|
|
print(f"[*] Initializing {protocol.upper()} protocol database")
|
|
db_engine = create_db_engine(proto_db_path)
|
|
protocol_object.database.db_schema(db_engine)
|
|
db_engine.dispose()
|
|
|
|
|
|
def create_workspace(workspace_name, p_loader=None):
|
|
"""
|
|
Create a new workspace with the given name.
|
|
|
|
Args:
|
|
----
|
|
workspace_name (str): The name of the workspace.
|
|
|
|
Returns:
|
|
-------
|
|
None
|
|
"""
|
|
if exists(path_join(WORKSPACE_DIR, workspace_name)):
|
|
print(f"[-] Workspace {workspace_name} already exists")
|
|
else:
|
|
print(f"[*] Creating {workspace_name} workspace")
|
|
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
|
|
|
init_protocol_dbs(workspace_name, p_loader)
|
|
|
|
|
|
def delete_workspace(workspace_name):
|
|
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
|
print(f"[*] Workspace {workspace_name} deleted")
|
|
|
|
|
|
def initialize_db():
|
|
if not exists(path_join(WORKSPACE_DIR, "default")):
|
|
create_workspace("default")
|
|
|
|
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
|
|
init_protocol_dbs("default")
|
|
|
|
|
|
def format_host_query(q, filter_term, HostsTable):
|
|
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
|
|
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
|
|
"""
|
|
# the FTP and SSH protocols call the column host instead of IP
|
|
# TODO: normalize these column names
|
|
if hasattr(HostsTable.c, "ip"):
|
|
ip_column = HostsTable.c.ip
|
|
nxc_logger.debug("Using 'ip' column for filtering")
|
|
elif hasattr(HostsTable.c, "host"):
|
|
ip_column = HostsTable.c.host
|
|
nxc_logger.debug("Using 'host' column for filtering")
|
|
else:
|
|
nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table")
|
|
return q
|
|
|
|
# first we check if its an ip address
|
|
try:
|
|
ipaddress.ip_address(filter_term)
|
|
nxc_logger.debug(f"filter_term is an IP address: {filter_term}")
|
|
q = q.filter(ip_column == filter_term)
|
|
except ValueError:
|
|
nxc_logger.debug(f"filter_term is not an IP address: {filter_term}")
|
|
like_term = func.lower(f"%{filter_term}%")
|
|
|
|
# check if the hostname column exists for hostname searching
|
|
q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term))
|
|
|
|
return q
|
|
|
|
|
|
class BaseDB:
|
|
def __init__(self, db_engine):
|
|
self.db_engine = db_engine
|
|
self.db_path = self.db_engine.url.database
|
|
self.protocol = Path(self.db_path).stem.upper()
|
|
self.metadata = MetaData()
|
|
self.reflect_tables()
|
|
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
|
|
|
session = scoped_session(session_factory)
|
|
self.sess = session()
|
|
self.lock = Lock()
|
|
|
|
def reflect_tables(self):
|
|
raise NotImplementedError("Reflect tables not implemented")
|
|
|
|
def reflect_table(self, table):
|
|
with self.db_engine.connect():
|
|
try:
|
|
reflected_table = Table(table.__tablename__, self.metadata, autoload_with=self.db_engine)
|
|
|
|
# Check for column addition / deletion
|
|
reflected_columns = set(reflected_table.columns.keys())
|
|
orm_columns = {column.name for column in table.__table__.columns}
|
|
if reflected_columns != orm_columns:
|
|
raise ValueError(f"Schema mismatch detected! ORM columns: {orm_columns}, Reflected columns: {reflected_columns}")
|
|
|
|
# Check for constraint changes
|
|
reflected_constraints = [(type(c), c.columns.keys()) for c in reflected_table._sorted_constraints]
|
|
orm_constraints = [(type(c), c.columns.keys()) for c in table.__table__._sorted_constraints]
|
|
if reflected_constraints != orm_constraints:
|
|
raise ValueError(f"Schema mismatch detected! ORM constraints: {orm_constraints}, Reflected constraints: {reflected_constraints}")
|
|
|
|
return reflected_table
|
|
except (NoInspectionAvailable, NoSuchTableError, ValueError) as e:
|
|
commands_platform = {
|
|
"Windows": [
|
|
f"cmd /c copy {self.db_path} %USERPROFILE%\\nxc_{self.protocol.lower()}.bak",
|
|
f"del {self.db_path}"
|
|
],
|
|
"Linux": [
|
|
f"cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak",
|
|
f"rm -f {self.db_path}"
|
|
]
|
|
}
|
|
copy_command = commands_platform["Windows"][0] if platform.system() == "Windows" else commands_platform["Linux"][0]
|
|
delete_command = commands_platform["Windows"][1] if platform.system() == "Windows" else commands_platform["Linux"][1]
|
|
nxc_logger.fail(f"Schema mismatch detected for table '{table.__tablename__}' in protocol '{self.protocol}'")
|
|
nxc_logger.debug(e)
|
|
nxc_logger.fail("This is probably because a newer version of nxc is being run on an old DB schema.")
|
|
nxc_logger.fail(f"Optionally save the old DB data (`{copy_command}`)")
|
|
nxc_logger.fail(f"Then remove the {self.protocol} DB (`{delete_command}`) and run nxc to initialize the new DB")
|
|
sys.exit()
|
|
|
|
def shutdown_db(self):
|
|
try:
|
|
self.sess.close()
|
|
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
|
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
|
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
|
except IllegalStateChangeError as e:
|
|
nxc_logger.debug(f"Error while closing session db object: {e}")
|
|
|
|
def clear_database(self):
|
|
for table in self.metadata.sorted_tables:
|
|
self.db_execute(table.delete())
|
|
|
|
def db_execute(self, *args):
|
|
self.lock.acquire()
|
|
res = self.sess.execute(*args)
|
|
self.lock.release()
|
|
return res
|
|
|
|
def get_keys(self, key_id=None, cred_id=None):
|
|
"""Return an empty list by default. Protocols that support keys (e.g. SSH) override this."""
|
|
return []
|