Skip to content

Commit c32b0fe

Browse files
committed
Land rapid7#6377, Add post mod to generate CSV Org Chart Data From AD
2 parents 8d016ca + 880697d commit c32b0fe

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
This module can be used to aid the generation of an organizational chart based on information
2+
contained in Active Directory. The module itself uses ADSI to retrieve key information from AD
3+
(manager, title, description etc) fields and then present it in a CSV file in the form:
4+
5+
```
6+
cn,description,title,phone,department,division,e-mail,company,reports_to
7+
```
8+
9+
The reports_to field is the only one which is generated; everything else is taken directly from AD.
10+
The 'manager' field contains the DN of the manager assigned to that user, and this module simply
11+
uses a regular expression to obtain the CN field of the manager.
12+
13+
This can then be imported into tools like [Microsoft Visio](https://products.office.com/en-us/visio/flowchart-software)
14+
(using the organizational chart wizard) and it will construct a visual org chart from the
15+
information there. Although visio supports the ability to generate Org charts if it is on a domain
16+
joined machine, but there does not seem to be a way of doing this remotely (e.g. during a
17+
red teaming exercise).
18+
19+
This should not be confused with security groups and AD managed groups; this is purely an
20+
internal organizational hierarchy representation but could be very useful for situational awareness
21+
or in order to construct a more plausible or targeted internal phishing exercise.
22+
23+
# Options
24+
25+
Option | Value
26+
-------------------| ---
27+
ACTIVE_USERS_ONLY | This will restrict the search for users to those whose accounts are Active. This would have the effect of excluding disabled accounts (e.g. employees who have resigned).
28+
FILTER | Any additional LDAP filtering that is required when searching for users.
29+
WITH_MANAGERS_ONLY | If this is TRUE, the module will only include users who have a manger set (internally, this is implemented by adding (manager=*) to the ADSI query filter). This could be useful if not everyone has a manager set, but could mean that the top executive is not included either.
30+
STORE_LOOT | Store the results in a CSV file in loot. You'll almost certainly want this set to TRUE.
31+
32+
# Demo
33+
34+
For the purposes of this contrived example, the module has been configured to generate the CSV
35+
reporting information for everyone with 'IT' somewhere in their common name.
36+
37+
```
38+
msf post(make_csv_orgchart) > show options
39+
40+
Module options (post/windows/gather/make_csv_orgchart):
41+
42+
Name Current Setting Required Description
43+
---- --------------- -------- -----------
44+
ACTIVE_USERS_ONLY true yes Only include active users (i.e. not disabled ones)
45+
DOMAIN no The domain to query or distinguished name (e.g. DC=test,DC=com)
46+
FILTER cn=*IT* no Additional LDAP filter to use when searching for users
47+
MAX_SEARCH 500 yes Maximum values to retrieve, 0 for all.
48+
SESSION 2 yes The session to run this module on.
49+
STORE_LOOT true yes Store the organisational chart information in CSV format in loot
50+
WITH_MANAGERS_ONLY false no Only users with managers
51+
52+
msf post(make_csv_orgchart) > run
53+
54+
Users & Managers
55+
================
56+
57+
cn description title phone department division e-mail company reports_to
58+
-- ----------- ----- ----- ---------- -------- ------ ------- ----------
59+
IT Manager Deputy GOAT IT Director [email protected] IT Director
60+
IT Director Director of Goat IT [email protected]
61+
IT Leader: Badger Team Leader of Blue Team Operations [email protected] IT Manager
62+
IT Leader: Otter Team Leader: Offensive Operations [email protected] IT Manager
63+
Oswold Otter (IT Team) Consultant [email protected] IT Leader: Otter
64+
Bertie Badger (IT Security Team) Default pass is badger123 IT Security Team Deputy [email protected] IT Leader: Badger
65+
66+
[*] CSV Organisational Chart Information saved to: /usr/home/s/stuart/.msf4/loot/20151221175733_stufusdev_192.0.2.140_ad.orgchart_189769.txt
67+
[*] Post module execution completed
68+
```
69+
70+
The contents of the CSV file are shown below:
71+
72+
```
73+
$ cat /usr/home/s/stuart/.msf4/loot/20151221175733_stufusdev_192.0.2.140_ad.orgchart_189769.txt
74+
cn,description,title,phone,department,division,e-mail,company,reports_to
75+
"IT Manager","","Deputy GOAT IT Director","","","","[email protected]","","IT Director"
76+
"IT Director","","Director of Goat IT","","","","[email protected]","",""
77+
"IT Leader: Badger","","Team Leader of Blue Team Operations","","","","[email protected]","","IT Manager"
78+
"IT Leader: Otter","","Team Leader: Offensive Operations","","","","[email protected]","","IT Manager"
79+
"Oswold Otter (IT Team)","","Consultant","","","","[email protected]","","IT Leader: Otter"
80+
"Bertie Badger (IT Security Team)","Default pass is badger123","IT Security Team Deputy","","","","[email protected]","","IT Leader: Badger"
81+
```
82+
83+
When this was imported into Visio with default options set, it produced the following organisational chart:
84+
85+
![screenshot_orgchart](https://cloud.githubusercontent.com/assets/12296344/11937572/f5906320-a80c-11e5-8faa-6439872df362.png)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'rex'
7+
require 'msf/core'
8+
9+
class MetasploitModule < Msf::Post
10+
include Msf::Auxiliary::Report
11+
include Msf::Post::Windows::LDAP
12+
13+
def initialize(info = {})
14+
super(update_info(
15+
info,
16+
'Name' => 'Generate CSV Organizational Chart Data Using Manager Information',
17+
'Description' => %(
18+
This module will generate a CSV file containing all users and their managers, which can be
19+
imported into Visio which will render it.
20+
),
21+
'License' => MSF_LICENSE,
22+
'Author' => [
23+
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'
24+
],
25+
'Platform' => [ 'win' ],
26+
'SessionTypes' => [ 'meterpreter' ]
27+
))
28+
29+
register_options([
30+
OptBool.new('WITH_MANAGERS_ONLY', [true, 'Only users with managers', false]),
31+
OptBool.new('ACTIVE_USERS_ONLY', [true, 'Only include active users (i.e. not disabled ones)', true]),
32+
OptBool.new('STORE_LOOT', [true, 'Store the organizational chart information in CSV format in loot', true]),
33+
OptString.new('FILTER', [false, 'Additional LDAP filter to use when searching for users', ''])
34+
], self.class)
35+
end
36+
37+
def run
38+
max_search = datastore['MAX_SEARCH']
39+
user_fields = ['cn', 'manager', 'description', 'title', 'telephoneNumber', 'department', 'division', 'userPrincipalName', 'company']
40+
41+
begin
42+
qs = []
43+
qs << '(objectCategory=person)'
44+
qs << '(objectClass=user)'
45+
qs << '(!userAccountControl:1.2.840.113556.1.4.803:=2)' if datastore['ACTIVE_USERS_ONLY']
46+
qs << '(manager=*)' if datastore['WITH_MANAGERS_ONLY']
47+
qs << "(#{datastore['FILTER']})" if datastore['FILTER'] != ""
48+
49+
query_string = "(&(#{qs.join('')}))"
50+
vprint_status("Executing #{query_string}")
51+
q = query(query_string, max_search, user_fields)
52+
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
53+
# Can't bind or in a network w/ limited accounts
54+
print_error(e.message)
55+
return
56+
end
57+
58+
if q.nil? || q[:results].empty?
59+
print_status('No results returned.')
60+
else
61+
user_fields << 'reports_to'
62+
results_table = parse_results(q[:results])
63+
print_line results_table.to_s
64+
if datastore['STORE_LOOT']
65+
stored_path = store_loot('ad.orgchart', 'text/csv', session, results_table.to_csv)
66+
print_status("CSV Organisational Chart Information saved to: #{stored_path}")
67+
end
68+
end
69+
end
70+
71+
# Takes the results of LDAP query, parses them into a table
72+
def parse_results(results)
73+
results_table = Rex::Ui::Text::Table.new(
74+
'Header' => "Users & Managers",
75+
'Indent' => 1,
76+
'SortIndex' => -1,
77+
'Columns' => ['cn', 'description', 'title', 'phone', 'department', 'division', 'e-mail', 'company', 'reports_to']
78+
)
79+
80+
results.each do |result|
81+
row = []
82+
83+
result.each_with_index do |field, idx|
84+
next if idx == 1 # Don't include the manager DN
85+
86+
if field.nil?
87+
row << ""
88+
else
89+
row << field[:value]
90+
end
91+
end
92+
93+
# Parse the manager CN string to grab the CN= field only.
94+
# Note that it needs the negative lookbehind to avoid escaped characters.
95+
reports_to = /^CN=(?<cn>.+?),(?<!\\,)/.match(result[1][:value])
96+
if reports_to.nil?
97+
row << ""
98+
else
99+
row << reports_to['cn'].gsub('\,', ',')
100+
end
101+
102+
results_table << row
103+
end
104+
results_table
105+
end
106+
end

0 commit comments

Comments
 (0)