Skip to content

Commit 2446c50

Browse files
committed
v0.2.1
added bloodhound support
1 parent da9a820 commit 2446c50

File tree

8 files changed

+138
-19
lines changed

8 files changed

+138
-19
lines changed

README.md

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11

2-
## about revealhashed-python v0.1.4
2+
3+
## about revealhashed-python v0.2.1
34
revealhashed is a streamlined utility to correlate ntds usernames, nt hashes, and cracked passwords in one view while cutting out time-consuming manual tasks.
45

56
## dependencies
67
hashcat
78
impacket or python3-impacket
9+
neo4j
810

911
## how to install
1012
from pypi:
@@ -14,17 +16,17 @@ from github:
1416
`pipx install git+https://github.com/crosscutsaw/revealhashed-python`
1517

1618
from deb package:
17-
`wget https://github.com/crosscutsaw/revealhashed-python/releases/latest/download/revealhashed_0.1.4_all.deb; apt install ./revealhashed_0.1.4_all.deb`
19+
`wget https://github.com/crosscutsaw/revealhashed-python/releases/latest/download/revealhashed_0.2.1_all.deb; apt install ./revealhashed_0.2.1_all.deb`
1820

1921
from whl package:
20-
`wget https://github.com/crosscutsaw/revealhashed-python/releases/latest/download/revealhashed-0.1.4-py3-none-any.whl; pipx install revealhashed-0.1.4-py3-none-any.whl`
22+
`wget https://github.com/crosscutsaw/revealhashed-python/releases/latest/download/revealhashed-0.2.1-py3-none-any.whl; pipx install revealhashed-0.2.1-py3-none-any.whl`
2123

