NetExec/cme/protocols/smb/database.py

954 lines
36 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import base64
import warnings
from datetime import datetime
from sqlalchemy import MetaData, func, Table, select, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import sessionmaker, scoped_session
from cme.logger import cme_logger
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
warnings.filterwarnings("ignore", category=SAWarning)
class database:
def __init__(self, db_engine):
self.HostsTable = None
self.UsersTable = None
self.GroupsTable = None
self.SharesTable = None
self.AdminRelationsTable = None
self.GroupRelationsTable = None
self.LoggedinRelationsTable = None
self.DpapiBackupkey = None
self.DpapiSecrets = None
self.db_engine = db_engine
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@staticmethod
def db_schema(db_conn):
db_conn.execute(
"""CREATE TABLE "hosts" (
"id" integer PRIMARY KEY,
"ip" text,
"hostname" text,
"domain" text,
"os" text,
"dc" boolean,
"smbv1" boolean,
"signing" boolean,
"spooler" boolean,
"zerologon" boolean,
"petitpotam" boolean
)"""
)
# type = hash, plaintext
db_conn.execute(
"""CREATE TABLE "users" (
"id" integer PRIMARY KEY,
"domain" text,
"username" text,
"password" text,
"credtype" text,
"pillaged_from_hostid" integer,
FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id)
)"""
)
db_conn.execute(
"""CREATE TABLE "groups" (
"id" integer PRIMARY KEY,
"domain" text,
"name" text,
"rid" text,
"member_count_ad" integer,
"last_query_time" text
)"""
)
# This table keeps track of which credential has admin access over which machine and vice-versa
db_conn.execute(
"""CREATE TABLE "admin_relations" (
"id" integer PRIMARY KEY,
"userid" integer,
"hostid" integer,
FOREIGN KEY(userid) REFERENCES users(id),
FOREIGN KEY(hostid) REFERENCES hosts(id)
)"""
)
db_conn.execute(
"""CREATE TABLE "group_relations" (
"id" integer PRIMARY KEY,
"userid" integer,
"groupid" integer,
FOREIGN KEY(userid) REFERENCES users(id),
FOREIGN KEY(groupid) REFERENCES groups(id)
)"""
)
db_conn.execute(
"""CREATE TABLE "shares" (
"id" integer PRIMARY KEY,
"hostid" text,
"userid" integer,
"name" text,
"remark" text,
"read" boolean,
"write" boolean,
FOREIGN KEY(userid) REFERENCES users(id)
UNIQUE(hostid, userid, name)
)"""
)
db_conn.execute(
"""CREATE TABLE "loggedin_relations" (
"id" integer PRIMARY KEY,
"userid" integer,
"hostid" integer,
FOREIGN KEY(userid) REFERENCES users(id),
FOREIGN KEY(hostid) REFERENCES hosts(id)
)"""
)
db_conn.execute(
"""CREATE TABLE "dpapi_secrets" (
"id" integer PRIMARY KEY,
"host" text,
"dpapi_type" text,
"windows_user" text,
"username" text,
"password" text,
"url" text,
UNIQUE(host, dpapi_type, windows_user, username, password, url)
)"""
)
db_conn.execute(
"""CREATE TABLE "dpapi_backupkey" (
"id" integer PRIMARY KEY,
"domain" text,
"pvk" text,
UNIQUE(domain)
)"""
)
# db_conn.execute('''CREATE TABLE "ntds_dumps" (
# "id" integer PRIMARY KEY,
# "hostid", integer,
# "domain" text,
# "username" text,
# "hash" text,
# FOREIGN KEY(hostid) REFERENCES hosts(id)
# )''')
def reflect_tables(self):
with self.db_engine.connect() as conn:
try:
self.HostsTable = Table(
"hosts", self.metadata, autoload_with=self.db_engine
)
self.UsersTable = Table(
"users", self.metadata, autoload_with=self.db_engine
)
self.GroupsTable = Table(
"groups", self.metadata, autoload_with=self.db_engine
)
self.SharesTable = Table(
"shares", self.metadata, autoload_with=self.db_engine
)
self.AdminRelationsTable = Table(
"admin_relations", self.metadata, autoload_with=self.db_engine
)
self.GroupRelationsTable = Table(
"group_relations", self.metadata, autoload_with=self.db_engine
)
self.LoggedinRelationsTable = Table(
"loggedin_relations", self.metadata, autoload_with=self.db_engine
)
self.DpapiSecrets = Table(
"dpapi_secrets", self.metadata, autoload_with=self.db_engine
)
self.DpapiBackupkey = Table(
"dpapi_backupkey", self.metadata, autoload_with=self.db_engine
)
except (NoInspectionAvailable, NoSuchTableError):
print(
"[-] Error reflecting tables - this means there is a DB schema mismatch \n"
"[-] This is probably because a newer version of CME is being ran on an old DB schema\n"
"[-] If you wish to save the old DB data, copy it to a new location (`cp -r ~/.cme/workspaces/ ~/old_cme_workspaces/`)\n"
"[-] Then remove the CME DB folders (`rm -rf ~/.cme/workspaces/`) and rerun CME to initialize the new DB schema"
)
exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of CME, 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:
cme_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
# pull/545
def add_host(
self,
ip,
hostname,
domain,
os,
smbv1,
signing,
spooler=None,
zerologon=None,
petitpotam=None,
dc=None,
):
"""
Check if this host has already been added to the database, if not, add it in.
"""
domain = domain.split(".")[0]
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.conn.execute(q).all()
# create new host
if not results:
new_host = {
"ip": ip,
"hostname": hostname,
"domain": domain,
"os": os if os is not None else "",
"dc": dc,
"smbv1": smbv1,
"signing": signing,
"spooler": spooler,
"zerologon": zerologon,
"petitpotam": petitpotam,
}
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 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 smbv1 is not None:
host_data["smbv1"] = smbv1
if signing is not None:
host_data["signing"] = signing
if spooler is not None:
host_data["spooler"] = spooler
if zerologon is not None:
host_data["zerologon"] = zerologon
if petitpotam is not None:
host_data["petitpotam"] = petitpotam
if dc is not None:
host_data["dc"] = dc
# only add host to be updated if it has changed
if host_data not in hosts:
hosts.append(host_data)
updated_ids.append(host_data["id"])
cme_logger.debug(f"Update Hosts: {hosts}")
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
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.conn.execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
cme_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
def add_credential(
self, credtype, domain, username, password, group_id=None, pillaged_from=None
):
"""
Check if this credential has already been added to the database, if not add it in.
"""
domain = domain.split(".")[0]
credentials = []
groups = []
if (group_id and not self.is_group_valid(group_id)) or (
pillaged_from and not self.is_host_valid(pillaged_from)
):
cme_logger.debug(f"Invalid group or host")
return
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.conn.execute(q).all()
# add new credential
if not results:
new_cred = {
"credtype": credtype,
"domain": domain,
"username": username,
"password": password,
"groupid": group_id,
"pillaged_from": pillaged_from,
}
credentials = [new_cred]
# update existing cred data
else:
for creds in results:
# this will include the id, so we don't touch it
cred_data = creds._asdict()
# only update column if it is being passed in
if credtype is not None:
cred_data["credtype"] = credtype
if domain is not None:
cred_data["domain"] = domain
if username is not None:
cred_data["username"] = username
if password is not None:
cred_data["password"] = password
if group_id is not None:
cred_data["groupid"] = group_id
groups.append({"userid": cred_data["id"], "groupid": group_id})
if pillaged_from is not None:
cred_data["pillaged_from"] = pillaged_from
# only add cred to be updated if it has changed
if cred_data not in credentials:
credentials.append(cred_data)
# TODO: find a way to abstract this away to a single Upsert call
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
update_columns_users = {
col.name: col for col in q_users.excluded if col.name not in "id"
}
q_users = q_users.on_conflict_do_update(
index_elements=self.UsersTable.primary_key, set_=update_columns_users
)
cme_logger.debug(f"Adding credentials: {credentials}")
self.conn.execute(q_users, credentials) # .scalar()
if groups:
q_groups = Insert(self.GroupRelationsTable)
self.conn.execute(q_groups, groups)
# return user_ids
def remove_credentials(self, creds_id):
"""
Removes a credential ID from the database
"""
del_hosts = []
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.conn.execute(q)
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
domain = domain.split(".")[0]
add_links = []
creds_q = select(self.UsersTable)
if user_id:
creds_q = creds_q.filter(self.UsersTable.c.id == user_id)
else:
creds_q = creds_q.filter(
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
self.UsersTable.c.password == password,
)
users = self.conn.execute(creds_q)
hosts = self.get_hosts(host)
if users and hosts:
for user, host in zip(users, hosts):
user_id = user[0]
host_id = host[0]
link = {"userid": user_id, "hostid": host_id}
admin_relations_select = select(self.AdminRelationsTable).filter(
self.AdminRelationsTable.c.userid == user_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.conn.execute(admin_relations_select).all()
if not links:
add_links.append(link)
admin_relations_insert = Insert(self.AdminRelationsTable)
if add_links:
self.conn.execute(admin_relations_insert, add_links)
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
q = select(self.AdminRelationsTable).filter(
self.AdminRelationsTable.c.userid == user_id
)
elif host_id:
q = select(self.AdminRelationsTable).filter(
self.AdminRelationsTable.c.hostid == host_id
)
else:
q = select(self.AdminRelationsTable)
results = self.conn.execute(q).all()
return results
def remove_admin_relation(self, user_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
if user_ids:
for user_id in user_ids:
q = q.filter(self.AdminRelationsTable.c.userid == user_id)
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.conn.execute(q)
def is_credential_valid(self, credential_id):
"""
Check if this credential ID is valid.
"""
q = select(self.UsersTable).filter(
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.conn.execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
"""
Return credentials from the database.
"""
# if we're returning a single credential by ID
if self.is_credential_valid(filter_term):
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
elif cred_type:
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
# if we're filtering by username
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.username).like(like_term)
)
# otherwise return all credentials
else:
q = select(self.UsersTable)
results = self.conn.execute(q).all()
return results
def get_credential(self, cred_type, domain, username, password):
domain = domain.split(".")[0]
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
self.UsersTable.c.username == username,
self.UsersTable.c.password == password,
self.UsersTable.c.credtype == cred_type,
)
results = self.conn.execute(q).first()
return results.id
def is_credential_local(self, credential_id):
q = select(self.UsersTable.c.domain).filter(
self.UsersTable.c.id == credential_id
)
user_domain = self.conn.execute(q).all()
if user_domain:
q = select(self.HostsTable).filter(
func.lower(self.HostsTable.c.id) == func.lower(user_domain)
)
results = self.conn.execute(q).all()
return len(results) > 0
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.conn.execute(q).all()
return len(results) > 0
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.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by domain controllers
elif filter_term == "dc":
q = q.filter(self.HostsTable.c.dc == True)
if domain:
q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain))
elif filter_term == "signing":
# generally we want hosts that are vulnerable, so signing disabled
q = q.filter(self.HostsTable.c.signing == False)
elif filter_term == "spooler":
q = q.filter(self.HostsTable.c.spooler == True)
elif filter_term == "zerologon":
q = q.filter(self.HostsTable.c.zerologon == True)
elif filter_term == "petitpotam":
q = q.filter(self.HostsTable.c.petitpotam == True)
elif filter_term is not None and filter_term.startswith("domain"):
domain = filter_term.split()[1]
like_term = func.lower(f"%{domain}%")
q = q.filter(self.HostsTable.c.domain.like(like_term))
# if we're filtering by ip/hostname
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(
self.HostsTable.c.ip.like(like_term)
| func.lower(self.HostsTable.c.hostname).like(like_term)
)
results = self.conn.execute(q).all()
cme_logger.debug(f"smb hosts() - results: {results}")
return results
def is_group_valid(self, group_id):
"""
Check if this group ID is valid.
"""
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == group_id)
results = self.conn.execute(q).first()
valid = True if results else False
cme_logger.debug(f"is_group_valid(groupID={group_id}) => {valid}")
return valid
def add_group(self, domain, name, rid=None, member_count_ad=None):
results = self.get_groups(group_name=name, group_domain=domain)
domain = domain.split(".")[0]
groups = []
updated_ids = []
group_data = {
"domain": domain,
"name": name,
"rid": rid,
"member_count_ad": member_count_ad,
"last_query_time": None,
}
if not results:
if member_count_ad is not None:
group_data["member_count_ad"] = member_count_ad
today = datetime.now()
iso_date = today.isoformat()
group_data["last_query_time"] = iso_date
groups = [group_data]
# insert the group and get the returned id right away, this can be refactored when we can use RETURNING
q = Insert(self.GroupsTable)
self.conn.execute(q, groups)
new_group_data = self.get_groups(
group_name=group_data["name"], group_domain=group_data["domain"]
)
returned_id = [new_group_data[0].id]
cme_logger.debug(f"Inserted group with ID: {returned_id[0]}")
return returned_id
else:
for group in results:
g_data = group._asdict()
if domain is not None:
g_data["domain"] = domain
if name is not None:
g_data["name"] = name
if rid is not None:
g_data["rid"] = rid
if member_count_ad is not None:
g_data["member_count_ad"] = member_count_ad
today = datetime.now()
iso_date = today.isoformat()
g_data["last_query_time"] = iso_date
# only add it to the upsert query if it's changed to save query execution time
if g_data not in groups:
groups.append(g_data)
updated_ids.append(g_data["id"])
cme_logger.debug(f"Update Groups: {groups}")
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.GroupsTable) # .returning(self.GroupsTable.c.id)
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.GroupsTable.primary_key, set_=update_columns
)
self.conn.execute(q, groups)
# TODO: always return a list and fix code references to not expect a single integer
# inserted_result = res_inserted_result.first()
# gid = inserted_result.id
#
# logger.debug(f"inserted_results: {inserted_result}\ntype: {type(inserted_result)}")
# logger.debug('add_group(domain={}, name={}) => {}'.format(domain, name, gid))
if updated_ids:
cme_logger.debug(f"Updated groups with IDs: {updated_ids}")
return updated_ids
def get_groups(self, filter_term=None, group_name=None, group_domain=None):
"""
Return groups from the database
"""
if group_domain:
group_domain = group_domain.split(".")[0]
if filter_term and self.is_group_valid(filter_term):
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == filter_term)
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif group_name and group_domain:
q = select(self.GroupsTable).filter(
func.lower(self.GroupsTable.c.name) == func.lower(group_name),
func.lower(self.GroupsTable.c.domain) == func.lower(group_domain),
)
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = select(self.GroupsTable).filter(self.GroupsTable.c.name.like(like_term))
else:
q = select(self.GroupsTable).filter()
results = self.conn.execute(q).all()
cme_logger.debug(
f"get_groups(filter_term={filter_term}, groupName={group_name}, groupDomain={group_domain}) => {results}"
)
return results
def get_group_relations(self, user_id=None, group_id=None):
if user_id and group_id:
q = select(self.GroupRelationsTable).filter(
self.GroupRelationsTable.c.id == user_id,
self.GroupRelationsTable.c.groupid == group_id,
)
elif user_id:
q = select(self.GroupRelationsTable).filter(
self.GroupRelationsTable.c.id == user_id
)
elif group_id:
q = select(self.GroupRelationsTable).filter(
self.GroupRelationsTable.c.groupid == group_id
)
results = self.conn.execute(q).all()
return results
def remove_group_relations(self, user_id=None, group_id=None):
q = delete(self.GroupRelationsTable)
if user_id:
q = q.filter(self.GroupRelationsTable.c.userid == user_id)
elif group_id:
q = q.filter(self.GroupRelationsTable.c.groupid == group_id)
self.conn.execute(q)
def is_user_valid(self, user_id):
"""
Check if this User ID is valid.
"""
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
results = self.conn.execute(q).all()
return len(results) > 0
def get_users(self, filter_term=None):
q = select(self.UsersTable)
if self.is_user_valid(filter_term):
q = q.filter(self.UsersTable.c.id == filter_term)
# if we're filtering by username
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
results = self.conn.execute(q).all()
return results
def get_user(self, domain, username):
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
)
results = self.conn.execute(q).all()
return results
def get_domain_controllers(self, domain=None):
return self.get_hosts(filter_term="dc", domain=domain)
def is_share_valid(self, share_id):
"""
Check if this share ID is valid.
"""
q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id)
results = self.conn.execute(q).all()
cme_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}")
return len(results) > 0
def add_share(self, host_id, user_id, name, remark, read, write):
share_data = {
"hostid": host_id,
"userid": user_id,
"name": name,
"remark": remark,
"read": read,
"write": write,
}
share_id = self.conn.execute(
Insert(
self.SharesTable
).on_conflict_do_nothing(), # .returning(self.SharesTable.c.id),
share_data,
) # .scalar_one()
# return share_id
def get_shares(self, filter_term=None):
if self.is_share_valid(filter_term):
q = select(self.SharesTable).filter(self.SharesTable.c.id == filter_term)
elif filter_term:
like_term = func.lower(f"%{filter_term}%")
q = select(self.SharesTable).filter(self.SharesTable.c.name.like(like_term))
else:
q = select(self.SharesTable)
results = self.conn.execute(q).all()
return results
def get_shares_by_access(self, permissions, share_id=None):
permissions = permissions.lower()
q = select(self.SharesTable)
if share_id:
q = q.filter(self.SharesTable.c.id == share_id)
if "r" in permissions:
q = q.filter(self.SharesTable.c.read == 1)
if "w" in permissions:
q = q.filter(self.SharesTable.c.write == 1)
results = self.conn.execute(q).all()
return results
def get_users_with_share_access(self, host_id, share_name, permissions):
permissions = permissions.lower()
q = select(self.SharesTable.c.userid).filter(
self.SharesTable.c.name == share_name, self.SharesTable.c.hostid == host_id
)
if "r" in permissions:
q = q.filter(self.SharesTable.c.read == 1)
if "w" in permissions:
q = q.filter(self.SharesTable.c.write == 1)
results = self.conn.execute(q).all()
return results
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
Add domain backupkey
:domain is the domain fqdn
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkey).filter(
func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain)
)
results = self.conn.execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
backup_key = {"domain": domain, "pvk": pvk_encoded}
try:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkey) # .returning(self.DpapiBackupkey.c.id)
self.conn.execute(q, [backup_key]) # .scalar()
cme_logger.debug(
f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})"
)
# return inserted_id
except Exception as e:
cme_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
def get_domain_backupkey(self, domain: str = None):
"""
Get domain backupkey
:domain is the domain fqdn
"""
q = select(self.DpapiBackupkey)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
results = self.conn.execute(q).all()
cme_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
if len(results) > 0:
results = [
(id_key, domain, base64.b64decode(pvk))
for id_key, domain, pvk in results
]
return results
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this group ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecrets).filter(
func.lower(self.DpapiSecrets.c.id) == dpapi_secret_id
)
results = self.conn.execute(q).first()
valid = True if results is not None else False
cme_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
self,
host: str,
dpapi_type: str,
windows_user: str,
username: str,
password: str,
url: str = "",
):
"""
Add dpapi secrets to cmedb
"""
secret = {
"host": host,
"dpapi_type": dpapi_type,
"windows_user": windows_user,
"username": username,
"password": password,
"url": url,
}
q = Insert(
self.DpapiSecrets
).on_conflict_do_nothing() # .returning(self.DpapiSecrets.c.id)
self.conn.execute(q, [secret]) # .scalar()
# inserted_result = res_inserted_result.first()
# inserted_id = inserted_result.id
cme_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})"
)
def get_dpapi_secrets(
self,
filter_term=None,
host: str = None,
dpapi_type: str = None,
windows_user: str = None,
username: str = None,
url: str = None,
):
"""
Get dpapi secrets from cmedb
"""
q = select(self.DpapiSecrets)
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecrets.c.id == filter_term)
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecrets.c.host == host)
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
q = q.filter(
func.lower(self.DpapiSecrets.c.dpapi_type) == func.lower(dpapi_type)
)
elif windows_user:
like_term = func.lower(f"%{windows_user}%")
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
elif username:
like_term = func.lower(f"%{username}%")
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecrets.c.url) == func.lower(url))
results = self.conn.execute(q).all()
cme_logger.debug(
f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}"
)
return results
def add_loggedin_relation(self, user_id, host_id):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.userid == user_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.conn.execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
relation = {"userid": user_id, "hostid": host_id}
try:
cme_logger.debug(f"Inserting loggedin_relations: {relation}")
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(
self.LoggedinRelationsTable
) # .returning(self.LoggedinRelationsTable.c.id)
self.conn.execute(q, [relation]) # .scalar()
inserted_id_results = self.get_loggedin_relations(user_id, host_id)
cme_logger.debug(
f"Checking if relation was added: {inserted_id_results}"
)
return inserted_id_results[0].id
except Exception as e:
cme_logger.debug(f"Error inserting LoggedinRelation: {e}")
def get_loggedin_relations(self, user_id=None, host_id=None):
q = select(
self.LoggedinRelationsTable
) # .returning(self.LoggedinRelationsTable.c.id)
if user_id:
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
results = self.conn.execute(q).all()
return results
def remove_loggedin_relations(self, user_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
if user_id:
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.conn.execute(q)