Skip to content

Commit 55b5b76

Browse files
committed
error checking, --reset option, general feedback improvements
1 parent d068ec5 commit 55b5b76

File tree

1 file changed

+106
-16
lines changed

1 file changed

+106
-16
lines changed

bh-owned.rb

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/ruby env
22
#Encoding: UTF-8
33

4-
# Written by: @porterhau5 - 3/27/17
4+
# Written by: @porterhau5 - 3/29/17
55

66
require 'net/http'
77
require 'uri'
@@ -67,17 +67,33 @@ def craft(options)
6767
if options.nodes
6868
hash['statements'] << {'statement' => "MATCH (n) RETURN (n.name)"}
6969
return hash.to_json
70+
71+
# remove owned and wave properties, delete SharesPasswordWith relationships
72+
elsif options.reset
73+
puts blue("[*]") + " Removing all custom properties and SharesPasswordWith relationships"
74+
hash['statements'] << {'statement' => "MATCH (n) WHERE exists(n.wave) OR exists(n.owned) REMOVE n.wave, n.owned"}
75+
hash['statements'] << {'statement' => "MATCH (n)-[r:SharesPasswordWith]-(m) DELETE r"}
76+
return hash.to_json
77+
78+
# once nodes are added, set 'wave' for newly owned nodes
79+
elsif options.spread
80+
hash['statements'] << {'statement' => "OPTIONAL MATCH (n1:User {wave:#{options.wave}}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:#{options.wave}}) 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=#{options.wave} RETURN m.name, #{options.wave}", 'includeStats' => true}
81+
return hash.to_json
82+
7083
# add 'owned' property to nodes from file
7184
elsif options.add
7285
File.foreach(options.add) do |node|
7386
name, method = node.split(',', 2)
74-
puts green("[+]") + " Adding #{name.chomp} to wave #{options.wave} via #{method.chomp}"
75-
hash['statements'] << {'statement' => "MATCH (n) WHERE (n.name = \"#{name.chomp}\") SET n.owned = \"#{method.chomp}\", n.wave = #{options.wave}"}
87+
# if -w flag set, then overwrite previous property if it exists, otherwise don't overwrite
88+
if options.forceWave.nil?
89+
hash['statements'] << {'statement' => "MATCH (n) WHERE (n.name = \"#{name.chomp}\") SET n.owned = \"#{method.chomp}\", n.wave = #{options.wave} RETURN \'#{name.chomp}\', \'#{options.wave}\', \'#{method.chomp}\'", 'includeStats' => true}
90+
else
91+
# this uses a Cypher hack for doing if/else conditionals
92+
hash['statements'] << {'statement' => "MATCH (n) WHERE (n.name = \"#{name.chomp}\") FOREACH (ignoreMe in CASE WHEN exists(n.wave) THEN [1] ELSE [] END | SET n.wave=n.wave) FOREACH (ignoreMe in CASE WHEN not(exists(n.wave)) THEN [1] ELSE [] END | SET n.owned = \"#{method.chomp}\", n.wave = #{options.wave}) RETURN \'#{name.chomp}\',\'#{options.wave}\',\'#{method.chomp}\'", 'includeStats' => true}
93+
end
7694
end
77-
# once nodes are added, set "wave" for newly owned nodes
78-
puts green("[+]") + " Querying and updating new owned nodes"
79-
hash['statements'] << {'statement' => "OPTIONAL MATCH (n1:User {wave:#{options.wave}}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:#{options.wave}}) 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=#{options.wave}"}
8095
return hash.to_json
96+
8197
# Create SharesPasswordWith relationships between all nodes in file
8298
elsif options.spw
8399
nodes = []
@@ -115,7 +131,9 @@ def sendrequest(options)
115131
end
116132