2224
## don't want to install?
2325
grab revealhashed binary from [here](https://github.com/crosscutsaw/revealhashed-python/releases/latest/download/revealhashed).
2426

2527
## how to use
2628
```
27-
revealhashed v0.1.4
29+
revealhashed v0.2.1
2830
2931
usage: revealhashed [-h] [-r] {dump,reveal} ...
3032
@@ -42,26 +44,50 @@ just execute `revealhashed -r` to remove contents of ~/.revealhashed
4244

4345
### revealhashed dump
4446
```
45-
revealhashed v0.1.4
47+
revealhashed v0.2.1
48+
49+
usage: revealhashed dump [-h] [-debug] [-hashes HASHES] [-no-pass] [-k] [-aesKey AESKEY] [-dc-ip DC_IP] [-codec CODEC] -w WORDLIST WORDLIST2 [WORDLIST WORDLIST2 ...] [-e] [-nd] [-csv] [-bh] [--dburi DBURI] [--dbuser DBUSER] [--dbpassword DBPASSWORD] target
50+
51+
positional arguments:
52+
target Target for NTDS dumping (e.g. domain/user:pass@host)
4653
47-
usage: revealhashed dump [-h] [-debug] [-hashes HASHES] [-no-pass] [-k] [-aesKey AESKEY] [-dc-ip DC_IP] [-codec CODEC] -w WORDLIST WORDLIST2 [WORDLIST WORDLIST2 ...] [-e] [-nd] [-csv] target
54+
options:
55+
-h, --help show this help message and exit
56+
-debug
57+
-hashes HASHES
58+
-no-pass
59+
-k
60+
-aesKey AESKEY
61+
-dc-ip DC_IP
62+
-codec CODEC
63+
-w WORDLIST WORDLIST2 [WORDLIST WORDLIST2 ...], --wordlists WORDLIST WORDLIST2 [WORDLIST WORDLIST2 ...]
64+
Wordlists to use with hashcat
65+
-e, --enabled-only Only show enabled accounts
66+
-nd, --no-domain Don't display domain in usernames
67+
-csv Save output in CSV format
68+
-bh Mark cracked users as owned in BloodHound
69+
--dburi DBURI BloodHound Neo4j URI
70+
--dbuser DBUSER BloodHound Neo4j username
71+
--dbpassword DBPASSWORD
72+
BloodHound Neo4j password
4873
```
4974

5075
this command executes [zblurx's ntdsutil.py](https://github.com/zblurx/ntdsutil.py) to dump ntds safely then does classic revealhashed operations.
5176

5277
-w (wordlist) switch is needed. one or more wordlists can be supplied.
53-
-e (enabled-only) switch is suggested. it's self explanatory; only shows enabled users.
78+
-e (enabled-only) switch is suggested. it's only shows enabled users.
5479
-nd (no-domain) switch hides domain names in usernames.
55-
-csv (csv) switch is self explanatory; saves output to csv, together with txt.
80+
-bh (bloodhound) switch marks cracked users as owned in bloodhound. if used, `--dburi`, `--dbuser` and `--dbpassword` are also needed to connect neo4j database. it supports both legacy and ce.
81+
-csv (csv) switch saves output to csv, together with txt.
5682

5783
for example:
58-
`revealhashed dump '<domain>/<username>:<password>'@<dc_ip> -w wordlist1.txt wordlist2.txt -e -nd -csv`
84+
`revealhashed dump '<domain>/<username>:<password>'@<dc_ip> -w wordlist1.txt wordlist2.txt -e -nd -csv -bh --dburi bolt://localhost:7687 --dbuser neo4j --dbpassword 1234`
5985

6086
### revealhashed reveal
6187
```
62-
revealhashed v0.1.4
88+
revealhashed v0.2.1
6389
64-
usage: revealhashed reveal [-h] [-ntds NTDS] [-nxc] [-w WORDLIST WORDLIST2 [WORDLIST WORDLIST2 ...]] [-e] [-nd] [-csv]
90+
usage: revealhashed reveal [-h] [-ntds NTDS] [-nxc] [-w WORDLIST WORDLIST2 [WORDLIST WORDLIST2 ...]] [-e] [-nd] [-csv] [-bh] [--dburi DBURI] [--dbuser DBUSER] [--dbpassword DBPASSWORD]
6591
6692
options:
6793
-h, --help show this help message and exit
@@ -72,6 +98,11 @@ options:
7298
-e, --enabled-only Only show enabled accounts
7399
-nd, --no-domain Don't display domain in usernames
74100
-csv Save output in CSV format
101+
-bh Mark cracked users as owned in BloodHound
102+
--dburi DBURI BloodHound Neo4j URI
103+
--dbuser DBUSER BloodHound Neo4j username
104+
--dbpassword DBPASSWORD
105+
BloodHound Neo4j password
75106
```
76107

77108
this command wants to get supplied with ntds file by user or netexec then does classic revealhashed operations.
@@ -80,9 +111,10 @@ this command wants to get supplied with ntds file by user or netexec then does c
80111

81112
-ntds or -nxc switch is needed. -ntds switch is for a file you own with hashes. -nxc switch is for scanning ~/.nxc/logs/ntds directory then selecting .ntds file.
82113
-w (wordlist) switch is needed. one or more wordlists can be supplied.
83-
-e (enabled-only) switch is suggested. it's self explanatory; only shows enabled users.
114+
-e (enabled-only) switch is suggested. it's only shows enabled users.
84115
-nd (no-domain) switch hides domain names in usernames.
85-
-csv (csv) switch is self explanatory; saves output to csv, together with txt.
116+
-bh (bloodhound) switch marks cracked users as owned in bloodhound. if used, `--dburi`, `--dbuser` and `--dbpassword` are also needed to connect neo4j database. it supports both legacy and ce.
117+
-csv (csv) switch saves output to csv, together with txt.
86118

87119
for example:
88120
`revealhashed reveal -ntds <ntds_file>.ntds -w wordlist1.txt -e -nd -csv`
@@ -96,3 +128,5 @@ for example:
96128
![](https://raw.githubusercontent.com/crosscutsaw/revealhashed-python/main/rp3.PNG)
97129

98130
![](https://raw.githubusercontent.com/crosscutsaw/revealhashed-python/main/rp4.PNG)
131+
132+
![](https://raw.githubusercontent.com/crosscutsaw/revealhashed-python/main/rp5.PNG)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[project]
22
name = "revealhashed"
3-
version = "0.1.4"
3+
version = "0.2.1"
44
description = "Dump or analyze existing NTDS data, crack NT hashes with hashcat and match them to their corresponding user accounts."
55
authors = [{ name = "aslan emre aslan", email = "emre@zurrak.com" }]
66
license = { text = "MIT" }
77
readme = "README.md"
88
requires-python = ">=3.7"
9-
dependencies = ["impacket"]
9+
dependencies = ["impacket","neo4j"]
1010

1111
[project.scripts]
1212
revealhashed = "revealhashed.__main__:main"

revealhashed/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.0"
1+
__version__ = "0.2.1"

revealhashed/core.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
# ntds class
2323
from impacket.examples.secretsdump import NTDSHashes, LocalOperations
2424

25+
# neo4j
26+
from neo4j import Auth, GraphDatabase
27+
from neo4j.exceptions import ServiceUnavailable
28+
2529
# constants
2630
HOME = Path.home()
2731
TMP_DIR = HOME / ".revealhashed"
@@ -60,6 +64,10 @@ def parse_args():
6064
dump_parser.add_argument("-e", "--enabled-only", action="store_true", help="Only show enabled accounts")
6165
dump_parser.add_argument('-nd', '--no-domain', action='store_true', help="Don't display domain in usernames")
6266
dump_parser.add_argument('-csv', action='store_true', help="Save output in CSV format")
67+
dump_parser.add_argument('-bh', action='store_true', help="Mark cracked users as owned in BloodHound")
68+
dump_parser.add_argument('--dburi', help='BloodHound Neo4j URI')
69+
dump_parser.add_argument('--dbuser', help='BloodHound Neo4j username')
70+
dump_parser.add_argument('--dbpassword', help='BloodHound Neo4j password')
6371
# do not include -outputdir
6472

6573
# subparser: reveal
@@ -70,7 +78,10 @@ def parse_args():
7078
reveal_parser.add_argument("-e", "--enabled-only", action="store_true", help="Only show enabled accounts")
7179
reveal_parser.add_argument('-nd', '--no-domain', action='store_true', help="Don't display domain in usernames")
7280
reveal_parser.add_argument('-csv', action='store_true', help="Save output in CSV format")
73-
81+
reveal_parser.add_argument('-bh', action='store_true', help="Mark cracked users as owned in BloodHound")
82+
reveal_parser.add_argument('--dburi', help='BloodHound Neo4j URI')
83+
reveal_parser.add_argument('--dbuser', help='BloodHound Neo4j username')
84+
reveal_parser.add_argument('--dbpassword', help='BloodHound Neo4j password')
7485
return parser
7586

7687
def reset_tmp_dir():
@@ -87,6 +98,76 @@ def create_session_dir():
8798
session_path.mkdir(parents=True, exist_ok=True)
8899
return session_path
89100

101+
def mark_bloodhound_owned(txt_file_path, dburi, dbuser, dbpassword):
102+
try:
103+
driver = GraphDatabase.driver(dburi, auth=Auth(scheme="basic", principal=dbuser, credentials=dbpassword))
104+
driver.verify_connectivity()
105+
print(f"\n{BOLD_GREEN}[+]{RESET} Connected to BloodHound Neo4j database at {dburi} as {dbuser}\n")
106+
107+
# infer domain from first valid user line
108+
inferred_domain = None
109+
with open(txt_file_path) as f:
110+
for line in f:
111+
if "<no password>" in line:
112+
continue
113+
parts = line.strip().split()
114+
if not parts:
115+
continue
116+
user_field = parts[0]
117+
if "\\" in user_field:
118+
inferred_domain = user_field.split("\\", 1)[0].upper()
119+
break
120+
121+
with driver.session() as session:
122+
with open(txt_file_path) as f:
123+
for line in f:
124+
if "<no password>" in line:
125+
continue
126+
parts = line.strip().split()
127+
if not parts:
128+
continue
129+
130+
user_field = parts[0]
131+
if "\\" in user_field:
132+
domain, user = user_field.split("\\", 1)
133+
else:
134+
user = user_field
135+
domain = inferred_domain or ""
136+
137+
is_computer = user.endswith("$")
138+
if is_computer:
139+
user = user.rstrip("$")
140+
full_name = f"{user}.{domain}".upper()
141+
label = "Computer"
142+
else:
143+
full_name = f"{user}@{domain}".upper()
144+
label = "User"
145+
146+
# try marking as owned
147+
query = f"MATCH (c:{label} {{name:'{full_name}'}}) RETURN c.owned AS owned"
148+
result = session.run(query).data()
149+
150+
if not result:
151+
print(f"{BOLD_RED}[-]{RESET} Node {full_name} not found in BloodHound")
152+
continue
153+
154+
if result[0]["owned"] is True:
155+
print(f"{BOLD_GREEN}[+]{RESET} {full_name} already marked as owned")
156+
continue
157+
158+
update_query = f"MATCH (c:{label} {{name:'{full_name}'}}) SET c.owned=true RETURN c.name AS name"
159+
update_result = session.run(update_query).data()
160+
161+
if update_result:
162+
print(f"{BOLD_GREEN}[+]{RESET} Marked {full_name} as owned in BloodHound")
163+
else:
164+
print(f"{BOLD_RED}[-]{RESET} Failed to mark {full_name} as owned in BloodHound")
165+
166+
except ServiceUnavailable:
167+
print(f"{BOLD_RED}[-]{RESET} BloodHound DB unreachable at {dburi}")
168+
except Exception as e:
169+
print(f"{BOLD_RED}[-]{RESET} Error while marking BloodHound: {e}")
170+
90171
def extract_unique_hashes(ntds_path, output_path, full_output_path, write_full_output=True):
91172
print(f"{BOLD_GREEN}[+]{RESET} Extracting unique NT hashes from: {ntds_path}")
92173
seen_hashes = set()
@@ -199,7 +280,7 @@ def reveal_credentials(individual_ntds_path, cracked_hashes, session_dir, enable
199280
print(f"{BOLD_GREEN}[+]{RESET} Output saved to {output_file_csv}")
200281

201282
def main():
202-
print(f"\n{BOLD_BLUE}revealhashed v0.1.4{RESET}\n")
283+
print(f"\n{BOLD_BLUE}revealhashed v0.2.1{RESET}\n")
203284

204285
parser = parse_args()
205286
args = parser.parse_args()
@@ -289,6 +370,8 @@ def main():
289370
shutil.copy(HASHCAT_POT, session_dir)
290371
cracked = parse_potfile(HASHCAT_POT)
291372
reveal_credentials(ind_path, cracked, session_dir, enabled_only=args.enabled_only, no_domain=args.no_domain, to_csv=args.csv)
373+
if getattr(args, "bh", False):
374+
mark_bloodhound_owned(session_dir / "revealhashed.txt", args.dburi, args.dbuser, args.dbpassword)
292375

293376
elif args.command == "reveal":
294377
if args.nxc:
@@ -340,11 +423,13 @@ def main():
340423
shutil.copy(HASHCAT_POT, session_dir)
341424
cracked = parse_potfile(HASHCAT_POT)
342425
reveal_credentials(ind_path, cracked, session_dir, enabled_only=args.enabled_only, no_domain=args.no_domain, to_csv=args.csv)
426+
if getattr(args, "bh", False):
427+
mark_bloodhound_owned(session_dir / "revealhashed.txt", args.dburi, args.dbuser, args.dbpassword)
343428

344429
if __name__ == "__main__":
345430
main()
346431

347-
# revealhashed v0.1.4
432+
# revealhashed v0.2.1
348433
#
349434
# contact options
350435
# mail: https://blog.zurrak.com/contact.html

rp1.PNG

90.3 KB
Loading

rp2.PNG

-3.47 KB
Loading

rp3.PNG

-1.23 KB
Loading

rp5.PNG

84.1 KB
Loading

0 commit comments

Comments
 (0)