Skip to content

Latest commit

 

History

History
292 lines (245 loc) · 15.5 KB

README.md

File metadata and controls

292 lines (245 loc) · 15.5 KB

BloodHound Owned

A collection of files for adding and leveraging custom properties in BloodHound. A thorough overview of the ideas that led to these Custom Queries & Ruby script can be found in this blog post: http://porterhau5.com/blog/extending-bloodhound-track-and-visualize-your-compromise/

These are intended, although not required, to be used with a forked version of BloodHound found here: https://github.com/porterhau5/BloodHound

Files in the example-files directory can be used with the Ruby script and the BloodHoundExampleDB.graphdb for demonstration or development purposes. Usage examples are shown below.

Quickstart

Using these requires Neo4j, a populated database, and a BloodHound app.

The Ruby script (bh-owned.rb) and custom queries (customqueries.json) can be used with the official BloodHound app. However, the UI customizations are currently only available in the customized app found in this forked BloodHound repo.

Current UI customizations include:

  • Node highlighting
  • Custom properties displayed on Node Info tab
  • Run custom queries from Node Info tab
  • Adding or removing nodes from blacklist via tooltip
  • Marking or unmarking nodes as owned via tooltip

If you'd like to try out the features added in the customized BloodHound app, then either download a pre-compiled binary here or build the app from source. If building from source, then follow the official BloodHound install directions but substitute the forked repo URL (https://github.com/porterhau5/BloodHound) for the official repo URL in step 2 (cloning the repository). The building instructions are on BloodHound's wiki.

To use the custom queries, first copy the customqueries.json file to the Electron project's home folder:

  • Windows: ~\AppData\Roaming\bloodhound\
  • Mac: ~/Library/Application Support/bloodhound/
  • Linux: I'm not sure. If someone does this on Linux and figures this out then let me know.

Refresh or restart BloodHound for the changes to take effect. Custom Queries can be found in the Search Container (top-left) on the Queries tab underneath the Custom Queries header.

bh-owned.rb Usage

This script is the primary means for updating the Neo4j database to support the custom queries and UI enhancements.

$ ruby bh-owned.rb
Usage: ruby bh-owned.rb [options]
Server Details:
    -u, --username <username>        Neo4j database username (default: 'neo4j')
    -p, --password <password>        Neo4j database password (default: 'BloodHound')
    -U, --url <url>                  URL of Neo4j RESTful host  (default: 'http://127.0.0.1:7474/')
Owned/Wave/SPW:
    -a, --add <file>                 add 'owned' and 'wave' property to nodes in <file>
    -A, --add-no-wave <file>         add 'owned' property to nodes in <file> (skip 'wave' property)
    -w, --wave <num>                 value to set 'wave' property (override default behavior)
    -s, --spw <file>                 add 'SharesPasswordWith' relationship between all nodes in <file>
Blacklisting:
    -b, --bl-node <file>             add 'blacklist' property to nodes in <file>
    -B, --bl-rel <file>              add 'blacklist' property to relationships in <file>
    -r, --remove-bl-node <file>      remove 'blacklist' property from nodes in <file>
    -R, --remove-bl-rel <file>       remove 'blacklist' property from relationships in <file>
Connections:
    -c, --connections <file>         add connection info from netstat <file>
    -d, --dns <file>                 contains DNS mapping of IP to computer name (10.2.3.4,srv1.int.local)
Misc Queries:
    -n, --nodes                      get all node names
    -e, --examples                   reference doc of custom Cypher queries for BloodHound
        --reset                      remove all custom properties and SharesPasswordWith relationships

