feat|fix(winrm): update winrm to use database to save hosts and credentials; closes #739 and closes #740

main
Marshall Hallenbeck 2023-03-12 03:00:00 -04:00
parent 39502bc210
commit b21e450f90
2 changed files with 149 additions and 70 deletions

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
import datetime
from datetime import datetime
import os
import requests
from impacket.smbconnection import SMBConnection, SessionError
from sqlalchemy import select
from urllib3.exceptions import NewConnectionError
from cme.connection import *
from cme.helpers.logger import highlight
@ -50,6 +50,7 @@ class winrm(connection):
winrm_parser.add_argument("--ssl", action='store_true', help="Connect to SSL Enabled WINRM")
winrm_parser.add_argument("--ignore-ssl-cert", action='store_true', help="Ignore Certificate Verification")
winrm_parser.add_argument("--laps", dest='laps', metavar="LAPS", type=str, help="LAPS authentification", nargs='?', const='administrator')
winrm_parser.add_argument("--http-timeout", dest='http_timeout', type=int, default=10, help="HTTP timeout for WinRM connections")
dgroup = winrm_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action='store_true', help='authenticate locally to each target')
@ -92,7 +93,7 @@ class winrm(connection):
self.logger.extra['hostname'] = self.hostname
else:
try:
smb_conn = SMBConnection(self.host, self.host, None)
smb_conn = SMBConnection(self.host, self.host, None, timeout=5)
try:
smb_conn.login('', '')
except SessionError as e:
@ -103,11 +104,18 @@ class winrm(connection):
self.server_os = smb_conn.getServerOS()
self.logger.extra['hostname'] = self.hostname
self.output_filename = os.path.expanduser(
'~/.cme/logs/{}_{}_{}'.format(
self.hostname,
self.host,
datetime.now().strftime("%Y-%m-%d_%H%M%S")
)
)
try:
smb_conn.logoff()
except:
pass
except Exception as e:
logging.debug("Error retrieving host domain: {} specify one manually with the '-d' flag".format(e))
@ -117,6 +125,11 @@ class winrm(connection):
if self.args.local_auth:
self.domain = self.hostname
if self.server_os is None:
self.server_os = ''
if self.domain is None:
self.domain = ''
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
self.output_filename = os.path.expanduser('~/.cme/logs/{}_{}_{}'.format(self.hostname, self.host, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
@ -186,20 +199,23 @@ class winrm(connection):
for url in endpoints:
try:
requests.get(url, verify=False, timeout=3)
logging.debug(f"winrm create_conn_obj() - Requesting URL: {url}")
res = requests.post(url, verify=False, timeout=self.args.http_timeout)
logging.debug(f"winrm create_conn_obj() - Received response code: {res.status_code}")
self.endpoint = url
if self.endpoint.startswith('https://'):
self.port = self.args.port if self.args.port else 5986
else:
self.port = self.args.port if self.args.port else 5985
self.logger.extra['port'] = self.port
return True
except Exception as e:
if 'Max retries exceeded with url' not in str(e):
logging.debug('Error in WinRM create_conn_obj:' + str(e))
except requests.exceptions.Timeout as e:
logging.debug(f"Connection Timed out to WinRM service: {e}")
except requests.exceptions.ConnectionError as e:
if 'Max retries exceeded with url' in str(e):
logging.debug(f"Connection Timeout to WinRM service (max retries exceeded)")
else:
logging.debug(f"Other ConnectionError to WinRM service: {e}")
return False
def plaintext_login(self, domain, username, password):
@ -248,17 +264,16 @@ class winrm(connection):
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else '')
)
)
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
self.db.add_credential('plaintext', domain, self.username, self.password)
q = select(self.HostsTable).filter(
self.HostsTable.c.ip == self.host
)
results = asyncio.run(self.conn.execute(q)).first().id
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
user_id = self.db.add_credential('plaintext', domain, self.username, self.password)
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(user_id, host_id)
if self.admin_privs:
self.logger.debug(f"Inside admin privs")
self.db.add_admin_user('plaintext', domain, self.username, self.password, self.host)
self.db.add_admin_user('plaintext', domain, self.username, self.password, self.host, user_id=user_id)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)

View File

