Mitigating PHP’s long standing issue with OPCache leaking sensitive data

Mattias Geniar, Tuesday, February 28, 2017

A very old security vulnerability has been fixed in PHP regarding the way it handles its OPCaches in environments where a single master process shares multiple PHP-FPM pools. This is the most common way to run PHP nowadays and might affect you, too.

The vulnerability

PHP has a method to speed up the dynamic nature of its interpreter, called bytecode caching. PHP gets interpreted on every pageload, meaning the PHP gets translated to bytecode which the server understands and can execute. Since most PHP pages don't change every second, PHP caches that bytecode in memory and can serve that as the response instead of having to compile (or "interpret") the PHP scripts every time.

In a default PHP-FPM setup, the process tree looks like this.

php-fpm: master process (/etc/php-fpm.conf)
\_ php-fpm: pool site1 (uid: 701)
\_ php-fpm: pool site2 (uid: 702)
\_ php-fpm: pool site3 (uid: 703)

There is a single PHP-FPM master process that gets started as the root user. It then spawns additional FPM pools, that can each run as their own user, to serve websites. The PHP OPCache (that bytecode cache) is held in the master process.

So here's the problem: PHP does not validate the userid when it fetches a script from memory, stored in its bytecode. The concept of a "shared memory" in PHP means everything is stored in the same memory segment, and including a file just checks if a version already exists in bytecode.

If a version of the script exists in bytecode, it's served without additional check.

If a version of the script does not exist in bytecode, the FPM pool (running as an unprivileged uid) will try to read it from disk, and Linux will prevent reads from files that the process has no access to. You know, like it's supposed to happen.

This is the one of the primary reasons I've advocated running multiple masters instead of multiple pools for years.

The solution: upgrade PHP & set additional configurations

After a way too long period, this bug is now resolved and you can fix it with additional master configurations.

$ cat /etc/php-fpm.conf
...
opcache.validate_permission (default "0")
       Leads OPcache to check file readability on each access to cached file.
       This directive should be enabled in shared hosting environment, when few
       users (PHP-FPM pools) reuse the common OPcache shared memory.

opcache.validate_root (default "0")
       This directive prevents file name collisions in different "chroot"
       environments. It should be enabled for sites that may serve requests in
       different "chroot" environments.

The introduction of the opcache.validate_permission and opcache.validate_root means you can now force PHP's OPCache to also check the permissions of the file and force a validation of the root path of the file, to avoid chrooted environments from reading eachothers' files (more on that in the original bugreport).

The default values, however, are insecure.

I understand why they are like this, to keep compatibility with previous versions and avoid breaking changes in minor versions, but you have to explicitly enable them to prevent this behaviour.

Minimum versions: PHP 5.6.29, 7.0.14, 7.1.0

This fix didn't get much attention and I only noticed it after someone posted to the oss-security mailing list. To mitigate this properly, you'll need at least;

Anything lower than 5.6 is already end of life and won't get this fix, even though OPCache got introduced in PHP 5.5. You'll need a recent 5.6 or newer to mitigate this.

If you still run PHP 5.5 (which many do) and want to be safe, your best bet is to either run multiple PHP masters (a single master per pool) or disable OPCache entirely.

Indirect local privilege escalation to root

This has been a long standing issue with PHP that's actually more serious than it looks at first glance.

If you manage to compromise a single website (which for most CMS's in PHP that don't get updated, isn't very hard), this shared memory allows you access to all other websites, if their pages have been cached in the OPCache.

You can effectively use this shared memory buffer as a passage way to read other website's PHP files, read their configs, get access to their database and steal all their data. To do so, you usually need root access to a server, but PHP's OPCache gives you a convenient shortcut to accomplish similar things.

All you need is the path to their files, which can either be retrieved via opcache_get_status() (if enabled, which again -- it is by default), or you can guess the paths, which on shared hosting environments usually isn't hard either.

This actually makes this shared memory architecture almost as bad as a local privilege escalation vulnerability, depending on what you want to accomplish. If your goal is to steal data from a server that normally requires root privileges, you got it.

If your goal is to actually get root and run root-only scripts (aka: bind on lower ports etc.), that won't be possible.

The good part is, there's now a fix with the new PHP configurations. The bad news is, you need to update to the latest releases to get it and explicitly enable it.

Additional sources

For your reading pleasure:



Hi! My name is Mattias Geniar. I'm a Support Manager at Nucleus Hosting in Belgium, a general web geek, public speaker and podcaster. Currently working on DNS Spy. Follow me on Twitter as @mattiasgeniar.

I respect your privacy and you won't get spam. Ever.
Just a weekly newsletter about Linux and open source.

SysCast podcast

In the SysCast podcast I talk about Linux & open source projects, interview sysadmins or developers and discuss web-related technologies. A show by and for geeks!

cron.weekly newsletter

A weekly newsletter - delivered every Sunday - for Linux sysadmins and open source users. It helps keeps you informed about open source projects, Linux guides & tutorials and the latest news.

Share this post

Did you like this post? Will you help me share it on social media? Thanks!

Comments

Hanno Tuesday, February 28, 2017 at 12:37

Thanks for this blogpost.

I tried to better understand this issue and wanted to share a few thoughts:
First of all it seems this specific attack only works under chroot’ed environments. Am I right with that? So the only issue is that if you have different chroots with identical file paths, correct?

Second what’s notable and what you might want to mention: Even with both the options set you can still call opcache_get_status(), which will tell you file paths from all the users on the same server that have been accessed lately. This might be sensitive enough that you want to hide it. I’ve now set this:

opcache.restrict_api=”/root/”

Which means only scripts in /root/ can access the opcache api functions. (I don’t have any php scripts in /root/, I just had to choose some path where I’m sure no user-controlled php is running. Leaving it empty means no restriction.)

Reply


Mattias Geniar Tuesday, February 28, 2017 at 13:46

Am I right with that? So the only issue is that if you have different chroots with identical file paths, correct?

Yes, that indeed seems to be the case, which is a common scenario on shared hosting servers.

Reply


Doug Wednesday, March 1, 2017 at 19:09

Instead of default “0” should a value of “1” be used to fix the vulnerability?

Reply


Mattias Geniar Wednesday, March 1, 2017 at 21:24

I probably should have made that more clear in the blogpost: yes, chance those default 0 values to 1.

Reply


Doug Thursday, March 2, 2017 at 01:04

If I look at the contents of the default php-fpm.conf file in the php-7.1.2/sapi/fpm directory I do not see these two parameters. Shouldn’t they be included in this default file?

Reply


Leave a Reply

Your email address will not be published. Required fields are marked *

Inbound links