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

Commit 618ab8a

Browse files
author
mpgn
authored
Merge pull request #374 from byt3bl33d3r/v5-dev
Merge branch V5 dev to Master
2 parents db9166f + 9ae444a commit 618ab8a

File tree

22 files changed

+665
-413
lines changed

22 files changed

+665
-413
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ If applicable, add screenshots to help explain your problem.
2525

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

3131
**Additional context**

Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ termcolor = "*"
1717
msgpack = "*"
1818
pylnk3 = "*"
1919
paramiko = "*"
20-
pywinrm = "*"
20+
pypsrp = "*"
2121
xmltodict = "*"
2222
terminaltables = "*"
2323
impacket = "*"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Unintentional contributors:
2222

2323
This repository contains the following repositories as submodules:
2424
- [Impacket](https://github.com/CoreSecurity/impacket)
25-
- [Pywinrm](https://github.com/diyan/pywinrm)
25+
- [Pypsrp](https://github.com/jborean93/pypsrp)
2626
- [Pywerview](https://github.com/the-useless-one/pywerview)
2727
- [PowerSploit](https://github.com/PowerShellMafia/PowerSploit)
2828
- [Invoke-Obfuscation](https://github.com/danielbohannon/Invoke-Obfuscation)

cme/cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
def gen_cli_args():
88

9-
VERSION = '5.0.1dev'
9+
VERSION = '5.0.2dev'
1010
CODENAME = 'P3l1as'
1111

1212
p_loader = protocol_loader()
@@ -47,6 +47,10 @@ def gen_cli_args():
4747
std_parser.add_argument('-id', metavar="CRED_ID", nargs='+', default=[], type=str, dest='cred_id', help='database credential ID(s) to use for authentication')
4848
std_parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="username(s) or file(s) containing usernames")
4949
std_parser.add_argument("-p", metavar="PASSWORD", dest='password', nargs='+', default=[], help="password(s) or file(s) containing passwords")
50+
std_parser.add_argument("-k", "--kerberos", action='store_true', help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
51+
std_parser.add_argument("--aesKey", action='store_true', help="AES key to use for Kerberos Authentication (128 or 256 bits)")
52+
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")
53+
5054
fail_group = std_parser.add_mutually_exclusive_group()
5155
fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='max number of global failed login attempts')
5256
fail_group.add_argument("--ufail-limit", metavar='LIMIT', type=int, help='max number of failed login attempts per username')

cme/connection.py

Lines changed: 114 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,18 @@ def __init__(self, args, db, host):
2828
self.conn = None
2929
self.admin_privs = False
3030
self.logger = None
31-
self.password = None
32-
self.username = None
31+
self.password = ''
32+
self.username = ''
33+
self.kerberos = True if self.args.kerberos else False
34+
self.aesKey = None if not self.args.aesKey else self.args.aesKey
35+
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
3336
self.failed_logins = 0
3437
self.local_ip = None
3538

3639
try:
3740
self.host = gethostbyname(self.hostname)
41+
if self.args.kerberos:
42+
self.host = self.hostname
3843
except Exception as e:
3944
logging.debug('Error resolving hostname {}: {}'.format(self.hostname, e))
4045
return
@@ -60,6 +65,9 @@ def create_conn_obj(self):
6065
def check_if_admin(self):
6166
return
6267

68+
def kerberos_login(self):
69+
return
70+
6371
def plaintext_login(self, domain, username, password):
6472
return
6573

@@ -133,94 +141,113 @@ def over_fail_limit(self, username):
133141
return False
134142

135143
def login(self):
136-
for cred_id in self.args.cred_id:
137-
with sem:
138-
if cred_id.lower() == 'all':
139-
creds = self.db.get_credentials()
140-
else:
141-
creds = self.db.get_credentials(filterTerm=int(cred_id))
142-
143-
for cred in creds:
144-
logging.debug(cred)
145-
try:
146-
c_id, domain, username, password, credtype, pillaged_from = cred
147-
148-
if credtype and password:
149-
150-
if not domain: domain = self.domain
151-
152-
if self.args.local_auth:
153-
domain = self.domain
154-
elif self.args.domain:
155-
domain = self.args.domain
156-
157-
if credtype == 'hash' and not self.over_fail_limit(username):
158-
if self.hash_login(domain, username, password): return True
159-
160-
elif credtype == 'plaintext' and not self.over_fail_limit(username):
161-
if self.plaintext_login(domain, username, password): return True
162-
163-
except IndexError:
164-
self.logger.error("Invalid database credential ID!")
165-
166-
for user in self.args.username:
167-
if not isinstance(user, str) and isfile(user.name):
168-
for usr in user:
169-
if self.args.hash:
170-
with sem:
171-
for ntlm_hash in self.args.hash:
172-
if isinstance(ntlm_hash, str):
173-
if not self.over_fail_limit(usr.strip()):
174-
if self.hash_login(self.domain, usr.strip(), ntlm_hash): return True
175-
176-
elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name):
177-
for f_hash in ntlm_hash:
144+
if self.args.kerberos:
145+
if self.kerberos_login(self.aesKey, self.kdcHost): return True
146+
else:
147+
for cred_id in self.args.cred_id:
148+
with sem:
149+
if cred_id.lower() == 'all':
150+
creds = self.db.get_credentials()
151+
else:
152+
creds = self.db.get_credentials(filterTerm=int(cred_id))
153+
154+
for cred in creds:
155+
logging.debug(cred)
156+
try:
157+
c_id, domain, username, password, credtype, pillaged_from = cred
158+
159+
if credtype and password:
160+
161+
if not domain: domain = self.domain
162+
163+
if self.args.local_auth:
164+
domain = self.domain
165+
elif self.args.domain:
166+
domain = self.args.domain
167+
168+
if credtype == 'hash' and not self.over_fail_limit(username):
169+
if self.hash_login(domain, username, password): return True
170+
171+
elif credtype == 'plaintext' and not self.over_fail_limit(username):
172+
if self.plaintext_login(domain, username, password): return True
173+
174+
except IndexError:
175+
self.logger.error("Invalid database credential ID!")
176+
177+
for user in self.args.username:
178+
if not isinstance(user, str) and isfile(user.name):
179+
for usr in user:
180+
if "\\" in usr:
181+
tmp = usr
182+
usr = tmp.split('\\')[1].strip()
183+
self.domain = tmp.split('\\')[0]
184+
if self.args.hash:
185+
with sem:
186+
for ntlm_hash in self.args.hash:
187+
if isinstance(ntlm_hash, str):
178188
if not self.over_fail_limit(usr.strip()):
179-
if self.hash_login(self.domain, usr.strip(), f_hash.strip()): return True
180-
ntlm_hash.seek(0)
181-
182-
elif self.args.password:
183-
with sem:
184-
for password in self.args.password:
185-
if isinstance(password, str):
186-
if not self.over_fail_limit(usr.strip()):
187-
if self.plaintext_login(self.domain, usr.strip(), password): return True
188-
189-
elif not isinstance(password, str) and isfile(password.name):
190-
for f_pass in password:
189+
if self.hash_login(self.domain, usr.strip(), ntlm_hash): return True
190+
191+
elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name) and self.args.no_bruteforce == False:
192+
for f_hash in ntlm_hash:
193+
if not self.over_fail_limit(usr.strip()):
194+
if self.hash_login(self.domain, usr.strip(), f_hash.strip()): return True
195+
ntlm_hash.seek(0)
196+
197+
elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name) and self.args.no_bruteforce == True:
198+
user.seek(0)
199+
for usr, f_pass in zip(user, ntlm_hash):
200+
if not self.over_fail_limit(usr.strip()):
201+
if self.plaintext_login(self.domain, usr.strip(), f_hash.strip()): return True
202+
203+
elif self.args.password:
204+
with sem:
205+
for password in self.args.password:
206+
if isinstance(password, str):
191207
if not self.over_fail_limit(usr.strip()):
192-
if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True
193-
password.seek(0)
194-
195-
elif isinstance(user, str):
196-
if hasattr(self.args, 'hash') and self.args.hash:
197-
with sem:
198-
for ntlm_hash in self.args.hash:
199-
if isinstance(ntlm_hash, str):
200-
if not self.over_fail_limit(user):
201-
if self.hash_login(self.domain, user, ntlm_hash): return True
202-
203-
elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name):
204-
for f_hash in ntlm_hash:
208+
if self.plaintext_login(self.domain, usr.strip(), password): return True
209+
210+
elif not isinstance(password, str) and isfile(password.name) and self.args.no_bruteforce == False:
211+
for f_pass in password:
212+
if not self.over_fail_limit(usr.strip()):
213+
if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True
214+
password.seek(0)
215+
216+
elif not isinstance(password, str) and isfile(password.name) and self.args.no_bruteforce == True:
217+
user.seek(0)
218+
for usr, f_pass in zip(user, password):
219+
if not self.over_fail_limit(usr.strip()):
220+
if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True
221+
222+
elif isinstance(user, str):
223+
if hasattr(self.args, 'hash') and self.args.hash:
224+
with sem:
225+
for ntlm_hash in self.args.hash:
226+
if isinstance(ntlm_hash, str):
205227
if not self.over_fail_limit(user):
206-
if self.hash_login(self.domain, user, f_hash.strip()): return True
207-
ntlm_hash.seek(0)
208-
209-
elif self.args.password:
210-
with sem:
211-
for password in self.args.password:
212-
if isinstance(password, str):
213-
if not self.over_fail_limit(user):
214-
if hasattr(self.args, 'domain'):
215-
if self.plaintext_login(self.domain, user, password): return True
216-
else:
217-
if self.plaintext_login(user, password): return True
218-
219-
elif not isinstance(password, str) and isfile(password.name):
220-
for f_pass in password:
228+
if self.hash_login(self.domain, user, ntlm_hash): return True
229+
230+
elif not isinstance(ntlm_hash, str) and isfile(ntlm_hash.name):
231+
for f_hash in ntlm_hash:
232+
if not self.over_fail_limit(user):
233+
if self.hash_login(self.domain, user, f_hash.strip()): return True
234+
ntlm_hash.seek(0)
235+
236+
elif self.args.password:
237+
with sem:
238+
for password in self.args.password:
239+
if isinstance(password, str):
221240
if not self.over_fail_limit(user):
222241
if hasattr(self.args, 'domain'):
223-
if self.plaintext_login(self.domain, user, f_pass.strip()): return True
242+
if self.plaintext_login(self.domain, user, password): return True
224243
else:
225-
if self.plaintext_login(user, f_pass.strip()): return True
226-
password.seek(0)
244+
if self.plaintext_login(user, password): return True
245+
246+
elif not isinstance(password, str) and isfile(password.name):
247+
for f_pass in password:
248+
if not self.over_fail_limit(user):
249+
if hasattr(self.args, 'domain'):
250+
if self.plaintext_login(self.domain, user, f_pass.strip()): return True
251+
else:
252+
if self.plaintext_login(user, f_pass.strip()): return True
253+
password.seek(0)

cme/crackmapexec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def main():
107107
else:
108108
with open(target, 'r') as target_file:
109109
for target_entry in target_file:
110-
targets.extend(parse_targets(target_entry))
110+
targets.extend(parse_targets(target_entry.strip()))
111111
else:
112112
targets.extend(parse_targets(target))
113113

cme/helpers/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def called_from_cmd_args():
3333
for stack in inspect.stack():
3434
if stack[3] == 'print_host_info':
3535
return True
36-
if stack[3] == 'plaintext_login' or stack[3] == 'hash_login':
36+
if stack[3] == 'plaintext_login' or stack[3] == 'hash_login' or stack[3] == 'kerberos_login':
3737
return True
3838
if stack[3] == 'call_cmd_args':
3939
return True

cme/modules/bh_owned.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Author:
2+
# Romain Bentz (pixis - @hackanddo)
3+
# Website:
4+
# https://beta.hackndo.com [FR]
5+
# https://en.hackndo.com [EN]
6+
7+
import json
8+
import sys
9+
10+
11+
class CMEModule:
12+
name = 'bh_owned'
13+
description = "Set pwned computer as owned in Bloodhound"
14+
supported_protocols = ['smb']
15+
opsec_safe = True
16+
multiple_hosts = True
17+
18+
def options(self, context, module_options):
19+
"""
20+
URI URI for Neo4j database (default: 127.0.0.1)
21+
PORT Listeninfg port for Neo4j database (default: 7687)
22+
USER Username for Neo4j database (default: 'neo4j')
23+
PASS Password for Neo4j database (default: 'neo4j')
24+
"""
25+
26+
self.neo4j_URI = "127.0.0.1"
27+
self.neo4j_Port = "7687"
28+
self.neo4j_user = "neo4j"
29+
self.neo4j_pass = "neo4j"
30+
31+
if module_options and 'URI' in module_options:
32+
self.neo4j_URI = module_options['URI']
33+
if module_options and 'PORT' in module_options:
34+
self.neo4j_Port = module_options['PORT']
35+
if module_options and 'USER' in module_options:
36+
self.neo4j_user = module_options['USER']
37+
if module_options and 'PASS' in module_options:
38+
self.neo4j_pass = module_options['PASS']
39+
40+
def on_admin_login(self, context, connection):
41+
try:
42+
from neo4j.v1 import GraphDatabase
43+
except:
44+
from neo4j import GraphDatabase
45+
46+
from neo4j.exceptions import AuthError, ServiceUnavailable
47+
48+
if context.local_auth:
49+
domain = connection.conn.getServerDNSDomainName()
50+
else:
51+
domain = connection.domain
52+
53+
54+
host_fqdn = (connection.hostname + "." + domain).upper()
55+
uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port)
56+
57+
try:
58+
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
59+
except AuthError as e:
60+
context.log.error(
61+
"Provided Neo4J credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass))
62+
sys.exit()
63+
except ServiceUnavailable as e:
64+
context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri))
65+
sys.exit()
66+
except Exception as e:
67+
context.log.error("Unexpected error with Neo4J")
68+
context.log.debug("Error : ".format(str(e)))
69+
sys.exit()
70+
71+
with driver.session() as session:
72+
with session.begin_transaction() as tx:
73+
result = tx.run(
74+
"MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn))
75+
if len(result.value()) > 0:
76+
context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn))
77+
else:
78+
context.log.error(
79+
"Node {} does not appear to be in Neo4J database. Have you imported correct data?".format(host_fqdn))
80+
driver.close()

0 commit comments

Comments
 (0)