feat|fix(winrm): update winrm to use database to save hosts and credentials; closes #739 and closes #740
parent
39502bc210
commit
b21e450f90
|
@ -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)
|
||||
|
|
|
@ -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))
|
Loading…
Reference in New Issue