Resolve merge conflicts

main
Alexander Neff 2023-05-02 12:47:05 +02:00
commit a6c77294dc
5 changed files with 960 additions and 83 deletions

View File

@ -11,11 +11,16 @@ assignees: ''
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
Steps to reproduce the behavior i.e.:
Command: `crackmapexec smb -u username -p password`
Resulted in:
```
crackmapexec smb 10.10.10.10 -u username -p password -x "whoami"
SMB 10.10.10.10 445 DC01 [*] Windows 10.0 Build 17763 x64 (name:DC01) (domain:domain) (signing:True) (SMBv1:False)
SMB 10.10.10.10 445 DC01 [+] domain\username:password
Traceback (most recent call last):
...
```
**Expected behavior**
A clear and concise description of what you expected to happen.
@ -26,7 +31,7 @@ If applicable, add screenshots to help explain your problem.
**Crackmapexec info**
- OS: [e.g. Kali]
- Version of CME [e.g. v5.0.2]
- Installed from apt or using latest release ? Please try with latest release before openning an issue
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue
**Additional context**
Add any other context about the problem here.

View File

@ -19,14 +19,6 @@ from cme.loaders.protocolloader import ProtocolLoader
from cme.paths import CONFIG_PATH, WS_PATH, WORKSPACE_DIR
# # The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message
# from requests.packages.urllib3.exceptions import InsecureRequestWarning
# requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
#
# # 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 UserExitedProto(Exception):
pass
@ -84,7 +76,7 @@ def complete_export(text, line):
"""
Tab-complete 'creds' commands.
"""
commands = ["creds", "plaintext", "hashes", "shares", "local_admins", "signing"]
commands = ["creds", "plaintext", "hashes", "shares", "local_admins", "signing", "keys"]
mline = line.partition(' ')[2]
offs = len(mline) - len(text)
return [s[offs:] for s in commands if s.startswith(mline)]
@ -121,11 +113,11 @@ class DatabaseNavigator(cmd.Cmd):
print("[-] not enough arguments")
return
line = line.split()
command = line[0].lower()
# Need to use if/elif/else to keep compatibility with py3.8/3.9
# Reference DB Function cme/protocols/smb/database.py
# Users
if line[0].lower() == 'creds':
if command == "creds":
if len(line) < 3:
print("[-] invalid arguments, export creds <simple|detailed> <filename>")
return
@ -154,11 +146,11 @@ class DatabaseNavigator(cmd.Cmd):
formatted_creds.append(entry)
write_csv(filename, csv_header, formatted_creds)
else:
print('[-] No such export option: %s' % line[1])
print(f"[-] No such export option: {line[1]}")
return
print('[+] Creds exported')
# Hosts
elif line[0].lower() == 'hosts':
elif command == "hosts":
if len(line) < 3:
print("[-] invalid arguments, export hosts <simple|detailed|signing> <filename>")
return
@ -180,11 +172,11 @@ class DatabaseNavigator(cmd.Cmd):
signing_hosts = [host[1] for host in hosts]
write_list(filename, signing_hosts)
else:
print('[-] No such export option: %s' % line[1])
print(f"[-] No such export option: {line[1]}")
return
print('[+] Hosts exported')
# Shares
elif line[0].lower() == 'shares':
elif command == "shares":
if len(line) < 3:
print("[-] invalid arguments, export shares <simple|detailed> <filename>")
return
@ -193,11 +185,11 @@ class DatabaseNavigator(cmd.Cmd):
csv_header = ["id", "host", "userid", "name", "remark", "read", "write"]
filename = line[2]
if line[1].lower() == 'simple':
if line[1].lower() == "simple":
write_csv(filename, csv_header, shares)
print('[+] shares exported')
print("[+] shares exported")
# Detailed view gets hostname, usernames, and true false statement
elif line[1].lower() == 'detailed':
elif line[1].lower() == "detailed":
formatted_shares = []
for share in shares:
user = self.db.get_users(share[2])[0]
@ -214,11 +206,11 @@ class DatabaseNavigator(cmd.Cmd):
formatted_shares.append(entry)
write_csv(filename, csv_header, formatted_shares)
else:
print('[-] No such export option: %s' % line[1])
print(f"[-] No such export option: {line[1]}")
return
print('[+] Shares exported')
print("[+] Shares exported")
# Local Admin
elif line[0].lower() == 'local_admins':
elif command == "local_admins":
if len(line) < 3:
print("[-] invalid arguments, export local_admins <simple|detailed> <filename>")
return
@ -228,9 +220,9 @@ class DatabaseNavigator(cmd.Cmd):
csv_header = ["id", "userid", "host"]
filename = line[2]
if line[1].lower() == 'simple':
if line[1].lower() == "simple":
write_csv(filename, csv_header, local_admins)
elif line[1].lower() == 'detailed':
elif line[1].lower() == "detailed":
formatted_local_admins = []
for entry in local_admins:
user = self.db.get_users(filter_term=entry[1])[0]
@ -244,10 +236,10 @@ class DatabaseNavigator(cmd.Cmd):
formatted_local_admins.append(formatted_entry)
write_csv(filename, csv_header, formatted_local_admins)
else:
print('[-] No such export option: %s' % line[1])
print(f"[-] No such export option: {line[1]}")
return
print('[+] Local Admins exported')
elif line[0].lower() == 'dpapi':
elif command == "dpapi":
if len(line) < 3:
print("[-] invalid arguments, export dpapi <simple|detailed> <filename>")
return
@ -257,9 +249,9 @@ class DatabaseNavigator(cmd.Cmd):
csv_header = ["id", "host", "dpapi_type", "windows_user", "username", "password", "url"]
filename = line[2]
if line[1].lower() == 'simple':
if line[1].lower() == "simple":
write_csv(filename, csv_header, dpapi_secrets)
elif line[1].lower() == 'detailed':
elif line[1].lower() == "detailed":
formatted_dpapi_secret = []
for entry in dpapi_secrets:
@ -279,16 +271,26 @@ class DatabaseNavigator(cmd.Cmd):
print('[-] No such export option: %s' % line[1])
return
print('[+] DPAPI secrets exported')
elif command == "keys":
if line[1].lower() == "all":
keys = self.db.get_keys()
else:
keys = self.db.get_keys(key_id=int(line[1]))
writable_keys = [key[2] for key in keys]
filename = line[2]
write_list(filename, writable_keys)
else:
print("[-] Invalid argument, specify creds, hosts, local_admins, shares or dpapi")
def help_export(self):
help_string = """
export [creds|hosts|local_admins|shares|signing] [simple|detailed|*] [filename]
export [creds|hosts|local_admins|shares|signing|keys] [simple|detailed|*] [filename]
Exports information to a specified file
* hosts has an additional third option from simple and detailed: signing - this simply writes a list of ips of
hosts where signing is enabled
* keys' third option is either "all" or an id of a key to export
export keys [all|id] [filename]
"""
print_help(help_string)

