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

Commit

Permalink
Merge pull request #374 from byt3bl33d3r/v5-dev
Browse files Browse the repository at this point in the history
Merge branch V5 dev to Master
  • Loading branch information
mpgn authored May 5, 2020
2 parents db9166f + 9ae444a commit 618ab8a
Show file tree
Hide file tree
Showing 22 changed files with 665 additions and 413 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ If applicable, add screenshots to help explain your problem.

**Crackmapexec info**
- OS: [e.g. Kali]
- Version of CME [e.g. v5.0.1]
- Version of CME [e.g. v5.0.2]
- Installed from apt or using latest release ? Please try with latest release before openning an issue

**Additional context**
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ termcolor = "*"
msgpack = "*"
pylnk3 = "*"
paramiko = "*"
pywinrm = "*"
pypsrp = "*"
xmltodict = "*"
terminaltables = "*"
impacket = "*"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Unintentional contributors:

This repository contains the following repositories as submodules:
- [Impacket](https://github.com/CoreSecurity/impacket)
- [Pywinrm](https://github.com/diyan/pywinrm)
- [Pypsrp](https://github.com/jborean93/pypsrp)
- [Pywerview](https://github.com/the-useless-one/pywerview)
- [PowerSploit](https://github.com/PowerShellMafia/PowerSploit)
- [Invoke-Obfuscation](https://github.com/danielbohannon/Invoke-Obfuscation)
Expand Down
6 changes: 5 additions & 1 deletion cme/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

def gen_cli_args():

VERSION = '5.0.1dev'
VERSION = '5.0.2dev'
CODENAME = 'P3l1as'

p_loader = protocol_loader()
Expand Down Expand Up @@ -47,6 +47,10 @@ def gen_cli_args():
std_parser.add_argument('-id', metavar="CRED_ID", nargs='+', default=[], type=str, dest='cred_id', help='database credential ID(s) to use for authentication')
std_parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="username(s) or file(s) containing usernames")
std_parser.add_argument("-p", metavar="PASSWORD", dest='password', nargs='+', default=[], help="password(s) or file(s) containing passwords")
std_parser.add_argument("-k", "--kerberos", action='store_true', help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
std_parser.add_argument("--aesKey", action='store_true', help="AES key to use for Kerberos Authentication (128 or 256 bits)")
std_parser.add_argument("--kdcHost", action='store_true', help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")

fail_group = std_parser.add_mutually_exclusive_group()
fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='max number of global failed login attempts')
fail_group.add_argument("--ufail-limit", metavar='LIMIT', type=int, help='max number of failed login attempts per username')
Expand Down
201 changes: 114 additions & 87 deletions cme/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ def __init__(self, args, db, host):
self.conn = None
self.admin_privs = False
self.logger = None
self.password = None
self.username = None
self.password = ''
self.username = ''
self.kerberos = True if self.args.kerberos else False
self.aesKey = None if not self.args.aesKey else self.args.aesKey
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.failed_logins = 0
self.local_ip = None

try:
self.host = gethostbyname(self.hostname)
if self.args.kerberos:
self.host = self.hostname
except Exception as e:
logging.debug('Error resolving hostname {}: {}'.format(self.hostname, e))
return
Expand All @@ -60,6 +65,9 @@ def create_conn_obj(self):
def check_if_admin(self):
return

def kerberos_login(self):
return

def plaintext_login(self, domain, username, password):
return

Expand Down Expand Up @@ -133,94 +141,113 @@ def over_fail_limit(self, username):
return False

def login(self):
for cred_id in self.args.cred_id:
with sem:
if cred_id.lower() == 'all':
creds = self.db.get_credentials()
else:
creds = self.db.get_credentials(filterTerm=int(cred_id))

for cred in creds:
logging.debug(cred)
try:
c_id, domain, username, password, credtype, pillaged_from = cred

if credtype and password:

if not domain: domain = self.domain

if self.args.local_auth:
domain = self.domain
elif self.args.domain:
domain = self.args.domain

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

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

except IndexError:
self.logger.error("Invalid database credential ID!")

for user in self.args.username:
if not isinstance(user, str) and isfile(user.name):
for usr in user:
if self.args.hash:
with sem:
for ntlm_hash in self.args.hash:
if isinstance(ntlm_hash, str):
if not self.over_fail_limit(usr.strip()):
if self.hash_login(self.domain, usr.strip(), ntlm_hash): return True

elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name):
for f_hash in ntlm_hash:
if self.args.kerberos:
if self.kerberos_login(self.aesKey, self.kdcHost): return True
else:
for cred_id in self.args.cred_id:
with sem:
if cred_id.lower() == 'all':
creds = self.db.get_credentials()
else:
creds = self.db.get_credentials(filterTerm=int(cred_id))

for cred in creds:
logging.debug(cred)
try:
c_id, domain, username, password, credtype, pillaged_from = cred

if credtype and password:

if not domain: domain = self.domain

if self.args.local_auth:
domain = self.domain
elif self.args.domain:
domain = self.args.domain

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

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

except IndexError:
self.logger.error("Invalid database credential ID!")

for user in self.args.username:
if not isinstance(user, str) and isfile(user.name):
for usr in user:
if "\\" in usr:
tmp = usr
usr = tmp.split('\\')[1].strip()
self.domain = tmp.split('\\')[0]
if self.args.hash:
with sem:
for ntlm_hash in self.args.hash:
if isinstance(ntlm_hash, str):
if not self.over_fail_limit(usr.strip()):
if self.hash_login(self.domain, usr.strip(), f_hash.strip()): return True
ntlm_hash.seek(0)

elif self.args.password:
with sem:
for password in self.args.password:
if isinstance(password, str):
if not self.over_fail_limit(usr.strip()):
if self.plaintext_login(self.domain, usr.strip(), password): return True

elif not isinstance(password, str) and isfile(password.name):
for f_pass in password:
if self.hash_login(self.domain, usr.strip(), ntlm_hash): return True

elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name) and self.args.no_bruteforce == False:
for f_hash in ntlm_hash:
if not self.over_fail_limit(usr.strip()):
if self.hash_login(self.domain, usr.strip(), f_hash.strip()): return True
ntlm_hash.seek(0)

elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name) and self.args.no_bruteforce == True:
user.seek(0)
for usr, f_pass in zip(user, ntlm_hash):
if not self.over_fail_limit(usr.strip()):
if self.plaintext_login(self.domain, usr.strip(), f_hash.strip()): return True

elif self.args.password:
with sem:
for password in self.args.password:
if isinstance(password, str):
if not self.over_fail_limit(usr.strip()):
if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True
password.seek(0)

elif isinstance(user, str):
if hasattr(self.args, 'hash') and self.args.hash:
with sem:
for ntlm_hash in self.args.hash:
if isinstance(ntlm_hash, str):
if not self.over_fail_limit(user):
if self.hash_login(self.domain, user, ntlm_hash): return True

elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name):
for f_hash in ntlm_hash:
if self.plaintext_login(self.domain, usr.strip(), password): return True

elif not isinstance(password, str) and isfile(password.name) and self.args.no_bruteforce == False:
for f_pass in password:
if not self.over_fail_limit(usr.strip()):
if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True
password.seek(0)

elif not isinstance(password, str) and isfile(password.name) and self.args.no_bruteforce == True:
user.seek(0)
for usr, f_pass in zip(user, password):
if not self.over_fail_limit(usr.strip()):
if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True

elif isinstance(user, str):
if hasattr(self.args, 'hash') and self.args.hash:
with sem:
for ntlm_hash in self.args.hash:
if isinstance(ntlm_hash, str):
if not self.over_fail_limit(user):
if self.hash_login(self.domain, user, f_hash.strip()): return True
ntlm_hash.seek(0)

elif self.args.password:
with sem:
for password in self.args.password:
if isinstance(password, str):
if not self.over_fail_limit(user):
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 not isinstance(password, str) and isfile(password.name):
for f_pass in password:
if self.hash_login(self.domain, user, ntlm_hash): return True

elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name):
for f_hash in ntlm_hash:
if not self.over_fail_limit(user):
if self.hash_login(self.domain, user, f_hash.strip()): return True
ntlm_hash.seek(0)

elif self.args.password:
with sem:
for password in self.args.password:
if isinstance(password, str):
if not self.over_fail_limit(user):
if hasattr(self.args, 'domain'):
if self.plaintext_login(self.domain, user, f_pass.strip()): return True
if self.plaintext_login(self.domain, user, password): return True
else:
if self.plaintext_login(user, f_pass.strip()): return True
password.seek(0)
if self.plaintext_login(user, password): return True

elif not isinstance(password, str) and isfile(password.name):
for f_pass in password:
if not self.over_fail_limit(user):
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)
2 changes: 1 addition & 1 deletion cme/crackmapexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def main():
else:
with open(target, 'r') as target_file:
for target_entry in target_file:
targets.extend(parse_targets(target_entry))
targets.extend(parse_targets(target_entry.strip()))
else:
targets.extend(parse_targets(target))

