From 29e17543bb2587f8f3bf5c088e2ac926488ce152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Fri, 12 Dec 2025 15:42:38 +0100 Subject: [PATCH] Add nxcdb hosts [nla] --- nxc/protocols/rdp.py | 1 + nxc/protocols/rdp/database.py | 109 ++++++++++++++++++++++------ nxc/protocols/rdp/db_navigator.py | 114 +++++++++++++++++++++++++++--- 3 files changed, 193 insertions(+), 31 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index fb9b5bd6..7233ec44 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -107,6 +107,7 @@ class rdp(connection): self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})") else: self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})") + self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla) def create_conn_obj(self): self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout) diff --git a/nxc/protocols/rdp/database.py b/nxc/protocols/rdp/database.py index ef651540..f7e95694 100644 --- a/nxc/protocols/rdp/database.py +++ b/nxc/protocols/rdp/database.py @@ -1,46 +1,113 @@ - -from sqlalchemy import Column, Integer, PrimaryKeyConstraint, String +from sqlalchemy import Column, Boolean, UniqueConstraint, Integer, PrimaryKeyConstraint, String, select +from sqlalchemy.dialects.sqlite import Insert from sqlalchemy.orm import declarative_base -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query +from nxc.logger import nxc_logger -Base = declarative_base() +BaseTable = declarative_base() class database(BaseDB): def __init__(self, db_engine): - self.CredentialsTable = None self.HostsTable = None super().__init__(db_engine) - class Credential(Base): - __tablename__ = "credentials" - id = Column(Integer) - username = Column(String) - password = Column(String) - pkey = Column(String) - - __table_args__ = ( - PrimaryKeyConstraint("id"), - ) - - class Host(Base): + class Host(BaseTable): __tablename__ = "hosts" id = Column(Integer) ip = Column(String) - hostname = Column(String) port = Column(Integer) - server_banner = Column(String) + hostname = Column(String) + domain = Column(String) + os = Column(String) + nla = Column(Boolean) __table_args__ = ( PrimaryKeyConstraint("id"), + UniqueConstraint("ip"), ) @staticmethod def db_schema(db_conn): - Base.metadata.create_all(db_conn) + BaseTable.metadata.create_all(db_conn) def reflect_tables(self): - self.CredentialsTable = self.reflect_table(self.Credential) self.HostsTable = self.reflect_table(self.Host) + + def add_host(self, ip, port, hostname, domain, os, nla): + """ + Check if this host has already been added to the database, if not, add it in. + TODO: return inserted or updated row ids as a list + """ + hosts = [] + + q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip) + results = self.db_execute(q).all() + nxc_logger.debug(f"rdp add_host() - hosts returned: {results}") + + # create new host + if not results: + new_host = { + "ip": ip, + "port": port, + "hostname": hostname, + "domain": domain, + "os": os, + "nla": nla + } + hosts = [new_host] + # update existing hosts data + else: + for host in results: + host_data = host._asdict() + # only update column if it is being passed in + if ip is not None: + host_data["ip"] = ip + if port is not None: + host_data["port"] = port + if hostname is not None: + host_data["hostname"] = hostname + if domain is not None: + host_data["domain"] = domain + if os is not None: + host_data["os"] = os + if nla is not None: + host_data["nla"] = nla + # only add host to be updated if it has changed + if host_data not in hosts: + hosts.append(host_data) + nxc_logger.debug(f"Update Hosts: {hosts}") + + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.HostsTable) + update_columns = {col.name: col for col in q.excluded if col.name not in "id"} + q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns) + self.db_execute(q, hosts) + + def get_hosts(self, filter_term=None, domain=None): + """Return hosts from the database.""" + q = select(self.HostsTable) + # if we're returning a single host by ID + if self.is_host_valid(filter_term): + q = q.filter(self.HostsTable.c.id == filter_term) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + # filtering nla False because this is the misconfiguration we are looking for + elif filter_term == "nla": + q = q.filter(self.HostsTable.c.nla == False) # noqa: E712 + # if we're filtering by ip/hostname + elif filter_term and filter_term != "": + q = format_host_query(q, filter_term, self.HostsTable) + + results = self.db_execute(q).all() + nxc_logger.debug(f"rdp hosts() - results: {results}") + return results + + def is_host_valid(self, host_id): + """Check if this host ID is valid.""" + q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id) + results = self.db_execute(q).all() + return len(results) > 0 \ No newline at end of file diff --git a/nxc/protocols/rdp/db_navigator.py b/nxc/protocols/rdp/db_navigator.py index c712309b..8a23415c 100644 --- a/nxc/protocols/rdp/db_navigator.py +++ b/nxc/protocols/rdp/db_navigator.py @@ -1,15 +1,109 @@ -from nxc.nxcdb import DatabaseNavigator, print_help +from nxc.nxcdb import DatabaseNavigator, print_table, print_help +from termcolor import colored +import functools +help_header = functools.partial(colored, color="cyan", attrs=["bold"]) +help_kw = functools.partial(colored, color="green", attrs=["bold"]) class navigator(DatabaseNavigator): - def do_clear_database(self, line): - if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y": - self.db.clear_database() + def display_hosts(self, hosts): + data = [ + [ + "HostID", + "IP", + "Port", + "Hostname", + "Domain", + "OS", + "NLA", + ] + ] - def help_clear_database(self): + for host in hosts: + host_id = host[0] + ip = host[1] + port = host[2] + hostname = host[3] + domain = host[4] + + try: + os = host[5].decode() + except Exception: + os = host[5] + + nla = host[6] + + data.append( + [ + host_id, + ip, + port, + hostname, + domain, + os, + nla + ] + ) + print_table(data, title="Hosts") + + def do_hosts(self, line): + filter_term = line.strip() + + if filter_term == "": + hosts = self.db.get_hosts() + self.display_hosts(hosts) + else: + hosts = self.db.get_hosts(filter_term=filter_term) + + if len(hosts) > 1: + self.display_hosts(hosts) + elif len(hosts) == 1: + data = [ + [ + "HostID", + "IP", + "Port", + "Hostname", + "Domain", + "OS", + "NLA", + ] + ] + + for host in hosts: + host_id = host[0] + ip = host[1] + port = host[2] + hostname = host[3] + domain = host[4] + + try: + os = host[5].decode() + except Exception: + os = host[5] + + nla = host[6] + + data.append( + [ + host_id, + ip, + port, + hostname, + domain, + os, + nla + ] + ) + print_table(data, title="Host") + + + def help_hosts(self): help_string = """ - clear_database - THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE - YOU CANNOT UNDO THIS COMMAND - """ - print_help(help_string) + By default prints all hosts + Table format: + | 'HostID', 'IP', 'Port', 'Hostname', 'Domain', 'OS', 'NLA' | + Subcommands: + nla - list hosts with NLA disabled + """ + print_help(help_string) \ No newline at end of file