├── 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 |
--------------------------------------------------------------------------------