diff --git a/README.md b/README.md index 9c3a2c8..50b4702 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 ``` - diff --git a/hashcathelper/bloodhound.py b/hashcathelper/bloodhound.py index 2332669..c912e30 100644 --- a/hashcathelper/bloodhound.py +++ b/hashcathelper/bloodhound.py @@ -1,4 +1,5 @@ import logging +import hashcathelper.bloodhound_ce as bloodhound_ce from neo4j import GraphDatabase @@ -32,21 +33,28 @@ def get_driver(url): if not url: log.critical("No BloodHound URL given") exit(1) - regex = r"^bolt(?Ps?)://(?P[^:]+):(?P.+)@" + regex = r"^(?P[a-z]{4})(?Ps?)://(?P[^:]+):(?P.+)@" regex += r"(?P[^:]*)(:(?P[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 @@ -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...") @@ -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): @@ -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()) diff --git a/hashcathelper/bloodhound_ce.py b/hashcathelper/bloodhound_ce.py new file mode 100644 index 0000000..0cefc30 --- /dev/null +++ b/hashcathelper/bloodhound_ce.py @@ -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) diff --git a/hashcathelper/subcommands/bloodhound.py b/hashcathelper/subcommands/bloodhound.py index 8425d81..a8c16f3 100644 --- a/hashcathelper/subcommands/bloodhound.py +++ b/hashcathelper/subcommands/bloodhound.py @@ -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]://:@[:]""", +URL to a Neo4j database containing BloodHound data (bolt scheme) or to a BloodHound CE API (http scheme). Format: +://:@[:]""", ) ) diff --git a/pyproject.toml b/pyproject.toml index c8a14b6..7636a42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ 'neo4j>=4.2, <5.20', 'importlib-metadata', 'tqdm', + 'requests' ] [tool.setuptools]