It helps to create a few new indexes to help with query performance. This can be done using Neo4j's web browser or BloodHound's Raw Query feature (I recommend Neo4j's web browser for this):

CREATE INDEX ON :Group(wave)
CREATE INDEX ON :User(wave)
CREATE INDEX ON :Computer(wave)
CREATE INDEX ON :Group(blacklist)
CREATE INDEX ON :User(blacklist)
CREATE INDEX ON :Computer(blacklist)
Owned/Wave

Data is ingested using the script's -a flag with a file passed as an argument. Files should be in CSV format with the name of the compromised node first, followed by the method of compromise. If no method of compromise is provided then it will default to "Not specified" (these files can be found in the example-files dir):

$ cat 1st-wave.txt
BLOPER@INTERNAL.LOCAL,LLMNR wpad
JCARNEAL@INTERNAL.LOCAL,NBNS wpad

$ ruby bh-owned.rb -a 1st-wave.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[*] No previously owned nodes found, setting wave to 1
[+] Success, marked 'BLOPER@INTERNAL.LOCAL' as owned in wave '1' via 'LLMNR wpad'
[+] Success, marked 'JCARNEAL@INTERNAL.LOCAL' as owned in wave '1' via 'NBNS wpad'
[*] Finding spread of compromise for wave 1
[+] 2 nodes found:
DOMAIN USERS@INTERNAL.LOCAL
SYSTEM38.INTERNAL.LOCAL

The script will first query the database and determine the latest wave added. It then increments it by one so that the incoming additions will be in the new wave. You can override this behavior by setting the -w flag to the preferred wave value.

Once the wave number is determined, the script takes the following steps:

  • Creates the Cypher queries to add the nodes
  • Creates the Cypher query to find the spread of compromise for the new wave
  • Wraps it all in JSON
  • POSTs the request to the REST endpoint

Until more options and error-checking is thrown in, each wave should be added separately:

$ cat 2nd-wave.txt
ZDEVENS@INTERNAL.LOCAL,Password spray
BPICKEREL@INTERNAL.LOCAL,Password spray

$ ruby bh-owned.rb -a 2nd-wave.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Success, marked 'ZDEVENS@INTERNAL.LOCAL' as owned in wave '2' via 'Password spray'
[+] Success, marked 'BPICKEREL@INTERNAL.LOCAL' as owned in wave '2' via 'Password spray'
[*] Finding spread of compromise for wave 2
[+] 5 nodes found:
BACKUP3@INTERNAL.LOCAL
BACKUP_SVC@INTERNAL.LOCAL
CONTRACTINGS@INTERNAL.LOCAL
DATABASE5.INTERNAL.LOCAL
MANAGEMENT3.INTERNAL.LOCAL

The -A flag will skip the wave process entirely. This is useful if you just want to mark nodes as owned and move on:

$ cat example-files/owned-no-wave.txt 
AMURPHY@INTERNAL.LOCAL,Social engineered
OPIERCE@INTERNAL.LOCAL
TROBINSON@INTERNAL.LOCAL,Phish
$ ruby bh-owned.rb -A example-files/owned-no-wave.txt 
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Success, marked 'AMURPHY@INTERNAL.LOCAL' as owned via 'Social engineered'
[+] Success, marked 'OPIERCE@INTERNAL.LOCAL' as owned via 'Not specified'
[+] Success, marked 'TROBINSON@INTERNAL.LOCAL' as owned via 'Phish'
SharesPasswordWith

Use -s to add a file containing a list of nodes with the same password. This will create a new relationship, "SharesPasswordWith", between each node in the list. Useful for representing Computers with a common local admin password, or Users that use the same password for multiple accounts:

$ cat common-local-admins.txt
MANAGEMENT3.INTERNAL.LOCAL
FILESERVER6.INTERNAL.LOCAL
SYSTEM38.INTERNAL.LOCAL
DESKTOP40.EXTERNAL.LOCAL

$ ruby bh-owned.rb -s common-local-admins.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Created SharesPasswordWith relationship between 'MANAGEMENT3.INTERNAL.LOCAL' and 'FILESERVER6.INTERNAL.LOCAL'
[+] Created SharesPasswordWith relationship between 'MANAGEMENT3.INTERNAL.LOCAL' and 'SYSTEM38.INTERNAL.LOCAL'
[+] Created SharesPasswordWith relationship between 'MANAGEMENT3.INTERNAL.LOCAL' and 'DESKTOP40.EXTERNAL.LOCAL'
[+] Created SharesPasswordWith relationship between 'FILESERVER6.INTERNAL.LOCAL' and 'SYSTEM38.INTERNAL.LOCAL'
[+] Created SharesPasswordWith relationship between 'FILESERVER6.INTERNAL.LOCAL' and 'DESKTOP40.EXTERNAL.LOCAL'
[+] Created SharesPasswordWith relationship between 'SYSTEM38.INTERNAL.LOCAL' and 'DESKTOP40.EXTERNAL.LOCAL'

Blacklisting

Specific nodes or relationships can be added to the blacklist. For adding nodes, put the name of each node in a newline-delimited file and add it using the -b flag. Note that blacklist features are only compatible with the custom queries:

$ cat blacklist-nodes.txt
MANAGEMENT3.INTERNAL.LOCAL
BGRIFFIN@EXTERNAL.LOCAL
BPICKEREL@INTERNAL.LOCAL
DESKTOP20.EXTERNAL.LOCAL

ruby bh-owned.rb -b blacklist-nodes.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Success, marked 'MANAGEMENT3.INTERNAL.LOCAL' as blacklisted
[+] Success, marked 'BGRIFFIN@EXTERNAL.LOCAL' as blacklisted
[+] Success, marked 'BPICKEREL@INTERNAL.LOCAL' as blacklisted
[+] Success, marked 'DESKTOP20.EXTERNAL.LOCAL' as blacklisted

Relationships are added to the blacklist by specifying the start node and end node for the desired relationship. Each relationship should be in CSV format with the start node first, relationship second, and end node last, with one path per line. They are added to the blacklist using the -B flag:

$ cat blacklist-rels.txt
ZDEVENS@INTERNAL.LOCAL,AdminTo,MANAGEMENT3.INTERNAL.LOCAL
DESKTOP21.EXTERNAL.LOCAL,HasSession,JANTHONY@EXTERNAL.LOCAL

$ ruby bh-owned.rb -B blacklist-rels.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Success, marked 'AdminTo' as blacklisted from 'ZDEVENS@INTERNAL.LOCAL' to 'MANAGEMENT3.INTERNAL.LOCAL'
[+] Success, marked 'HasSession' as blacklisted from 'DESKTOP21.EXTERNAL.LOCAL' to 'JANTHONY@EXTERNAL.LOCAL'

Nodes are removed from the blacklist in the same manner they are added, but use the -r flag. Use -R to remove relationships from the blacklist:

$ ruby bh-owned.rb -r blacklist-nodes.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[*] Removing blacklist property from 'MANAGEMENT3.INTERNAL.LOCAL'
[*] Removing blacklist property from 'BGRIFFIN@EXTERNAL.LOCAL'
[*] Removing blacklist property from 'BPICKEREL@INTERNAL.LOCAL'
[*] Removing blacklist property from 'DESKTOP20.EXTERNAL.LOCAL'

ruby bh-owned.rb -R blacklist-rels.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[*] Removing blacklist property from relationship 'AdminTo' between 'ZDEVENS@INTERNAL.LOCAL' and 'MANAGEMENT3.INTERNAL.LOCAL'
[*] Removing blacklist property from relationship 'HasSession' between 'DESKTOP21.EXTERNAL.LOCAL' and 'JANTHONY@EXTERNAL.LOCAL'
Miscellaneous options

The -n flag can be used to dump the names of all nodes from the database:

$ ruby bh-owned.rb -n
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
AANSTETT@EXTERNAL.LOCAL
ABRENES@INTERNAL.LOCAL
ABROOKS@EXTERNAL.LOCAL
ABROOKS_A@EXTERNAL.LOCAL
ACASTERLINE@INTERNAL.LOCAL
ACHAVARIN@EXTERNAL.LOCAL
ACLAUSS@INTERNAL.LOCAL
<snipped>

The -e flag can be used to show examples of Cypher queries leveraging the custom properties. The juicy ones have been rolled up into the 'Custom Queries' available in the app:

$ ruby bh-owned.rb -e
Find all owned Domain Admins:
MATCH (n:Group) WHERE n.name =~ '.*DOMAIN ADMINS.*' WITH n MATCH p=(n)<-[r:MemberOf*1..]-(m) WHERE exists(m.owned) RETURN nodes(p),relationships(p)

Find Shortest Path from owned node to Domain Admins:
MATCH p=shortestPath((n)-[*1..]->(m)) WHERE exists(n.owned) AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p

List all directly owned nodes:
MATCH (n) WHERE exists(n.owned) RETURN n

Find all nodes in wave $num:
MATCH (n)-[r]->(m) WHERE n.wave=$num AND m.wave=$num RETURN n,r,m

Show all waves up to and including wave $num:
MATCH (n)-[r]->(m) WHERE n.wave<=$num RETURN n,r,m

Set owned and wave properties for a node (named $name, compromised via $method in wave $num):
MATCH (n) WHERE (n.name = '$name') SET n.owned = '$method', n.wave = $num

Find spread of compromise for owned nodes in wave $num:
OPTIONAL MATCH (n1:User {wave:$num}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:$num}) WITH collect(distinct n2) + c1 as c2 UNWIND c2 as n OPTIONAL MATCH p=shortestPath((n)-[*..20]->(m)) WHERE not(exists(m.wave)) WITH DISTINCT(m) SET m.wave=$num

