php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #78599 env_path_info underflow in fpm_main.c can lead to RCE
Submitted: 2019-09-26 16:17 UTC Modified: 2019-10-21 20:18 UTC
From: neex dot emil+phpeb at gmail dot com Assigned: stas (profile)
Status: Closed Package: FPM related
PHP Version: master-Git-2019-09-26 (Git) OS: linux
Private report: No CVE-ID: 2019-11043
 [2019-09-26 16:17 UTC] neex dot emil+phpeb at gmail dot com
Description:
------------
The line 1140 in file sapi/fpm/fpm/fpm_main.c (https://github.com/php/php-src/blob/master/sapi/fpm/fpm/fpm_main.c#L1140) contains pointer arithmetics that assumes that env_path_info has a prefix equal to the path to the php script. However, the code does not check this assumption is satisfied. The absence of the check can lead to an invalid pointer in the "path_info" variable.

Such conditions can be achieved in a pretty standard Nginx configuration. If one has Nginx config like this:

```
   location ~ [^/]\.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_pass   php:9000;
        ...
  }
}
```

The regexp in `fastcgi_split_path_info` directive can be broken using the newline character (in encoded form, %0a). Broken regexp leads to empty PATH_INFO, which triggers the bug.

This issue leads to code execution. Later in the code, the value of path_info[0] is set to zero (https://github.com/php/php-src/blob/master/sapi/fpm/fpm/fpm_main.c#L1150); then FCGI_PUTENV is called. Using a carefully chosen length of the URL path and query string, an attacker can make path_info point precisely to the first byte of _fcgi_data_seg structure. Putting zero into it moves `char* pos` field backwards, and following FCGI_PUTENV overwrites some data (including other fast cgi variables) with the script path. Using this technique, I was able to create a fake PHP_VALUE fcgi variable and then use a chain of carefully chosen config values to get code execution.


I have a working exploit PoC, but I'm not sure how to share it using this form. This security research is done by three people: me, @beched and @d90pwn.


Test script:
---------------
To reproduce the issue, you need to take the following steps:

1. Build php with --enable-fpm and ASAN enabled.
2. Download https://www.dropbox.com/s/eio9zikkg1juuj7/reproducer.tar.xz?dl=0. The following steps assume you're in the `reproducer` directory from the archive.
4. Run nginx using `sudo /usr/sbin/nginx -p $PWD -c nginx.conf` 
3. Run php-fpm using `path/to/php-fpm -y ./php-fpm.conf -F`
4. Visit a (pretty long) link from crash_link.txt using a tool another tool, like curl $(cat crash_link.txt).


Expected result:
----------------
No crash should happen.

Actual result:
--------------
You will get a crash:

==6629==ERROR: AddressSanitizer: SEGV on unknown address 0x620000005203 (pc 0x7efd1341a47f bp 0x7ffe980574e0 sp 0x7ffe98056c98 T0)
==6629==The signal is caused by a WRITE memory access.
    #0 0x7efd1341a47e in memcpy /build/glibc-OTsEL5/glibc-2.27/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:140
    #1 0x4b7c57 in __asan_memcpy /home/emil/llvm/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:22:3
    #2 0x13a88df in fcgi_hash_strndup /home/emil/php-src/main/fastcgi.c:322:2
    #3 0x13a88df in fcgi_hash_set /home/emil/php-src/main/fastcgi.c:359:11
    #4 0x13c4121 in init_request_info /home/emil/php-src/sapi/fpm/fpm/fpm_main.c:1154:12
    #5 0x13c4121 in main /home/emil/php-src/sapi/fpm/fpm/fpm_main.c:1864:4
    #6 0x7efd13380b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
    #7 0x440219 in _start (/home/emil/php-src/builded/sbin/php-fpm+0x440219)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /build/glibc-OTsEL5/glibc-2.27/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:140 in memcpy


Patches

0001-Fix-bug-78599-env_path_info-underflow-can-lead-to-RC.patch (last revision 2019-10-12 14:59 UTC by bukka@php.net)
initial-no-test (last revision 2019-10-06 18:24 UTC by bukka@php.net)

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2019-09-27 20:43 UTC] neex dot emil+phpeb at gmail dot com
Hello,

I'm planning to make the PoC exploit for this bug public on Tuesday. Is it OK to you?
 [2019-09-27 21:33 UTC] stas@php.net
We would certainly appreciate more time for looking into this. Is there any reason why you need to publicly disclose this 5 days after notification? 

I understand the issue required misconfigured FPM but if this is a common occurrence then it'd be nice to hold till the next release which fixes the issue if possible.
 [2019-09-28 08:26 UTC] neex dot emil+phpeb at gmail dot com
Well, the date can be rescheduled, I just want to get something about this. What is this next release date you're talking about?

Second, I believe this issue requires a CVE. The configuration required to reproduce the issue (which is uee fastcgi_split_pathinfo in nginx config with cgi.fix_pathinfo = 1) is not that uncommon. What is the process of getting it?
 [2019-09-28 21:12 UTC] cmb@php.net
Thanks for reporting this issue!

CVEs are preferrably issued by php.net[1].  The next security
relevant releases are scheduled for 24 Okt 2019.

Does this issue affect PHP 7.1, 7.2 and/or 7.3?

[1] <https://wiki.php.net/cve>
 [2019-09-29 19:20 UTC] neex dot emil+phpeb at gmail dot com
Well, I think that 24 October is too far. Please note that a task with a similar configuration was present on a Real World CTF at the beginning of September (ctf is a competition where teams hack things). While the issue was not the intended way to solve for the task, there might be other people who have noticed strange behavior and was able to understand what happens.

For now, let's define 24 October as a strict deadline. However, I would appreciate it if you allow disclosing the issue earlier.

My exploit works on all 7+ versions, but the core issue seems to exists since the mentioned code was written. I was able to reproduce the crash even on php 5.6.

More details on this:

The code that wrongly assumes that env_path_info is not empty was written in 2013, according to git blame. That means that out-of-bound read and out-of-bound write of a single byte exist in virtually all versions.

However, my exploit uses the presence of _fcgi_data_seg structure and related hash table optimization. It is here since 7.0. So 7.1, 7.2 and 7.3 all have both the issue and a way exploit for it (which is described in the first message).
 [2019-09-29 19:41 UTC] stas@php.net
-CVE-ID: +CVE-ID: 2019-11043
 [2019-09-29 19:41 UTC] stas@php.net
> Well, I think that 24 October is too far.

Too far for what? Are you on some kind of deadline? What is the nature of that deadline? Is this problem exploited in the wild? Can you provide more information about it? If we waited to October 24, what problem would happen?
We had emergency releases before, but for that we'd need to have strong justification, From your description I conclude that the issue can not happen unless FPM is misconfigured in a way that essentially makes it broken (i.e., PATH_INFO is empty) - something that somebody configuring their own server would likely notice? But I may be wrong, I am just trying so far to evaluate how common is such misconfiguration and assess the urgency of the fix from this. CTF contests are fun, but these are specifically set up to be broken into, and our primary interest is assessing danger to real world production users. 

Thanks again for your report. I have assigned CVE-2019-11043 for it.
 [2019-09-30 20:36 UTC] neex dot emil+phpeb at gmail dot com
I will try to answer your questions in the most detail.

First, while the mentioned misconfiguration is somewhat uncommon, it is not that uncommon, and it does not require the site to be completely broken. As I wrote in the report, the core thing that allows empty PATH_INFO is `fastcgi_split_path_info` directive, which has nothing wrong by itself. However, it usually contains a regexp that can be broken by an attacker by supplying %0a in the path info (that is the crucial thing here!). The $fastcgi_path_info will remain empty in this case, and the thing that usually follows, which is `fastcgi_param PATH_INFO $fastcgi_path_info;`, will set PATH_INFO to an empty value.

A website with such configuration won't seem broken as %0a isn't common in URL paths. Everything will be working as usual.

Of course, there are factors that can prevent exploitation even if fastcgi_split_path_info is present. For example, if nginx and php-fpm share the same filesystem, one can first check the script actually exists, so the malicious request will never make it to php-fpm (it is usually done using something like `try_files $uri =404`). Or there may be cgi.fix_pathinfo=0 --- it will also prevent exploitation. Both settings are present in a lot of config snippets back from the days when php-fpm didn't check extensions of the scripts, but now it does, so some more recent configs lack the checks.

Second, the impact of the vulnerability is pretty significant. Using the overflow, I was able to add fast cgi environment variables for the request, and PHP_VALUE is a very precious one. There is a limitation --- you can't supply values longer than 23 bytes --- however, I was able to get code execution under this condition (and that is pretty tricky stuff by itself, but it is another story).

Third, there's a misunderstanding about the CTF thing.

> CTF contests are fun, but these are specifically set up to be broken into, and our primary interest is assessing danger to real world production users.

As I wrote, the challenge was not about nginx/php-fpm misconfiguration at all (it was a client-side challenge actually). However, the one who configured nginx for the challenge made the configuration mistake mentioned earlier. I'm sure it was accidental, the challenge author probably just copy-pasted nginx config from somewhere.

So, the fact that the vulnerable config was present on a CTF doesn't make the issue like "someone need to intentionally break his own server to make the exploit work", it is more like a (very) weak evidence that the issue is severe, like "someone probably copy-pasted vulnerable config once, so there are vulnerable snippets around".

But the reason why I mentioned the CTF was not this. Our research started by noticing the strange behavior of $_SERVER variables after randomly inserting %0a in the URL. Maybe someone in some other team did the same and investigated it in the same way.

Summing up the CTF thing:
1. Someone wanted to make a challenge for a CTF. She chooses nginx + php-fpm for its backend, but the challenge itself was not related to PHP and could be done in any language.
2. A config for nginx was needed, so she (presumably) took one of nginx config snippets (like from "configure nginx + php-fpm" google query), which appeared to suffer from the flaw above.
3. One of the guys I mentioned in my report just %0a randomly in the url and noticed the strange values of some $_SERVER variables.

I hope it is clear now that using the vulnerability was not one of the intended ways to solve the challenge (at least, it seems very unlikely).

So, recapping everything above, my desire to fix this as soon as possible is based on three things:
1. Uncommon, but not that uncommon configuration.
2. Big impact.
3. The mentioned configuration was around lots of security folk recently, and maybe someone found the same thing.

From the tone of the previous comment I've concluded that I've caused some irritation — this was not my intention at all. Obviously, the final decision on the disclosure date is made on your side. If you say October 24th — October 24th it is.

I admit intentionally writing kind of "pushing" comments; however, I'm not trying to push you towards something like an emergency update, I just think the issue deserves to be analyzed.
 [2019-09-30 21:19 UTC] cmb@php.net
-Assigned To: +Assigned To: bukka
 [2019-09-30 21:19 UTC] cmb@php.net
Jakub, could you please have a look at this?
 [2019-10-03 17:30 UTC] bukka@php.net
Will try find some time over the weekend for this!
 [2019-10-06 18:19 UTC] bukka@php.net
-Summary: env_path_info underflow on sapi/fpm/fpm/fpm_main.c:1140 can lead to RCE +Summary: env_path_info underflow in fpm_main.c can lead to RCE -Package: Reproducible crash +Package: FPM related
 [2019-10-06 18:19 UTC] bukka@php.net
I have been looking into it and looks quite nasty. I think something like this (based on PHP-7.1 branch) should probably fix it:

diff --git a/sapi/fpm/fpm/fpm_main.c b/sapi/fpm/fpm/fpm_main.c
index 24a7e5d56a..50f92981f1 100644
--- a/sapi/fpm/fpm/fpm_main.c
+++ b/sapi/fpm/fpm/fpm_main.c
@@ -1209,8 +1209,8 @@ static void init_request_info(void)
                                                                path_info = script_path_translated + ptlen;
                                                                tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
                                                        } else {
-                                                               path_info = env_path_info ? env_path_info + pilen - slen : NULL;
-                                                               tflag = (orig_path_info != path_info);
+                                                               path_info = (env_path_info && pilen > slen) ? env_path_info + pilen - slen : NULL;
+                                                               tflag = path_info && (orig_path_info != path_info);
                                                        }
 
                                                        if (tflag) {

However it's just based on code logic. It means it's not really tested. I started looking to creating a test but need to figure out what exactly nginx sends. Tried with
        [
            'SCRIPT_FILENAME'   => $uri,
            'SCRIPT_NAME'       => $uri . '/abcd',
            'PATH_INFO'         => '',
        ]

but it didn't crash so will need to try it with nginx and gdb or wireshark to get all fcgi params. If anyone can get all fcgi params causing a crash, that would be really helpful as I don't have much free time...
 [2019-10-06 18:24 UTC] bukka@php.net
The following patch has been added/updated:

Patch Name: initial-no-test
Revision:   1570386249
URL:        https://bugs.php.net/patch-display.php?bug=78599&patch=initial-no-test&revision=1570386249
 [2019-10-06 18:25 UTC] bukka@php.net
Formatting is not exactly great in the comment so attached it as a patch in case someone wants to try it.

Just a note that it's not ready for merging as it doesn't have a test...
 [2019-10-06 23:49 UTC] neex dot emil+phpeb at gmail dot com
From your comment, it looks like you're sending correct values. Please note that the length matters here: $uri must be about 2000 bytes to make the crash happen. The reason is the following:

The nearly only place where the crash can happen is here:

https://github.com/php/php-src/blob/master/sapi/fpm/fpm/fpm_main.c#L1150

the first byte of the path_info points outside of the string because of the bug.

However, most of the time it will point to the region of memory obtained with the same allocation, so the crash won't happen even under ASAN. Names and values of FastCGI variables are stored in this structure:

https://github.com/php/php-src/blob/master/main/fastcgi.c#L186

The "data" field here will be actually 4096 bytes long, and both names and values of FastCGI variables are stored in it like this: | name_1 | 00 | value_1 | 00 | ...

If there's no room left for a name or value, a new _fcgi_data_seg is created:

https://github.com/php/php-src/blob/master/main/fastcgi.c#L312

To make the crash happen, you need the name and value of PATH_INFO to be stored precisely at the beginning of a _fcgi_data_seg. Otherwise, the underflowed path_info pointer will point to the previous variable, which is SCRIPT_NAME (both in your example and in nginx). You cannot make it point before SCRIPT_NAME: to get slen bigger, you need to make SCRIPT_NAME longer, so path_info will actually point to the same byte of SCRIPT_NAME.

Please note that the total length of all previous variables obviously matters. That means that e.g., the length of the filesystem path to the script must be considered. I'm just bruting QUERY_STRING length to get PATH_INFO placed precisely at the beginning of a _fcgi_data_seg.

Also, "/asdf" is too short for path info. Even if you have PATH_INFO value placed at the beginning of a segment, you need to jump over the header of the _fcgi_data_seg structure. That means you need to send longer path info to get the crash.

I think I can just dump the full set of the fastcgi variables using some printf's if you need it (but there will be some long ones). However, I'm not sure this is good for a test: it will be relying not only on the code related to the bug but also on the hash table code in fastcgi.c. If, for example, you change the length of the _fcgi_data_seg's data length to another value, the crash won't happen even if the bug returns.
 [2019-10-07 14:37 UTC] bukka@php.net
Thanks for the reply. It makes a bit more sense. I was thinking about having a test that just confirms that the issue got fixed. It means crashes before the fix and does not crash afterwards.

The tester script allows customizing the variables and I could modify the currently default ones:

https://github.com/php/php-src/blob/9b28553d4581f21e8e788ad60fc9948565093ad8/sapi/fpm/tests/tester.inc#L516-L534

The length should not be an issue as we could just use str_repeat for some values so it should be still relatively clean in the test.

Do you still think it might not be a good test? If so, do you have any suggestion for a good test?
 [2019-10-08 00:19 UTC] neex dot emil+phpeb at gmail dot com
No, currently I don't have any ideas on making the test more reliable. However, I was able to reproduce the crash using the test suite --- hope it helps. The patch indeed fixes it.

I'm using the following set of variables (did it by locally modifying tester.inc):

[
    'GATEWAY_INTERFACE' => 'FastCGI/1.0',
    'REQUEST_METHOD'    => 'GET',
    'SCRIPT_FILENAME'   => $uri."/".str_repeat('A', 35),
    'SCRIPT_NAME'       => $uri,
    'QUERY_STRING'      => $query,
    'REQUEST_URI'       => $uri . ($query ? '?'.$query : ""),
    'DOCUMENT_URI'      => $uri,
    'SERVER_SOFTWARE'   => 'php/fcgiclient',
    'REMOTE_ADDR'       => '127.0.0.1',
    'REMOTE_PORT'       => '7777',
    'SERVER_ADDR'       => '127.0.0.1',
    'SERVER_PORT'       => '80',
    'SERVER_NAME'       => php_uname('n'),
    'SERVER_PROTOCOL'   => 'HTTP/1.1',
    'DOCUMENT_ROOT'     => __DIR__,
    'CONTENT_TYPE'      => '',
    'CONTENT_LENGTH'    => 0,
    'HTTP_HUI'          => str_repeat('PIZDA', 1000),
    'PATH_INFO'         => '',
],

Changes are: SCRIPT_FILENAME has an appended suffix, HTTP_HUI and PATH_INFO have been added.

Things to note:
1. There was a mistake in all recent comments; the path_info should be appended to SCRIPT_FILENAME, not to SCRIPT_NAME.
2. There's a "request header" above right before PATH_INFO. The length of its' value is long enough to make fcgi_hash_strndup create new _fcgi_data_seg both for the said value and the following PATH_INFO (name and value).
3. The path info (which is appended to SCRIPT_FILENAME) is exactly 35 bytes long. This number is calculated as follows: size of _fcgi_data_seg's header is 3 * 8 = 24 bytes, length of string "PATH_INFO" (which is the variable name and is also stored inside _fcgi_data_seg) is 9 bytes plus the final zero byte, and an additional byte is needed to make path_info variable point right before the _fcgi_data_seg to make ASAN scream.

This test is still not very nice as it relies on the order of variables, length of _fcgi_data_seg, etc. However, if you just need a test that don't pass without the patch, it is it. The error message is "Not in white list. Check listen.allowed_clients.", but I believe it is always shown in case of incorrect FastCGI reply.

It looks like to make the set of the variables above a regular test, we need to implement path_info support in Tester::request. Currently, it doesn't call makeFile if $uri is supplied and doesn't allow to append path_info otherwise.
 [2019-10-12 14:59 UTC] bukka@php.net
The following patch has been added/updated:

Patch Name: 0001-Fix-bug-78599-env_path_info-underflow-can-lead-to-RC.patch
Revision:   1570892355
URL:        https://bugs.php.net/patch-display.php?bug=78599&patch=0001-Fix-bug-78599-env_path_info-underflow-can-lead-to-RC.patch&revision=1570892355
 [2019-10-12 15:17 UTC] bukka@php.net
Thanks for the info about the variables needed for the test. I managed to see the issue and create a test for it. Also thanks for confirming the fix. It also fixed the failing test.

I have attached the full patch. It's ready to be applied to 7.1!

Stas, are you ok to handle it from here?

Thanks
 [2019-10-15 06:33 UTC] stas@php.net
-Assigned To: bukka +Assigned To: stas
 [2019-10-21 20:18 UTC] stas@php.net
Automatic comment on behalf of bukka
Revision: http://git.php.net/?p=php-src.git;a=commit;h=ab061f95ca966731b1c84cf5b7b20155c0a1c06a
Log: Fix bug #78599 (env_path_info underflow can lead to RCE) (CVE-2019-11043)
 [2019-10-21 20:18 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2019-10-22 07:16 UTC] cmb@php.net
Automatic comment on behalf of bukka
Revision: http://git.php.net/?p=php-src.git;a=commit;h=19e17d3807e6cc0b1ba9443ec5facbd33a61f8fe
Log: Fix bug #78599 (env_path_info underflow can lead to RCE) (CVE-2019-11043)
 [2019-10-22 16:25 UTC] neex dot emil+phpeb at gmail dot com
As the fixes have been released, I've published the exploit. It is available at https://github.com/neex/phuip-fpizdam
 [2019-10-24 09:16 UTC] cfgxy2008 at gmail dot com
A method to quick fix this problem.
If you want to use PATH_INFO in php, and do not want to patch and recompile PHP.

Add this line before ALL YOUR "location ~ \.php(/|$) {" LINES in nginx confs:
```
rewrite ^(.*?)\n $1;  #Fix CVE-2019-11043 (THIS LINE!!!)
location ~ \.php(/|$) {
  ...
  fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
  ...
```

That will truncate PATH_INFO after "\n" while URL contains "%0a".
 [2019-10-25 12:32 UTC] me at fixxxer dot me
As fpm_main.c is based on sapi/cgi/cgi_main.c, could it be that the same vulnerability exists in the CGI sapi too?

This pointer arithmetics looks very similar:
https://github.com/php/php-src/blob/master/sapi/cgi/cgi_main.c#L1293
 [2019-10-26 13:24 UTC] beuc at beuc dot net
> As fpm_main.c is based on sapi/cgi/cgi_main.c,
> could it be that the same vulnerability exists in the CGI sapi too?

FWIW I did a quick test with FCGI using:
USER=www-data PATH=/usr/bin PHP_FCGI_CHILDREN=1 PHP_FCGI_MAX_REQUESTS=10 valgrind /usr/bin/php5-cgi -b 127.0.0.1:9000
(same nginx conf)
and could not reproduce the crash.

I'd welcome other eyeballs on this though.
 [2019-10-28 20:04 UTC] dzuelke at gmail dot com
There are better workarounds for this on the Nginx level than the one presented in an earlier comment.

The simplest is to conditionally set PATH_INFO if it's not empty:

fastcgi_param PATH_INFO $fastcgi_path_info if_not_empty;

In my tests, that successfully prevents an attack.

Another option is to explicitly test whether the FCGI script path exists; this is useful anyway because otherwise, PHP-FPM will produce the 404 error page, which isn't customizable:

if (!-f $document_root$fastcgi_script_name) {
	# check if the script exists
	# otherwise, /foo.jpg/bar.php would get passed to FPM, which wouldn't run it as it's not in the list of allowed extensions, but this check is a good idea anyway, just in case
	return 404;
}
 
PHP Copyright © 2001-2019 The PHP Group
All rights reserved.
Last updated: Wed Nov 13 22:01:29 2019 UTC