Expand Down
2 changes: 1 addition & 1 deletion cme/helpers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def called_from_cmd_args():
for stack in inspect.stack():
if stack[3] == 'print_host_info':
return True
if stack[3] == 'plaintext_login' or stack[3] == 'hash_login':
if stack[3] == 'plaintext_login' or stack[3] == 'hash_login' or stack[3] == 'kerberos_login':
return True
if stack[3] == 'call_cmd_args':
return True
Expand Down
80 changes: 80 additions & 0 deletions cme/modules/bh_owned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Author:
# Romain Bentz (pixis - @hackanddo)
# Website:
# https://beta.hackndo.com [FR]
# https://en.hackndo.com [EN]

import json
import sys


class CMEModule:
name = 'bh_owned'
description = "Set pwned computer as owned in Bloodhound"
supported_protocols = ['smb']
opsec_safe = True
multiple_hosts = True

def options(self, context, module_options):
"""
URI URI for Neo4j database (default: 127.0.0.1)
PORT Listeninfg port for Neo4j database (default: 7687)
USER Username for Neo4j database (default: 'neo4j')
PASS Password for Neo4j database (default: 'neo4j')
"""

self.neo4j_URI = "127.0.0.1"
self.neo4j_Port = "7687"
self.neo4j_user = "neo4j"
self.neo4j_pass = "neo4j"

