Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ This subcommand lets you insert new relationships into an existing
[BloodHound](https://github.com/BloodHoundAD/BloodHound) database. It takes
a BloodHound URI, a report in JSON format (with degree of detail equal to
three or higher) and the domain name and creates edges between user objects
that share the same password. This enables you to create graphs like this,
that share the same password. Both neo4j `bolt://` as well as Bloodhound CE `http://` API URIs are supported. This enables you to create graphs like this,
which immediately shows you offenders of password reuse among the
administrator team:

Expand Down Expand Up @@ -249,4 +249,3 @@ db_uri = sqlite:////home/cracker/.local/share/hashcathelper/stats.sqlite
# Download here: https://haveibeenpwned.com/Passwords
hibp_db = /home/cracker/wordlists/pwned-passwords-ntlm-ordered-by-hash-v8.txt
```

85 changes: 62 additions & 23 deletions hashcathelper/bloodhound.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import hashcathelper.bloodhound_ce as bloodhound_ce

from neo4j import GraphDatabase

Expand Down Expand Up @@ -32,21 +33,28 @@ def get_driver(url):
if not url:
log.critical("No BloodHound URL given")
exit(1)
regex = r"^bolt(?P<encrypted>s?)://(?P<user>[^:]+):(?P<password>.+)@"
regex = r"^(?P<protocol>[a-z]{4})(?P<encrypted>s?)://(?P<user>[^:]+):(?P<password>.+)@"
regex += r"(?P<host>[^:]*)(:(?P<port>[0-9]+))?$"
m = re.match(regex, url)
if not m:
log.error("Couldn't parse BloodHound URL: %s" % url)
exit(1)

encrypted, user, password, host, _, port = m.groups()
protocol, encrypted, user, password, host, _, port = m.groups()
encrypted = encrypted == "s"

url = "bolt://%s:%s" % (host, port or 7687)
url = "%s://%s:%s" % (protocol, host, port or 7687)

log.debug("Connecting to %s..." % url)
driver = GraphDatabase.driver(url, auth=(user, password), encrypted=encrypted)

if protocol == "bolt":
# BOLT protocol for old Bloodhound
driver = GraphDatabase.driver(url, auth=(user, password), encrypted=encrypted)
elif protocol == "http":
# HTTP protocol for Bloodhound CE API
driver = bloodhound_ce.driver(url, auth=(user, password))
else:
log.error("Unknown Protocol: %s" % protocol)
exit(1)
return driver


Expand All @@ -57,6 +65,10 @@ def query_neo4j(driver, cypher_query, domain=None):
"""
from hashcathelper.utils import User

if (type(driver) == bloodhound_ce.driver):
log.error("Direct queries to Bloodhound CE are currently not supported.")
exit(1)

log.debug("Given Cypher query: %s" % cypher_query)
log.info("Querying BloodHound database...")

Expand Down Expand Up @@ -100,16 +112,29 @@ def add_edges(driver, clusters):


def add_many_edges(tx, edges):
q = """
UNWIND $edges as edge
MATCH (a:User), (b:User)
WHERE a.name = toUpper(edge.a)
AND b.name = toUpper(edge.b)
CREATE (a)-[r:SamePassword]->(b), (b)-[k:SamePassword]->(a)
RETURN r
"""
result = tx.run(q, edges=edges)
return len(result.value())
# Is it the new Bloodhound CE?
is_bloodhound_ce = (type(tx) == bloodhound_ce.Sender)
if is_bloodhound_ce:
q = """
MATCH (a:User), (b:User)
WHERE a.name = toUpper(\"{a}\")
AND b.name = toUpper(\"{b}\")
CREATE (a)-[r:SamePassword]->(b), (b)-[k:SamePassword]->(a)
RETURN r
"""
result = tx.run(q, edges=edges)
return result
else:
q = """
UNWIND $edges as edge
MATCH (a:User), (b:User)
WHERE a.name = toUpper(edge.a)
AND b.name = toUpper(edge.b)
CREATE (a)-[r:SamePassword]->(b), (b)-[k:SamePassword]->(a)
RETURN r
"""
result = tx.run(q, edges=edges)
return len(result.value())


def mark_cracked(driver, users):
Expand All @@ -122,11 +147,25 @@ def mark_cracked(driver, users):


def mark_cracked_tx(tx, users):
q = """
UNWIND $users as user
MATCH (u:User {name: user})
SET u.cracked = True
RETURN u
"""
result = tx.run(q, users=users)
return len(result.value())
# Is it the new Bloodhound CE?
is_bloodhound_ce = (type(tx) == bloodhound_ce.Sender)
if is_bloodhound_ce:
# New Bloodhound CE query
q = """
MATCH (u)
WHERE toLower(u.name) = toLower(\"{user}\")
SET u.cracked = True
RETURN u
"""
result = tx.run(q, users=users)
return result
else:
# Old neo4j Query
q = """
UNWIND $users as user
MATCH (u:User {name: user})
SET u.cracked = True
RETURN u
"""
result = tx.run(q, users=users)
return len(result.value())
101 changes: 101 additions & 0 deletions hashcathelper/bloodhound_ce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import requests, json, asyncio, logging

log = logging.getLogger(__name__)

class Sender:
sessionToken = None
url = None

def __init__(self, url, sessionToken):
self.sessionToken = sessionToken
self.url = url

# Method to execute a cypher query for all users or edges
def run(self, query, users="", edges=""):
counter = 0
for user in users:
data = {
"query": query.format(user=user),
"include_properties": True
}

if self.sendRequest(data) == 200:
counter += 1
for edge in edges:
data = {
"query": query.format(**edge),
"include_properties": True
}

if self.sendRequest(data) == 200:
counter += 1
return counter

# Method to send a cypher query to the Bloodhound CE API endpoint
def sendRequest(self, data):
try:
return requests.post(url = self.url + "/api/v2/graphs/cypher", json = data, headers={'Authorization': 'Bearer ' + self.sessionToken}).status_code
except:
log.error("Cannot connect to %s" % self.url)
exit(1)

# The class Session is responsible for obtaining an API session token and for returnung a Sender object similar to the neo4j Session
class Session:
driver = None
sessionToken = None

def __init__(self, driver):
self.driver = driver
self.getAuthToken()

def __enter__(self):
return self

def __exit__(self, exception_type, exception_value, traceback):
if exception_type:
if issubclass(exception_type, asyncio.CancelledError):
self._handle_cancellation(message="__exit__")
self._closed = True
return
self._state_failed = True

def __with__(self):
with super() as s:
yield s

# Obtain an API auth token using the username/password combination provided in the URI
def getAuthToken(self):
data = {
'login_method':'secret',
'username':self.driver.username,
'secret':self.driver.password
}
try:
response = requests.post(url = self.driver.url + "/api/v2/login", json = data)
except:
log.error("Cannot connect to %s" % self.driver.url)
exit(1)
self.sessionToken = response.json()['data']['session_token']

def write_transaction(
self,
transaction_function,
data):
return transaction_function(self.sendRequest(), data)

def sendRequest(self):
return Sender(self.driver.url, self.sessionToken)

# Class driver just exists for compatibility reasons with the old neo4j driver
class driver:
url = None
username = None
password = None

def __init__(self, url, auth, encrypted=False):
self.url = url
self.username = auth[0]
self.password = auth[1]

def session(self):
return Session(self)
4 changes: 2 additions & 2 deletions hashcathelper/subcommands/bloodhound.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def domain_filepath_pair(arg):
argument(
dest="bloodhound_url",
help="""
URL to a Neo4j database containing BloodHound data. Format:
bolt[s]://<user>:<password>@<host>[:<port>]""",
URL to a Neo4j database containing BloodHound data (bolt scheme) or to a BloodHound CE API (http scheme). Format:
<scheme>://<user>:<password>@<host>[:<port>]""",
)
)

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
'neo4j>=4.2, <5.20',
'importlib-metadata',
'tqdm',
'requests'
]

[tool.setuptools]
Expand Down