@ -25,7 +25,7 @@ class database:
expire_on_commit=True,
class_=AsyncSession
)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@ -86,7 +86,7 @@ class database:
for table in self.metadata.sorted_tables:
asyncio.run(self.conn.execute(table.delete()))
def add_host(self, ip, port, hostname, domain, os):
def add_host(self, ip, port, hostname, domain, os=None):
"""
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
@ -149,7 +149,7 @@ class database:
Check if this credential has already been added to the database, if not add it in.
"""
domain = domain.split('.')[0].upper()
user_rowid = None
credentials = []
credential_data = {}
if credtype is not None:
@ -170,39 +170,50 @@ class database:
)
results = asyncio.run(self.conn.execute(q)).all()
logging.debug(f"Credential results: {results}")
# add new credential
if not results:
user_data = {
new_cred = {
"credtype": credtype,
"domain": domain,
"username": username,
"password": password,
"credtype": credtype,
"pillaged_from_hostid": pillaged_from,
"pillaged_from": pillaged_from,
}
q = Insert(self.UsersTable).values(user_data).returning(self.UsersTable.c.id)
results = asyncio.run(self.conn.execute(q)).first()
user_rowid = results.id
logging.debug(f"User RowID: {user_rowid}")
credentials = [new_cred]
# update existing cred data
else:
for user in results:
# might be able to just remove this if check, but leaving it in for now
if not user[3] and not user[4] and not user[5]:
q = update(self.UsersTable).values(credential_data).returning(self.UsersTable.c.id)
results = asyncio.run(self.conn.execute(q)).first()
user_rowid = results.id
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 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)
logging.debug(
'add_credential(credtype={}, domain={}, username={}, password={}, pillaged_from={}) => {}'.format(
credtype,
domain,
username,
password,
pillaged_from,
user_rowid
))
return user_rowid
# 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
)
user_ids = asyncio.run(
self.conn.execute(
q_users,
credentials
)
).scalar()
return user_ids
def remove_credentials(self, creds_id):
"""
@ -217,46 +228,47 @@ class database:
asyncio.run(self.conn.execute(q))
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
domain = domain.split('.')[0].upper()
domain = domain.split('.')[0]
add_links = []
creds_q = select(self.UsersTable)
if user_id:
q = select(self.UsersTable).filter(
creds_q = creds_q.filter(
self.UsersTable.c.id == user_id
)
users = asyncio.run(self.conn.execute(q)).all()
else:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == credtype,
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 = asyncio.run(self.conn.execute(q)).all()
logging.debug(f"Users: {users}")
users = asyncio.run(self.conn.execute(creds_q))
hosts = self.get_hosts(host)
like_term = func.lower(f"%{host}%")
q = select(self.HostsTable).filter(
self.HostsTable.c.ip.like(like_term)
)
hosts = asyncio.run(self.conn.execute(q)).all()
logging.debug(f"Hosts: {hosts}")
if users is not None and hosts is not None:
if users and hosts:
for user, host in zip(users, hosts):
user_id = user[0]
host_id = host[0]
q = select(self.AdminRelationsTable).filter(
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 = asyncio.run(self.conn.execute(q)).all()
links = asyncio.run(self.conn.execute(admin_relations_select)).all()
if not links:
asyncio.run(self.conn.execute(
Insert(self.AdminRelationsTable),
links
))
add_links.append(link)
admin_relations_insert = Insert(self.AdminRelationsTable)
asyncio.run(self.conn.execute(
admin_relations_insert,
add_links
))
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
@ -377,6 +389,7 @@ class database:
func.lower(self.HostsTable.c.hostname).like(like_term)
)
results = asyncio.run(self.conn.execute(q)).all()
logging.debug(f"winrm get_hosts() - results: {results}")
return results
def is_user_valid(self, user_id):
@ -411,4 +424,55 @@ class database:
func.lower(self.UsersTable.c.username) == func.lower(username)
)
results = asyncio.run(self.conn.execute(q)).all()
return 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 = asyncio.run(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:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable).returning(self.LoggedinRelationsTable.c.id)
inserted_ids = asyncio.run(
self.conn.execute(
q,
[relation]
)
).scalar()
return inserted_ids
except Exception as e:
logging.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 = asyncio.run(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
)
asyncio.run(self.conn.execute(q))