117133
def parse(options, response)
134+
#
118135
# print all nodes
136+
#
119137
if options.nodes
120138
out = []
121139
data = JSON.parse(response.body)
@@ -129,6 +147,9 @@ def parse(options, response)
129147
end if data['results'].any?
130148
# sort, uniq, display
131149
puts out.sort.uniq
150+
#
151+
# determine wave number
152+
#
132153
elsif options.wave == -1
133154
resp = JSON.parse(response.body)
134155
resp['results'].each do |r|
@@ -141,17 +162,86 @@ def parse(options, response)
141162
end
142163
end if r['data'].any?
143164
end if resp['results'].any?
165+
#
166+
# parse spread of compromise
167+
#
168+
elsif options.spread
169+
resp = JSON.parse(response.body)
170+
resp['results'].each do |r|
171+
puts blue("[*]") + " Finding spread of compromise for wave #{options.wave}"
172+
r['stats'].each do |s|
173+
# check stats to see if properties were set
174+
if s.first == "properties_set" and s.last == 0
175+
# if there are records in data, then properties already set
176+
if r['data'].any?
177+
r['data'].each do |d|
178+
puts blue("[*]") + " No additional nodes found for wave #{options.wave}"
179+
end
180+
else
181+
puts red("[-]") + " No additional nodes found for wave #{options.wave}"
182+
end
183+
elsif s.first == "properties_set" and s.last != 0
184+
out = []
185+
count = 0
186+
# cycle through rows returned
187+
r['data'].each do |d|
188+
next unless not d['row'][0].to_s.empty?
189+
out.push(d['row'][0])
190+
count += 1
191+
end if r['data'].any?
192+
puts green("[+]") + " #{count} nodes found:"
193+
puts out.sort.uniq
194+
end
195+
end if r['stats'].any?
196+
end if resp['results'].any?
197+
#
198+
# parse nodes added
199+
#
200+
elsif options.add
201+
success = true
202+
resp = JSON.parse(response.body)
203+
resp['results'].each do |r|
204+
# node names provided are returned as columns
205+
names = []
206+
r['columns'].each do |c|
207+
names.push(c)
208+
end
209+
r['stats'].each do |s|
210+
# check stats to see if properties were set
211+
if s.first == "properties_set" and s.last == 0
212+
# if there are records in data, then properties already set
213+
if not r['data'].any?
214+
puts red("[-]") + " Properties not added for #{names.first} (node not found, check spelling?)"
215+
success = false
216+
end
217+
elsif s.first == "properties_set" and s.last == 2
218+
puts green("[+]") + " Success, marked #{names.first} as owned in wave #{names[1]} via #{names.last}"
219+
elsif s.first == "properties_set" and s.last == 1
220+
puts blue("[*]") + " Properties already exist for #{names.first}, skipping (overwrite with flag -w <num>)"
221+
end
222+
end if r['stats'].any?
223+
end if resp['results'].any?
224+
# if all nodes were added successfully or skipped, find spread for new nodes
225+
if success
226+
options.spread = true
227+
sendrequest(options)
228+
else
229+
puts red("[-]") + " Skipping finding spread of compromise due to \"node not found\" error"
230+
end
231+
#
232+
# parse SharesPasswordWith
233+
#
144234
elsif options.spw
145235
resp = JSON.parse(response.body)
146236
resp['results'].each do |r|
237+
# node names provided are returned as columns
238+
names = []
239+
r['columns'].each do |c|
240+
names.push(c)
241+
end
147242
r['stats'].each do |s|
148243
# check stats to see if a relationship was created
149244
if s.first == "relationships_created" and s.last == 0
150-
names = []
151-
# node names provided are returned as columns
152-
r['columns'].each do |c|
153-
names.push(c)
154-
end
155245
# if there are records in data, then relationship already exists
156246
if r['data'].any?
157247
r['data'].each do |d|
@@ -161,16 +251,12 @@ def parse(options, response)
161251
puts red("[-]") + " Relationship not created for #{names.first} and #{names.last} (check spelling)"
162252
end
163253
elsif s.first == "relationships_created" and s.last == 2
164-
names = []
165-
r['columns'].each do |c|
166-
names.push(c)
167-
end
168254
puts green("[+]") + " Created SharesPasswordWith relationship between #{names.first} and #{names.last}"
169255
elsif s.first == "relationships_created" and (s.last != 0 or s.last != 2)
170256
puts "Something went wrong when creating SharesPasswordWith relationship"
171257
end
172258
end if r['stats'].any?
173-
end if resp['results'].any?
259+
end if resp['results'].any?
174260
end
175261
# uncomment line below to debug
176262
#puts JSON.pretty_generate(JSON.parse(response.body))
@@ -188,6 +274,7 @@ def main()
188274
opt.on('-a', '--add <file>', 'add \'owned\' and \'wave\' property to nodes in <file>') { |o| options.add = o }
189275
opt.on('-s', '--spw <file>', 'add \'SharesPasswordWith\' relationship between all nodes in <file>') { |o| options.spw = o }
190276
opt.on('-w', '--wave <num>', Integer, 'value to set \'wave\' property (override default behavior)') { |o| options.wave = o }
277+
opt.on('--reset', 'remove all custom properties and SharesPasswordWith relationships') { |o| options.reset = o }
191278
opt.on('-e', '--examples', 'reference doc of customized Cypher queries for BloodHound') { |o| options.examples = o }
192279
end.parse!
193280

@@ -221,6 +308,7 @@ def main()
221308
# -1 means we don't know current max "n.wave" value
222309
options.wave = -1
223310
sendrequest(options)
311+
options.forceWave = true
224312
end
225313
end
226314

@@ -231,6 +319,8 @@ def main()
231319
end
232320
end
233321

322+
options.spread = false
323+
234324
sendrequest(options)
235325
end
236326

0 commit comments

Comments
 (0)