View File

@ -1,36 +1,74 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import paramiko
from cme.config import process_secret
from cme.connection import *
from cme.helpers.logger import highlight
from cme.logger import CMEAdapter
from paramiko.ssh_exception import AuthenticationException, NoValidConnectionsError, SSHException
class ssh(connection):
def __init__(self, args, db, host):
super().__init__(args, db, host)
self.remote_version = None
self.server_os = None
@staticmethod
def proto_args(parser, std_parser, module_parser):
ssh_parser = parser.add_parser('ssh', help="own stuff using SSH", parents=[std_parser, module_parser])
ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase.")
ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)")
ssh_parser = parser.add_parser(
"ssh",
help="own stuff using SSH",
parents=[std_parser, module_parser]
)
ssh_parser.add_argument(
"--key-file",
type=str,
help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase."
)
ssh_parser.add_argument(
"--port",
type=int,
default=22,
help="SSH port (default: 22)"
)
cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands")
cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output')
cgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command")
cgroup = ssh_parser.add_argument_group(
"Command Execution",
"Options for executing commands"
)
cgroup.add_argument(
"--no-output",
action="store_true",
help="do not retrieve command output"
)
cgroup.add_argument(
"-x",
metavar="COMMAND",
dest="execute",
help="execute the specified command"
)
cgroup.add_argument(
"--remote-enum",
action="store_true",
help="executes remote commands for enumeration"
)
return parser
def proto_logger(self):
self.logger = CMEAdapter(
extra={
'protocol': 'SSH',
'host': self.host,
'port': self.args.port,
'hostname': self.hostname
"protocol": "SSH",
"host": self.host,
"port": self.args.port,
"hostname": self.hostname
}
)
logging.getLogger("paramiko").setLevel(logging.WARNING)
def print_host_info(self):
self.logger.display(self.remote_version)
@ -38,6 +76,13 @@ class ssh(connection):
def enum_host_info(self):
self.remote_version = self.conn._transport.remote_version
self.logger.debug(f"Remote version: {self.remote_version}")
self.server_os = ""
if self.args.remote_enum:
stdin, stdout, stderr = self.conn.exec_command("uname -r")
self.server_os = stdout.read().decode("utf-8")
self.logger.debug(f"OS retrieved: {self.server_os}")
self.db.add_host(self.host, self.args.port, self.remote_version, os=self.server_os)
def create_conn_obj(self):
self.conn = paramiko.SSHClient()
@ -58,25 +103,38 @@ class ssh(connection):
self.conn.close()
def check_if_admin(self):
# we could add in another method to check by piping in the password to sudo
# but that might be too much of an opsec concern - maybe add in a flag to do more checks?
stdin, stdout, stderr = self.conn.exec_command("id")
if stdout.read().decode("utf-8").find("uid=0(root)") != -1:
self.logger.info(f"Determined user is root via `id` command")
self.admin_privs = True
return True
stdin, stdout, stderr = self.conn.exec_command("sudo -ln | grep 'NOPASSWD: ALL'")
if stdout.read().decode("utf-8").find("NOPASSWD: ALL")!= -1:
self.logger.info(f"Determined user is root via `sudo -ln` command")
self.admin_privs = True
return True
def plaintext_login(self, username, password):
try:
if self.args.key_file:
passwd = password
password = f"{passwd} (keyfile: {self.args.key_file})"
self.logger.debug(f"Logging in with keyfile: {self.args.key_file}")
with open(self.args.key_file, "r") as f:
key_data = f.read()
self.conn.connect(
self.host,
port=self.args.port,
username=username,
passphrase=passwd,
passphrase=password,
key_filename=self.args.key_file,
look_for_keys=False,
allow_agent=False
)
cred_id = self.db.add_credential("key", username, password, key=key_data)
else:
self.logger.debug(f"Logging in with password")
self.conn.connect(
self.host,
port=self.args.port,
@ -85,30 +143,57 @@ class ssh(connection):
look_for_keys=False,
allow_agent=False
)
cred_id = self.db.add_credential("plaintext", username, password)
shell_access = False
host_id = self.db.get_hosts(self.host)[0].id
if self.check_if_admin():
shell_access = True
self.logger.debug(f"User {username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", username, password, host_id=host_id, cred_id=cred_id)
else:
stdin, stdout, stderr = self.conn.exec_command("id")
output = stdout.read().decode("utf-8")
if not output:
self.logger.debug(f"User cannot get a shell")
shell_access = False
else:
shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access)
if self.args.key_file:
password = f"{password} (keyfile: {self.args.key_file})"
display_shell_access = f" - shell access!" if shell_access else ""
self.check_if_admin()
self.logger.success(
u"{}:{} {}".format(
username,
password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else '')
)
f"{username}:{process_secret(password)} {self.mark_pwned()}{highlight(display_shell_access)}"
)
return True
except Exception as e:
except (AuthenticationException, NoValidConnectionsError, ConnectionResetError) as e:
self.logger.fail(
f"{username}:{password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {e}"
f"{username}:{process_secret(password)} {e}"
)
self.client_close()
return False
except Exception as e:
self.logger.exception(e)
self.client_close()
return False
def execute(self, payload=None, get_output=False):
def execute(self, payload=None, output=False):
try:
stdin, stdout, stderr = self.conn.exec_command(self.args.execute)
command = payload if payload is not None else self.args.execute
stdin, stdout, stderr = self.conn.exec_command(command)
except AttributeError:
return ""
self.logger.success("Executed command")
for line in stdout:
self.logger.highlight(line.strip())
return stdout
if output:
self.logger.success("Executed command")
for line in stdout:
self.logger.highlight(line.strip())
return stdout