Show clusters of password reuse:
MATCH p=(n)-[r:SharesPasswordWith]-(m) RETURN p

Show blacklisted nodes:
MATCH (n) WHERE exists(n.blacklist) RETURN n

Show blacklisted relationships:
MATCH (n)-[r]->(m) WHERE exists(r.blacklist) RETURN n,r,m

If you want to start over and remove any custom properties & relationships from your database nodes, use --reset:

$ ruby bh-owned.rb --reset
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[*] Removing all custom properties and SharesPasswordWith relationships

Custom Queries

There are the current custom queries in this set:

  • Find all owned Domain Admins: Same as the "Find all Domain Admins" query, but instead only show Users with owned property.
  • Find Shortest Paths from owned node to Domain Admins: Same as the "Find Shortest Paths to Domain Admins" query, but instead only show paths originating from an owned node.
  • Show wave: Show only the nodes compromised in a selected wave. Useful for focusing in on newly-compromised nodes.
  • Highlight delta for wave: Show all compromised nodes up to a selected wave, and will highlight the nodes gained in that wave. Useful for visualizing privilege gains as access expands.
  • Find clusters of password reuse: Show all nodes with a SharesPasswordWith relationship to another node.
  • Show blacklisted nodes: Show all nodes with the blacklist property set.
  • Show blacklisted relationships: Show all relationships (and their start and end node) with the blacklist property set.
  • Show blacklist: Show all nodes and relationships with the blacklist property set.
  • Show owned nodes: Show all nodes with the owned property set.

All queries (except for the blacklist-specific queries) will filter out nodes & relationships from the blacklist.

Check out the "UI Customizations and Custom Queries" section of the blog post to see examples of these custom queries in action.

Known Issues and Future Development

  • Add additional queries to pull specific nodes from database and write to output file
  • Showing deltas for large waves can be stressful on the UI, should add ability to LIMIT results to a user-defined threshold
  • General HTTP error-checking
  • More, broader ideas here

Acknowledgements

hackerjiv and pfizzell for the sound advice and feedback. CptJesus, _wald0, and harmj0y for making a tremendous platform.