Skip to content

Add readline support to interactive shells#21442

Open
ShorterKing wants to merge 1 commit into
rapid7:masterfrom
ShorterKing:meterpreter-readline-support
Open

Add readline support to interactive shells#21442
ShorterKing wants to merge 1 commit into
rapid7:masterfrom
ShorterKing:meterpreter-readline-support

Conversation

@ShorterKing
Copy link
Copy Markdown

@ShorterKing ShorterKing commented May 11, 2026

Adds readline support to interactive shells (command_shell and meterpreter
interactive_channel) when the input driver supports it (pgets). Falls back
to the existing behavior transparently when readline is not available, so
nothing is broken for existing users.

Also adds Ctrl+Arrow (word jump) and Ctrl+Delete (kill word) key bindings via
RbReadline where available.

Changes

  • lib/msf/base/sessions/command_shell.rb — readline path in _interact_stream,
    drains remote output and updates prompt dynamically before each pgets call
  • lib/rex/post/meterpreter/ui/console/interactive_channel.rb — same pattern
    for meterpreter interactive channels
  • lib/rex/ui/text/input/readline.rb — registers word-navigation key bindings
    on init if RbReadline is present

Verification

  • Start msfconsole
  • use exploit/multi/handler, set a reverse TCP payload, run
  • Open a session and drop into shell with shell
  • Verify readline editing (arrow keys, history, Ctrl+Arrow word jump) works in the shell
  • Verify session still works normally without readline (piped/non-TTY input)
  • Background and resume session, verify prompt is restored correctly

Testing

Tested on:

  • meterpreter x64/windowsshell on Windows 10 (22H2)
  • shell x64/linux reverse shell on Linux (root, WSL2-based)

Both readline interaction and fallback path confirmed working. Further testing
may be needed for edge cases such as background jobs (exploit -j), raw TTY
mode, and piped input.

@msutovsky-r7
Copy link
Copy Markdown
Contributor

I don't see benefit of this PR - would you mind elaborating? Is this related to any issue?

@bwatters-r7
Copy link
Copy Markdown
Contributor

@ShorterKing, we noticed that this did not run our cmd shell tests- this is not a problem for this PR, but we need to fix that in the background- #21484.
We are going to wait on that to get landed to make sure our tests are complete for this before moving forward with it. When that PR gets landed, could you please rebase this PR so that the tests will kick off as expected?
In the meantime, I'm going to kick off a copilot review. Just know this might take a little longer so that we can get the tests ready for it.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds enhanced interactive shell input handling by leveraging pgets-based prompting (to enable readline-style editing when available) for both Msf::Sessions::CommandShell and Meterpreter InteractiveChannel, and registers additional word-navigation key bindings when RbReadline is present.

Changes:

  • Add RbReadline keybindings for Ctrl+Arrow word navigation and Ctrl+Delete word kill in Rex::Ui::Text::Input::Readline.
  • Introduce a new “drain remote output + set local prompt + then read input via pgets” interaction loop for Meterpreter interactive channels.
  • Introduce the same pgets-driven prompt loop for command shell sessions, with a fallback for inputs without pgets.

Impact Analysis:

  • Blast radius: high — affects interactive session UX for command shells and meterpreter interactive channels across console users; downstream behavior depends on the concrete user_input implementation (TTY vs non-TTY, readline vs non-readline).
  • Data and contract effects: no schema changes; however, interactive I/O behavior (prompt display, newline handling, output display timing) is materially altered.
  • Rollback and test focus: rollback is straightforward (revert interaction-loop changes); focus testing on non-readline inputs that still implement pgets, correct prompt restoration after errors/backgrounding, and avoiding double-newline/hidden-output regressions.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
lib/rex/ui/text/input/readline.rb Adds RbReadline keybindings during readline input initialization.
lib/rex/post/meterpreter/ui/console/interactive_channel.rb Adds a pgets-driven interaction loop that drains remote output and updates the local prompt.
lib/msf/base/sessions/command_shell.rb Adds a pgets-driven command shell interaction loop with prompt parsing and fallback behavior.

Comment on lines +30 to +35
elsif !user_input.respond_to?(:pgets)
interact_stream(self)
else
old_prompt = user_input.prompt
user_input.prompt = ''


