Interactive subshells in BASH
2022-11-191369 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 sudo
2 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.
"cd /home ; du -s * | sort -rn > USAGE" $ sudo sh -c
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]------->
- Pipe the password to
sudo
- Invalidate cached credentials (
-k
, and read new ones fromstdin
(i.e., the pipe,-S
) - Call
sh -c
, which will execute the string following as a command line - 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!