if module_options and 'URI' in module_options:
self.neo4j_URI = module_options['URI']
if module_options and 'PORT' in module_options:
self.neo4j_Port = module_options['PORT']
if module_options and 'USER' in module_options:
self.neo4j_user = module_options['USER']
if module_options and 'PASS' in module_options:
self.neo4j_pass = module_options['PASS']

def on_admin_login(self, context, connection):
try:
from neo4j.v1 import GraphDatabase
except:
from neo4j import GraphDatabase

from neo4j.exceptions import AuthError, ServiceUnavailable

if context.local_auth:
domain = connection.conn.getServerDNSDomainName()
else:
domain = connection.domain


host_fqdn = (connection.hostname + "." + domain).upper()
uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port)

try:
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
except AuthError as e:
context.log.error(
"Provided Neo4J credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass))
sys.exit()
except ServiceUnavailable as e:
context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri))
sys.exit()
except Exception as e:
context.log.error("Unexpected error with Neo4J")
context.log.debug("Error : ".format(str(e)))
sys.exit()

with driver.session() as session:
with session.begin_transaction() as tx:
result = tx.run(
"MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn))
if len(result.value()) > 0:
context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn))
else:
context.log.error(
"Node {} does not appear to be in Neo4J database. Have you imported correct data?".format(host_fqdn))
driver.close()
Loading

0 comments on commit 618ab8a

Please sign in to comment.