escalatedquickly

Hacker || Malware Enthusiast || Nerd

Interactive subshells in BASH

2022-11-19
1369 words

A while ago, someone on a Discord server that I spend way too much time on asked a question. They were trying to brute force the three-letter sudo password of a lab user through a pipe wrapped in some python code, something to the effect of:

import os

alphabet= [chr(x) for x in range(0x41,0x5A)] # Uppercase
alphabet+=[chr(x) for x in range(0x61,0x7A)] # Lowercase
alphabet+=[chr(x) for x in range(0x21,0x40)] # Numbers and upper case

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            os.system('echo "%c%c%c" | sudo -k -S' % (a,b,c))

The code generates all three-letter combinations of upper and lowercase letters, as well as numbers and special characters in ASCII, and runs echo "<CANDIDATE>" | sudo -k -S in the system shell.1

The person asking was wondering why this didn’t give them a shell when the right password was given, and at first it seemed like a pretty obvious question: just add -i to the sudo command, so that it spawns an interactive shell. It is not quite that simple though, and I thought the solution was pretty neat.

Like I said, the obvious solution would be to simply add -i to the sudo call, as that opens an interactive shell. Assuming the password is pass, it would make sense if we could just do the following to get a shell:

$ echo 'pass' | sudo -k -S -i

That’s not the case however, the session is closed immediately. The reason for this is that an interactive session requires a TTY to be attached, but here there is none. In other words, the interactive session has to “talk directly” to the terminal. We can test this using a simple BASH one-liner:

$ echo 'test' | [[ -t 0 ]] && echo "TTY" || echo "Pipe"
Pipe

Here, we pipe test into the test [[ -t 0 ]], which tests if file descriptor 0 (i.e. stdin) is attached to a TTY, and returns a non-zero value it it’s not. If the returned value is zero, the code prints TTY, otherwise it prints Pipe. As we can see from the example, the printout is indeed Pipe.

Since stdin is not a terminal, the session is terminated as soon as the data in the pipe is processed and stdin is closed. When piping into e.g. sudo or bash, the pipe going into the command is passed along to the command invoked by sudo or bash. This becomes quite clear if we pipe some text into sudo cat or bash -c cat. Normally, cat executed without any provided input will read from stdin, but here it reads from the pipe and terminates.

$ echo "test" | bash -c cat
test
$ echo "test" | sudo cat
test

Since the pipe going into sudo is duplicated to stdin of the command invoked by sudo2 this means that the invoked command doesn’t get a TTY either – it is stuck in the same pipe. Again, we can confirm this using the same one-liner as before, wrapped up in a sudo call:

$ echo 'test' | sudo bash -c '[[ -t 0 ]] && echo "TTY" || echo "Pipe"'
Pipe

Adding more layers to this won’t do any good either, as the pipe will just be passed along forever. If we want to break out of the pipe and get an interactive shell, we need to get the TTY back.

Enter /dev/tty. This is a character device that points at the controlling terminal process of a command. Reading from it is the same as reading from stdin of said terminal, which essentially means reading from the keyboard. With this new tool in our tool belt, we are ready to take on the task. But it will require quite some shenanigans to get it done.

Shenanigans ensue

The main issue that we need to solve is that the TTY needs to be re-attached to the process living in the pipe. We can do this using the fact that in Linux, everything is treated like a file, and can be interacted with as if it was a file. This include non-file objects like character devices, such as /dev/tty. As a result, we can tell a command to read from /dev/tty by using the < redirection, which writes the content of a file (or something treated as a file) to stdin of the command preceding the redirection. If we, for instance, redirect /dev/tty into cat this way, cat will echo everything we type into the terminal:

$ cat < /dev/tty
test
test

Important to note is that this redirection overrides any pipe in the command line, as demonstrated below.

$ echo "test" | cat
test
$ echo "test" | cat < /dev/tty
test2
test2

Because of this, we’ll need to do some trickery to put this all together and get an interactive shell. At first glance, it would make sense to simply reattach the TTY to the interactive session started by sudo -i, so that the final command would look like this:

$ echo "pass" | sudo -S -k -i < /dev/tty

While this will actually spawn a shell that we can interact with, it now doesn’t read the password from the pipe anymore because of the redirection, meaning it doesn’t quite fit the bill. What we need is for sudo to read from the pipe, and for the command invoked by sudo to read from the TTY. The command given to sudo itself can, to the best of my knowledge, not be redirected. If we try something like sudo foo < /dev/tty, then it is the sudo command that is redirected, and if we do something like sudo 'foo < /dev/tty', the sudo command will treat the whole single-quoted string as the name of the file to execute.

What we need to do, instead, is run e.g. bash or sh through sudo, and use those to run the command the we want to redirect the TTY into. There is even a note about this in the sudo manual:

To make a usage listing of the directories in the /home partition. Note that
this runs the commands in a sub-shell to make the cd and file redirection work.

      $ sudo sh -c "cd /home ; du -s * | sort -rn > USAGE"

Armed with this new knowledge, we can finally craft a command that will do what we’re looking for:

$ echo "pass" | sudo -k -S sh -c "bash < /dev/tty"
# whoami
root

Let’s break it down

Hopefully, this all makes sense, but let’s break it down a bit regardless. Essentially, this is how the command works:

   echo "pass" | sudo  -k -S  sh -c  "bash < /dev/tty"
# <-------[1]--------><-[2]-><-[3]-><-------[4]------->
  1. Pipe the password to sudo
  2. Invalidate cached credentials (-k, and read new ones from stdin (i.e., the pipe, -S)
  3. Call sh -c, which will execute the string following as a command line
  4. Run bash with the reattached TTY

All put together, it gives us an interactive bash session that allows us to, in theory, brute force the super user password.

In fairness, using this method to brute force the password – while possible – is extremely slow. Just printing all the three-letter password candidates generated by the code at the start of the post takes over three minutes on my computer, and that doesn’t even go through the process of trying to authenticate with the candidates. So it’s really not a method I would personally use, but it did serve as an interesting platform for exploring some neat quirks bash shenanigans. And I’m always up for bash shenanigans.

Happy hacking!


  1. Code remedying the fact that the characters " and $( will cause errors due to bash syntax errors is left out, to make it a bit cleaner. Left as an exercise for the reader, if you will.↩︎

  2. This is a qualified guess, I haven’t read the source code of sudo to verify it.↩︎