View File

@ -1,16 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
from sqlalchemy import MetaData, Table, select, func, delete
from sqlalchemy.exc import IllegalStateChangeError, NoInspectionAvailable, NoSuchTableError
import os
import configparser
from cme.logger import cme_logger
from cme.paths import CME_PATH
# we can't import config.py due to a circular dependency, so we have to create redundant code unfortunately
cme_config = configparser.ConfigParser()
cme_config.read(os.path.join(CME_PATH, "cme.conf"))
cme_workspace = cme_config.get("CME", "workspace", fallback="default")
class database:
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
self.AdminRelationsTable = None
self.KeysTable = None
self.db_engine = db_engine
self.metadata = MetaData()
@ -21,41 +34,72 @@ class database:
)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
self.sess = Session()
@staticmethod
def db_schema(db_conn):
db_conn.execute('''CREATE TABLE "credentials" (
db_conn.execute(
'''CREATE TABLE "credentials" (
"id" integer PRIMARY KEY,
"username" text,
"password" text
)''')
db_conn.execute('''CREATE TABLE "hosts" (
"password" text,
"credtype" text
)''')
db_conn.execute(
'''CREATE TABLE "hosts" (
"id" integer PRIMARY KEY,
"ip" text,
"host" text,
"port" integer,
"server_banner" text
)''')
"banner" text,
"os" text
)''')
db_conn.execute(
'''CREATE TABLE "loggedin_relations" (
"id" integer PRIMARY KEY,
"credid" integer,
"hostid" integer,
"shell" boolean,
FOREIGN KEY(credid) REFERENCES credentials(id),
FOREIGN KEY(hostid) REFERENCES hosts(id)
)''')
# "admin" access with SSH means we have root access, which implies shell access since we run commands to check
db_conn.execute(
'''CREATE TABLE "admin_relations" (
"id" integer PRIMARY KEY,
"credid" integer,
"hostid" integer,
FOREIGN KEY(credid) REFERENCES credentials(id),
FOREIGN KEY(hostid) REFERENCES hosts(id)
)''')
db_conn.execute(
'''CREATE TABLE "keys" (
"id" integer PRIMARY KEY,
"credid" integer,
"data" text,
FOREIGN KEY(credid) REFERENCES credentials(id)
)''')
def reflect_tables(self):
with self.db_engine.connect() as conn:
with self.db_engine.connect():
try:
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
self.AdminRelationsTable = Table("admin_relations", self.metadata, autoload_with=self.db_engine)
self.KeysTable = Table("keys", self.metadata, autoload_with=self.db_engine)
except (NoInspectionAvailable, NoSuchTableError):
ssh_workspace = f"~/.cme/workspaces/{cme_workspace}/ssh.db"
print(
"[-] Error reflecting tables - this means there is a DB schema mismatch \n"
"[-] Error reflecting tables for SSH protocol - 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"
f"[-] Optionally save the old DB data (`cp {ssh_workspace} ~/cme_ssh.bak`)\n"
f"[-] Then remove the CME SSH DB (`rm -rf {ssh_workspace}`) and run CME to initialize the new DB"
)
exit()
def shutdown_db(self):
try:
self.conn.close()
self.sess.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>
@ -64,4 +108,429 @@ class database:
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
self.sess.execute(table.delete())
def add_host(self, host, port, banner, os=None):
"""
Check if this host has already been added to the database, if not, add it in.
"""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(
self.HostsTable.c.host == host
)
results = self.sess.execute(q).all()
cme_logger.debug(f"add_host(): Initial hosts results: {results}")
# create new host
if not results:
new_host = {
"host": host,
"port": port,
"banner": banner if banner is not None else '',
"os": os if os is not None else ''
}
hosts = [new_host]
# update existing hosts data
else:
for host_result in results:
host_data = host_result._asdict()
cme_logger.debug(f"host: {host_result}")
cme_logger.debug(f"host_data: {host_data}")
# only update column if it is being passed in
if host is not None:
host_data["host"] = host
if port is not None:
host_data["port"] = port
if banner is not None:
host_data["banner"] = banner
if os is not None:
host_data["os"] = os
# 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"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.sess.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, username, password, key=None):
"""
Check if this credential has already been added to the database, if not add it in.
"""
credentials = []
# a user can have multiple keys, all with passphrases, and a separate login password
if key is not None:
q = select(self.CredentialsTable).join(self.KeysTable).filter(
func.lower(self.CredentialsTable.c.username) == func.lower(username),
func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype),
self.KeysTable.c.data == key
)
results = self.sess.execute(q).all()
else:
q = select(self.CredentialsTable).filter(
func.lower(self.CredentialsTable.c.username) == func.lower(username),
func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype)
)
results = self.sess.execute(q).all()
# add new credential
if not results:
new_cred = {
"credtype": credtype,
"username": username,
"password": password,
}
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 username is not None:
cred_data["username"] = username
if password is not None:
cred_data["password"] = password
# 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.CredentialsTable) # .returning(self.CredentialsTable.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.CredentialsTable.primary_key,
set_=update_columns_users
)
cme_logger.debug(f"Adding credentials: {credentials}")
self.sess.execute(
q_users,
credentials
) # .scalar()
# return cred_ids
# hacky way to get cred_id since we can't use returning() yet
if len(credentials) == 1:
cred_id = self.get_credential(credtype, username, password)
if key is not None:
self.add_key(cred_id, key)
return cred_id
else:
return credentials
def remove_credentials(self, creds_id):
"""
Removes a credential ID from the database
"""
del_hosts = []
for cred_id in creds_id:
q = delete(self.CredentialsTable).filter(
self.CredentialsTable.c.id == cred_id
)
del_hosts.append(q)
self.sess.execute(q)
def add_key(self, cred_id, key):
# check if key relation already exists
check_q = self.sess.execute(
select(self.KeysTable).filter(
self.KeysTable.c.credid == cred_id
)
).all()
cme_logger.debug(f"check_q: {check_q}")
if check_q:
cme_logger.debug(f"Key already exists for cred_id {cred_id}")
return
key_data = {
"credid": cred_id,
"data": key
}
self.sess.execute(
Insert(self.KeysTable),
key_data
)
key_id = self.sess.execute(
select(self.KeysTable).filter(
self.KeysTable.c.credid == cred_id
)
).all()[0].id
cme_logger.debug(f"Key added: {key_id}")
return key_id
def get_keys(self, key_id=None, cred_id=None):
q = select(self.KeysTable)
if key_id is not None:
q = q.filter(
self.KeysTable.c.id == key_id
)
elif cred_id is not None:
q = q.filter(
self.KeysTable.c.credid == cred_id
)
results = self.sess.execute(q)
return results
def add_admin_user(self, credtype, username, secret, host_id=None, cred_id=None):
add_links = []
creds_q = select(self.CredentialsTable)
if cred_id:
creds_q = creds_q.filter(
self.CredentialsTable.c.id == cred_id
)
else:
creds_q = creds_q.filter(
func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype),
func.lower(self.CredentialsTable.c.username) == func.lower(username),
self.CredentialsTable.c.password == secret
)
creds = self.sess.execute(creds_q)
hosts = self.get_hosts(host_id)
if creds and hosts:
for cred, host in zip(creds, hosts):
cred_id = cred[0]
host_id = host[0]
link = {
"credid": cred_id,
"hostid": host_id
}
admin_relations_select = select(self.AdminRelationsTable).filter(
self.AdminRelationsTable.c.credid == cred_id,
self.AdminRelationsTable.c.hostid == host_id
)
links = self.sess.execute(admin_relations_select).all()
if not links:
add_links.append(link)
admin_relations_insert = Insert(self.AdminRelationsTable)
if add_links:
self.sess.execute(
admin_relations_insert,
add_links
)
def get_admin_relations(self, cred_id=None, host_id=None):
if cred_id:
q = select(self.AdminRelationsTable).filter(
self.AdminRelationsTable.c.credid == cred_id
)
elif host_id:
q = select(self.AdminRelationsTable).filter(
self.AdminRelationsTable.c.hostid == host_id
)
else:
q = select(self.AdminRelationsTable)
results = self.sess.execute(q).all()
return results
def remove_admin_relation(self, cred_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
if cred_ids:
for cred_id in cred_ids:
q = q.filter(
self.AdminRelationsTable.c.credid == cred_id
)
elif host_ids:
for host_id in host_ids:
q = q.filter(
self.AdminRelationsTable.c.hostid == host_id
)
self.sess.execute(q)
def is_credential_valid(self, credential_id):
"""
Check if this credential ID is valid.
"""
q = select(self.CredentialsTable).filter(
self.CredentialsTable.c.id == credential_id,
self.CredentialsTable.c.password is not None
)
results = self.sess.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.CredentialsTable).filter(
self.CredentialsTable.c.id == filter_term
)
elif cred_type:
q = select(self.CredentialsTable).filter(
self.CredentialsTable.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.CredentialsTable).filter(
func.lower(self.CredentialsTable.c.username).like(like_term)
)
# otherwise return all credentials
else:
q = select(self.CredentialsTable)
results = self.sess.execute(q).all()
return results
def get_credential(self, cred_type, username, password):
q = select(self.CredentialsTable).filter(
self.CredentialsTable.c.username == username,
self.CredentialsTable.c.password == password,
self.CredentialsTable.c.credtype == cred_type
)
results = self.sess.execute(q).first()
return results.id
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.sess.execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=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.sess.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 host
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(
self.HostsTable.c.host.like(like_term)
)
results = self.sess.execute(q).all()
cme_logger.debug(f"SSH get_hosts() - results: {results}")
return results
def is_user_valid(self, cred_id):
"""
Check if this User ID is valid.
"""
q = select(self.CredentialsTable).filter(
self.CredentialsTable.c.id == cred_id
)
results = self.sess.execute(q).all()
return len(results) > 0
def get_users(self, filter_term=None):
q = select(self.CredentialsTable)
if self.is_user_valid(filter_term):
q = q.filter(
self.CredentialsTable.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.CredentialsTable.c.username).like(like_term)
)
results = self.sess.execute(q).all()
return results
def get_user(self, domain, username):
q = select(self.CredentialsTable).filter(
func.lower(self.CredentialsTable.c.username) == func.lower(username)
)
results = self.sess.execute(q).all()
return results
def add_loggedin_relation(self, cred_id, host_id, shell=False):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.credid == cred_id,
self.LoggedinRelationsTable.c.hostid == host_id
)
results = self.sess.execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
relation = {
"credid": cred_id,
"hostid": host_id,
"shell": shell
}
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.sess.execute(
q,
[relation]
) # .scalar()
inserted_id_results = self.get_loggedin_relations(cred_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, cred_id=None, host_id=None, shell=None):
q = select(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
if cred_id:
q = q.filter(
self.LoggedinRelationsTable.c.credid == cred_id
)
if host_id:
q = q.filter(
self.LoggedinRelationsTable.c.hostid == host_id
)
if shell:
q = q.filter(
self.LoggedinRelationsTable.c.shell == shell
)
results = self.sess.execute(q).all()
return results
def remove_loggedin_relations(self, cred_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
if cred_id:
q = q.filter(
self.LoggedinRelationsTable.c.credid == cred_id
)
elif host_id:
q = q.filter(
self.LoggedinRelationsTable.c.hostid == host_id
)
self.sess.execute(q)

View File

@ -1,12 +1,307 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from cme.cmedb import DatabaseNavigator, print_help
from cme.helpers.misc import validate_ntlm
from cme.cmedb import DatabaseNavigator, print_table, print_help
class navigator(DatabaseNavigator):
def display_creds(self, creds):
data = [["CredID", "Admin On", "Total Login", "Total Shell", "Username", "Password", "CredType"]]
for cred in creds:
cred_id = cred[0]
username = cred[1]
password = cred[2]
credtype = cred[3]
admin_links = self.db.get_admin_relations(cred_id=cred_id)
total_users = self.db.get_loggedin_relations(cred_id=cred_id)
total_shell = total_users = self.db.get_loggedin_relations(cred_id=cred_id, shell=True)
data.append([
cred_id,
str(len(admin_links)) + " Host(s)",
str(len(total_users)) + " Host(s)",
str(len(total_shell)) + " Shells(s)",
username,
password,
credtype
])
print_table(data, title="Credentials")
# pull/545
def display_hosts(self, hosts):
data = [[
"HostID",
"Admins",
"Total Users",
"Host",
"Port",
'Banner',
'OS'
]]
for h in hosts:
host_id = h[0]
host = h[1]
port = h[2]
banner = h[3]
os = h[4]
admin_users = self.db.get_admin_relations(host_id=host_id)
total_users = self.db.get_loggedin_relations(host_id=host_id)
data.append(
[
host_id,
str(len(admin_users)) + " Cred(s)",
str(len(total_users)) + " User(s)",
host,
port,
banner,
os
]
)
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",
"Host",
"Port",
"Banner",
"OS"
]]
host_id_list = []
for h in hosts:
host_id = h[0]
host_id_list.append(host_id)
host = h[1]
port = h[2]
banner = h[3]
os = h[4]
data.append(
[
host_id,
host,
port,
banner,
os
]
)
print_table(data, title="Host")
admin_access_data = [["CredID", "CredType", "UserName", "Password", "Shell"]]
nonadmin_access_data = [["CredID", "CredType", "UserName", "Password", "Shell"]]
for host_id in host_id_list:
admin_links = self.db.get_admin_relations(host_id=host_id)
nonadmin_links = self.db.get_loggedin_relations(host_id=host_id)
for link in admin_links:
link_id, cred_id, host_id = link
creds = self.db.get_credentials(filter_term=cred_id)
for cred in creds:
cred_id = cred[0]
username = cred[1]
password = cred[2]
credtype = cred[3]
shell = True
admin_access_data.append([cred_id, credtype, username, password, shell])
# probably a better way to do this without looping through and requesting them all again,
# but I just want to get this working for now
for link in nonadmin_links:
link_id, cred_id, host_id, shell = link
creds = self.db.get_credentials(filter_term=cred_id)
for cred in creds:
cred_id = cred[0]
username = cred[1]
password = cred[2]
credtype = cred[3]
shell = shell
cred_data = [cred_id, credtype, username, password, shell]
if cred_data not in admin_access_data:
nonadmin_access_data.append(cred_data)
if len(nonadmin_access_data) > 1:
print_table(nonadmin_access_data, title="Credential(s) with Non Admin Access")
if len(admin_access_data) > 1:
print_table(admin_access_data, title="Credential(s) with Admin Access")
def help_hosts(self):
help_string = """
hosts [filter_term]
By default prints all hosts
Table format:
| 'HostID', 'Host', 'Port', 'Banner', 'OS' |
"""
print_help(help_string)
def do_creds(self, line):
filter_term = line.strip()
if filter_term == "":
creds = self.db.get_credentials()
self.display_creds(creds)
# TODO
# elif filter_term.split()[0].lower() == "add":
# # add format: "domain username password <notes> <credType> <sid>
# args = filter_term.split()[1:]
#
# if len(args) == 3:
# domain, username, password = args
# if validate_ntlm(password):
# self.db.add_credential("hash", domain, username, password)
# else:
# self.db.add_credential("plaintext", domain, username, password)
# else:
# print("[!] Format is 'add username password")
# return
elif filter_term.split()[0].lower() == "remove":
args = filter_term.split()[1:]
if len(args) != 1:
print("[!] Format is 'remove <credID>'")
return
else:
self.db.remove_credentials(args)
self.db.remove_admin_relation(user_ids=args)
elif filter_term.split()[0].lower() == "plaintext":
creds = self.db.get_credentials(cred_type="plaintext")
self.display_creds(creds)
elif filter_term.split()[0].lower() == "key":
creds = self.db.get_credentials(cred_type="key")
self.display_creds(creds)
else:
creds = self.db.get_credentials(filter_term=filter_term)
if len(creds) != 1:
self.display_creds(creds)
elif len(creds) == 1:
cred_data = [["CredID", "UserName", "Password", "CredType"]]
cred_id_list = []
for cred in creds:
cred_id = cred[0]
cred_id_list.append(cred_id)
username = cred[1]
password = cred[2]
credtype = cred[3]
cred_data.append([cred_id, username, password, credtype])
print_table(cred_data, title='Credential(s)')
admin_access_data = [["HostID", "Host", "Port", "Banner", "OS", "Shell"]]
nonadmin_access_data = [["HostID", "Host", "Port", "Banner", "OS", "Shell"]]
for cred_id in cred_id_list:
admin_links = self.db.get_admin_relations(cred_id=cred_id)
nonadmin_links = self.db.get_loggedin_relations(cred_id=cred_id)
for link in admin_links:
link_id, cred_id, host_id = link
hosts = self.db.get_hosts(host_id)
for h in hosts:
host_id = h[0]
host = h[1]
port = h[2]
banner = h[3]
os = h[4]
shell = True # if we have root via SSH, we know it's a shell
admin_access_data.append([host_id, host, port, banner, os, shell])
# probably a better way to do this without looping through and requesting them all again,
# but I just want to get this working for now
for link in nonadmin_links:
link_id, cred_id, host_id, shell = link
hosts = self.db.get_hosts(host_id)
for h in hosts:
host_id = h[0]
host = h[1]
port = h[2]
banner = h[3]
os = h[4]
host_data = [host_id, host, port, banner, os, shell]
if host_data not in admin_access_data:
nonadmin_access_data.append(host_data)
# we look if it's greater than one because the header row always exists
if len(nonadmin_access_data) > 1:
print_table(nonadmin_access_data, title="Non-Admin Access to Host(s)")
if len(admin_access_data) > 1:
print_table(admin_access_data, title="Admin Access to Host(s)")
def help_creds(self):
help_string = """
creds [add|remove|plaintext|key|filter_term]
By default prints all creds
Table format:
| 'CredID', 'Admin On', 'CredType', 'UserName', 'Password', 'Key' (if key type) |
Subcommands:
add - format: "add username password <notes> <credType>"
remove - format: "remove <credID>"
plaintext - prints plaintext creds
key - prints ssh key creds
filter_term - filters creds with filter_term
If a single credential is returned (e.g. `creds 15`, it prints the following tables:
Credential(s) | 'CredID', 'CredType', 'UserName', 'Password', 'Key' |
Admin Access to Host(s) | 'HostID', 'Host', 'OS', 'Banner'
Otherwise, it prints the default credential table from a `like` query on the `username` column
"""
print_help(help_string)
def display_keys(self, keys):
data = [[
"Key ID",
"Cred ID",
"Key Data"
]]
for key in keys:
data.append([key[0], key[1], key[2]])
print_table(data, "Keys")
def do_keys(self, line):
filter_term = line.strip()
if filter_term == "":
keys = self.db.get_keys()
self.display_keys(keys)
elif filter_term == "cred_id":
cred_id = filter_term.split()[1]
keys = self.db.get_keys(cred_id=cred_id)
self.display_keys(keys)
else:
key_id = filter_term
keys = self.db.get_keys(key_id=key_id)
self.display_keys(keys)
def help_keys(self):
help_string = """
list SSH keys
keys [id]
"""
print_help(help_string)
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":
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 help_clear_database(self):
@ -16,3 +311,24 @@ class navigator(DatabaseNavigator):
YOU CANNOT UNDO THIS COMMAND
"""
print_help(help_string)
@staticmethod
def complete_hosts(self, text, line):
"""
Tab-complete 'hosts' commands.
"""
commands = ["add", "remove"]
mline = line.partition(' ')[2]
offs = len(mline) - len(text)
return [s[offs:] for s in commands if s.startswith(mline)]
def complete_creds(self, text, line):
"""
Tab-complete 'creds' commands.
"""
commands = ["add", "remove", "key", "plaintext"]
mline = line.partition(' ')[2]
offs = len(mline) - len(text)
return [s[offs:] for s in commands if s.startswith(mline)]