begin
line = user_input.pgets
break unless line
Comment on lines +34 to +74
user_input.prompt = ''

while self.interacting && _remote_fd(self)
data = ''
while self.interacting && _remote_fd(self)
sd = Rex::ThreadSafe.select([_remote_fd(self)], nil, nil, 0.1)
break unless sd
begin
chunk = self.lsock.sysread(16384)
break if chunk.nil? || chunk.empty?
data << chunk
rescue Exception
break
end
end

if data.length > 0
self.on_print_proc.call(data.strip) if self.on_print_proc
self.on_log_proc.call(data.strip) if self.on_log_proc

lines = data.split("\n", -1)
prompt_part = lines.pop || ''

if lines.length > 0
user_output.print(lines.join("\n") + "\n")
end

user_input.prompt = prompt_part
end

begin
line = user_input.pgets
break unless line
self.on_command_proc.call(line.strip) if self.on_command_proc
self.write(line + "\n")
rescue Exception
break
end
end

user_input.prompt = old_prompt if old_prompt
Comment on lines +817 to +833
if !user_input.respond_to?(:pgets)
while self.interacting
sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)
next unless sd

if sd[0].include? rstream.fd
user_output.print(shell_read)
if sd[0].include? rstream.fd
user_output.print(shell_read)
end
if sd[0].include? user_input.fd
run_single((user_input.gets || '').chomp("\n"))
end
Thread.pass
end
if sd[0].include? user_input.fd
run_single((user_input.gets || '').chomp("\n"))
else
old_prompt = user_input.prompt
user_input.prompt = ''


begin
line = user_input.pgets
break unless line
Comment on lines +832 to +869
user_input.prompt = ''

while self.interacting
data = ''
while self.interacting
sd = Rex::ThreadSafe.select([rstream.fd], nil, nil, 0.1)
break unless sd
begin
chunk = shell_read(-1, 0.01)
break if chunk.nil? || chunk.empty?
data << chunk
rescue Exception
break
end
end

if data.length > 0
lines = data.split("\n", -1)
prompt_part = lines.pop || ''

if lines.length > 0
user_output.print(lines.join("\n") + "\n")
end

user_input.prompt = prompt_part
end

begin
line = user_input.pgets
break unless line
run_single(line)
rescue Exception
break
end
end
Thread.pass

user_input.prompt = old_prompt if old_prompt
RbReadline.rl_parse_and_bind('"\e[1;5D": backward-word')
RbReadline.rl_parse_and_bind('"\e[1;5C": forward-word')
RbReadline.rl_parse_and_bind('"\e[3;5~": kill-word')
rescue Exception
Comment on lines +817 to +823
if !user_input.respond_to?(:pgets)
while self.interacting
sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)
next unless sd

if sd[0].include? rstream.fd
user_output.print(shell_read)
if sd[0].include? rstream.fd
user_output.print(shell_read)
@ShorterKing
Copy link
Copy Markdown
Author

Hey @msutovsky-r7, here's the issue this fixes. When you drop into an interactive shell or meterpreter channel, readline breaks entirely. Arrow keys print garbage like ^[[D^[[C instead of moving the cursor, and Ctrl+R spams escape sequences instead of doing history search (see screenshot).

This just wires up the existing pgets path so readline works properly in shell sessions. Falls back to the old behavior for non-TTY/piped inputs so nothing breaks.

No existing issue for it, just something that comes up in daily use. Happy to open one if needed.
Screenshot 2026-05-22 131156

@ShorterKing
Copy link
Copy Markdown
Author

@ShorterKing, we noticed that this did not run our cmd shell tests- this is not a problem for this PR, but we need to fix that in the background- #21484. We are going to wait on that to get landed to make sure our tests are complete for this before moving forward with it. When that PR gets landed, could you please rebase this PR so that the tests will kick off as expected? In the meantime, I'm going to kick off a copilot review. Just know this might take a little longer so that we can get the tests ready for it.

@bwatters-r7 Sounds good, I'll rebase once #21484 lands.

@ShorterKing ShorterKing force-pushed the meterpreter-readline-support branch from 3b74b8b to 4213d33 Compare May 27, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

5 participants