├── LICENSE ├── README.md ├── Screen_Shot_2019-04-02_at_3.31.18_PM.png ├── Screen_Shot_2019-04-02_at_3.31.54_PM.png ├── owned_utils.py ├── vampire.cna └── vampire_creds.cna /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Patrick Hurd 2 | 3 | Commercial use of this software is prohibited. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction except to commercial use, this includes the rights to use, copy, modify, merge, publish, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vampire 2 | 3 | __Vampire__ is an aggressor script which adds a "Mark Owned" right click option to beacons. This allows you to select either the Computer or User (or Default, which will choose based on your user), along with the domain they belong to. There is an additional optional cna script for marking new credentials as owned. Vampire will communicate with your neo4j REST API on localhost:7474 to mark the node as owned. 4 | 5 | 6 |
7 | 8 | How to use 9 | --- 10 | 11 | 0. Put `vampire.cna`, `vampire_creds.cna`, and `owned_utils.py` in the root of your cobaltstrike folder 12 | 1. `chmod u+x owned_utils.py` 13 | 1. Load `vampire.cna` and `vampire_creds.cna` into Cobalt Strike through the Script Manager 14 | 1. Rain shells 15 | 2. Start neo4j and BloodHound as normal 16 | 2. Run BloodHound data collection and import data 17 | 3. Right click your beacon(s) and mark them as owned 18 | 4. Run logonpasswords 19 | 20 | Considerations 21 | --- 22 | 23 | - neo4j must be running on localhost, on the standard port - 7474 24 | - Your neo4j database creds should be Kali standard `neo4j:BloodHound` (you can change the base64 in `owned_utils.py` otherwise) 25 | - `echo -n 'neo4j:yourpassword' | base64` and then replace the auth in owned_utils.py 26 | 27 | Benefits 28 | --- 29 | 30 | - Never miss an attack path 31 | - Quickly keep up with other team members' movement 32 | 33 | How it works 34 | --- 35 | 36 | 0. Uses `owned_utils.py` to query the list of domains from neo4j 37 | 1. Obtain user selection 38 | 2. Foreach selected beacon ID: 39 | 3. Append `@` + the specified domain to the user/computer name 40 | 4. For `Default`, it will choose based on whether you're a local admin 41 | 4. Uses `owned_utils.py` to query the neo4j REST API 42 | - `'MATCH (n:*) WHERE lower(n.name) = "' + nodelabel.lower() + '" SET n.owned = TRUE'` 43 | 44 | --- 45 | 46 | 1. Listens for the `on credentials` callback 47 | 1. Loops through all the credentials, keeping an internal state 48 | 1. Optionally excludes 32 byte passwords (NTLM hashes - see $ignore_hash) 49 | 1. Reconstructs a valid domain for the user 50 | 1. Checks the user exists 51 | 1. Marks new credentials as owned 52 | 53 | Extensibility 54 | --- 55 | 56 | The cna script handles the Cobalt Strike GUI, while the Python script handles Bloodhound/neo4j interaction. The reason I did it this way is because I couldn't get the HTTP request working nicely through Sleep sockets. The plus side is, you can call/import the Python code into your own project which doesn't use Cobalt Strike. The code in the functions is pretty much ripped from the neo4j syntax examples in the Bloodhound Github wiki. 57 | 58 | Author 59 | --- 60 | 61 | Patrick Hurd 62 | -------------------------------------------------------------------------------- /Screen_Shot_2019-04-02_at_3.31.18_PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coalfire-Research/Vampire/5c8fbd689f3b8958e462669296d5bec9c94d24f5/Screen_Shot_2019-04-02_at_3.31.18_PM.png -------------------------------------------------------------------------------- /Screen_Shot_2019-04-02_at_3.31.54_PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coalfire-Research/Vampire/5c8fbd689f3b8958e462669296d5bec9c94d24f5/Screen_Shot_2019-04-02_at_3.31.54_PM.png -------------------------------------------------------------------------------- /owned_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Author: Patrick Hurd, Penetration Tester, Coalfire Federal 2019, 2020 4 | 5 | import requests, json 6 | import getopt, sys 7 | 8 | headers = { "Accept": "application/json; charset=UTF-8", 9 | "Content-Type": "application/json", 10 | "Authorization": "bmVvNGo6Qmxvb2Rob3VuZA==" } 11 | 12 | url = 'http://localhost:7474/db/data/transaction/commit' 13 | 14 | def main(argv): 15 | node_type = '' 16 | node_label = '' 17 | request = '' 18 | domain = '' 19 | 20 | try: 21 | opts, args = getopt.getopt(argv,"hr:t:l:d:",["request=", "type=","label=","domain="]) 22 | except getopt.GetoptError: 23 | print('test.py -r -t -l ') 24 | sys.exit(2) 25 | for opt, arg in opts: 26 | if opt == '-h': 27 | print ('test.py -r -t -l ') 28 | sys.exit() 29 | elif opt in ("-r", "--request"): 30 | request = arg 31 | elif opt in ("-t", "--type"): 32 | node_type = arg 33 | elif opt in ("-l", "--label"): 34 | node_label = arg 35 | elif opt in ("-d", "--domain"): 36 | domain = arg 37 | mux(request, node_type, node_label) 38 | 39 | def mux(request, node_type, node_label): 40 | if request == 'domains': 41 | get_domains(); 42 | elif request == 'owned': 43 | mark_owned(node_type, node_label) 44 | elif request == 'create': 45 | create(node_type, node_label) 46 | elif request == 'exists': 47 | exists(node_type, node_label) 48 | elif request == 'exists_like': 49 | exists_starts_with(node_type, node_label) 50 | elif request == 'owned_like': 51 | owned_starts_with(node_type, node_label) 52 | else: 53 | print("Error: unknown request type") 54 | 55 | def mark_owned(nodetype, nodelabel): 56 | statement = f'MATCH (n:{nodetype}) WHERE lower(n.name) = "{nodelabel.lower()}" SET n.owned = TRUE' 57 | data = {"statements": [{'statement': statement}]} 58 | r = requests.post(url=url,headers=headers,json=data) 59 | print(r.text) 60 | 61 | def create(nodetype, nodelabel): 62 | statement = "CREATE (n:" + nodetype + ') SET n.name="' + nodelabel + '"' 63 | data = {"statements": [{'statement': statement}]} 64 | r = requests.post(url=url,headers=headers,json=data) 65 | 66 | def exists_starts_with(nodetype, nodelabel): 67 | statement = f'MATCH (n:{nodetype}) WHERE lower(n.name) STARTS WITH "{nodelabel.lower()}" RETURN n' 68 | data = {"statements": [{'statement': statement}]} 69 | r = requests.post(url=url,headers=headers,json=data) 70 | if nodelabel in r.text: 71 | print(1) 72 | else: 73 | print(r.text) 74 | print(0) 75 | 76 | def owned_starts_with(nodetype, nodelabel): 77 | statement = f'MATCH (n:{nodetype}) WHERE lower(n.name) STARTS WITH "{nodelabel.lower()}" SET n.owned = TRUE' 78 | data = {"statements": [{'statement': statement}]} 79 | r = requests.post(url=url,headers=headers,json=data) 80 | print(r.text) 81 | 82 | def exists(nodetype, nodelabel): 83 | statement = 'MATCH (n:*) WHERE lower(n.name) = "' + nodelabel.lower() + '" RETURN n' 84 | data = {"statements": [{'statement': statement}]} 85 | r = requests.post(url=url,headers=headers,json=data) 86 | if nodelabel in r.text: 87 | return 1 88 | else: 89 | return 0 90 | 91 | def get_domains(): 92 | statement = "MATCH (n:Domain) RETURN n" 93 | data = {"statements": [{'statement': statement}]} 94 | r = requests.post(url=url,headers=headers,json=data) 95 | j = json.loads(r.text) 96 | output = '' 97 | for x in range(len(j["results"][0]["data"])): 98 | output = output + j["results"][0]["data"][x]["row"][0]["name"] + ',' 99 | print(output[0:len(output)-1]) 100 | 101 | def test(nodetype, nodelabel): 102 | statement = "MATCH (n:" + nodetype + " {name:'" + nodelabel + "'}) RETURN n" 103 | data = {"statements": [{'statement': statement}]} 104 | r = requests.post(url=url,headers=headers,json=data) 105 | print(r.text) 106 | 107 | if __name__ == '__main__': 108 | main(sys.argv[1:]) 109 | -------------------------------------------------------------------------------- /vampire.cna: -------------------------------------------------------------------------------- 1 | 2 | # Author: Patrick Hurd, Penetration Tester, Coalfire Federal 2019 3 | 4 | popup beacon_bottom { 5 | item "Mark Owned" { 6 | dialog_show(create_owned_dialog($1)); 7 | } 8 | } 9 | 10 | sub create_owned_dialog { 11 | $beacon = $1; 12 | $owned_dialog = dialog("Mark Owned", %(), lambda({owned_dialog_callback($beacon, $2, $3)})); 13 | dialog_description($owned_dialog, "Mark node(s) as owned in BloodHound. Neo4j must be up and running on localhost:7474."); 14 | 15 | drow_combobox($owned_dialog, "node_type", "Node Type:", @('Default', 'User', 'Computer')); 16 | $proc = exec("./owned_utils.py -r domains"); 17 | $d_line = readln($proc); 18 | @domains = split(",", $d_line); 19 | closef($proc); 20 | drow_combobox($owned_dialog, "domain", "Domain:", @domains); 21 | 22 | dbutton_action($owned_dialog, "Go"); 23 | return $owned_dialog; 24 | } 25 | 26 | sub owned_dialog_callback { 27 | foreach $id ($1) { 28 | $username = ""; 29 | $computer = ""; 30 | foreach $b (beacons()) { 31 | if ($b["id"] == int($id)) { 32 | $username = $b["user"]; 33 | $computer = $b["computer"]; 34 | } 35 | } 36 | # Cobalt Strike shows local admins as "CompUser *" 37 | $is_admin = 0; 38 | if ($username ismatch '.* \*') { 39 | $is_admin = 1; 40 | } 41 | $is_system = 0; 42 | if ($username ismatch 'SYSTEM \*') { 43 | $is_system = 1; 44 | } else if ($username ismatch 'Administrator \*') { 45 | $is_system = 1; 46 | } 47 | $username = matches($username, "([^ ]*)")[0]; 48 | # Append @domain.tld 49 | $username = $username . "@" . $3["domain"]; 50 | $computer = $computer . "." . $3["domain"]; 51 | if ($3["node_type"] eq "Default") { 52 | if ($is_system) { 53 | exec(@("./owned_utils.py", "-r", "owned", "-t", "Computer", "-l", $computer)); 54 | } else if ($is_admin) { 55 | exec(@("./owned_utils.py", "-r", "owned", "-t", "User", "-l", $username)); 56 | exec(@("./owned_utils.py", "-r", "owned", "-t", "Computer", "-l", $computer)); 57 | } else { 58 | exec(@("./owned_utils.py", "-r", "owned", "-t", "User", "-l", $username)); 59 | } 60 | } 61 | if ($3["node_type"] eq "User") { 62 | exec(@("./owned_utils.py", "-r", "owned", "-t", "User", "-l", $username)); 63 | } 64 | if ($3["node_type"] eq "Computer") { 65 | exec(@("./owned_utils.py", "-r", "owned", "-t", "Computer", "-l", $computer)); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /vampire_creds.cna: -------------------------------------------------------------------------------- 1 | 2 | # Author: Patrick Hurd, Penetration Tester, Coalfire Federal 2019, 2020 3 | 4 | # Can use `on credentials`, which puts the entirety of the cobalt credentials in $1 5 | # This isn't a documented use case, but it works. 6 | # The event fires whenever credentials change - add, remove, etc. 7 | 8 | # It doesn't hurt to re-mark users as owned, but let's not. 9 | @old_creds = @(); 10 | 11 | # Set to 1 to ignore hashed credentials 12 | # Usually there will also be a 6 digit pin in this case 13 | # TODO: Take the 6 digit pin into account 14 | $ignore_hash = 0; 15 | 16 | sub parse_creds { 17 | @creds = $1; 18 | @new_creds = @(); 19 | foreach $cred (@creds) { 20 | println($cred); 21 | # The user isn't owned until you have an elevated beacon 22 | # with which to pass the hash (optional) 23 | if (strlen($cred["password"]) == 32 && $ignore_hash) { 24 | continue; 25 | } 26 | if (strlen($cred["realm"]) <= 1) { 27 | continue; 28 | } 29 | # Construct uppercase USER@REALM 30 | $useratrealm = uc($cred["user"]) . "@" . uc($cred["realm"]); 31 | if ($useratrealm in @old_creds) { 32 | continue; 33 | } 34 | # Search BloodHound for this account 35 | println("Checking if account $useratrealm exists"); 36 | if (!account_exists($useratrealm)) { 37 | add(@new_creds, $useratrealm); 38 | } 39 | add(@old_creds, $useratrealm); 40 | } 41 | println(@old_creds); 42 | println(@new_creds); 43 | return @new_creds; 44 | } 45 | 46 | sub account_exists { 47 | $account = $1; 48 | $proc = exec(@("./owned_utils.py", "-r", "exists_like", "-t", "User", "-l", $account)); 49 | $exists = readln($proc); 50 | println($exists); 51 | return $exists == "1"; 52 | } 53 | 54 | sub mark_creds_owned { 55 | println("Arg 1: "); 56 | println($1); 57 | @accounts = $1; 58 | foreach $account (@accounts) { 59 | exec(@("./owned_utils.py", "-r", "owned_like", "-t", "User", "-l", $account)); 60 | println("Marking $account as owned."); 61 | } 62 | } 63 | 64 | on credentials { 65 | @creds = parse_creds($1); 66 | mark_creds_owned(@creds); 67 | } 68 | --------------------------------------------------------------------------------